Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.11",
"@types/passport-jwt": "^4.0.1",
"@types/superagent": "^8.1.9",
"@types/supertest": "^6.0.2",
"@types/uuid": "^11.0.0",
"@types/validator": "^13.15.10",
"eslint": "^9.39.3",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
Expand Down
6 changes: 6 additions & 0 deletions backend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ const envValidationSchema = Joi.object({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const isProduction = configService.get<string>('NODE_ENV') === 'production';
const isProduction =
configService.get<string>('NODE_ENV') === 'production';
return {
pinoHttp: {
transport: isProduction
Expand Down Expand Up @@ -200,4 +201,4 @@ const envValidationSchema = Joi.object({
},
],
})
export class AppModule { }
export class AppModule {}
66 changes: 33 additions & 33 deletions backend/src/common/entities/audit-log.entity.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
Index,
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
Index,
} from 'typeorm';

/**
* AuditLog Entity
*
*
* Stores structured audit entries for all trade and dispute mutations.
* Enables forensic traceability and incident debugging.
*
*
* Indexed by:
* - correlation_id: Trace full request lifecycle
* - resource_id: Find all mutations for a specific trade/dispute
Expand All @@ -26,42 +26,42 @@ import {
@Index('idx_audit_logs_timestamp', ['timestamp'])
@Index('idx_audit_logs_action', ['action'])
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
correlationId: string;
@Column()
correlationId: string;

@CreateDateColumn()
timestamp: Date;
@CreateDateColumn()
timestamp: Date;

@Column()
endpoint: string;
@Column()
endpoint: string;

@Column()
method: string;
@Column()
method: string;

@Column()
action: string; // CREATE, UPDATE, DELETE
@Column()
action: string; // CREATE, UPDATE, DELETE

@Column()
actor: string; // wallet or email
@Column()
actor: string; // wallet or email

@Column({ nullable: true, type: 'uuid' })
resourceId: string | null;
@Column({ nullable: true, type: 'uuid' })
resourceId: string | null;

@Column()
resourceType: string; // TRADE, DISPUTE, CLAIM
@Column()
resourceType: string; // TRADE, DISPUTE, CLAIM

@Column()
statusCode: number;
@Column()
statusCode: number;

@Column()
durationMs: number;
@Column()
durationMs: number;

@Column({ default: true })
success: boolean;
@Column({ default: true })
success: boolean;

@Column({ nullable: true, type: 'text' })
errorMessage: string | null;
@Column({ nullable: true, type: 'text' })
errorMessage: string | null;
}
223 changes: 110 additions & 113 deletions backend/src/common/interceptors/audit-log.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
Expand All @@ -12,7 +12,7 @@ import { throwError } from 'rxjs';

/**
* Audit Log Interceptor
*
*
* Logs structured audit entries for trade and dispute mutations.
* Captures:
* - Request ID (correlation ID)
Expand All @@ -21,122 +21,119 @@ import { throwError } from 'rxjs';
* - Trade/Dispute ID from params or body
* - Request/response status
* - Timestamp
*
*
* Enables forensic traceability for incident debugging.
*/
@Injectable()
export class AuditLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(AuditLogInterceptor.name);

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

// Extract correlation ID from request
const correlationId = (request as any).correlationId || 'unknown';

// Determine if this is a mutation endpoint (POST, PATCH, PUT, DELETE)
const isMutation = ['POST', 'PATCH', 'PUT', 'DELETE'].includes(
request.method,
);

// Extract audit-relevant paths
const isMutationEndpoint =
isMutation &&
(request.url.includes('/claims') ||
request.url.includes('/disputes') ||
request.url.includes('/trades'));

if (!isMutationEndpoint) {
return next.handle();
}

const startTime = Date.now();
const auditEntry = this.buildAuditEntry(request, correlationId);

return next.handle().pipe(
tap((data) => {
const duration = Date.now() - startTime;
this.logAuditEntry({
...auditEntry,
status: response.statusCode,
duration,
success: true,
});
}),
catchError((error) => {
const duration = Date.now() - startTime;
this.logAuditEntry({
...auditEntry,
status: error.status || 500,
duration,
success: false,
error: error.message,
});
return throwError(() => error);
}),
);
}
private readonly logger = new Logger(AuditLogInterceptor.name);

private buildAuditEntry(request: Request, correlationId: string) {
const body = request.body || {};
const params = request.params || {};

// Extract resource IDs
const tradeId = params.id || body.tradeId || body.claimId || null;
const disputeId = params.id || body.disputeId || null;
const resourceId = tradeId || disputeId;

// Extract actor (wallet or user email)
const actor =
body.actor ||
body.wallet ||
body.email ||
(request.user as any)?.email ||
'anonymous';

// Determine action type
const action = this.getActionType(request.method, request.url);

return {
correlationId,
timestamp: new Date().toISOString(),
endpoint: request.url,
method: request.method,
action,
actor,
resourceId,
resourceType: this.getResourceType(request.url),
};
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

private getActionType(method: string, url: string): string {
if (method === 'POST') return 'CREATE';
if (method === 'PATCH' || method === 'PUT') return 'UPDATE';
if (method === 'DELETE') return 'DELETE';
return 'UNKNOWN';
}
// Extract correlation ID from request
const correlationId = (request as any).correlationId || 'unknown';

private getResourceType(url: string): string {
if (url.includes('/claims')) return 'CLAIM';
if (url.includes('/disputes')) return 'DISPUTE';
if (url.includes('/trades')) return 'TRADE';
return 'UNKNOWN';
}
// Determine if this is a mutation endpoint (POST, PATCH, PUT, DELETE)
const isMutation = ['POST', 'PATCH', 'PUT', 'DELETE'].includes(
request.method,
);

private logAuditEntry(entry: any) {
const logMessage = `[AUDIT] ${entry.correlationId} | ${entry.action} ${entry.resourceType} | Actor: ${entry.actor} | Resource: ${entry.resourceId} | Status: ${entry.status} | Duration: ${entry.duration}ms`;
// Extract audit-relevant paths
const isMutationEndpoint =
isMutation &&
(request.url.includes('/claims') ||
request.url.includes('/disputes') ||
request.url.includes('/trades'));

if (entry.success) {
this.logger.log(logMessage);
} else {
this.logger.error(
`${logMessage} | Error: ${entry.error}`,
'AuditLog',
);
}
if (!isMutationEndpoint) {
return next.handle();
}

// Structured logging for log aggregation systems
this.logger.debug(JSON.stringify(entry), 'AuditLogStructured');
const startTime = Date.now();
const auditEntry = this.buildAuditEntry(request, correlationId);

return next.handle().pipe(
tap((data) => {
const duration = Date.now() - startTime;
this.logAuditEntry({
...auditEntry,
status: response.statusCode,
duration,
success: true,
});
}),
catchError((error) => {
const duration = Date.now() - startTime;
this.logAuditEntry({
...auditEntry,
status: error.status || 500,
duration,
success: false,
error: error.message,
});
return throwError(() => error);
}),
);
}

private buildAuditEntry(request: Request, correlationId: string) {
const body = request.body || {};
const params = request.params || {};

// Extract resource IDs
const tradeId = params.id || body.tradeId || body.claimId || null;
const disputeId = params.id || body.disputeId || null;
const resourceId = tradeId || disputeId;

// Extract actor (wallet or user email)
const actor =
body.actor ||
body.wallet ||
body.email ||
(request.user as any)?.email ||
'anonymous';

// Determine action type
const action = this.getActionType(request.method, request.url);

return {
correlationId,
timestamp: new Date().toISOString(),
endpoint: request.url,
method: request.method,
action,
actor,
resourceId,
resourceType: this.getResourceType(request.url),
};
}

private getActionType(method: string, url: string): string {
if (method === 'POST') return 'CREATE';
if (method === 'PATCH' || method === 'PUT') return 'UPDATE';
if (method === 'DELETE') return 'DELETE';
return 'UNKNOWN';
}

private getResourceType(url: string): string {
if (url.includes('/claims')) return 'CLAIM';
if (url.includes('/disputes')) return 'DISPUTE';
if (url.includes('/trades')) return 'TRADE';
return 'UNKNOWN';
}

private logAuditEntry(entry: any) {
const logMessage = `[AUDIT] ${entry.correlationId} | ${entry.action} ${entry.resourceType} | Actor: ${entry.actor} | Resource: ${entry.resourceId} | Status: ${entry.status} | Duration: ${entry.duration}ms`;

if (entry.success) {
this.logger.log(logMessage);
} else {
this.logger.error(`${logMessage} | Error: ${entry.error}`, 'AuditLog');
}

// Structured logging for log aggregation systems
this.logger.debug(JSON.stringify(entry), 'AuditLogStructured');
}
}
Loading
Loading