import { Express, Request, Response, NextFunction } from "express"; import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; import session from "express-session"; import connectPgSimple from "connect-pg-simple"; import { z } from "zod"; import crypto from "crypto"; import { insertUserSchema } from "@shared/schema"; import { storage } from "./storage"; import { pool } from "./db"; // Simple password hashing function hashPassword(password: string): string { return crypto.createHash("sha256").update(password).digest("hex"); } // Setup passport strategies export function setupPassport() { // Configure Passport with a local strategy passport.use( new LocalStrategy(async (username, password, done) => { try { // Find the user by username const user = await storage.getUserByUsername(username); // If user doesn't exist or password doesn't match if (!user || hashPassword(password) !== user.password) { return done(null, false, { message: "Identifiants incorrects" }); } // Login successful return done(null, user); } catch (error) { return done(error); } }) ); // Serialize user for the session passport.serializeUser((user: any, done) => { done(null, user.id); }); // Deserialize user from the session passport.deserializeUser(async (id: number, done) => { try { const user = await storage.getUser(id); if (!user) { return done(null, false); } return done(null, user); } catch (error) { return done(error); } }); } // Setup express middleware for authentication export function setupAuthMiddleware(app: Express) { // Create PostgreSQL session store const PgStore = connectPgSimple(session); // Configure session middleware app.use( session({ store: new PgStore({ pool: pool, tableName: "session" // Sessions will be stored in the database }), secret: process.env.SESSION_SECRET || "votre_secret_temporaire", // Ideally use environment variable resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days httpOnly: true, secure: process.env.NODE_ENV === "production" } }) ); // Initialize Passport app.use(passport.initialize()); app.use(passport.session()); } // Register authentication routes export function registerAuthRoutes(app: Express) { // Registration route app.post("/api/auth/register", async (req: Request, res: Response) => { try { // Extend the insertUserSchema to validate password const registerSchema = insertUserSchema.extend({ password: z.string().min(6, "Le mot de passe doit contenir au moins 6 caractères") }); const userData = registerSchema.parse(req.body); // Check if username already exists const existingUser = await storage.getUserByUsername(userData.username); if (existingUser) { return res.status(400).json({ message: "Ce nom d'utilisateur est déjà pris" }); } // Check if email already exists const existingEmail = await storage.getUserByEmail(userData.email); if (existingEmail) { return res.status(400).json({ message: "Cette adresse email est déjà utilisée" }); } // Hash the password before storing const hashedPassword = hashPassword(userData.password); // Create the user with the hashed password const user = await storage.createUser({ ...userData, password: hashedPassword }); // Remove password from response const { password, ...userWithoutPassword } = user; // Auto login after registration req.login(user, (err) => { if (err) { return res.status(500).json({ message: "Erreur lors de la connexion après inscription", error: err }); } return res.status(201).json(userWithoutPassword); }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Données d'inscription invalides", errors: error.errors }); } console.error("Error in registration:", error); res.status(500).json({ message: "Échec de l'inscription" }); } }); // Route for registering with invitation token app.post("/api/auth/register-with-invitation", async (req: Request, res: Response) => { try { const { token, ...userData } = req.body; if (!token) { return res.status(400).json({ message: "Token d'invitation manquant" }); } // Check if invitation exists and is valid const invitation = await storage.getInvitationByToken(token); if (!invitation) { return res.status(400).json({ message: "Invitation invalide" }); } if (invitation.used) { return res.status(400).json({ message: "Cette invitation a déjà été utilisée" }); } const now = new Date(); if (new Date(invitation.expiresAt) < now) { return res.status(400).json({ message: "Cette invitation a expiré" }); } // Validate user data const registerSchema = insertUserSchema.extend({ password: z.string().min(6, "Le mot de passe doit contenir au moins 6 caractères") }); const validatedUserData = registerSchema.parse({ ...userData, email: invitation.email // Force the email from the invitation }); // Check if username already exists const existingUser = await storage.getUserByUsername(validatedUserData.username); if (existingUser) { return res.status(400).json({ message: "Ce nom d'utilisateur est déjà pris" }); } // Hash the password before storing const hashedPassword = hashPassword(validatedUserData.password); // Create the user with the hashed password and role from invitation const user = await storage.createUser({ ...validatedUserData, role: invitation.role, password: hashedPassword }); // Mark invitation as used await storage.markInvitationAsUsed(invitation.id); // Remove password from response const { password, ...userWithoutPassword } = user; // Auto login after registration req.login(user, (err) => { if (err) { return res.status(500).json({ message: "Erreur lors de la connexion après inscription", error: err }); } return res.status(201).json(userWithoutPassword); }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Données d'inscription invalides", errors: error.errors }); } console.error("Error in registration with invitation:", error); res.status(500).json({ message: "Échec de l'inscription avec invitation" }); } }); // Login route app.post("/api/auth/login", (req: Request, res: Response, next: NextFunction) => { passport.authenticate("local", (err: Error, user: any, info: any) => { if (err) { return next(err); } if (!user) { return res.status(401).json({ message: info?.message || "Échec de l'authentification" }); } req.login(user, (err) => { if (err) { return next(err); } // Remove password from response const { password, ...userWithoutPassword } = user; return res.json(userWithoutPassword); }); })(req, res, next); }); // Logout route app.post("/api/auth/logout", (req: Request, res: Response) => { req.logout((err) => { if (err) { return res.status(500).json({ message: "Erreur lors de la déconnexion" }); } res.json({ message: "Déconnexion réussie" }); }); }); // Get current user app.get("/api/auth/me", (req: Request, res: Response) => { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Non authentifié" }); } // Remove password from response const { password, ...userWithoutPassword } = req.user as any; res.json(userWithoutPassword); }); // Authentication check middleware app.use("/api/auth/check", (req: Request, res: Response) => { res.json({ authenticated: req.isAuthenticated() }); }); // Routes for invitations (admin only) app.post("/api/invitations", ensureAdmin, async (req: Request, res: Response) => { try { const { email, role = "user" } = req.body; if (!email) { return res.status(400).json({ message: "L'adresse email est requise" }); } // Check if email already has an active invitation const existingInvitation = await storage.getInvitationByEmail(email); if (existingInvitation && !existingInvitation.used && new Date(existingInvitation.expiresAt) > new Date()) { return res.status(400).json({ message: "Une invitation est déjà active pour cette adresse email" }); } // Check if email is already used by a user const existingUser = await storage.getUserByEmail(email); if (existingUser) { return res.status(400).json({ message: "Cette adresse email est déjà utilisée par un utilisateur" }); } // Generate a random token const token = crypto.randomBytes(32).toString('hex'); // Set expiration date (48 hours from now) const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 48); // Create the invitation const invitation = await storage.createInvitation({ email, role, createdBy: (req.user as any).id, token, expiresAt, }); // Return the invitation without the token (for security) const { token: _, ...invitationWithoutToken } = invitation; return res.status(201).json({ ...invitationWithoutToken, invitationLink: `/register/${token}` }); } catch (error) { console.error("Error creating invitation:", error); return res.status(500).json({ message: "Erreur lors de la création de l'invitation" }); } }); // Get all invitations (admin only) app.get("/api/invitations", ensureAdmin, async (req: Request, res: Response) => { try { const invitations = await storage.getInvitations(); return res.json(invitations); } catch (error) { console.error("Error fetching invitations:", error); return res.status(500).json({ message: "Erreur lors de la récupération des invitations" }); } }); // Delete an invitation (admin only) app.delete("/api/invitations/:id", ensureAdmin, async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); if (isNaN(id)) { return res.status(400).json({ message: "ID d'invitation invalide" }); } const deleted = await storage.deleteInvitation(id); if (!deleted) { return res.status(404).json({ message: "Invitation non trouvée" }); } return res.json({ message: "Invitation supprimée avec succès" }); } catch (error) { console.error("Error deleting invitation:", error); return res.status(500).json({ message: "Erreur lors de la suppression de l'invitation" }); } }); // Verify invitation token app.get("/api/invitations/verify/:token", async (req: Request, res: Response) => { try { const { token } = req.params; const invitation = await storage.getInvitationByToken(token); if (!invitation) { return res.status(404).json({ message: "Invitation invalide" }); } if (invitation.used) { return res.status(400).json({ message: "Cette invitation a déjà été utilisée" }); } if (new Date(invitation.expiresAt) < new Date()) { return res.status(400).json({ message: "Cette invitation a expiré" }); } // Return basic information about the invitation return res.json({ email: invitation.email, role: invitation.role, expiresAt: invitation.expiresAt }); } catch (error) { console.error("Error verifying invitation:", error); return res.status(500).json({ message: "Erreur lors de la vérification de l'invitation" }); } }); } // Middleware to ensure a user is authenticated export function ensureAuthenticated(req: Request, res: Response, next: NextFunction) { if (req.isAuthenticated()) { return next(); } res.status(401).json({ message: "Authentification requise" }); } // Middleware to ensure user has admin role export function ensureAdmin(req: Request, res: Response, next: NextFunction) { if (req.isAuthenticated() && (req.user as any).role === "admin") { return next(); } res.status(403).json({ message: "Accès refusé - Droits administrateur requis" }); }