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}`; } export 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, cookie: { secure: process.env.NODE_ENV === "production" && process.env.SECURE_COOKIES === "true", sameSite: "lax", // maxAge not set by default (session cookie) }, }; 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) => { if (req.body.rememberMe) { req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days } 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", async (req, res) => { if (!req.isAuthenticated()) return res.sendStatus(401); // Check for Daily Streak const user = req.user as User; const now = new Date(); const lastActive = user.lastActive ? new Date(user.lastActive) : new Date(0); // Normalize to dates (ignore time) const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const lastDate = new Date(lastActive.getFullYear(), lastActive.getMonth(), lastActive.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); // If last active was yesterday, increment streak // If last active was today, do nothing // If last active was before yesterday, reset streak (unless we decide to be lenient) // We need GamificationService here const { GamificationService } = await import("./gamification"); const gamificationService = new GamificationService(storage); if (lastDate.getTime() < today.getTime()) { if (lastDate.getTime() === yesterday.getTime()) { // Perfect streak await gamificationService.awardXP(user.id, 'daily_streak'); // Check bonuses const updatedUser = await storage.getUser(user.id); if (updatedUser) { await gamificationService.checkStreakBonuses(user.id, updatedUser.currentStreak); } } else if (lastDate.getTime() < yesterday.getTime()) { // Streak broken // Reset streak to 1 (today is day 1) await storage.updateUser(user.id, { currentStreak: 1 }); // Still award daily XP for today? Yes. await gamificationService.awardXP(user.id, 'daily_streak'); } else { // Should not happen if < today } // Update lastActive await storage.updateUser(user.id, { lastActive: now }); } // Re-fetch user to get latest XP and Streak const freshUser = await storage.getUser(user.id); res.json(freshUser); }); }