import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; import { Express } from "express"; import session from "express-session"; import { scrypt, randomBytes, timingSafeEqual } from "crypto"; import { promisify } from "util"; import { storage } from "./storage"; import { User } from "../shared/schema"; const scryptAsync = promisify(scrypt); export async function hashPassword(password: string) { const salt = randomBytes(16).toString("hex"); const buf = (await scryptAsync(password, salt, 64)) as Buffer; return `${buf.toString("hex")}.${salt}`; } async function comparePassword(supplied: string, stored: string) { const [hashed, salt] = stored.split("."); const hashedBuf = Buffer.from(hashed, "hex"); const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer; return timingSafeEqual(hashedBuf, suppliedBuf); } export function setupAuth(app: Express) { const sessionSettings: session.SessionOptions = { secret: process.env.SESSION_SECRET || "s3cr3t_m3ss4g3", resave: false, saveUninitialized: false, store: storage.sessionStore, }; if (app.get("env") === "production") { app.set("trust proxy", 1); } app.use(session(sessionSettings)); app.use(passport.initialize()); app.use(passport.session()); passport.use( new LocalStrategy(async (username, password, done) => { try { let user; // Check if input looks like an email if (username.includes('@')) { user = await storage.getUserByEmail(username); } // Fallback to username lookup if not found by email, or if input wasn't an email if (!user) { user = await storage.getUserByUsername(username); } if (!user) { return done(null, false, { message: "Incorrect username or password." }); } if (!user.isActive) { return done(null, false, { message: "Account is deactivated." }); } const isValid = await comparePassword(password, user.password); if (!isValid) { return done(null, false, { message: "Incorrect username or password." }); } return done(null, user); } catch (err) { return done(err); } }) ); // ... serialize/deserialize ... passport.serializeUser((user, done) => { done(null, (user as User).id); }); passport.deserializeUser(async (id: string, done) => { try { const user = await storage.getUser(id); if (!user) { return done(null, false); } done(null, user); } catch (err) { done(err); } }); app.post("/api/register", async (req, res, next) => { try { // Check if registration is allowed const regEnabled = await storage.getSystemSettings("registration_enabled"); if (regEnabled === "false") { // But wait, if it's the FIRST user (Setup), this route isn't used. Setup uses /api/setup. // So we can enforce this check here for public registration. return res.status(403).send("Registration is currently disabled."); } const existingUser = await storage.getUserByUsername(req.body.username); if (existingUser) { return res.status(400).send("Username already exists"); } const existingEmail = await storage.getUserByEmail(req.body.email); if (existingEmail) { return res.status(400).send("Email already exists"); } const hashedPassword = await hashPassword(req.body.password); const user = await storage.createUser({ ...req.body, password: hashedPassword, role: 'user', // Default role for public registration isActive: true }); req.login(user, (err) => { if (err) return next(err); res.status(201).json(user); }); } catch (err) { next(err); } }); app.post("/api/login", passport.authenticate("local"), (req, res) => { res.status(200).json(req.user); }); app.post("/api/logout", (req, res, next) => { req.logout((err) => { if (err) return next(err); res.redirect("/"); }); }); app.get("/api/logout", (req, res, next) => { req.logout((err) => { if (err) return next(err); res.redirect("/"); }); }); app.get("/api/user", (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); res.json(req.user); }); }