feat(api-gateway): add Swagger, admin endpoints, and scheduler

- Add Swagger/OpenAPI documentation at /docs endpoint
- Add admin module for system-wide API key management
- Add scheduler for monthly credit reset and usage cleanup
- Add Docker Compose entry for Mac Mini deployment
- Document all endpoints with descriptions and examples
This commit is contained in:
Till-JS 2026-01-29 18:03:16 +01:00
parent 4b322f59b1
commit fc0ed636fc
21 changed files with 1059 additions and 1 deletions

View file

@ -9,11 +9,21 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
import { ApiKeysService } from './api-keys.service';
import { CreateApiKeyDto, UpdateApiKeyDto } from './dto';
import { UsageService } from '../usage/usage.service';
@ApiTags('API Keys')
@ApiBearerAuth('jwt')
@Controller('api-keys')
@UseGuards(JwtAuthGuard)
export class ApiKeysController {
@ -23,6 +33,12 @@ export class ApiKeysController {
) {}
@Post()
@ApiOperation({
summary: 'Create API key',
description: 'Creates a new API key. The full key is only returned once - save it securely.',
})
@ApiResponse({ status: 201, description: 'API key created successfully' })
@ApiResponse({ status: 401, description: 'Unauthorized - invalid or missing JWT' })
async create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateApiKeyDto) {
const result = await this.apiKeyService.create(user.userId, dto);
return {
@ -33,18 +49,37 @@ export class ApiKeysController {
}
@Get()
@ApiOperation({
summary: 'List API keys',
description: 'Returns all API keys for the authenticated user (without full key values)',
})
@ApiResponse({ status: 200, description: 'List of API keys' })
async list(@CurrentUser() user: CurrentUserData) {
const keys = await this.apiKeyService.listByUser(user.userId);
return { apiKeys: keys };
}
@Get(':id')
@ApiOperation({
summary: 'Get API key details',
description: 'Returns details for a specific API key',
})
@ApiParam({ name: 'id', description: 'API key ID (UUID)' })
@ApiResponse({ status: 200, description: 'API key details' })
@ApiResponse({ status: 404, description: 'API key not found' })
async get(@CurrentUser() user: CurrentUserData, @Param('id') id: string) {
const key = await this.apiKeyService.getByIdAndUser(id, user.userId);
return { apiKey: key };
}
@Patch(':id')
@ApiOperation({
summary: 'Update API key',
description: 'Updates name, description, allowed endpoints, IP whitelist, or active status',
})
@ApiParam({ name: 'id', description: 'API key ID (UUID)' })
@ApiResponse({ status: 200, description: 'API key updated successfully' })
@ApiResponse({ status: 404, description: 'API key not found' })
async update(
@CurrentUser() user: CurrentUserData,
@Param('id') id: string,
@ -55,12 +90,27 @@ export class ApiKeysController {
}
@Delete(':id')
@ApiOperation({
summary: 'Delete API key',
description: 'Permanently deletes an API key. This action cannot be undone.',
})
@ApiParam({ name: 'id', description: 'API key ID (UUID)' })
@ApiResponse({ status: 200, description: 'API key deleted successfully' })
@ApiResponse({ status: 404, description: 'API key not found' })
async delete(@CurrentUser() user: CurrentUserData, @Param('id') id: string) {
await this.apiKeyService.delete(id, user.userId);
return { message: 'API key deleted successfully' };
}
@Post(':id/regenerate')
@ApiOperation({
summary: 'Regenerate API key',
description:
'Generates a new key value for an existing API key. The old key immediately stops working.',
})
@ApiParam({ name: 'id', description: 'API key ID (UUID)' })
@ApiResponse({ status: 200, description: 'New key generated successfully' })
@ApiResponse({ status: 404, description: 'API key not found' })
async regenerate(@CurrentUser() user: CurrentUserData, @Param('id') id: string) {
const result = await this.apiKeyService.regenerate(id, user.userId);
return {
@ -71,6 +121,13 @@ export class ApiKeysController {
}
@Get(':id/usage')
@ApiOperation({
summary: 'Get daily usage',
description: 'Returns daily usage statistics for an API key',
})
@ApiParam({ name: 'id', description: 'API key ID (UUID)' })
@ApiQuery({ name: 'days', required: false, description: 'Number of days (default: 30)' })
@ApiResponse({ status: 200, description: 'Daily usage statistics' })
async getUsage(
@CurrentUser() user: CurrentUserData,
@Param('id') id: string,
@ -86,6 +143,12 @@ export class ApiKeysController {
}
@Get(':id/usage/summary')
@ApiOperation({
summary: 'Get usage summary',
description: 'Returns aggregated usage summary for an API key',
})
@ApiParam({ name: 'id', description: 'API key ID (UUID)' })
@ApiResponse({ status: 200, description: 'Usage summary' })
async getUsageSummary(@CurrentUser() user: CurrentUserData, @Param('id') id: string) {
// Verify ownership
await this.apiKeyService.getByIdAndUser(id, user.userId);

View file

@ -1,33 +1,66 @@
import { IsString, IsOptional, IsEnum, IsArray, IsDateString } from 'class-validator';
import { IsString, IsOptional, IsEnum, IsArray, IsDateString, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { PricingTier } from '../../config/pricing';
export class CreateApiKeyDto {
@ApiProperty({
description: 'Display name for the API key',
example: 'Production API Key',
})
@IsString()
name: string;
@ApiPropertyOptional({
description: 'Optional description for the API key',
example: 'Used for production web application',
})
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({
description: 'Pricing tier (determines rate limits and credits)',
enum: ['free', 'pro', 'enterprise'],
default: 'free',
})
@IsString()
@IsOptional()
@IsEnum(['free', 'pro', 'enterprise'])
tier?: PricingTier;
@ApiPropertyOptional({
description: 'List of allowed endpoints (null = all endpoints allowed)',
example: ['search', 'tts'],
type: [String],
})
@IsArray()
@IsString({ each: true })
@IsOptional()
allowedEndpoints?: string[];
@ApiPropertyOptional({
description: 'IP whitelist for this key (null = all IPs allowed)',
example: ['192.168.1.0/24', '10.0.0.1'],
type: [String],
})
@IsArray()
@IsString({ each: true })
@IsOptional()
allowedIps?: string[];
@ApiPropertyOptional({
description: 'Expiration date for the API key (ISO 8601)',
example: '2025-12-31T23:59:59Z',
})
@IsDateString()
@IsOptional()
expiresAt?: string;
@ApiPropertyOptional({
description: 'Create a test key (sk_test_ prefix) instead of live key',
default: false,
})
@IsBoolean()
@IsOptional()
isTest?: boolean;
}

View file

@ -1,28 +1,55 @@
import { IsString, IsOptional, IsBoolean, IsArray, IsDateString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class UpdateApiKeyDto {
@ApiPropertyOptional({
description: 'Update the display name',
example: 'Updated API Key Name',
})
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({
description: 'Update the description',
example: 'Updated description for this key',
})
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({
description: 'Update allowed endpoints',
example: ['search', 'stt', 'tts'],
type: [String],
})
@IsArray()
@IsString({ each: true })
@IsOptional()
allowedEndpoints?: string[];
@ApiPropertyOptional({
description: 'Update IP whitelist',
example: ['192.168.1.0/24'],
type: [String],
})
@IsArray()
@IsString({ each: true })
@IsOptional()
allowedIps?: string[];
@ApiPropertyOptional({
description: 'Enable or disable the API key',
example: true,
})
@IsBoolean()
@IsOptional()
active?: boolean;
@ApiPropertyOptional({
description: 'Update expiration date (ISO 8601)',
example: '2025-12-31T23:59:59Z',
})
@IsDateString()
@IsOptional()
expiresAt?: string;