import bcrypt from "bcryptjs"; import User from "@/models/user"; import { getDb } from "@/lib/db"; import { createSession } from "@/lib/auth/session"; import { withErrorHandling, json, badRequest, unauthorized, } from "@/lib/api/errors"; /** * POST /api/auth/login * * Body (JSON): * { * "username": "example.user", * "password": "plain-text-password" * } * * Error contract (standardized): * { * "error": { "message": "...", "code": "...", "details"?: {...} } * } */ export const POST = withErrorHandling( async function POST(request) { // --- 1) Parse body ------------------------------------------------------ // request.json() can throw if the JSON is invalid (e.g. broken body). let body; try { body = await request.json(); } catch { throw badRequest("VALIDATION_INVALID_JSON", "Invalid request body"); } // We only accept objects as JSON body for this endpoint. if (!body || typeof body !== "object") { throw badRequest("VALIDATION_INVALID_BODY", "Invalid request body"); } // --- 2) Validate credentials input ------------------------------------- const { username, password } = body; // Keep validation strict and predictable: // - Must be strings // - Must not be empty/whitespace if ( typeof username !== "string" || typeof password !== "string" || !username.trim() || !password.trim() ) { throw badRequest( "VALIDATION_MISSING_FIELD", "Missing username or password", { fields: ["username", "password"], } ); } // Normalize usernames to avoid case/whitespace issues. const normalizedUsername = username.trim().toLowerCase(); // --- 3) Load user from DB ---------------------------------------------- // Ensure DB (Mongoose) connection is established before using models. await getDb(); const user = await User.findOne({ username: normalizedUsername }).exec(); // Do not leak whether a username exists; always return "Invalid credentials". if (!user) { throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials"); } // Defensive: never let missing/invalid passwordHash crash the endpoint. // Treat it like invalid credentials. if (typeof user.passwordHash !== "string" || !user.passwordHash) { throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials"); } // --- 4) Verify password ------------------------------------------------- const passwordMatches = await bcrypt.compare(password, user.passwordHash); if (!passwordMatches) { throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials"); } // --- 5) Create session cookie ------------------------------------------ await createSession({ userId: user._id.toString(), role: user.role, branchId: user.branchId ?? null, }); // Happy path response stays unchanged: return json({ ok: true }, 200); }, { logPrefix: "[api/auth/login]" } );