Skip to content

Commit eda24b0

Browse files
authored
Merge pull request #92 from nafiuishaaq/feat/apikey
Feat/apikey
2 parents 041e110 + 01114ad commit eda24b0

10 files changed

Lines changed: 1237 additions & 9 deletions

File tree

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { ScheduleModule } from '@nestjs/schedule';
34
import { AuditLog, ApiKey } from './entities';
4-
import { AuditLogService, AuditLogRepository, AuditEventEmitter } from './services';
5+
import { AuditLogService, AuditLogRepository, AuditEventEmitter, ApiKeyService, ApiKeyRepository } from './services';
6+
import { ApiKeyExpirationService } from './services/api-key-expiration.service';
57
import { AuditController } from './controllers/audit.controller';
8+
import { ApiKeyController } from './controllers/api-key.controller';
69
import { AuditInterceptor } from './interceptors';
710

811
@Module({
9-
imports: [TypeOrmModule.forFeature([AuditLog, ApiKey])],
10-
controllers: [AuditController],
12+
imports: [
13+
TypeOrmModule.forFeature([AuditLog, ApiKey]),
14+
ScheduleModule.forRoot(),
15+
],
16+
controllers: [AuditController, ApiKeyController],
1117
providers: [
1218
AuditLogService,
1319
AuditLogRepository,
1420
AuditEventEmitter,
1521
AuditInterceptor,
22+
ApiKeyService,
23+
ApiKeyRepository,
24+
ApiKeyExpirationService,
1625
],
17-
exports: [AuditLogService, AuditEventEmitter, AuditInterceptor],
26+
exports: [AuditLogService, AuditEventEmitter, AuditInterceptor, ApiKeyService, ApiKeyRepository],
1827
})
1928
export class AuditModule {}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {
2+
Controller,
3+
Post,
4+
Get,
5+
Delete,
6+
Body,
7+
Param,
8+
Query,
9+
HttpCode,
10+
HttpStatus,
11+
} from '@nestjs/common';
12+
import { ApiKeyService } from '../services/api-key.service';
13+
import {
14+
CreateApiKeyDto,
15+
ListApiKeysQueryDto,
16+
RevokeApiKeyDto,
17+
RotateApiKeyDto,
18+
} from '../dto/api-key.dto';
19+
20+
/**
21+
* API Key Management Controller
22+
* Handles creation, rotation, and revocation of API keys
23+
* Note: JWT Auth Guard should be applied at route level or globally
24+
*/
25+
@Controller('api-keys')
26+
export class ApiKeyController {
27+
constructor(private readonly apiKeyService: ApiKeyService) {}
28+
29+
/**
30+
* Create a new API key
31+
* POST /api-keys
32+
*/
33+
@Post()
34+
async createApiKey(
35+
@Body() createDto: CreateApiKeyDto,
36+
req: any,
37+
) {
38+
// Get merchantId from authenticated user
39+
const merchantId = req.user?.merchantId || req.user?.sub;
40+
41+
const result = await this.apiKeyService.createApiKey(merchantId, createDto);
42+
43+
return {
44+
success: true,
45+
data: result,
46+
message: 'API key created successfully. Store the key securely - it will not be shown again.',
47+
};
48+
}
49+
50+
/**
51+
* List all API keys for the authenticated merchant
52+
* GET /api-keys
53+
*/
54+
@Get()
55+
async listApiKeys(
56+
@Query() query: ListApiKeysQueryDto,
57+
req: any,
58+
) {
59+
const merchantId = req.user?.merchantId || req.user?.sub;
60+
61+
const result = await this.apiKeyService.listApiKeys(
62+
merchantId,
63+
query.limit,
64+
query.offset,
65+
query.status,
66+
);
67+
68+
return {
69+
success: true,
70+
data: result,
71+
};
72+
}
73+
74+
/**
75+
* Get API key status
76+
* GET /api-keys/:id/status
77+
*/
78+
@Get(':id/status')
79+
async getApiKeyStatus(
80+
@Param('id') keyId: string,
81+
req: any,
82+
) {
83+
const merchantId = req.user?.merchantId || req.user?.sub;
84+
85+
const result = await this.apiKeyService.getApiKeyStatus(keyId, merchantId);
86+
87+
return {
88+
success: true,
89+
data: result,
90+
};
91+
}
92+
93+
/**
94+
* Rotate an API key
95+
* POST /api-keys/:id/rotate
96+
*/
97+
@Post(':id/rotate')
98+
async rotateApiKey(
99+
@Param('id') keyId: string,
100+
@Body() rotateDto: RotateApiKeyDto,
101+
req: any,
102+
) {
103+
const merchantId = req.user?.merchantId || req.user?.sub;
104+
105+
const result = await this.apiKeyService.rotateApiKey(
106+
keyId,
107+
merchantId,
108+
rotateDto.reason,
109+
);
110+
111+
return {
112+
success: true,
113+
data: result,
114+
message: 'API key rotated successfully. The old key will remain valid for 24 hours.',
115+
};
116+
}
117+
118+
/**
119+
* Revoke an API key
120+
* POST /api-keys/:id/revoke
121+
*/
122+
@Post(':id/revoke')
123+
@HttpCode(HttpStatus.OK)
124+
async revokeApiKey(
125+
@Param('id') keyId: string,
126+
@Body() revokeDto: RevokeApiKeyDto,
127+
req: any,
128+
) {
129+
const merchantId = req.user?.merchantId || req.user?.sub;
130+
131+
await this.apiKeyService.revokeApiKey(keyId, merchantId, revokeDto.reason);
132+
133+
return {
134+
success: true,
135+
message: 'API key revoked successfully.',
136+
};
137+
}
138+
139+
/**
140+
* Delete (revoke) an API key
141+
* DELETE /api-keys/:id
142+
*/
143+
@Delete(':id')
144+
async deleteApiKey(
145+
@Param('id') keyId: string,
146+
req: any,
147+
) {
148+
const merchantId = req.user?.merchantId || req.user?.sub;
149+
150+
await this.apiKeyService.revokeApiKey(keyId, merchantId, 'deleted-via-api');
151+
152+
return {
153+
success: true,
154+
message: 'API key deleted successfully.',
155+
};
156+
}
157+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { IsString, IsOptional, IsInt, IsEnum, Min, Max, IsUUID } from 'class-validator';
2+
import { ApiKeyStatus } from '../entities/api-key.entity';
3+
4+
/**
5+
* DTO for creating a new API key
6+
*/
7+
export class CreateApiKeyDto {
8+
@IsString()
9+
name: string;
10+
11+
@IsString()
12+
@IsOptional()
13+
description?: string;
14+
15+
@IsInt()
16+
@IsOptional()
17+
@Min(1)
18+
@Max(365)
19+
expiresInDays?: number;
20+
21+
@IsString()
22+
@IsOptional()
23+
role?: string;
24+
}
25+
26+
/**
27+
* DTO for API key response (includes the actual key - only shown once)
28+
*/
29+
export class ApiKeyResponseDto {
30+
id: string;
31+
name: string;
32+
apiKey?: string; // Only present on creation/rotation
33+
keyHash: string;
34+
status: ApiKeyStatus;
35+
expiresAt: Date;
36+
createdAt: Date;
37+
lastUsedAt?: Date;
38+
requestCount: number;
39+
description?: string;
40+
role: string;
41+
rotatedFromId?: string;
42+
gracePeriodEndsAt?: Date; // For rotated keys
43+
}
44+
45+
/**
46+
* DTO for API key status response
47+
*/
48+
export class ApiKeyStatusDto {
49+
id: string;
50+
name: string;
51+
status: ApiKeyStatus;
52+
expiresAt: Date;
53+
createdAt: Date;
54+
lastUsedAt?: Date;
55+
requestCount: number;
56+
description?: string;
57+
role: string;
58+
rotatedFromId?: string;
59+
isExpired: boolean;
60+
daysUntilExpiry: number;
61+
}
62+
63+
/**
64+
* DTO for rotating an API key
65+
*/
66+
export class RotateApiKeyDto {
67+
@IsString()
68+
@IsOptional()
69+
reason?: string;
70+
}
71+
72+
/**
73+
* DTO for revoking an API key
74+
*/
75+
export class RevokeApiKeyDto {
76+
@IsString()
77+
@IsOptional()
78+
reason?: string;
79+
}
80+
81+
/**
82+
* DTO for listing API keys with pagination
83+
*/
84+
export class ListApiKeysQueryDto {
85+
@IsInt()
86+
@IsOptional()
87+
@Min(1)
88+
limit?: number = 50;
89+
90+
@IsInt()
91+
@IsOptional()
92+
@Min(0)
93+
offset?: number = 0;
94+
95+
@IsEnum(ApiKeyStatus)
96+
@IsOptional()
97+
status?: ApiKeyStatus;
98+
}
99+
100+
/**
101+
* DTO for paginated API key list response
102+
*/
103+
export class ApiKeyListResponseDto {
104+
data: ApiKeyStatusDto[];
105+
total: number;
106+
limit: number;
107+
offset: number;
108+
}
109+
110+
/**
111+
* DTO for API key rotation response
112+
*/
113+
export class ApiKeyRotationResponseDto {
114+
id: string;
115+
name: string;
116+
apiKey: string; // New key (only shown once)
117+
expiresAt: Date;
118+
oldKeyId: string;
119+
oldKeyGracePeriodEndsAt: Date;
120+
}
121+
122+
/**
123+
* API Key Expired Error Response
124+
*/
125+
export interface ApiKeyExpiredError {
126+
error: 'APIKeyExpired';
127+
message: string;
128+
expiredAt: string;
129+
keyId: string;
130+
}

0 commit comments

Comments
 (0)