import express from "express"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import crypto from "crypto"; import { JWT_SECRET, authMiddleware } from "../middleware/auth.js"; import { createUser, getUserByUsername, getUserByEmail, getUserById, createEmailToken, getEmailToken, deleteEmailToken, markEmailVerified, updatePassword, } from "../db/usersDb.js"; import { sendConfirmationEmail, sendPasswordResetEmail } from "../email.js"; const router = express.Router(); function issueToken(user) { return jwt.sign( { userId: user.id, username: user.username, team: user.team, role: user.role }, JWT_SECRET, { expiresIn: "7d" } ); } function generateToken() { return crypto.randomBytes(32).toString("hex"); } // POST /api/auth/register router.post("/register", async (req, res) => { const { username, email, password, team } = req.body ?? {}; if (!username || !email || !password || !team) { return res.status(400).json({ error: "missing_fields" }); } if (team !== "blue" && team !== "red") { return res.status(400).json({ error: "invalid_team" }); } if (typeof username !== "string" || username.length < 2 || username.length > 32) { return res.status(400).json({ error: "invalid_username" }); } if (typeof password !== "string" || password.length < 6) { return res.status(400).json({ error: "password_too_short" }); } try { const passwordHash = await bcrypt.hash(password, 12); const user = await createUser(username.trim(), email.trim().toLowerCase(), passwordHash, team); // Create and send confirmation token const token = generateToken(); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); await createEmailToken(user.id, token, "confirm", expiresAt); sendConfirmationEmail(user.email, token).catch((e) => console.error("[email] Failed to send confirmation email:", e.message) ); return res.status(201).json({ pending_verification: true }); } catch (e) { if (e.code === "23505") { if (e.constraint?.includes("email")) return res.status(409).json({ error: "email_taken" }); return res.status(409).json({ error: "username_taken" }); } console.error(e); return res.status(500).json({ error: "database_error" }); } }); // POST /api/auth/login router.post("/login", async (req, res) => { const { username, password } = req.body ?? {}; if (!username || !password) return res.status(400).json({ error: "missing_fields" }); try { const user = await getUserByUsername(username.trim()); if (!user) return res.status(401).json({ error: "invalid_credentials" }); const valid = await bcrypt.compare(password, user.password_hash); if (!valid) return res.status(401).json({ error: "invalid_credentials" }); if (!user.email_verified) return res.status(403).json({ error: "email_not_verified" }); const token = issueToken(user); return res.json({ token, user: { id: user.id, username: user.username, team: user.team, role: user.role }, }); } catch (e) { console.error(e); return res.status(500).json({ error: "database_error" }); } }); // GET /api/auth/me router.get("/me", authMiddleware, async (req, res) => { try { const user = await getUserById(req.user.userId); if (!user) return res.status(404).json({ error: "user_not_found" }); const token = issueToken(user); return res.json({ token, user }); } catch (e) { console.error(e); return res.status(500).json({ error: "database_error" }); } }); // POST /api/auth/resend-confirmation router.post("/resend-confirmation", async (req, res) => { const { email } = req.body ?? {}; if (!email) return res.status(400).json({ error: "missing_fields" }); try { const user = await getUserByEmail(email.trim().toLowerCase()); // Always respond OK to avoid user enumeration if (!user || user.email_verified) return res.json({ ok: true }); const token = generateToken(); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); await createEmailToken(user.id, token, "confirm", expiresAt); sendConfirmationEmail(user.email, token).catch((e) => console.error("[email] Failed to resend confirmation email:", e.message) ); return res.json({ ok: true }); } catch (e) { console.error(e); return res.status(500).json({ error: "database_error" }); } }); // GET /api/auth/confirm-email?token=TOKEN router.get("/confirm-email", async (req, res) => { const { token } = req.query; if (!token) return res.redirect("/?confirm_error=invalid"); try { const row = await getEmailToken(token, "confirm"); if (!row) return res.redirect("/?confirm_error=invalid"); if (new Date(row.expires_at) < new Date()) { await deleteEmailToken(token); return res.redirect("/?confirm_error=expired"); } await markEmailVerified(row.user_id); await deleteEmailToken(token); return res.redirect("/?email_confirmed=1"); } catch (e) { console.error(e); return res.redirect("/?confirm_error=server_error"); } }); // POST /api/auth/forgot-password router.post("/forgot-password", async (req, res) => { const { email } = req.body ?? {}; if (!email) return res.status(400).json({ error: "missing_fields" }); try { const user = await getUserByEmail(email.trim().toLowerCase()); // Always respond OK to avoid user enumeration if (!user) return res.json({ ok: true }); const token = generateToken(); const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour await createEmailToken(user.id, token, "reset", expiresAt); sendPasswordResetEmail(user.email, token).catch((e) => console.error("[email] Failed to send reset email:", e.message) ); return res.json({ ok: true }); } catch (e) { console.error(e); return res.status(500).json({ error: "database_error" }); } }); // POST /api/auth/reset-password router.post("/reset-password", async (req, res) => { const { token, password } = req.body ?? {}; if (!token || !password) return res.status(400).json({ error: "missing_fields" }); if (typeof password !== "string" || password.length < 6) { return res.status(400).json({ error: "password_too_short" }); } try { const row = await getEmailToken(token, "reset"); if (!row) return res.status(400).json({ error: "invalid_token" }); if (new Date(row.expires_at) < new Date()) { await deleteEmailToken(token); return res.status(400).json({ error: "token_expired" }); } const passwordHash = await bcrypt.hash(password, 12); await updatePassword(row.user_id, passwordHash); await deleteEmailToken(token); return res.json({ ok: true }); } catch (e) { console.error(e); return res.status(500).json({ error: "database_error" }); } }); export default router;