import { wrapAsyncHandler } from "@batch-cooking/express-tools"; import { deleteAccountSchema, loginSchema, signupSchema } from "@batch-cooking/shared"; import { Router } from "express"; import type { CookieOptions, Response } from "express"; import { env } from "../../config/env.js"; import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js"; import { deleteAccount, login, signup } from "./auth.service.js"; /** Router mounted at `/auth` in app.ts — signup, login, logout, current-profile. */ export const authRouter = Router(); // Deliberately independent from JWT_EXPIRES_IN: the JWT's own expiry is // what's actually enforced by requireAuth (a request with an expired JWT // is rejected regardless of the cookie still being present) — this only // bounds how long the browser keeps *sending* the cookie at all. const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; /** Cookie options shared by every route that sets the session cookie. */ const cookieOptions: CookieOptions = { httpOnly: true, // Defaults to requiring HTTPS in production, but overridable via // COOKIE_SECURE — see its doc comment in config/env.ts for why this // can't just be `NODE_ENV === "production"`. secure: env.COOKIE_SECURE ?? env.NODE_ENV === "production", sameSite: "lax", maxAge: SEVEN_DAYS_MS, }; // `res.clearCookie` sets its own expiry to clear the cookie — passing // `maxAge` alongside is deprecated (and pointless) as of Express 4.20, so // every clearing route reuses `cookieOptions` minus that one field rather // than duplicating the rest by hand. const { maxAge: _maxAge, ...clearCookieOptions } = cookieOptions; /** * Creates a profile (+ its household) and logs the new user in * immediately. `wrapAsyncHandler` forwards a thrown/rejected error to * Express's error middleware automatically — no manual try/catch needed. */ authRouter.post( "/signup", wrapAsyncHandler(async (req, res) => { const input = signupSchema.parse(req.body); const { profile, token } = await signup(input); res.cookie(env.AUTH_COOKIE_NAME, token, cookieOptions); res.status(201).json(profile); }), ); /** Verifies credentials and starts a new session. */ authRouter.post( "/login", wrapAsyncHandler(async (req, res) => { const input = loginSchema.parse(req.body); const { profile, token } = await login(input); res.cookie(env.AUTH_COOKIE_NAME, token, cookieOptions); res.status(200).json(profile); }), ); /** Ends the current session by clearing the cookie. Stateless JWT, so there's nothing to revoke server-side (yet — see tokenVersion). */ authRouter.post("/logout", (_req, res) => { res.clearCookie(env.AUTH_COOKIE_NAME, clearCookieOptions); res.status(204).end(); }); /** Returns the currently authenticated profile. Behind requireAuth — 401s if there's no valid session. */ authRouter.get("/me", requireAuth, (_req, res: Response) => { res.status(200).json(res.locals.userProfile); }); /** Permanently deletes the current profile (re-verifying its password first) and clears the session cookie. */ authRouter.delete( "/me", requireAuth, wrapAsyncHandler(async (req, res) => { const input = deleteAccountSchema.parse(req.body); await deleteAccount(res.locals.userProfile.id, input.password); res.clearCookie(env.AUTH_COOKIE_NAME, clearCookieOptions); res.status(204).end(); }), );