Premiere brique de l'app d'admin independante : une surface /admin/*
ajoutee a apps/api, avec une authentification totalement distincte de
celle des utilisateurs.
- Table AdminUser isolee (aucune relation vers UserProfile), migration
20260828120000_admin_user.
- lib/admin-jwt.ts : sign/verify d'un JWT admin, secret ADMIN_JWT_SECRET
propre (jamais interchangeable avec JWT_SECRET).
- middlewares/require-admin.ts : cookie admin_session dedie, re-check
tokenVersion, echoue ferme si ADMIN_JWT_SECRET absent (posture
requireInternalWorker). res.locals.adminUser type via AdminLocals.
- modules/admin/ : admin-auth.{routes,service}.ts (POST /login, POST
/logout, GET /me), admin.routes.ts agregateur monte /admin. Pas de
signup expose.
- lib/safe-admin.ts : mapping AdminUser -> AdminUserView (drop passwordHash
+ tokenVersion, dates ISO).
- scripts/create-admin.ts : creation du 1er admin hors-bande (flags ou
ADMIN_INITIAL_*).
- CORS : setupCore accepte string[] ; app.ts autorise CORS_ORIGIN +
ADMIN_CORS_ORIGIN.
- Shared : schemas/admin.ts (adminLoginSchema), types/admin.ts
(AdminUserView).
- Env : ADMIN_JWT_SECRET (optionnel), ADMIN_COOKIE_NAME, ADMIN_CORS_ORIGIN,
ADMIN_INITIAL_* ; .env.example, .env.test.example, docker-compose.yml,
ci.yml mis a jour.
- reset-db.ts truncate admin_users.
- Tests Mocha admin-auth.test.ts : 400 sans body, 401 email inconnu /
mauvais mdp, login OK (cookie pose, lastLoginAt, pas de hash/tokenVersion
dans la reponse), /me derriere requireAdmin, logout, et un cookie
`session` d'utilisateur normal ne donne pas acces a /admin/*.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
51 lines
2.2 KiB
TypeScript
51 lines
2.2 KiB
TypeScript
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import { adminLoginSchema } from "@batch-cooking/shared";
|
|
import { type CookieOptions, type Response, Router } from "express";
|
|
import { env } from "../../config/env.js";
|
|
import { type AdminLocals, requireAdmin } from "../../middlewares/require-admin.js";
|
|
import { adminLogin } from "./admin-auth.service.js";
|
|
|
|
/** Router mounted at `/admin/auth` (via `admin.routes.ts`) — admin login, logout, current-admin. No signup: admins are created out-of-band (`src/scripts/create-admin.ts`). */
|
|
export const adminAuthRouter = Router();
|
|
|
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
|
/**
|
|
* Cookie options for the admin session — same shape as `auth.routes.ts`'s
|
|
* end-user cookie (httpOnly, `Secure` in production unless `COOKIE_SECURE`
|
|
* overrides, `SameSite=Lax`), just written under {@link env.ADMIN_COOKIE_NAME}
|
|
* so the two sessions never collide in one browser.
|
|
*/
|
|
const adminCookieOptions: CookieOptions = {
|
|
httpOnly: true,
|
|
secure: env.COOKIE_SECURE ?? env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
maxAge: SEVEN_DAYS_MS,
|
|
};
|
|
|
|
// `res.clearCookie` sets its own expiry — passing `maxAge` alongside is
|
|
// deprecated as of Express 4.20, so the logout route reuses the options
|
|
// minus that one field (same trick as `auth.routes.ts`).
|
|
const { maxAge: _maxAge, ...clearAdminCookieOptions } = adminCookieOptions;
|
|
|
|
/** Verifies admin credentials and starts an admin session. */
|
|
adminAuthRouter.post(
|
|
"/login",
|
|
wrapAsyncHandler(async (req, res) => {
|
|
const input = adminLoginSchema.parse(req.body);
|
|
const { admin, token } = await adminLogin(input);
|
|
res.cookie(env.ADMIN_COOKIE_NAME, token, adminCookieOptions);
|
|
res.status(200).json(admin);
|
|
}),
|
|
);
|
|
|
|
/** Ends the admin session by clearing the cookie. Stateless JWT — nothing to revoke server-side beyond bumping `tokenVersion` (no UI for that yet). */
|
|
adminAuthRouter.post("/logout", (_req, res) => {
|
|
res.clearCookie(env.ADMIN_COOKIE_NAME, clearAdminCookieOptions);
|
|
res.status(204).end();
|
|
});
|
|
|
|
/** Returns the currently authenticated admin. Behind `requireAdmin` — 401s if there's no valid admin session. */
|
|
adminAuthRouter.get("/me", requireAdmin, (_req, res: Response<unknown, AdminLocals>) => {
|
|
res.status(200).json(res.locals.adminUser);
|
|
});
|