# Run migrations to create audit tables
npm run migration:runimport { AuditLogService } from './audit/services';
import { EventType } from './audit/entities';
export class ApiKeyService {
constructor(private auditLogService: AuditLogService) {}
async createApiKey(merchantId: string, details: any) {
const newKey = await this.repo.save(details);
// Emit creation event
this.auditLogService.emitApiKeyEvent(
EventType.API_KEY_CREATED,
merchantId,
{
keyId: newKey.id,
name: newKey.name,
role: newKey.role,
}
);
return newKey;
}
async rotateApiKey(merchantId: string, oldKeyId: string, newKey: any) {
await this.repo.save(newKey);
this.auditLogService.emitApiKeyEvent(
EventType.API_KEY_ROTATED,
merchantId,
{
oldKeyId,
newKeyId: newKey.id,
reason: 'scheduled rotation',
}
);
}
async revokeApiKey(merchantId: string, keyId: string) {
await this.repo.update(keyId, { status: 'revoked' });
this.auditLogService.emitApiKeyEvent(
EventType.API_KEY_REVOKED,
merchantId,
{
revokedKeyId: keyId,
reason: 'user-initiated',
}
);
}
}import { AuditLogService } from './audit/services';
import { EventType } from './audit/entities';
export class GasTransactionService {
constructor(private auditLogService: AuditLogService) {}
async submitGasTransaction(
merchantId: string,
chainId: number,
data: any
) {
// Process transaction...
const result = await this.submitToChain(data);
// Emit gas transaction event
this.auditLogService.emitGasTransaction(
merchantId,
chainId,
result.transactionHash,
result.gasUsed,
result.gasPrice,
result.senderAddress,
{
method: result.method,
value: result.value,
status: 'confirmed',
}
);
return result;
}
}import { AuditLogService } from './audit/services';
import { EventType } from './audit/entities';
export class ReportService {
constructor(private auditLogService: AuditLogService) {}
async generateUserActivityReport(merchantId: string, month: string) {
const [year, monthNum] = month.split('-');
const from = new Date(`${year}-${monthNum}-01`);
const to = new Date(from.getFullYear(), from.getMonth() + 1, 0);
const logs = await this.auditLogService.queryLogs({
user: merchantId,
from: from.toISOString(),
to: to.toISOString(),
limit: 1000,
offset: 0,
});
return logs;
}
async generateComplianceReport() {
// Get all key lifecycle events
const keyEvents = await this.auditLogService.getLogsByEventType(
EventType.API_KEY_CREATED,
10000
);
// Get all failed requests
const failures = await this.auditLogService.queryLogs({
eventType: EventType.API_REQUEST,
outcome: 'failure',
limit: 10000,
offset: 0,
});
return {
period: 'current_month',
keyCreations: keyEvents.length,
failedRequests: failures.total,
};
}
async exportAuditTrail(format: 'csv' | 'json') {
return this.auditLogService.exportLogs(format, {
limit: 10000,
offset: 0,
});
}
}# Get all audit logs with pagination
curl -X GET 'http://localhost:3000/audit/logs?limit=50&offset=0'
# Filter by event type
curl -X GET 'http://localhost:3000/audit/logs?eventType=APIRequest'
# Filter by user and date range
curl -X GET 'http://localhost:3000/audit/logs?user=merchant_123&from=2024-02-01&to=2024-02-28'
# Get specific log
curl -X GET 'http://localhost:3000/audit/logs/550e8400-e29b-41d4-a716-446655440000'
# Get logs by type
curl -X GET 'http://localhost:3000/audit/logs/type/KeyCreated?limit=100'
# Export logs as CSV
curl -X POST 'http://localhost:3000/audit/logs/export' \
-H 'Content-Type: application/json' \
-d '{
"format": "csv",
"eventType": "APIRequest",
"user": "merchant_123"
}' > audit-logs.csv
# Get audit statistics
curl -X GET 'http://localhost:3000/audit/stats'The AuditInterceptor automatically captures all API requests. Configure excluded patterns in audit.interceptor.ts:
private shouldSkipAudit(url: string): boolean {
const excludePatterns = [
'/health',
'/health/ready',
'/health/live',
'/metrics',
'/swagger',
'/api-docs',
];
return excludePatterns.some((pattern) => url.includes(pattern));
}API Key extraction (automatic):
- Authorization header:
Authorization: Bearer <api-key> - Custom header:
X-API-Key: <api-key> - Query parameter:
?apiKey=<api-key>
# Set retention policy (days)
AUDIT_LOG_RETENTION_DAYS=90
# Database settings (see main DB config)
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=gasguardSet up scheduled task with @nestjs/schedule:
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { AuditLogService } from './audit/services/audit-log.service';
@Injectable()
export class AuditMaintenanceService {
constructor(private auditLogService: AuditLogService) {}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupOldLogs() {
const retentionDays = parseInt(
process.env.AUDIT_LOG_RETENTION_DAYS || '90'
);
const deleted = await this.auditLogService.retentionCleanup(
retentionDays
);
console.log(
`🧹 Audit cleanup completed: ${deleted} logs removed`
);
}
}Add to module:
@Module({
providers: [AuditMaintenanceService],
})
export class ScheduleModule {}-
API Key Protection
- Keys stored as hashes only (SHA256)
- Never log raw API keys
- Rotate keys regularly
-
Access Control
- Restrict audit endpoints to admin users
- Log all access to audit logs
- Use HTTPS in production
-
Data Integrity
- Each log includes SHA256 hash
- Prevent unauthorized log deletion/modification
- Verify integrity on retrieval
-
Performance
- Use pagination (limit results)
- Implement query timeouts
- Archive old logs periodically
- Monitor database performance
| Requirement | Implementation |
|---|---|
| User activity tracking | API request events |
| Key management audit | KeyCreated, KeyRotated, KeyRevoked events |
| Transaction traceability | GasTransaction events |
| Access control | Admin-only audit endpoints |
| Immutable storage | Append-only design, integrity hashing |
| Data retention | Configurable retention policies |
| Reporting | CSV/JSON export with filtering |
| Accountability | User ID in all events |
Solution:
- Check
AuditModuleis imported inAppModule - Verify interceptor registered in
main.ts - Check database connection
- Monitor application logs
Solution:
- Verify date filters are correct
- Check user/eventType filters match data
- Try with larger limit (pagination issue)
- Query without filters to verify data exists
Solution:
- Verify csv parsing dependency installed
- Check disk space available
- Try smaller export (fewer records)
- Check file permissions
# Run all audit tests
npm test -- audit
# Run with coverage
npm run test:cov -- src/audit
# Run E2E tests
npm run test:e2e -- audit.controller.e2e.spec.ts
# Watch mode
npm test -- audit --watchSee AUDIT_LOGGING_SYSTEM.md for comprehensive documentation and examples.