import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; import { Express } from "express"; import session from "express-session"; import { scrypt, randomBytes, timingSafeEqual } from "crypto"; import { promisify } from "util"; import jwt from "jsonwebtoken"; import { storage } from "./storage"; import { User as SelectUser, loginSchema } from "@shared/schema"; declare global { namespace Express { interface User extends SelectUser {} } } const scryptAsync = promisify(scrypt); const JWT_SECRET = process.env.JWT_SECRET || "super-secret-key-change-in-production"; const SESSION_SECRET = process.env.SESSION_SECRET || "session-secret-change-in-production"; 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 comparePasswords(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: SESSION_SECRET, resave: false, saveUninitialized: false, store: storage.sessionStore, cookie: { maxAge: 24 * 60 * 60 * 1000, // 24 hours secure: process.env.NODE_ENV === "production", }, }; app.set("trust proxy", 1); app.use(session(sessionSettings)); app.use(passport.initialize()); app.use(passport.session()); // Local strategy for username/password authentication passport.use( new LocalStrategy(async (username, password, done) => { try { const user = await storage.getUserByUsername(username); if (!user || !(await comparePasswords(password, user.password))) { return done(null, false, { message: "Invalid username or password" }); } return done(null, user); } catch (error) { return done(error); } }), ); // JWT strategy for API token authentication passport.use( new JwtStrategy( { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: JWT_SECRET, }, async (payload, done) => { try { const user = await storage.getUser(payload.sub); if (!user) { return done(null, false, { message: "User not found" }); } return done(null, user); } catch (error) { return done(error); } } ) ); passport.serializeUser((user, done) => done(null, user.id)); passport.deserializeUser(async (id: number, done) => { try { const user = await storage.getUser(id); done(null, user); } catch (error) { done(error); } }); // Registration endpoint app.post("/api/register", async (req, res, next) => { try { const validationResult = loginSchema.safeParse(req.body); if (!validationResult.success) { return res.status(400).json({ message: "Invalid input", errors: validationResult.error.errors }); } const { username, password } = req.body; const existingUser = await storage.getUserByUsername(username); if (existingUser) { return res.status(400).json({ message: "Username already exists" }); } const hashedPassword = await hashPassword(password); // Get the default role if role ID isn't specified let roleId = req.body.roleId; if (!roleId) { const defaultRole = await storage.getDefaultRole(); roleId = defaultRole?.id; } const user = await storage.createUser({ username, password: hashedPassword, email: req.body.email, fullName: req.body.fullName, roleId: roleId, }); // Remove password from response const userResponse = { ...user, password: undefined }; req.login(user, (err) => { if (err) return next(err); res.status(201).json(userResponse); }); } catch (error) { next(error); } }); // Login endpoint app.post("/api/login", (req, res, next) => { passport.authenticate("local", (err: any, user: any, info: any) => { if (err) return next(err); if (!user) { return res.status(401).json({ message: info?.message || "Authentication failed" }); } req.login(user, (loginErr) => { if (loginErr) return next(loginErr); // Remove password from response const userResponse = { ...user, password: undefined }; res.json(userResponse); }); })(req, res, next); }); // Logout endpoint app.post("/api/logout", (req, res, next) => { req.logout((err) => { if (err) return next(err); req.session.destroy((sessionErr) => { if (sessionErr) return next(sessionErr); res.clearCookie("connect.sid"); res.status(200).json({ message: "Logged out successfully" }); }); }); }); // Get current user endpoint app.get("/api/user", (req, res) => { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } // Remove password from response const userResponse = { ...req.user, password: undefined }; res.json(userResponse); }); // Generate API token endpoint app.post("/api/tokens", (req, res, next) => { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } try { const { name, expiresAt, roleId, customPermissions } = req.body; if (!name) { return res.status(400).json({ message: "Token name is required" }); } // Create JWT token with user ID and optional permissions const token = jwt.sign( { sub: req.user.id, customPermissions }, JWT_SECRET, { expiresIn: expiresAt ? Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000) : '365d' } ); // Store the token in the database const apiToken = storage.createApiToken({ name, token, userId: req.user.id, roleId: roleId || null, customPermissions: customPermissions || null, expiresAt: expiresAt ? new Date(expiresAt) : null, }); res.status(201).json(apiToken); } catch (error) { next(error); } }); // Middleware to check API token authentication const authenticateApiToken = passport.authenticate("jwt", { session: false }); return { authenticateApiToken }; }