import bcrypt from "bcryptjs"; import { getDb } from "./db"; import { localUsers } from "../drizzle/schema"; import { eq, or } from "drizzle-orm"; import { SignJWT, jwtVerify } from "jose"; import { ENV } from "./_core/env"; const SALT_ROUNDS = 12; const JWT_EXPIRY = "7d"; export const LOCAL_AUTH_COOKIE = "veille_local_auth"; export async function hashPassword(password: string): Promise { return bcrypt.hash(password, SALT_ROUNDS); } export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } export async function generateLocalToken(userId: number, role: string): Promise { const secret = new TextEncoder().encode(ENV.cookieSecret); return new SignJWT({ sub: String(userId), role, type: "local" }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(JWT_EXPIRY) .sign(secret); } export async function verifyLocalToken(token: string): Promise<{ userId: number; role: string } | null> { try { const secret = new TextEncoder().encode(ENV.cookieSecret); const { payload } = await jwtVerify(token, secret); if (payload.type !== "local" || !payload.sub) return null; return { userId: parseInt(payload.sub), role: payload.role as string }; } catch { return null; } } /** * Connexion par username OU email (insensible à la casse pour l'email). * Le champ `identifier` peut être un nom d'utilisateur libre ou une adresse e-mail. */ export async function loginLocalUser(identifier: string, password: string) { const db = await getDb(); if (!db) throw new Error("Base de données indisponible"); const id = identifier.trim(); // Cherche d'abord par username exact, puis par email (insensible à la casse) const results = await db .select() .from(localUsers) .where( or( eq(localUsers.username, id), eq(localUsers.email, id.toLowerCase()), eq(localUsers.email, id) ) ) .limit(1); const user = results[0]; if (!user || !user.isActive) { throw new Error("Identifiants incorrects ou compte désactivé"); } const valid = await verifyPassword(password, user.passwordHash); if (!valid) throw new Error("Identifiants incorrects ou compte désactivé"); await db .update(localUsers) .set({ lastSignedIn: new Date() }) .where(eq(localUsers.id, user.id)); const token = await generateLocalToken(user.id, user.role); return { token, user: { id: user.id, name: user.name, username: user.username ?? null, email: user.email ?? null, role: user.role, }, }; } export async function getLocalUserById(id: number) { const db = await getDb(); if (!db) return null; const results = await db.select().from(localUsers).where(eq(localUsers.id, id)).limit(1); return results[0] ?? null; } export async function ensureAdminExists() { const db = await getDb(); if (!db) return; const admins = await db .select({ id: localUsers.id }) .from(localUsers) .where(eq(localUsers.role, "admin")) .limit(1); if (admins.length === 0) { const hash = await hashPassword("Admin@Itinova2024!"); await db.insert(localUsers).values({ name: "Administrateur", username: "admin", email: "admin@itinova.fr", passwordHash: hash, role: "admin", isActive: true, }); console.log("[LocalAuth] Compte admin par défaut créé : admin / Admin@Itinova2024!"); } }