Checkpoint: Application complète de dématérialisation de facturation avec extraction IA (Mistral), authentification locale + Azure AD, stockage local, et export SFTP.
Fonctionnalités implémentées : ✅ Authentification locale (email/password) + Azure AD + Manus OAuth ✅ Upload drag-and-drop de fichiers PDF avec suivi en temps réel ✅ Extraction automatique avec Mistral AI (OCR + LLM) ✅ Détection de doublons (fournisseur, numéro, date) ✅ Score de qualité d'extraction (0-100) ✅ Tableau de bord avec statistiques ✅ Liste des factures avec recherche et filtres ✅ Paramètres utilisateur (LLM, keywords, SFTP) ✅ Historique des imports avec logs détaillés ✅ Gestion des utilisateurs (admin) ✅ Export SFTP manuel/automatique ✅ Stockage local avec organisation YYYY-MM ✅ Tests unitaires d'authentification Architecture : - Frontend : React 19 + Vite + TailwindCSS + Radix UI - Backend : Express + tRPC + Drizzle ORM - Base de données : MySQL (6 tables) - IA : Mistral AI pour extraction - Stockage : Local filesystem - Export : SFTP Pages : - Login (choix local/Azure/Manus) - Dashboard (statistiques) - Upload (drag-and-drop) - Invoices (liste avec recherche) - Settings (LLM, keywords, SFTP) - History (logs d'import) - Users (gestion admin)
This commit is contained in:
166
server/auth.ts
Normal file
166
server/auth.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { ConfidentialClientApplication } from "@azure/msal-node";
|
||||
import { getUserByEmail, getUserByAzureAdId } from "./db";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// ============= LOCAL AUTHENTICATION =============
|
||||
|
||||
/**
|
||||
* Hash a password using bcrypt
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a password against a hash
|
||||
*/
|
||||
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user with email and password (local auth)
|
||||
* Returns user and JWT token if successful, null otherwise
|
||||
*/
|
||||
export async function loginLocal(email: string, password: string) {
|
||||
const user = await getUserByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if (user.isActive === 0) {
|
||||
throw new Error("Account is inactive");
|
||||
}
|
||||
|
||||
// Check if user has a password (local auth)
|
||||
if (!user.passwordHash) {
|
||||
throw new Error("This account does not support local authentication");
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValid = await verifyPassword(password, user.passwordHash);
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken(user);
|
||||
|
||||
return { user, token };
|
||||
}
|
||||
|
||||
// ============= JWT TOKEN GENERATION =============
|
||||
|
||||
/**
|
||||
* Generate a JWT token for a user
|
||||
*/
|
||||
export function generateToken(user: { id: number; email: string; role: string }): string {
|
||||
const secret = process.env.JWT_SECRET || "default-secret-change-in-production";
|
||||
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
},
|
||||
secret,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify and decode a JWT token
|
||||
*/
|
||||
export function verifyToken(token: string): { userId: number; email: string; role: string } | null {
|
||||
try {
|
||||
const secret = process.env.JWT_SECRET || "default-secret-change-in-production";
|
||||
const decoded = jwt.verify(token, secret) as { userId: number; email: string; role: string };
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============= AZURE AD AUTHENTICATION =============
|
||||
|
||||
let msalClient: ConfidentialClientApplication | null = null;
|
||||
|
||||
/**
|
||||
* Check if Azure AD is configured
|
||||
*/
|
||||
export function isAzureAdConfigured(): boolean {
|
||||
return !!(
|
||||
process.env.AZURE_AD_TENANT_ID &&
|
||||
process.env.AZURE_AD_CLIENT_ID &&
|
||||
process.env.AZURE_AD_CLIENT_SECRET
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MSAL client instance (lazy initialization)
|
||||
*/
|
||||
function getMsalClient(): ConfidentialClientApplication {
|
||||
if (!isAzureAdConfigured()) {
|
||||
throw new Error("Azure AD is not configured");
|
||||
}
|
||||
|
||||
if (!msalClient) {
|
||||
msalClient = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId: process.env.AZURE_AD_CLIENT_ID!,
|
||||
authority: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`,
|
||||
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return msalClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Azure AD authorization URL for user login
|
||||
*/
|
||||
export async function getAzureAuthUrl(): Promise<string> {
|
||||
const client = getMsalClient();
|
||||
|
||||
const redirectUri = process.env.AZURE_AD_REDIRECT_URI || "http://localhost:3000/api/auth/azure/callback";
|
||||
|
||||
const authCodeUrlParameters = {
|
||||
scopes: ["user.read"],
|
||||
redirectUri,
|
||||
};
|
||||
|
||||
return client.getAuthCodeUrl(authCodeUrlParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Azure AD callback and exchange code for tokens
|
||||
*/
|
||||
export async function handleAzureCallback(code: string) {
|
||||
const client = getMsalClient();
|
||||
|
||||
const redirectUri = process.env.AZURE_AD_REDIRECT_URI || "http://localhost:3000/api/auth/azure/callback";
|
||||
|
||||
const tokenRequest = {
|
||||
code,
|
||||
scopes: ["user.read"],
|
||||
redirectUri,
|
||||
};
|
||||
|
||||
const response = await client.acquireTokenByCode(tokenRequest);
|
||||
|
||||
if (!response || !response.account) {
|
||||
throw new Error("Failed to acquire token from Azure AD");
|
||||
}
|
||||
|
||||
return {
|
||||
azureAdId: response.account.homeAccountId,
|
||||
email: response.account.username,
|
||||
name: response.account.name || response.account.username,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user