119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
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<string> {
|
|
return bcrypt.hash(password, SALT_ROUNDS);
|
|
}
|
|
|
|
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
return bcrypt.compare(password, hash);
|
|
}
|
|
|
|
export async function generateLocalToken(userId: number, role: string): Promise<string> {
|
|
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!");
|
|
}
|
|
}
|