route.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import bcrypt from "bcryptjs";
  2. import User from "@/models/user";
  3. import { getDb } from "@/lib/db";
  4. import { createSession } from "@/lib/auth/session";
  5. import {
  6. withErrorHandling,
  7. json,
  8. badRequest,
  9. unauthorized,
  10. } from "@/lib/api/errors";
  11. /**
  12. * Next.js Route Handler caching configuration (RHL-006):
  13. *
  14. * We force this route to execute dynamically on every request.
  15. *
  16. * Reasons:
  17. * - NAS contents can change at any time (new scans).
  18. * - Auth/RBAC-protected responses must not be cached/shared across users.
  19. * - We rely on a small storage-layer TTL micro-cache instead of Next route caching.
  20. */
  21. export const dynamic = "force-dynamic";
  22. /**
  23. * POST /api/auth/login
  24. *
  25. * Body (JSON):
  26. * {
  27. * "username": "example.user",
  28. * "password": "plain-text-password"
  29. * }
  30. *
  31. * Error contract (standardized):
  32. * {
  33. * "error": { "message": "...", "code": "...", "details"?: {...} }
  34. * }
  35. */
  36. export const POST = withErrorHandling(
  37. async function POST(request) {
  38. // --- 1) Parse body ------------------------------------------------------
  39. // request.json() can throw if the JSON is invalid (e.g. broken body).
  40. let body;
  41. try {
  42. body = await request.json();
  43. } catch {
  44. throw badRequest("VALIDATION_INVALID_JSON", "Invalid request body");
  45. }
  46. // We only accept objects as JSON body for this endpoint.
  47. if (!body || typeof body !== "object") {
  48. throw badRequest("VALIDATION_INVALID_BODY", "Invalid request body");
  49. }
  50. // --- 2) Validate credentials input -------------------------------------
  51. const { username, password } = body;
  52. // Keep validation strict and predictable:
  53. // - Must be strings
  54. // - Must not be empty/whitespace
  55. if (
  56. typeof username !== "string" ||
  57. typeof password !== "string" ||
  58. !username.trim() ||
  59. !password.trim()
  60. ) {
  61. throw badRequest(
  62. "VALIDATION_MISSING_FIELD",
  63. "Missing username or password",
  64. {
  65. fields: ["username", "password"],
  66. }
  67. );
  68. }
  69. // Normalize usernames to avoid case/whitespace issues.
  70. const normalizedUsername = username.trim().toLowerCase();
  71. // --- 3) Load user from DB ----------------------------------------------
  72. // Ensure DB (Mongoose) connection is established before using models.
  73. await getDb();
  74. const user = await User.findOne({ username: normalizedUsername }).exec();
  75. // Do not leak whether a username exists; always return "Invalid credentials".
  76. if (!user) {
  77. throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials");
  78. }
  79. // Defensive: never let missing/invalid passwordHash crash the endpoint.
  80. // Treat it like invalid credentials.
  81. if (typeof user.passwordHash !== "string" || !user.passwordHash) {
  82. throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials");
  83. }
  84. // --- 4) Verify password -------------------------------------------------
  85. const passwordMatches = await bcrypt.compare(password, user.passwordHash);
  86. if (!passwordMatches) {
  87. throw unauthorized("AUTH_INVALID_CREDENTIALS", "Invalid credentials");
  88. }
  89. // --- 5) Create session cookie ------------------------------------------
  90. await createSession({
  91. userId: user._id.toString(),
  92. role: user.role,
  93. branchId: user.branchId ?? null,
  94. });
  95. // Happy path response stays unchanged:
  96. return json({ ok: true }, 200);
  97. },
  98. { logPrefix: "[api/auth/login]" }
  99. );