gammanexus/server/authRoutes.ts

204 lines
6.9 KiB
TypeScript

// eslint-disable-next-line @typescript-eslint/no-var-requires
import * as bcrypt from "bcryptjs";
import type { Express, Request, Response } from "express";
import { createUser, getUserByEmail, getUserById, updateUser, updateUserPassword } from "./db";
import { sanitizeInput } from "./middleware";
export function requireAuth(req: Request, res: Response, next: () => void) {
const session = req.session as Record<string, unknown>;
if (!session?.userId) {
return res.status(401).json({ error: "Not authenticated" });
}
next();
}
export function registerAuthRoutes(app: Express) {
// POST /api/auth/register
app.post("/api/auth/register", async (req: Request, res: Response) => {
const { name, email, password } = req.body as Record<string, string>;
// Validate required fields
if (!name || !email || !password) {
return res.status(400).json({ error: "Name, email, and password are required" });
}
// Validate password complexity
if (password.length < 8) {
return res.status(400).json({
error: "Password must be at least 8 characters long",
});
}
// Sanitize inputs to prevent XSS
const sanitizedName = sanitizeInput(name);
const sanitizedEmail = sanitizeInput(email).toLowerCase();
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(sanitizedEmail)) {
return res.status(400).json({ error: "Invalid email format" });
}
try {
const existing = await getUserByEmail(sanitizedEmail);
if (existing) {
return res.status(409).json({ error: "Email already in use" });
}
const hash = await bcrypt.hash(password, 12);
const user = await createUser({
name: sanitizedName,
email: sanitizedEmail,
passwordHash: hash,
});
const session = req.session as Record<string, unknown>;
session.userId = user.id;
res.status(201).json({ id: user.id, name: user.name, email: user.email });
} catch (error) {
console.error("Registration error:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// POST /api/auth/login
app.post("/api/auth/login", async (req: Request, res: Response) => {
const { email, password } = req.body as Record<string, string>;
if (!email || !password) {
return res.status(400).json({ error: "Email and password are required" });
}
try {
const user = await getUserByEmail(email.toLowerCase());
if (!user) {
// Use same error message to prevent email enumeration
await bcrypt.hash("dummy", 12); // Constant time operation
return res.status(401).json({ error: "Invalid email or password" });
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: "Invalid email or password" });
}
const session = req.session as Record<string, unknown>;
session.userId = user.id;
res.json({ id: user.id, name: user.name, email: user.email });
} catch (error) {
console.error("Login error:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// POST /api/auth/logout
app.post("/api/auth/logout", (req: Request, res: Response) => {
req.session.destroy(() => {
res.json({ ok: true });
});
});
// GET /api/auth/me
app.get("/api/auth/me", requireAuth, async (req: Request, res: Response) => {
try {
const session = req.session as Record<string, unknown>;
const user = await getUserById(session.userId as number);
if (!user) {
req.session.destroy();
return res.status(401).json({ error: "Not authenticated" });
}
res.json({ id: user.id, name: user.name, email: user.email });
} catch (error) {
console.error("Get user error:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// PUT /api/auth/profile
app.put("/api/auth/profile", requireAuth, async (req: Request, res: Response) => {
const { name, email } = req.body as Record<string, string>;
const session = req.session as Record<string, unknown>;
const updates: Record<string, string> = {};
if (name) updates.name = sanitizeInput(name);
if (email) {
const sanitizedEmail = sanitizeInput(email).toLowerCase();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(sanitizedEmail)) {
return res.status(400).json({ error: "Invalid email format" });
}
try {
const existing = await getUserByEmail(sanitizedEmail);
if (existing && existing.id !== session.userId) {
return res.status(409).json({ error: "Email already in use" });
}
updates.email = sanitizedEmail;
} catch (error) {
console.error("Email check error:", error);
return res.status(500).json({ error: "Internal server error" });
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: "No valid fields to update" });
}
try {
const user = await updateUser(session.userId as number, updates);
res.json({ id: user.id, name: user.name, email: user.email });
} catch (error) {
console.error("Update profile error:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// PUT /api/auth/password
app.put("/api/auth/password", requireAuth, async (req: Request, res: Response) => {
const { currentPassword, newPassword } = req.body as Record<string, string>;
const session = req.session as Record<string, unknown>;
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: "Current and new password are required" });
}
// Validate new password complexity
if (newPassword.length < 8) {
return res.status(400).json({
error: "New password must be at least 8 characters long",
});
}
try {
const user = await getUserById(session.userId as number);
if (!user) {
return res.status(401).json({ error: "Not authenticated" });
}
const valid = await bcrypt.compare(currentPassword, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: "Current password is incorrect" });
}
const hash = await bcrypt.hash(newPassword, 12);
await updateUserPassword(session.userId as number, hash);
res.json({ ok: true });
} catch (error) {
console.error("Password change error:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// POST /api/auth/request-reset (stub)
app.post("/api/auth/request-reset", (req: Request, res: Response) => {
const { email } = req.body as Record<string, string>;
if (!email) {
return res.status(400).json({ error: "Email is required" });
}
// Always return success to prevent email enumeration
res.json({ ok: true, message: "If an account exists, a reset link would be sent" });
});
}