feat(api): master-login stub + WorkspacePrismaPool (Frente E)

- Prisma 7: prisma.config.ts com datasource.url (API correta); schema gerado em CJS
- WorkspacePrismaPool: LRU cache (max 10) de PrismaClient por workspace (ADR 0006)
  PrismaPg adapter + pg.Pool por workspace; getOrCreate/health/onModuleDestroy
- JwtAuthGuard: global APP_GUARD, jose HS256, popula CLS com workspace_id/userId/prisma
  @Public() decorator marca ping/health/dev-auth como rotas abertas
- DevAuthController: POST /auth/dev/token — emite JWT dev (404 em produção)
- AuthTokenResponseSchema + DevTokenRequestSchema em @sar/api-interface
- WorkspacePoolHealthIndicator: health/ready reporta amostra LRU top-3 (nunca O(N))
- .npmrc: hoist @prisma/client-runtime-utils (requerido pelo Prisma 7 isolated mode)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 22:36:00 +00:00
parent bca2e3ebb3
commit 2a8be3fd82
22 changed files with 1204 additions and 39 deletions

View File

@@ -0,0 +1,79 @@
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 workspace real.
// CODING-RULES PGD-AUTHZ-002: workspaceId sempre do JWT, nunca de body/param.
// Ordem NestJS: middleware CLS (workspace default) → este guard (workspace real).
@Injectable()
export class JwtAuthGuard implements CanActivate {
private readonly secret: Uint8Array;
constructor(
private readonly reflector: Reflector,
private readonly cls: ClsService<WorkspaceClsStore>,
private readonly pool: WorkspacePrismaPool,
config: ConfigService<Env, true>,
) {
this.secret = new TextEncoder().encode(config.get('MASTER_LOGIN_JWT_SECRET', { infer: true }));
}
async canActivate(context: ExecutionContext): Promise<boolean> {
if (this.isPublic(context)) return true;
const req = context.switchToHttp().getRequest<Request>();
const token = this.extractBearer(req);
if (!token) {
throw new UnauthorizedException('token ausente');
}
try {
const { payload } = await jwtVerify<JwtPayload>(token, this.secret, {
algorithms: ['HS256'],
});
(req as Request & { user: JwtPayload }).user = payload as JwtPayload;
// Sobrescreve CLS com workspace real do JWT (corre depois do middleware).
const workspaceId = payload.workspace_id;
this.cls.set('workspaceId', workspaceId);
this.cls.set('userId', payload.sub);
const dbUrl =
process.env['DATABASE_URL'] ??
`postgresql://sar:sar_dev_password@localhost:5432/sar_workspace_${workspaceId}`;
this.cls.set('prisma', this.pool.getOrCreate(workspaceId, dbUrl));
return true;
} catch {
throw new UnauthorizedException('token inválido ou expirado');
}
}
private isPublic(ctx: ExecutionContext): boolean {
return (
this.reflector.getAllAndOverride<boolean>(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;
}
}