import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { IdentityService, InvalidAccessTokenError, } from '@task-stream/api/identity/application'; import { extractBearerToken, getAuthRequest } from './auth-request'; import { IS_OPTIONAL_AUTH, IS_PUBLIC, REQUIRED_APP_ROLES, } from './auth.decorators'; /** * Global guard: every HTTP/GraphQL operation needs a valid Pireon token unless * @Public(); with @OptionalAuth() the token may be missing (but not invalid). */ @Injectable() export class AuthGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly identity: IdentityService, ) {} async canActivate(context: ExecutionContext): Promise { const targets = [context.getHandler(), context.getClass()]; if (this.reflector.getAllAndOverride(IS_PUBLIC, targets)) return true; const req = getAuthRequest(context); // WebSocket subscriptions are authenticated once in connection_init. if (!req.user) { const token = extractBearerToken(req.headers['authorization']); if (!token) { if ( this.reflector.getAllAndOverride(IS_OPTIONAL_AUTH, targets) ) return true; throw new UnauthorizedException('Missing bearer token'); } try { req.user = await this.identity.resolveSession(token); req.accessToken = token; } catch (error) { if (error instanceof InvalidAccessTokenError) throw new UnauthorizedException(error.message); throw error; } } const roles = this.reflector.getAllAndOverride( REQUIRED_APP_ROLES, targets, ); const user = req.user; if (roles?.length && user && !roles.some((role) => user.hasAppRole(role))) { throw new ForbiddenException( `Requires one of the app roles: ${roles.join(', ')}`, ); } return true; } }