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:
79
apps/api/src/app/auth/jwt-auth.guard.ts
Normal file
79
apps/api/src/app/auth/jwt-auth.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user