| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- 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]" }
- );
|