import { Body, Controller, Get, Header, Headers, HttpCode, Inject, Post, Query, Redirect, Req, UnauthorizedException, UseGuards } from "@nestjs/common"; import type { Request } from "express"; import { RbacGuard, type AuthenticatedRequest } from "../rbac/rbac.guard.js"; import { requireSession } from "../http/validation.js"; import { OidcService, type TokenResponse, type UserInfo } from "./oidc.service.js"; import type { PublicJwk } from "./oidc.keys.js"; /** * The Hub's OpenID Connect provider (issuer https://api.occupymedia.cc/oidc). * * GET /oidc/.well-known/openid-configuration discovery * GET /oidc/jwks public ES256 keys * GET /oidc/authorize validate, then 302 → Hub SPA consent page * POST /oidc/authorize/consent (Bearer session) issue the single-use code * POST /oidc/token code + PKCE verifier → tokens * GET /oidc/userinfo Bearer access_token → claims * * Only /authorize/consent is session-guarded: every active member may sign in * to files, so it needs no permission. See OidcService for the cookie-less * authorize flow (a deliberate deviation from the design doc). */ @Controller("oidc") export class OidcController { constructor(@Inject(OidcService) private readonly oidc: OidcService) {} @Get(".well-known/openid-configuration") @Header("Cache-Control", "public, max-age=300") discovery(): Record { return this.oidc.discovery(); } @Get("jwks") @Header("Cache-Control", "public, max-age=300") jwks(): Promise<{ keys: PublicJwk[] }> { return this.oidc.jwks(); } @Get("authorize") @Redirect() @Header("Cache-Control", "no-store") authorize(@Query() query: unknown): { url: string; statusCode: number } { return { url: this.oidc.authorizeRedirect(query), statusCode: 302 }; } @Post("authorize/consent") @HttpCode(200) @UseGuards(RbacGuard) @Header("Cache-Control", "no-store") consent(@Req() request: Request & AuthenticatedRequest, @Body() body: unknown): Promise<{ redirectTo: string }> { const session = requireSession(request); return this.oidc.consent(session.workspaceId, session.userId, body, session.sid ?? null); } @Post("token") @HttpCode(200) @Header("Cache-Control", "no-store") @Header("Pragma", "no-cache") token(@Body() body: unknown): Promise { return this.oidc.token(body); } @Get("userinfo") @Header("Cache-Control", "no-store") async userinfo(@Req() request: Request, @Headers("authorization") authorization: string | undefined): Promise { try { return await this.oidc.userinfo(authorization); } catch (err) { if (err instanceof UnauthorizedException) request.res?.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"'); throw err; } } }