Spaces:
Runtime error
Runtime error
| import { Controller, Get, Post, Put, Delete, Body, Param, Req, HttpCode, HttpStatus } from '@nestjs/common'; | |
| import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; | |
| import type { Request } from 'express'; | |
| import { AuthService } from './auth.service'; | |
| import { CreateApiKeyDto, UpdateApiKeyDto, ApiKeyResponseDto, ApiKeyCreatedResponseDto } from './dto'; | |
| import { RequireRole, CurrentApiKey } from './decorators/auth.decorators'; | |
| import { type ApiKey, ApiKeyRole } from './entities/api-key.entity'; | |
| import { AuditService } from '../audit/audit.service'; | |
| import { AuditAction } from './../audit/entities/audit-log.entity'; | |
| ('auth') | |
| ('auth/api-keys') | |
| export class AuthController { | |
| constructor( | |
| private readonly authService: AuthService, | |
| private readonly auditService: AuditService, | |
| ) {} | |
| // Build the request-context block for an API-key lifecycle audit entry: who did it (the admin key from | |
| // the guard), the resolved client IP, and the HTTP method/path. | |
| private auditContext( | |
| req: Request, | |
| actor?: ApiKey, | |
| ): { apiKey?: ApiKey; ipAddress?: string; method?: string; path?: string } { | |
| return { | |
| apiKey: actor, | |
| ipAddress: (req as Request & { clientIp?: string }).clientIp ?? undefined, | |
| method: req.method, | |
| path: req.path, | |
| }; | |
| } | |
| () | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Create a new API key (admin only)' }) | |
| ({ | |
| status: 201, | |
| description: 'API key created', | |
| type: ApiKeyCreatedResponseDto, | |
| }) | |
| async create( | |
| () dto: CreateApiKeyDto, | |
| () req: Request, | |
| () actor?: ApiKey, | |
| ): Promise<ApiKeyCreatedResponseDto> { | |
| const { apiKey, rawKey } = await this.authService.createApiKey(dto); | |
| await this.auditService.logInfo(AuditAction.API_KEY_CREATED, { | |
| ...this.auditContext(req, actor), | |
| metadata: { targetKeyId: apiKey.id, targetKeyName: apiKey.name, role: apiKey.role }, | |
| }); | |
| return { | |
| id: apiKey.id, | |
| name: apiKey.name, | |
| keyPrefix: apiKey.keyPrefix, | |
| role: apiKey.role, | |
| allowedIps: apiKey.allowedIps || undefined, | |
| allowedSessions: apiKey.allowedSessions || undefined, | |
| isActive: apiKey.isActive, | |
| expiresAt: apiKey.expiresAt || undefined, | |
| lastUsedAt: apiKey.lastUsedAt || undefined, | |
| usageCount: apiKey.usageCount, | |
| createdAt: apiKey.createdAt, | |
| apiKey: rawKey, | |
| }; | |
| } | |
| () | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'List all API keys (admin only)' }) | |
| ({ | |
| status: 200, | |
| description: 'All API keys (the plaintext key is never returned; only the keyPrefix).', | |
| type: [ApiKeyResponseDto], | |
| }) | |
| async findAll(): Promise<ApiKeyResponseDto[]> { | |
| const keys = await this.authService.findAll(); | |
| return keys.map(k => ({ | |
| id: k.id, | |
| name: k.name, | |
| keyPrefix: k.keyPrefix, | |
| role: k.role, | |
| allowedIps: k.allowedIps || undefined, | |
| allowedSessions: k.allowedSessions || undefined, | |
| isActive: k.isActive, | |
| expiresAt: k.expiresAt || undefined, | |
| lastUsedAt: k.lastUsedAt || undefined, | |
| usageCount: k.usageCount, | |
| createdAt: k.createdAt, | |
| })); | |
| } | |
| (':id') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Get API key details (admin only)' }) | |
| ({ | |
| status: 200, | |
| description: 'The API key (plaintext never returned; only the keyPrefix).', | |
| type: ApiKeyResponseDto, | |
| }) | |
| async findOne(('id') id: string): Promise<ApiKeyResponseDto> { | |
| const k = await this.authService.findOne(id); | |
| return { | |
| id: k.id, | |
| name: k.name, | |
| keyPrefix: k.keyPrefix, | |
| role: k.role, | |
| allowedIps: k.allowedIps || undefined, | |
| allowedSessions: k.allowedSessions || undefined, | |
| isActive: k.isActive, | |
| expiresAt: k.expiresAt || undefined, | |
| lastUsedAt: k.lastUsedAt || undefined, | |
| usageCount: k.usageCount, | |
| createdAt: k.createdAt, | |
| }; | |
| } | |
| (':id') | |
| (ApiKeyRole.ADMIN) | |
| ({ summary: 'Update API key (admin only)' }) | |
| ({ status: 200, description: 'The updated API key.', type: ApiKeyResponseDto }) | |
| ({ status: 409, description: 'The change would remove the last usable admin key.' }) | |
| async update( | |
| ('id') id: string, | |
| () dto: UpdateApiKeyDto, | |
| () req: Request, | |
| () actor?: ApiKey, | |
| ): Promise<ApiKeyResponseDto> { | |
| const before = await this.authService.findOne(id); | |
| const k = await this.authService.update(id, dto); | |
| const authzSnapshot = (key: ApiKey) => ({ | |
| role: key.role, | |
| allowedIps: key.allowedIps, | |
| allowedSessions: key.allowedSessions, | |
| expiresAt: key.expiresAt, | |
| }); | |
| await this.auditService.logInfo(AuditAction.API_KEY_UPDATED, { | |
| ...this.auditContext(req, actor), | |
| metadata: { | |
| targetKeyId: k.id, | |
| targetKeyName: k.name, | |
| before: authzSnapshot(before), | |
| after: authzSnapshot(k), | |
| }, | |
| }); | |
| return { | |
| id: k.id, | |
| name: k.name, | |
| keyPrefix: k.keyPrefix, | |
| role: k.role, | |
| allowedIps: k.allowedIps || undefined, | |
| allowedSessions: k.allowedSessions || undefined, | |
| isActive: k.isActive, | |
| expiresAt: k.expiresAt || undefined, | |
| lastUsedAt: k.lastUsedAt || undefined, | |
| usageCount: k.usageCount, | |
| createdAt: k.createdAt, | |
| }; | |
| } | |
| (':id') | |
| (ApiKeyRole.ADMIN) | |
| (HttpStatus.NO_CONTENT) | |
| ({ summary: 'Delete API key (admin only)' }) | |
| ({ status: 204, description: 'API key deleted' }) | |
| ({ status: 409, description: 'The key is the last usable admin key.' }) | |
| async delete(('id') id: string, () req: Request, () actor?: ApiKey): Promise<void> { | |
| const target = await this.authService.findOne(id); | |
| await this.authService.delete(id); | |
| await this.auditService.logInfo(AuditAction.API_KEY_DELETED, { | |
| ...this.auditContext(req, actor), | |
| metadata: { targetKeyId: id, targetKeyName: target?.name }, | |
| }); | |
| } | |
| (':id/revoke') | |
| (ApiKeyRole.ADMIN) | |
| (HttpStatus.OK) | |
| ({ summary: 'Revoke API key (admin only)' }) | |
| ({ status: 200, description: 'The revoked API key (isActive now false).', type: ApiKeyResponseDto }) | |
| ({ status: 409, description: 'The key is the last usable admin key.' }) | |
| async revoke( | |
| ('id') id: string, | |
| () req: Request, | |
| () actor?: ApiKey, | |
| ): Promise<ApiKeyResponseDto> { | |
| const k = await this.authService.revoke(id); | |
| await this.auditService.logInfo(AuditAction.API_KEY_REVOKED, { | |
| ...this.auditContext(req, actor), | |
| metadata: { targetKeyId: k.id, targetKeyName: k.name }, | |
| }); | |
| return { | |
| id: k.id, | |
| name: k.name, | |
| keyPrefix: k.keyPrefix, | |
| role: k.role, | |
| allowedIps: k.allowedIps || undefined, | |
| allowedSessions: k.allowedSessions || undefined, | |
| isActive: k.isActive, | |
| expiresAt: k.expiresAt || undefined, | |
| lastUsedAt: k.lastUsedAt || undefined, | |
| usageCount: k.usageCount, | |
| createdAt: k.createdAt, | |
| }; | |
| } | |
| } | |