import { sql } from "@/lib/db"; import { SESSION_TTL_SECONDS, SHORT_SESSION_TTL_SECONDS } from "@/lib/jwt"; import { hashPassword, verifyPassword } from "@/lib/password"; import { issueSession, toProfile } from "@/lib/users"; // Compared against when no account matches, so a wrong email costs the same // scrypt work as a wrong password instead of answering noticeably faster. const DUMMY_HASH = hashPassword("waseel-no-such-account"); export async function POST(req: Request) { const { email, password, remember } = await req.json(); if (!email?.trim() || !password) { // Names only, never values: this is the one 400 that looks like a server // fault from the app, so say which field arrived empty. console.warn( "[LOGIN]: rejected empty", [!email?.trim() && "email", !password && "password"] .filter(Boolean) .join(" + "), ); return Response.json( { error: "Email and password are required." }, { status: 400 }, ); } try { const rows = await sql<{ id: string; name: string; email: string; role: string | null; password_hash: string | null; email_verified: boolean; }>` SELECT id, name, email, role, password_hash, email_verified FROM users WHERE email = ${email.trim().toLowerCase()} `; const user = rows[0]; const valid = verifyPassword(password, user?.password_hash ?? DUMMY_HASH); if (!user || !user.password_hash || !valid) { return Response.json( { error: "Invalid email or password." }, { status: 401 }, ); } if (!user.email_verified) { return Response.json( { error: "Please verify your email first." }, { status: 403 }, ); } const session = issueSession( user, remember === false ? SHORT_SESSION_TTL_SECONDS : SESSION_TTL_SECONDS, ); return Response.json({ data: { token: session.token, user: toProfile(user) }, }); } catch (error) { console.error("[LOGIN]: ", error); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } }