Checkpoint: Application complète : deux tableaux de bord (Veille Stratégique + AAP), import Excel quotidien avec déduplication, sources multiples (local/OneDrive/FTP/SharePoint), affichage liste/vignettes, filtres multi-critères, gestion utilisateurs, logs d'import, page paramètres, authentification locale, tâche cron 06h00, 13 tests Vitest passants.
This commit is contained in:
96
server/localAuth.ts
Normal file
96
server/localAuth.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { getDb } from "./db";
|
||||
import { localUsers } from "../drizzle/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { SignJWT, jwtVerify } from "jose";
|
||||
import { ENV } from "./_core/env";
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
const JWT_EXPIRY = "7d";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginLocalUser(email: string, password: string) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Base de données indisponible");
|
||||
|
||||
const users = await db
|
||||
.select()
|
||||
.from(localUsers)
|
||||
.where(eq(localUsers.email, email.toLowerCase().trim()))
|
||||
.limit(1);
|
||||
|
||||
const user = users[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é");
|
||||
|
||||
// Mise à jour lastSignedIn
|
||||
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, email: user.email, role: user.role } };
|
||||
}
|
||||
|
||||
export async function getLocalUserById(id: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const users = await db.select().from(localUsers).where(eq(localUsers.id, id)).limit(1);
|
||||
return users[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",
|
||||
email: "admin@itinova.fr",
|
||||
passwordHash: hash,
|
||||
role: "admin",
|
||||
isActive: true,
|
||||
});
|
||||
console.log("[LocalAuth] Compte admin par défaut créé : admin@itinova.fr / Admin@Itinova2024!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user