import { Body, Controller, Get, HttpCode, Inject, Post, Req, UnauthorizedException, UseGuards } from "@nestjs/common"; import type { Request } from "express"; import { RbacGuard, type AuthenticatedRequest } from "../rbac/rbac.guard.js"; import { optionalEmail, optionalString, parseId, requireObject, requireString, requireUsername, requireSession } from "../http/validation.js"; import { AuthService, type AuthResult, type LoginInput, type MeResponse, type RegisterInput } from "./auth.service.js"; import { verifySession } from "./session.js"; @Controller("auth") export class AuthController { constructor(@Inject(AuthService) private readonly auth: AuthService) {} @Post("register") async register(@Body() body: unknown): Promise { return this.auth.register(parseRegister(body)); } @Post("login") @HttpCode(200) async login(@Body() body: unknown): Promise { return this.auth.login(parseLogin(body)); } /** Any valid, epoch-current session may read itself — no permission gate beyond authentication. */ @Get("me") @UseGuards(RbacGuard) async me(@Req() request: Request & AuthenticatedRequest): Promise { return this.auth.me(requireSession(request)); } /** A signed, unexpired session can refresh its epoch and live role. */ @Post("reissue") @HttpCode(200) async reissue(@Req() request: Request): Promise { const authorization = request.headers.authorization; if (authorization === undefined || !authorization.startsWith("Bearer ")) throw new UnauthorizedException(); const session = verifySession(authorization.slice("Bearer ".length)); if (session === null) throw new UnauthorizedException(); return this.auth.reissue(session); } /** Signed session present ⇒ its server-side row is revoked (phase 2). Legacy tokens: client-side only. */ @Post("logout") @HttpCode(200) logout(@Req() request: Request): { ok: true } { const authorization = request.headers.authorization; const session = authorization !== undefined && authorization.startsWith("Bearer ") ? verifySession(authorization.slice("Bearer ".length)) : null; void this.auth.logout(session); return { ok: true }; } } function parseRegister(value: unknown): RegisterInput { const body = requireObject(value, "body"); const username = requireUsername(body.username); const email = optionalEmail(body.email); return { // Collectives opt-in: no name => solo account (personal workspace). workspaceName: optionalString(body.workspaceName, "workspaceName")?.trim().slice(0, 100), username, password: requireString(body.password, "password"), displayName: (optionalString(body.displayName, "displayName") ?? username).trim().slice(0, 100), ...(email === undefined ? {} : { email }), }; } function parseLogin(value: unknown): LoginInput { const body = requireObject(value, "body"); const workspaceId = optionalString(body.workspaceId, "workspaceId"); return { username: requireUsername(body.username), password: requireString(body.password, "password"), ...(workspaceId === undefined ? {} : { workspaceId: parseId(workspaceId, "workspaceId") }), }; }