Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
96
src/common/guards/api-key.guard.ts
Normal file
96
src/common/guards/api-key.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user