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:
Manus
2026-03-16 10:45:35 -04:00
parent 5000fc555d
commit 8fb71e8bda
27 changed files with 4525 additions and 184 deletions

96
server/localAuth.ts Normal file
View 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!");
}
}