import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Reflector } from '@nestjs/core'; import { ClsService } from 'nestjs-cls'; import { jwtVerify } from 'jose'; import type { Request } from 'express'; import type { Env } from '../config/env.schema'; import type { WorkspaceClsStore } from '../workspace/workspace.types'; import { WorkspacePrismaPool } from '../workspace/workspace-prisma-pool.service'; import type { JwtPayload } from './jwt.types'; import { IS_PUBLIC_KEY } from './public.decorator'; // Guard global (APP_GUARD). Valida Bearer HS256 e atualiza CLS com idEmpresa real. // CODING-RULES PGD-AUTHZ-002: idEmpresa sempre do JWT, nunca de body/param. // Ordem NestJS: middleware CLS (idEmpresa default) → este guard (idEmpresa real). // ADR 0006 revogado: workspace_id → id_empresa; URL inclui ?schema=sar @Injectable() export class JwtAuthGuard implements CanActivate { private readonly secret: Uint8Array; private readonly isProd: boolean; private readonly devRepCode: string; private readonly devEmpresaId: number; constructor( private readonly reflector: Reflector, private readonly cls: ClsService, private readonly pool: WorkspacePrismaPool, config: ConfigService, ) { this.secret = new TextEncoder().encode(config.get('MASTER_LOGIN_JWT_SECRET', { infer: true })); this.isProd = config.get('NODE_ENV', { infer: true }) === 'production'; this.devRepCode = String(config.get('DEV_REP_CODE', { infer: true })); this.devEmpresaId = config.get('DEV_EMPRESA_ID', { infer: true }); } async canActivate(context: ExecutionContext): Promise { if (this.isPublic(context)) return true; const req = context.switchToHttp().getRequest(); const token = this.extractBearer(req); if (!token) { throw new UnauthorizedException('token ausente'); } try { const { payload } = await jwtVerify(token, this.secret, { algorithms: ['HS256'], }); (req as Request & { user: JwtPayload }).user = payload as JwtPayload; // Em dev: força representante fixo (DEV_REP_CODE / DEV_EMPRESA_ID) ignorando o JWT. // Em prod: usa os valores reais do JWT. const idEmpresa = this.isProd ? payload.id_empresa : this.devEmpresaId; const userId = this.isProd ? payload.sub : this.devRepCode; this.cls.set('idEmpresa', idEmpresa); this.cls.set('userId', userId); this.cls.set('role', payload.role); const baseUrl = process.env['DATABASE_URL'] ?? 'postgresql://sar:sar_dev_password@localhost:5432/sar_workspace_dev'; this.cls.set('prisma', this.pool.getOrCreate(idEmpresa, baseUrl)); return true; } catch { throw new UnauthorizedException('token inválido ou expirado'); } } private isPublic(ctx: ExecutionContext): boolean { return ( this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ ctx.getHandler(), ctx.getClass(), ]) === true ); } private extractBearer(req: Request): string | undefined { const auth = req.headers['authorization']; if (typeof auth === 'string' && auth.startsWith('Bearer ')) { return auth.slice(7); } return undefined; } }