Commit inicial - upload de todos os arquivos da pasta

This commit is contained in:
2026-06-13 16:36:29 -03:00
commit 807be1c5ee
275 changed files with 29408 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IntegrationScope } from '@prisma/client';
import { createHash, timingSafeEqual } from 'crypto';
import { Request } from 'express';
import { REQUIRE_SCOPE_KEY } from '../decorators/require-scope.decorator';
import {
IntegrationApiKeyForGuard,
IntegrationApiKeysRepository,
} from '../../modules/integration-api-keys/integration-api-keys.repository';
export interface RequestWithApiKey extends Request {
apiKey?: IntegrationApiKeyForGuard;
}
@Injectable()
export class ApiKeyGuard implements CanActivate {
private readonly logger = new Logger(ApiKeyGuard.name);
constructor(
private readonly reflector: Reflector,
private readonly repository: IntegrationApiKeysRepository,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<RequestWithApiKey>();
const headerValue = this.extractHeader(request);
if (!headerValue) {
throw new UnauthorizedException('API Key ausente');
}
const computedHash = createHash('sha256').update(headerValue).digest('hex');
const apiKey = await this.repository.findByHashedKey(computedHash);
if (!apiKey) {
throw new UnauthorizedException('API Key inválida');
}
if (!this.constantTimeEqualsHex(computedHash, apiKey.hashedKey)) {
throw new UnauthorizedException('API Key inválida');
}
if (!apiKey.isActive) {
throw new UnauthorizedException('API Key revogada');
}
if (apiKey.expiresAt && apiKey.expiresAt.getTime() < Date.now()) {
throw new UnauthorizedException('API Key expirada');
}
const requiredScope = this.reflector.getAllAndOverride<IntegrationScope | undefined>(
REQUIRE_SCOPE_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredScope) {
throw new ForbiddenException('Endpoint sem escopo declarado');
}
if (!apiKey.scopes.includes(requiredScope)) {
throw new ForbiddenException('Escopo insuficiente');
}
request.apiKey = apiKey;
void this.repository.touchLastUsed(apiKey.id).catch((err: unknown) => {
this.logger.warn(`Falha ao atualizar lastUsedAt: ${(err as Error).message}`);
});
return true;
}
private extractHeader(request: Request): string | null {
const raw = request.headers['x-api-key'];
if (!raw) return null;
if (Array.isArray(raw)) return raw[0] ?? null;
return raw;
}
private constantTimeEqualsHex(a: string, b: string): boolean {
if (a.length !== b.length) return false;
try {
return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
} catch {
return false;
}
}
}