Audit Log Service
```typescript
// audit-log.ts
import { v4 as uuid } from 'uuid';
interface AuditEvent {
action: string; // e.g., "user.login", "order.create"
actor: {
id: string;
type: 'user' | 'system' | 'api_key';
email?: string;
};
resource: {
type: string; // e.g., "user", "order", "payment"
id?: string;
};
details?: Record;
outcome: 'success' | 'failure';
reason?: string; // For failures
}
interface AuditRecord extends AuditEvent {
id: string;
timestamp: Date;
requestId?: string;
ip?: string;
userAgent?: string;
organizationId?: string;
}
class AuditLogService {
private context: AsyncLocalStorage<{ requestId?: string; ip?: string; userAgent?: string }>;
constructor() {
this.context = new AsyncLocalStorage();
}
// Set request context (call from middleware)
setContext(ctx: { requestId?: string; ip?: string; userAgent?: string }) {
return this.context.run(ctx, () => {});
}
async record(event: AuditEvent): Promise {
const ctx = this.context.getStore() || {};
const record: AuditRecord = {
id: uuid(),
timestamp: new Date(),
...event,
requestId: ctx.requestId,
ip: ctx.ip,
userAgent: ctx.userAgent,
};
// Write to database (append-only table)
await db.auditLogs.create({ data: record });
// Also send to external logging (CloudWatch, DataDog, etc.)
if (process.env.AUDIT_LOG_STREAM) {
await this.sendToCloudWatch(record);
}
}
// Convenience methods
async logLogin(userId: string, success: boolean, details?: Record) {
await this.record({
action: 'user.login',
actor: { id: userId, type: 'user' },
resource: { type: 'session' },
outcome: success ? 'success' : 'failure',
details,
});
}
async logDataAccess(actorId: string, resourceType: string, resourceId: string) {
await this.record({
action: ${resourceType}.read,
actor: { id: actorId, type: 'user' },
resource: { type: resourceType, id: resourceId },
outcome: 'success',
});
}
async logDataChange(
actorId: string,
resourceType: string,
resourceId: string,
action: 'create' | 'update' | 'delete',
changes?: { before?: unknown; after?: unknown }
) {
await this.record({
action: ${resourceType}.${action},
actor: { id: actorId, type: 'user' },
resource: { type: resourceType, id: resourceId },
outcome: 'success',
details: changes,
});
}
private async sendToCloudWatch(record: AuditRecord) {
const cloudwatch = new CloudWatchLogsClient({});
await cloudwatch.send(new PutLogEventsCommand({
logGroupName: process.env.AUDIT_LOG_GROUP!,
logStreamName: process.env.AUDIT_LOG_STREAM!,
logEvents: [{
timestamp: record.timestamp.getTime(),
message: JSON.stringify(record),
}],
}));
}
}
export const auditLog = new AuditLogService();
```
Express Middleware
```typescript
// audit-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { auditLog } from './audit-log';
import { v4 as uuid } from 'uuid';
export function auditMiddleware(req: Request, res: Response, next: NextFunction) {
const requestId = req.headers['x-request-id'] as string || uuid();
const ip = req.ip || req.headers['x-forwarded-for'] as string;
const userAgent = req.headers['user-agent'];
// Set context for all audit logs in this request
auditLog.setContext({ requestId, ip, userAgent });
// Add request ID to response headers
res.setHeader('x-request-id', requestId);
next();
}
```
Database Schema
```sql
-- Append-only audit log table
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
action VARCHAR(100) NOT NULL,
actor_id VARCHAR(255) NOT NULL,
actor_type VARCHAR(50) NOT NULL,
actor_email VARCHAR(255),
resource_type VARCHAR(100) NOT NULL,
resource_id VARCHAR(255),
outcome VARCHAR(20) NOT NULL,
reason TEXT,
details JSONB,
request_id VARCHAR(255),
ip INET,
user_agent TEXT,
organization_id UUID
);
-- Indexes for common queries
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp DESC);
CREATE INDEX idx_audit_actor ON audit_logs(actor_id, timestamp DESC);
CREATE INDEX idx_audit_resource ON audit_logs(resource_type, resource_id, timestamp DESC);
CREATE INDEX idx_audit_action ON audit_logs(action, timestamp DESC);
CREATE INDEX idx_audit_org ON audit_logs(organization_id, timestamp DESC);
-- Prevent updates/deletes (append-only)
CREATE OR REPLACE FUNCTION prevent_audit_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Audit logs cannot be modified or deleted';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_immutable
BEFORE UPDATE OR DELETE ON audit_logs
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();
```