Files
task-manager/server/auth.ts
T
paul ccfb674318
continuous-integration/drone/push Build is passing
feat: add social features, leaderboard, auth enhancements, and admin fixes
- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable).
- Add Leaderboard Page and API.
- Enhance Auth: Support Email/Username login, explicit duplicate registration errors.
- Fix: Admin login password hash regression.
- Refactor: Move to wouter for routing, add Admin Dashboard and User Management.
- Add Setup Wizard.
- Update UI with Sidebar and Gamification elements.
2025-12-10 14:04:26 +01:00

153 lines
4.9 KiB
TypeScript

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);
});
}