Skip to content

Commit 8e2f25a

Browse files
authored
Merge pull request #463 from levi0005/fix/key-management-improvements
Fix/key management improvements
2 parents 5fb31f6 + 9619e3c commit 8e2f25a

9 files changed

Lines changed: 420 additions & 43 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class KeyGeneratedEvent {
2+
constructor(
3+
public readonly publicKey: string,
4+
public readonly keyType: string,
5+
public readonly timestamp: Date,
6+
) {}
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class KeyRotatedEvent {
2+
constructor(
3+
public readonly predecessorWalletId: string,
4+
public readonly successorWalletId: string,
5+
public readonly successorPublicKey: string,
6+
public readonly timestamp: Date,
7+
) {}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class KeySignedEvent {
2+
constructor(
3+
public readonly publicKey: string,
4+
public readonly timestamp: Date,
5+
) {}
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class KeyValidatedEvent {
2+
constructor(
3+
public readonly publicKey: string,
4+
public readonly keyType: string,
5+
public readonly valid: boolean,
6+
public readonly timestamp: Date,
7+
) {}
8+
}

src/key-management/key-management.controller.spec.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Test, TestingModule } from '@nestjs/testing';
2+
import { BadRequestException } from '@nestjs/common';
23
import { KeyManagementController } from './key-management.controller';
34
import { KeyManagementService } from './key-management.service';
45
import { KeyType } from './domain/key-types';
@@ -113,33 +114,100 @@ describe('KeyManagementController', () => {
113114
});
114115
});
115116

117+
describe('generateKey input validation', () => {
118+
it('should throw BadRequestException when keyType is missing', async () => {
119+
await expect(
120+
controller.generateKey({ keyType: undefined as any }),
121+
).rejects.toThrow(BadRequestException);
122+
});
123+
124+
it('should throw BadRequestException when keyType is invalid', async () => {
125+
await expect(
126+
controller.generateKey({ keyType: 'BOGUS' as any }),
127+
).rejects.toThrow(BadRequestException);
128+
});
129+
});
130+
116131
describe('getAuditLog', () => {
117-
it('should return audit logs with default limit', async () => {
118-
const mockLogs = [
132+
const mockResult = {
133+
data: [
119134
{
120135
operation: 'GENERATE',
121136
keyId: 'key-1',
122137
publicKey: 'GPUBLIC123...',
123138
timestamp: new Date(),
124139
success: true,
125140
},
126-
];
141+
],
142+
total: 1,
143+
limit: 100,
144+
offset: 0,
145+
hasMore: false,
146+
};
127147

128-
mockKeyManagementService.getAuditLog.mockReturnValue(mockLogs);
148+
it('should return paginated audit logs with default params', async () => {
149+
mockKeyManagementService.getAuditLog.mockReturnValue(mockResult);
129150

130151
const result = await controller.getAuditLog();
131152

132-
expect(result).toEqual({ logs: mockLogs });
133-
expect(service.getAuditLog).toHaveBeenCalledWith(100);
153+
expect(result).toEqual({
154+
logs: mockResult.data,
155+
total: mockResult.total,
156+
limit: mockResult.limit,
157+
offset: mockResult.offset,
158+
hasMore: mockResult.hasMore,
159+
});
160+
expect(service.getAuditLog).toHaveBeenCalledWith(
161+
expect.objectContaining({ limit: undefined, offset: undefined }),
162+
);
163+
});
164+
165+
it('should pass limit and offset to service', async () => {
166+
mockKeyManagementService.getAuditLog.mockReturnValue(mockResult);
167+
168+
await controller.getAuditLog(undefined, undefined, undefined, undefined, undefined, '50', '10');
169+
170+
expect(service.getAuditLog).toHaveBeenCalledWith(
171+
expect.objectContaining({ limit: 50, offset: 10 }),
172+
);
134173
});
135174

136-
it('should return audit logs with custom limit', async () => {
137-
const mockLogs = [];
138-
mockKeyManagementService.getAuditLog.mockReturnValue(mockLogs);
175+
it('should pass operation filter to service', async () => {
176+
mockKeyManagementService.getAuditLog.mockReturnValue(mockResult);
177+
178+
await controller.getAuditLog('GENERATE');
179+
180+
expect(service.getAuditLog).toHaveBeenCalledWith(
181+
expect.objectContaining({ operation: 'GENERATE' }),
182+
);
183+
});
184+
185+
it('should parse success filter', async () => {
186+
mockKeyManagementService.getAuditLog.mockReturnValue(mockResult);
187+
188+
await controller.getAuditLog(undefined, undefined, 'true');
139189

140-
await controller.getAuditLog('50');
190+
expect(service.getAuditLog).toHaveBeenCalledWith(
191+
expect.objectContaining({ success: true }),
192+
);
193+
});
194+
195+
it('should throw BadRequestException for non-integer limit', async () => {
196+
await expect(
197+
controller.getAuditLog(undefined, undefined, undefined, undefined, undefined, 'abc'),
198+
).rejects.toThrow(BadRequestException);
199+
});
200+
201+
it('should throw BadRequestException for limit exceeding 100', async () => {
202+
await expect(
203+
controller.getAuditLog(undefined, undefined, undefined, undefined, undefined, '200'),
204+
).rejects.toThrow(BadRequestException);
205+
});
141206

142-
expect(service.getAuditLog).toHaveBeenCalledWith(50);
207+
it('should throw BadRequestException for invalid startDate', async () => {
208+
await expect(
209+
controller.getAuditLog(undefined, undefined, undefined, 'not-a-date'),
210+
).rejects.toThrow(BadRequestException);
143211
});
144212
});
145213

src/key-management/key-management.controller.ts

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
BadRequestException,
23
Controller,
34
Post,
45
Body,
@@ -26,6 +27,31 @@ import {
2627
} from './key-rotation-audit.service';
2728
import { KeyOperation } from '../generated/prisma/client';
2829

30+
function parsePaginationParam(
31+
value: string | undefined,
32+
name: string,
33+
max = 100,
34+
): number | undefined {
35+
if (value === undefined) return undefined;
36+
const n = Number(value);
37+
if (!Number.isInteger(n) || n < 0) {
38+
throw new BadRequestException(`${name} must be a non-negative integer`);
39+
}
40+
if (name === 'limit' && n > max) {
41+
throw new BadRequestException(`limit must not exceed ${max}`);
42+
}
43+
return n;
44+
}
45+
46+
function parseDate(value: string | undefined, name: string): Date | undefined {
47+
if (value === undefined) return undefined;
48+
const d = new Date(value);
49+
if (isNaN(d.getTime())) {
50+
throw new BadRequestException(`${name} must be a valid ISO date string`);
51+
}
52+
return d;
53+
}
54+
2955
/**
3056
* Internal controller for key management operations
3157
*
@@ -50,6 +76,15 @@ export class KeyManagementController {
5076
@Post('generate')
5177
@HttpCode(HttpStatus.OK)
5278
async generateKey(@Body() request: GenerateKeyRequest) {
79+
if (!request?.keyType) {
80+
throw new BadRequestException('keyType is required');
81+
}
82+
if (!Object.values(KeyType).includes(request.keyType)) {
83+
throw new BadRequestException(
84+
`Invalid keyType: "${request.keyType}". Must be one of: ${Object.values(KeyType).join(', ')}`,
85+
);
86+
}
87+
5388
const result = await this.keyManagementService.generateKey(request);
5489

5590
return {
@@ -133,17 +168,49 @@ export class KeyManagementController {
133168
}
134169

135170
/**
136-
* Gets audit log (admin only)
171+
* Gets in-memory audit log with optional filtering and pagination
172+
*
173+
* Query parameters:
174+
* - operation: Filter by operation type (GENERATE, SIGN, ROTATE, etc.)
175+
* - publicKey: Filter by public key
176+
* - success: Filter by success status (true/false)
177+
* - startDate: Start of date range (ISO string)
178+
* - endDate: End of date range (ISO string)
179+
* - limit: Max results (default: 100, max: 100)
180+
* - offset: Pagination offset (default: 0)
137181
*/
138182
@ApiOperation({ summary: 'Retrieve in-memory audit log (internal, admin only)' })
139183
@ApiQuery({ name: 'limit', required: false, description: 'Max entries to return (default: 100)' })
140184
@ApiResponse({ status: 200, description: 'Array of audit log entries' })
141185
@Get('audit')
142-
async getAuditLog(@Query('limit') limit?: string) {
143-
const auditLimit = limit ? parseInt(limit, 10) : 100;
144-
const logs = this.keyManagementService.getAuditLog(auditLimit);
186+
async getAuditLog(
187+
@Query('operation') operation?: string,
188+
@Query('publicKey') publicKey?: string,
189+
@Query('success') success?: string,
190+
@Query('startDate') startDate?: string,
191+
@Query('endDate') endDate?: string,
192+
@Query('limit') limit?: string,
193+
@Query('offset') offset?: string,
194+
) {
195+
const query: AuditLogQuery = {
196+
operation,
197+
publicKey,
198+
success: success !== undefined ? success === 'true' : undefined,
199+
startDate: parseDate(startDate, 'startDate'),
200+
endDate: parseDate(endDate, 'endDate'),
201+
limit: parsePaginationParam(limit, 'limit'),
202+
offset: parsePaginationParam(offset, 'offset'),
203+
};
204+
205+
const result = this.keyManagementService.getAuditLog(query);
145206

146-
return { logs };
207+
return {
208+
logs: result.data,
209+
total: result.total,
210+
limit: result.limit,
211+
offset: result.offset,
212+
hasMore: result.hasMore,
213+
};
147214
}
148215

149216
/**
@@ -168,8 +235,8 @@ export class KeyManagementController {
168235
@Query('operation') operation?: string,
169236
) {
170237
const query: KeyStatisticsQuery = {
171-
startDate: startDate ? new Date(startDate) : undefined,
172-
endDate: endDate ? new Date(endDate) : undefined,
238+
startDate: parseDate(startDate, 'startDate'),
239+
endDate: parseDate(endDate, 'endDate'),
173240
operation,
174241
};
175242

@@ -203,8 +270,8 @@ export class KeyManagementController {
203270
@Query('includeTimeSeries') includeTimeSeries?: string,
204271
) {
205272
const query: KeyStatisticsQuery = {
206-
startDate: startDate ? new Date(startDate) : undefined,
207-
endDate: endDate ? new Date(endDate) : undefined,
273+
startDate: parseDate(startDate, 'startDate'),
274+
endDate: parseDate(endDate, 'endDate'),
208275
operation,
209276
includeTimeSeries: includeTimeSeries === 'true',
210277
};

src/key-management/key-management.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { PrismaModule } from '../prisma/prisma.module';
99
import { KeyManagementMetricsService } from './key-management-metrics.service';
1010

1111
@Module({
12-
imports: [EncryptionModule, PrismaModule],
12+
imports: [EncryptionModule, PrismaModule, EventEmitterModule.forRoot()],
1313
controllers: [KeyManagementController],
1414
providers: [
1515
KeyManagementService,

0 commit comments

Comments
 (0)