Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled

Full-stack web application for Telegram management
- Frontend: Vue 3 + Vben Admin
- Backend: NestJS
- Features: User management, group broadcast, statistics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
FROM node:18-alpine
# Install system dependencies
RUN apk add --no-cache python3 py3-pip make g++
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy application code
COPY . .
# Create nodejs user and group
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Create logs directory with proper permissions
RUN mkdir -p logs && chown -R nodejs:nodejs logs
# Expose port
EXPOSE 3006
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD node health-check.js || exit 1
# Switch to non-root user
USER nodejs
# Start the application
CMD ["node", "src/app.js"]

View File

@@ -0,0 +1,59 @@
{
"name": "compliance-guard-service",
"version": "1.0.0",
"description": "Compliance and regulatory service for Telegram Marketing Intelligence Agent",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src",
"format": "prettier --write src"
},
"keywords": [
"compliance",
"regulatory",
"privacy",
"gdpr",
"ccpa",
"telegram"
],
"author": "Marketing Agent Team",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.5.0",
"redis": "^4.6.5",
"amqplib": "^0.10.3",
"axios": "^1.5.0",
"joi": "^17.10.0",
"uuid": "^9.0.0",
"crypto-js": "^4.1.1",
"jsonwebtoken": "^9.0.2",
"rate-limiter-flexible": "^2.4.2",
"geoip-lite": "^1.4.7",
"node-cron": "^3.0.2",
"prom-client": "^14.2.0",
"winston": "^3.10.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"eslint": "^8.49.0",
"prettier": "^3.0.3",
"@types/jest": "^29.5.5"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "coverage",
"collectCoverageFrom": [
"src/**/*.js",
"!src/index.js"
]
}
}

View File

@@ -0,0 +1 @@
import './index.js';

View File

@@ -0,0 +1,106 @@
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: process.env.PORT || 3006,
env: process.env.NODE_ENV || 'development',
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/compliance',
options: {
useNewUrlParser: true,
useUnifiedTopology: true
}
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
prefix: process.env.REDIS_PREFIX || 'compliance:',
ttl: parseInt(process.env.REDIS_TTL || '3600')
},
rabbitmq: {
url: process.env.RABBITMQ_URL || 'amqp://localhost:5672',
exchange: process.env.RABBITMQ_EXCHANGE || 'compliance',
queues: {
violations: 'compliance.violations',
audits: 'compliance.audits',
consents: 'compliance.consents',
requests: 'compliance.requests'
}
},
compliance: {
regions: {
gdpr: ['EU', 'UK', 'EEA'],
ccpa: ['US-CA'],
lgpd: ['BR'],
pipeda: ['CA'],
appi: ['JP']
},
retentionPeriods: {
default: 365, // days
eu: 365,
us: 730,
marketing: 180,
analytics: 365
},
consentTypes: [
'marketing',
'analytics',
'personalization',
'data_sharing',
'cookies'
]
},
encryption: {
algorithm: process.env.ENCRYPTION_ALGORITHM || 'aes-256-gcm',
keyDerivation: process.env.KEY_DERIVATION || 'pbkdf2',
saltRounds: parseInt(process.env.SALT_ROUNDS || '10'),
encryptionKey: process.env.ENCRYPTION_KEY || 'your-encryption-key-here'
},
rateLimit: {
dataRequests: {
points: 10,
duration: 3600, // 1 hour
blockDuration: 3600
},
consentUpdates: {
points: 50,
duration: 86400, // 24 hours
blockDuration: 3600
}
},
monitoring: {
checkInterval: parseInt(process.env.CHECK_INTERVAL || '300000'), // 5 minutes
violationThreshold: parseInt(process.env.VIOLATION_THRESHOLD || '5'),
alertChannels: process.env.ALERT_CHANNELS?.split(',') || ['email', 'slack']
},
metrics: {
port: parseInt(process.env.METRICS_PORT || '9106'),
prefix: process.env.METRICS_PREFIX || 'compliance_guard_'
},
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'logs/compliance.log',
auditFile: process.env.AUDIT_LOG_FILE || 'logs/audit.log'
},
jwt: {
secret: process.env.JWT_SECRET || 'your-compliance-secret',
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
},
notifications: {
email: {
from: process.env.EMAIL_FROM || 'compliance@example.com',
dpo: process.env.DPO_EMAIL || 'dpo@example.com'
}
}
};

View File

@@ -0,0 +1,104 @@
import express from 'express';
import mongoose from 'mongoose';
import Redis from 'redis';
import { config } from './config/index.js';
import { logger } from './utils/logger.js';
import { setupRoutes } from './routes/index.js';
import { connectRabbitMQ } from './services/messaging.js';
import { startMetricsServer } from './utils/metrics.js';
import { ComplianceMonitor } from './services/complianceMonitor.js';
import { DataProtectionService } from './services/dataProtection.js';
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
service: 'compliance-guard',
timestamp: new Date().toISOString()
});
});
// Setup routes
setupRoutes(app);
// Error handling
app.use((err, req, res, next) => {
logger.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
message: err.message
});
});
// Initialize services
async function initializeServices() {
try {
// Connect to MongoDB
await mongoose.connect(config.mongodb.uri, config.mongodb.options);
logger.info('Connected to MongoDB');
// Connect to Redis
const redisClient = Redis.createClient({ url: config.redis.url });
await redisClient.connect();
logger.info('Connected to Redis');
// Store Redis client globally
app.locals.redis = redisClient;
// Connect to RabbitMQ
const channel = await connectRabbitMQ();
app.locals.rabbitmq = channel;
// Initialize compliance monitor
const complianceMonitor = new ComplianceMonitor(redisClient);
await complianceMonitor.start();
app.locals.complianceMonitor = complianceMonitor;
// Initialize data protection service
const dataProtection = new DataProtectionService();
app.locals.dataProtection = dataProtection;
// Start metrics server
startMetricsServer(config.metrics.port);
// Start main server
app.listen(config.port, () => {
logger.info(`Compliance Guard service listening on port ${config.port}`);
});
} catch (error) {
logger.error('Failed to initialize services:', error);
process.exit(1);
}
}
// Graceful shutdown
process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully');
try {
if (app.locals.complianceMonitor) {
await app.locals.complianceMonitor.stop();
}
if (app.locals.redis) {
await app.locals.redis.quit();
}
await mongoose.connection.close();
process.exit(0);
} catch (error) {
logger.error('Error during shutdown:', error);
process.exit(1);
}
});
// Start the service
initializeServices();

View File

@@ -0,0 +1,148 @@
import { logger } from '../utils/logger.js';
import { metrics } from '../utils/metrics.js';
/**
* Error handling middleware
*/
export const errorHandler = (err, req, res, next) => {
// Log error
logger.error('Error occurred:', {
error: {
message: err.message,
stack: err.stack,
code: err.code,
statusCode: err.statusCode
},
request: {
method: req.method,
url: req.url,
ip: req.ip,
userId: req.user?.id
}
});
// Record error metric
metrics.recordApiCall(req.route?.path || 'unknown', 'error');
// Determine status code
const statusCode = err.statusCode || err.status || 500;
// Prepare error response
const errorResponse = {
success: false,
error: {
message: err.message || 'Internal server error',
code: err.code || 'INTERNAL_ERROR'
}
};
// Add additional error details in development
if (process.env.NODE_ENV === 'development') {
errorResponse.error.stack = err.stack;
errorResponse.error.details = err.details;
}
// Handle specific error types
if (err.name === 'ValidationError') {
errorResponse.error.code = 'VALIDATION_ERROR';
errorResponse.error.details = err.details || [];
return res.status(400).json(errorResponse);
}
if (err.name === 'MongoError' && err.code === 11000) {
errorResponse.error.code = 'DUPLICATE_ENTRY';
errorResponse.error.message = 'Duplicate entry found';
return res.status(409).json(errorResponse);
}
if (err.name === 'CastError') {
errorResponse.error.code = 'INVALID_ID';
errorResponse.error.message = 'Invalid ID format';
return res.status(400).json(errorResponse);
}
// Default error response
res.status(statusCode).json(errorResponse);
};
/**
* Async error wrapper
*/
export const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
/**
* Not found middleware
*/
export const notFound = (req, res, next) => {
const error = new Error(`Not found - ${req.originalUrl}`);
error.statusCode = 404;
next(error);
};
/**
* Request validation middleware
*/
export const validateRequest = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const validationError = new Error('Validation failed');
validationError.name = 'ValidationError';
validationError.details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return next(validationError);
}
next();
};
};
/**
* Rate limit error handler
*/
export const rateLimitHandler = (req, res) => {
logger.warn('Rate limit exceeded:', {
ip: req.ip,
userId: req.user?.id,
endpoint: req.path
});
res.status(429).json({
success: false,
error: {
message: 'Too many requests',
code: 'RATE_LIMIT_EXCEEDED',
retryAfter: res.getHeader('Retry-After')
}
});
};
/**
* Timeout middleware
*/
export const timeout = (seconds = 30) => {
return (req, res, next) => {
const timeoutId = setTimeout(() => {
const error = new Error('Request timeout');
error.statusCode = 408;
error.code = 'REQUEST_TIMEOUT';
next(error);
}, seconds * 1000);
res.on('finish', () => {
clearTimeout(timeoutId);
});
next();
};
};

View File

@@ -0,0 +1,235 @@
import mongoose from 'mongoose';
const auditLogSchema = new mongoose.Schema({
// Multi-tenant support
tenantId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Tenant',
required: true,
index: true
},
auditId: {
type: String,
required: true,
unique: true,
index: true
},
accountId: {
type: String,
required: true,
index: true
},
userId: {
type: String,
index: true
},
action: {
type: String,
required: true,
index: true
},
category: {
type: String,
required: true,
enum: ['consent', 'data_access', 'data_modification', 'data_deletion', 'security', 'configuration', 'compliance'],
index: true
},
resource: {
type: String,
required: true
},
resourceId: {
type: String,
index: true
},
details: {
before: mongoose.Schema.Types.Mixed,
after: mongoose.Schema.Types.Mixed,
changes: [String],
reason: String,
metadata: mongoose.Schema.Types.Mixed
},
result: {
status: {
type: String,
enum: ['success', 'failure', 'partial'],
required: true
},
error: String,
affectedRecords: Number
},
actor: {
type: {
type: String,
enum: ['user', 'admin', 'system', 'api'],
required: true
},
id: String,
name: String,
role: String,
ip: String,
userAgent: String,
location: {
country: String,
region: String,
city: String
}
},
compliance: {
regulation: String,
legalBasis: String,
retention: {
days: {
type: Number,
default: 2555 // 7 years default
},
expiryDate: Date
}
},
security: {
encrypted: {
type: Boolean,
default: true
},
signed: {
type: Boolean,
default: true
},
signature: String,
hash: String
},
timestamp: {
type: Date,
default: Date.now,
required: true,
index: true
}
}, {
timestamps: false // We use custom timestamp field
});
// Indexes
auditLogSchema.index({ accountId: 1, timestamp: -1 });
auditLogSchema.index({ userId: 1, category: 1, timestamp: -1 });
auditLogSchema.index({ 'compliance.retention.expiryDate': 1 }, { sparse: true });
// Multi-tenant indexes
auditLogSchema.index({ tenantId: 1, accountId: 1, timestamp: -1 });
auditLogSchema.index({ tenantId: 1, userId: 1, category: 1, timestamp: -1 });
auditLogSchema.index({ tenantId: 1, 'compliance.retention.expiryDate': 1 }, { sparse: true });
// Make audit logs immutable
auditLogSchema.pre('save', function(next) {
if (!this.isNew) {
return next(new Error('Audit logs cannot be modified'));
}
// Calculate retention expiry
if (this.compliance.retention.days) {
const expiryDate = new Date(this.timestamp);
expiryDate.setDate(expiryDate.getDate() + this.compliance.retention.days);
this.compliance.retention.expiryDate = expiryDate;
}
next();
});
// Prevent updates and deletes
auditLogSchema.pre(['update', 'updateOne', 'updateMany', 'findOneAndUpdate'], function() {
throw new Error('Audit logs cannot be updated');
});
auditLogSchema.pre(['remove', 'deleteOne', 'deleteMany', 'findOneAndDelete'], function() {
throw new Error('Audit logs cannot be deleted manually');
});
// Methods
auditLogSchema.methods.isExpired = function() {
return this.compliance.retention.expiryDate &&
this.compliance.retention.expiryDate < new Date();
};
// Statics
auditLogSchema.statics.log = async function(data) {
const audit = new this({
...data,
auditId: `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
});
return await audit.save();
};
auditLogSchema.statics.search = function(criteria) {
const query = {};
if (criteria.accountId) query.accountId = criteria.accountId;
if (criteria.userId) query.userId = criteria.userId;
if (criteria.category) query.category = criteria.category;
if (criteria.action) query.action = new RegExp(criteria.action, 'i');
if (criteria.resource) query.resource = criteria.resource;
if (criteria.resourceId) query.resourceId = criteria.resourceId;
if (criteria.startDate || criteria.endDate) {
query.timestamp = {};
if (criteria.startDate) query.timestamp.$gte = criteria.startDate;
if (criteria.endDate) query.timestamp.$lte = criteria.endDate;
}
if (criteria.actorType) query['actor.type'] = criteria.actorType;
if (criteria.actorId) query['actor.id'] = criteria.actorId;
if (criteria.resultStatus) query['result.status'] = criteria.resultStatus;
return this.find(query).sort({ timestamp: -1 });
};
auditLogSchema.statics.getActivitySummary = async function(accountId, period = 7) {
const startDate = new Date();
startDate.setDate(startDate.getDate() - period);
return await this.aggregate([
{
$match: {
accountId,
timestamp: { $gte: startDate }
}
},
{
$group: {
_id: {
date: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } },
category: '$category',
status: '$result.status'
},
count: { $sum: 1 }
}
},
{
$group: {
_id: {
date: '$_id.date',
category: '$_id.category'
},
total: { $sum: '$count' },
success: {
$sum: { $cond: [{ $eq: ['$_id.status', 'success'] }, '$count', 0] }
},
failure: {
$sum: { $cond: [{ $eq: ['$_id.status', 'failure'] }, '$count', 0] }
}
}
},
{
$sort: { '_id.date': -1, '_id.category': 1 }
}
]);
};
auditLogSchema.statics.cleanupExpired = async function() {
const result = await this.deleteMany({
'compliance.retention.expiryDate': { $lt: new Date() }
});
return result.deletedCount;
};
export const AuditLog = mongoose.model('AuditLog', auditLogSchema);

View File

@@ -0,0 +1,299 @@
import mongoose from 'mongoose';
const violationSchema = new mongoose.Schema({
// Multi-tenant support
tenantId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Tenant',
required: true,
index: true
},
violationId: {
type: String,
required: true,
unique: true
},
accountId: {
type: String,
required: true,
index: true
},
type: {
type: String,
required: true,
enum: [
'data_retention',
'consent_missing',
'consent_expired',
'data_access_denied',
'processing_without_basis',
'cross_border_transfer',
'security_breach',
'notification_failure',
'age_verification',
'opt_out_failure',
'excessive_data_collection',
'purpose_limitation',
'third_party_sharing'
]
},
severity: {
type: String,
required: true,
enum: ['low', 'medium', 'high', 'critical'],
default: 'medium'
},
status: {
type: String,
required: true,
enum: ['detected', 'investigating', 'confirmed', 'remediated', 'closed', 'escalated'],
default: 'detected'
},
details: {
description: String,
affectedUsers: Number,
affectedData: [String],
regulation: {
type: String,
enum: ['gdpr', 'ccpa', 'lgpd', 'pipeda', 'appi', 'other']
},
articles: [String], // Violated articles/sections
detectionMethod: {
type: String,
enum: ['automated', 'manual', 'user_report', 'audit', 'external']
},
evidence: [{
type: String,
data: mongoose.Schema.Types.Mixed,
timestamp: Date
}]
},
timeline: {
detectedAt: {
type: Date,
default: Date.now
},
investigationStarted: Date,
confirmedAt: Date,
remediationStarted: Date,
remediationCompleted: Date,
closedAt: Date,
deadlines: {
notification: Date,
remediation: Date,
reporting: Date
}
},
remediation: {
plan: String,
actions: [{
action: String,
responsible: String,
deadline: Date,
status: {
type: String,
enum: ['pending', 'in_progress', 'completed', 'failed']
},
completedAt: Date,
notes: String
}],
preventiveMeasures: [String],
cost: {
estimated: Number,
actual: Number,
currency: {
type: String,
default: 'USD'
}
}
},
notifications: {
authorities: [{
name: String,
notifiedAt: Date,
method: String,
reference: String,
response: String
}],
users: {
required: Boolean,
notifiedCount: Number,
notificationMethod: String,
notifiedAt: Date,
template: String
},
internal: [{
role: String,
name: String,
notifiedAt: Date
}]
},
risk: {
score: {
type: Number,
min: 0,
max: 10
},
likelihood: {
type: String,
enum: ['very_low', 'low', 'medium', 'high', 'very_high']
},
impact: {
type: String,
enum: ['minimal', 'low', 'moderate', 'high', 'severe']
},
financialExposure: {
min: Number,
max: Number,
currency: {
type: String,
default: 'USD'
}
}
},
investigation: {
assignedTo: String,
team: [String],
findings: String,
rootCause: String,
contributingFactors: [String],
recommendations: [String]
},
compliance: {
reportable: {
type: Boolean,
default: false
},
reportingDeadline: Date,
fineAmount: Number,
appealStatus: String,
correctionNotice: String
},
metadata: {
source: {
service: String,
component: String,
version: String
},
tags: [String],
relatedViolations: [String],
parentViolation: String
}
}, {
timestamps: true
});
// Indexes
violationSchema.index({ accountId: 1, status: 1, severity: 1 });
violationSchema.index({ 'timeline.detectedAt': -1 });
violationSchema.index({ type: 1, status: 1 });
violationSchema.index({ 'compliance.reportingDeadline': 1 }, { sparse: true });
// Multi-tenant indexes
violationSchema.index({ tenantId: 1, accountId: 1, status: 1, severity: 1 });
violationSchema.index({ tenantId: 1, 'timeline.detectedAt': -1 });
violationSchema.index({ tenantId: 1, type: 1, status: 1 });
violationSchema.index({ tenantId: 1, 'compliance.reportingDeadline': 1 }, { sparse: true });
// Methods
violationSchema.methods.calculateRiskScore = function() {
const severityScores = { low: 2, medium: 4, high: 7, critical: 10 };
const impactScores = { minimal: 1, low: 2, moderate: 4, high: 7, severe: 10 };
const likelihoodScores = { very_low: 1, low: 2, medium: 4, high: 7, very_high: 10 };
const severityScore = severityScores[this.severity] || 5;
const impactScore = impactScores[this.risk.impact] || 5;
const likelihoodScore = likelihoodScores[this.risk.likelihood] || 5;
// Weighted calculation
this.risk.score = Math.min(10, (severityScore * 0.4 + impactScore * 0.4 + likelihoodScore * 0.2));
return this.risk.score;
};
violationSchema.methods.isOverdue = function() {
const now = new Date();
if (this.timeline.deadlines.notification && this.timeline.deadlines.notification < now &&
(!this.notifications.authorities || this.notifications.authorities.length === 0)) {
return true;
}
if (this.timeline.deadlines.remediation && this.timeline.deadlines.remediation < now &&
this.status !== 'remediated' && this.status !== 'closed') {
return true;
}
return false;
};
violationSchema.methods.needsEscalation = function() {
return this.severity === 'critical' ||
this.risk.score > 8 ||
this.isOverdue() ||
this.details.affectedUsers > 1000;
};
// Statics
violationSchema.statics.findActive = function(accountId) {
const query = {
status: { $nin: ['closed', 'remediated'] }
};
if (accountId) query.accountId = accountId;
return this.find(query).sort({ 'risk.score': -1, 'timeline.detectedAt': -1 });
};
violationSchema.statics.findRequiringNotification = function() {
return this.find({
'compliance.reportable': true,
'notifications.authorities': { $size: 0 },
'timeline.deadlines.notification': { $exists: true }
}).sort({ 'timeline.deadlines.notification': 1 });
};
violationSchema.statics.getViolationStats = async function(accountId, period) {
const startDate = new Date();
startDate.setDate(startDate.getDate() - (period || 30));
const match = {
'timeline.detectedAt': { $gte: startDate }
};
if (accountId) match.accountId = accountId;
return await this.aggregate([
{ $match: match },
{
$group: {
_id: {
type: '$type',
severity: '$severity',
status: '$status'
},
count: { $sum: 1 },
avgRiskScore: { $avg: '$risk.score' },
totalAffectedUsers: { $sum: '$details.affectedUsers' }
}
},
{
$group: {
_id: null,
total: { $sum: '$count' },
byType: {
$push: {
type: '$_id.type',
severity: '$_id.severity',
status: '$_id.status',
count: '$count'
}
},
avgRiskScore: { $avg: '$avgRiskScore' },
totalAffectedUsers: { $sum: '$totalAffectedUsers' }
}
}
]);
};
export const ComplianceViolation = mongoose.model('ComplianceViolation', violationSchema);

View File

@@ -0,0 +1,168 @@
import mongoose from 'mongoose';
const consentSchema = new mongoose.Schema({
// Multi-tenant support
tenantId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Tenant',
required: true,
index: true
},
consentId: {
type: String,
required: true,
unique: true
},
userId: {
type: String,
required: true,
index: true
},
accountId: {
type: String,
required: true,
index: true
},
type: {
type: String,
required: true,
enum: ['marketing', 'analytics', 'personalization', 'data_sharing', 'cookies']
},
status: {
type: String,
required: true,
enum: ['granted', 'denied', 'withdrawn'],
default: 'denied'
},
scope: {
type: String,
enum: ['all', 'specific'],
default: 'all'
},
specificScopes: [{
category: String,
granted: Boolean
}],
version: {
type: String,
required: true
},
language: {
type: String,
default: 'en'
},
method: {
type: String,
required: true,
enum: ['explicit', 'implicit', 'opt-out']
},
source: {
type: String,
required: true,
enum: ['web', 'app', 'api', 'import', 'manual']
},
metadata: {
ip: String,
userAgent: String,
location: {
country: String,
region: String,
city: String
},
referrer: String,
sessionId: String
},
legalBasis: {
type: String,
enum: ['consent', 'contract', 'legal_obligation', 'vital_interests', 'public_task', 'legitimate_interests']
},
withdrawalReason: String,
expiryDate: Date,
renewable: {
type: Boolean,
default: true
},
parentConsentId: String, // For consent updates/renewals
audit: {
createdBy: String,
updatedBy: String,
approvedBy: String,
notes: String
}
}, {
timestamps: true
});
// Indexes
consentSchema.index({ userId: 1, type: 1, createdAt: -1 });
consentSchema.index({ accountId: 1, status: 1 });
consentSchema.index({ expiryDate: 1 }, { sparse: true });
// Multi-tenant indexes
consentSchema.index({ tenantId: 1, userId: 1, type: 1, createdAt: -1 });
consentSchema.index({ tenantId: 1, accountId: 1, status: 1 });
consentSchema.index({ tenantId: 1, expiryDate: 1 }, { sparse: true });
// Methods
consentSchema.methods.isActive = function() {
if (this.status !== 'granted') return false;
if (this.expiryDate && this.expiryDate < new Date()) return false;
return true;
};
consentSchema.methods.needsRenewal = function(daysBeforeExpiry = 30) {
if (!this.renewable || !this.expiryDate) return false;
if (this.status !== 'granted') return false;
const expiryThreshold = new Date();
expiryThreshold.setDate(expiryThreshold.getDate() + daysBeforeExpiry);
return this.expiryDate <= expiryThreshold;
};
consentSchema.methods.withdraw = function(reason) {
this.status = 'withdrawn';
this.withdrawalReason = reason;
this.updatedAt = new Date();
};
// Statics
consentSchema.statics.findActiveConsents = function(userId, types) {
const query = {
userId,
status: 'granted',
$or: [
{ expiryDate: { $exists: false } },
{ expiryDate: { $gt: new Date() } }
]
};
if (types && types.length > 0) {
query.type = { $in: types };
}
return this.find(query);
};
consentSchema.statics.hasConsent = async function(userId, type) {
const consent = await this.findOne({
userId,
type,
status: 'granted',
$or: [
{ expiryDate: { $exists: false } },
{ expiryDate: { $gt: new Date() } }
]
}).sort({ createdAt: -1 });
return consent?.isActive() || false;
};
consentSchema.statics.getConsentHistory = function(userId, type) {
const query = { userId };
if (type) query.type = type;
return this.find(query).sort({ createdAt: -1 });
};
export const ConsentRecord = mongoose.model('ConsentRecord', consentSchema);

View File

@@ -0,0 +1,266 @@
import mongoose from 'mongoose';
const dataRequestSchema = new mongoose.Schema({
// Multi-tenant support
tenantId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Tenant',
required: true,
index: true
},
requestId: {
type: String,
required: true,
unique: true
},
userId: {
type: String,
required: true,
index: true
},
accountId: {
type: String,
required: true,
index: true
},
type: {
type: String,
required: true,
enum: ['access', 'portability', 'deletion', 'rectification', 'restriction', 'objection']
},
status: {
type: String,
required: true,
enum: ['pending', 'processing', 'completed', 'rejected', 'expired'],
default: 'pending'
},
priority: {
type: String,
enum: ['low', 'medium', 'high', 'urgent'],
default: 'medium'
},
requestDetails: {
scope: {
type: String,
enum: ['all', 'specific'],
default: 'all'
},
dataCategories: [String],
dateRange: {
start: Date,
end: Date
},
format: {
type: String,
enum: ['json', 'csv', 'pdf', 'xml'],
default: 'json'
},
reason: String,
additionalInfo: String
},
verification: {
method: {
type: String,
enum: ['email', 'sms', 'document', 'manual']
},
verified: {
type: Boolean,
default: false
},
verifiedAt: Date,
verifiedBy: String,
attempts: {
type: Number,
default: 0
}
},
processing: {
startedAt: Date,
completedAt: Date,
processedBy: String,
dataCollected: {
categories: [String],
recordCount: Number,
sizeInBytes: Number
},
errors: [{
timestamp: Date,
message: String,
service: String
}]
},
response: {
method: {
type: String,
enum: ['download', 'email', 'api', 'mail']
},
deliveredAt: Date,
downloadUrl: String,
downloadExpiry: Date,
emailSent: Boolean,
physicalMailTracking: String
},
compliance: {
regulation: {
type: String,
enum: ['gdpr', 'ccpa', 'lgpd', 'pipeda', 'appi', 'other']
},
deadline: Date,
sla: {
type: Number,
default: 30 // days
},
articles: [String], // Relevant articles/sections
legalBasis: String
},
rejection: {
reason: String,
legalJustification: String,
appealDeadline: Date,
rejectedBy: String,
rejectedAt: Date
},
audit: {
ipAddress: String,
userAgent: String,
channel: {
type: String,
enum: ['web', 'email', 'phone', 'mail', 'api']
},
notes: [{
timestamp: Date,
author: String,
note: String
}]
},
costs: {
estimatedCost: Number,
actualCost: Number,
currency: {
type: String,
default: 'USD'
},
waived: {
type: Boolean,
default: true
}
}
}, {
timestamps: true
});
// Indexes
dataRequestSchema.index({ userId: 1, type: 1, createdAt: -1 });
dataRequestSchema.index({ status: 1, 'compliance.deadline': 1 });
dataRequestSchema.index({ accountId: 1, status: 1 });
// Multi-tenant indexes
dataRequestSchema.index({ tenantId: 1, userId: 1, type: 1, createdAt: -1 });
dataRequestSchema.index({ tenantId: 1, status: 1, 'compliance.deadline': 1 });
dataRequestSchema.index({ tenantId: 1, accountId: 1, status: 1 });
// Methods
dataRequestSchema.methods.isOverdue = function() {
if (this.status === 'completed' || this.status === 'rejected') return false;
return this.compliance.deadline && this.compliance.deadline < new Date();
};
dataRequestSchema.methods.getDaysRemaining = function() {
if (!this.compliance.deadline) return null;
const now = new Date();
const diffTime = this.compliance.deadline - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
dataRequestSchema.methods.canProcess = function() {
return this.status === 'pending' && this.verification.verified;
};
dataRequestSchema.methods.markProcessing = function(processedBy) {
this.status = 'processing';
this.processing.startedAt = new Date();
this.processing.processedBy = processedBy;
};
dataRequestSchema.methods.markCompleted = function(responseData) {
this.status = 'completed';
this.processing.completedAt = new Date();
Object.assign(this.response, responseData);
};
dataRequestSchema.methods.reject = function(reason, legalJustification, rejectedBy) {
this.status = 'rejected';
this.rejection = {
reason,
legalJustification,
rejectedBy,
rejectedAt: new Date(),
appealDeadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
};
};
// Statics
dataRequestSchema.statics.findPending = function(accountId) {
const query = { status: 'pending' };
if (accountId) query.accountId = accountId;
return this.find(query).sort({ createdAt: 1 });
};
dataRequestSchema.statics.findOverdue = function(accountId) {
const query = {
status: { $in: ['pending', 'processing'] },
'compliance.deadline': { $lt: new Date() }
};
if (accountId) query.accountId = accountId;
return this.find(query);
};
dataRequestSchema.statics.getRequestStats = async function(accountId, startDate, endDate) {
const match = { accountId };
if (startDate || endDate) {
match.createdAt = {};
if (startDate) match.createdAt.$gte = startDate;
if (endDate) match.createdAt.$lte = endDate;
}
return await this.aggregate([
{ $match: match },
{
$group: {
_id: {
type: '$type',
status: '$status'
},
count: { $sum: 1 },
avgProcessingTime: {
$avg: {
$cond: [
{ $and: ['$processing.startedAt', '$processing.completedAt'] },
{ $subtract: ['$processing.completedAt', '$processing.startedAt'] },
null
]
}
}
}
},
{
$group: {
_id: '$_id.type',
statuses: {
$push: {
status: '$_id.status',
count: '$count'
}
},
totalCount: { $sum: '$count' },
avgProcessingTime: { $avg: '$avgProcessingTime' }
}
}
]);
};
export const DataRequest = mongoose.model('DataRequest', dataRequestSchema);

View File

@@ -0,0 +1,364 @@
import express from 'express';
import { AuditLog } from '../models/AuditLog.js';
import { AuditService } from '../services/auditService.js';
import { auth } from '../utils/auth.js';
import { metrics } from '../utils/metrics.js';
import { logger } from '../utils/logger.js';
const router = express.Router();
const auditService = new AuditService();
/**
* Search audit logs
*/
router.get('/audit-logs', auth(), async (req, res) => {
try {
const {
accountId,
userId,
category,
action,
resource,
startDate,
endDate,
page = 1,
limit = 50
} = req.query;
const criteria = {
accountId: accountId || req.user.accountId,
userId,
category,
action,
resource,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
page: parseInt(page),
limit: parseInt(limit)
};
const logs = await auditService.searchLogs(criteria);
res.json({
success: true,
data: logs,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
hasMore: logs.length === parseInt(limit)
}
});
metrics.recordApiCall('audit.search', 'success');
} catch (error) {
logger.error('Failed to search audit logs:', error);
metrics.recordApiCall('audit.search', 'error');
res.status(500).json({
success: false,
error: 'Failed to search audit logs'
});
}
});
/**
* Get audit trail for a resource
*/
router.get('/audit-trail/:resourceType/:resourceId', auth(), async (req, res) => {
try {
const { resourceType, resourceId } = req.params;
const trail = await auditService.getAuditTrail(resourceId, resourceType);
res.json({
success: true,
data: trail,
count: trail.length
});
metrics.recordApiCall('audit.trail', 'success');
} catch (error) {
logger.error('Failed to get audit trail:', error);
metrics.recordApiCall('audit.trail', 'error');
res.status(500).json({
success: false,
error: 'Failed to get audit trail'
});
}
});
/**
* Generate compliance report
*/
router.post('/audit-reports/compliance', auth(), async (req, res) => {
try {
const { startDate, endDate } = req.body;
if (!startDate || !endDate) {
return res.status(400).json({
success: false,
error: 'Start date and end date are required'
});
}
const report = await auditService.generateComplianceReport(
req.user.accountId,
new Date(startDate),
new Date(endDate)
);
res.json({
success: true,
data: report
});
metrics.recordApiCall('audit.compliance_report', 'success');
} catch (error) {
logger.error('Failed to generate compliance report:', error);
metrics.recordApiCall('audit.compliance_report', 'error');
res.status(500).json({
success: false,
error: 'Failed to generate compliance report'
});
}
});
/**
* Detect anomalies
*/
router.get('/audit-anomalies', auth(), async (req, res) => {
try {
const { window = 3600000 } = req.query; // Default 1 hour
const anomalies = await auditService.detectAnomalies(
req.user.accountId,
parseInt(window)
);
res.json({
success: true,
data: anomalies,
count: anomalies.length,
window: parseInt(window)
});
metrics.recordApiCall('audit.anomalies', 'success');
} catch (error) {
logger.error('Failed to detect anomalies:', error);
metrics.recordApiCall('audit.anomalies', 'error');
res.status(500).json({
success: false,
error: 'Failed to detect anomalies'
});
}
});
/**
* Get activity summary
*/
router.get('/audit-summary', auth(), async (req, res) => {
try {
const { days = 7 } = req.query;
const summary = await AuditLog.getActivitySummary(
req.user.accountId,
parseInt(days)
);
res.json({
success: true,
data: summary,
period: {
days: parseInt(days),
startDate: new Date(Date.now() - parseInt(days) * 24 * 60 * 60 * 1000),
endDate: new Date()
}
});
metrics.recordApiCall('audit.summary', 'success');
} catch (error) {
logger.error('Failed to get activity summary:', error);
metrics.recordApiCall('audit.summary', 'error');
res.status(500).json({
success: false,
error: 'Failed to get activity summary'
});
}
});
/**
* Get audit log by ID
*/
router.get('/audit-logs/:auditId', auth(), async (req, res) => {
try {
const { auditId } = req.params;
const log = await AuditLog.findOne({ auditId });
if (!log) {
return res.status(404).json({
success: false,
error: 'Audit log not found'
});
}
// Check access permissions
if (log.accountId !== req.user.accountId) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
res.json({
success: true,
data: log
});
metrics.recordApiCall('audit.get', 'success');
} catch (error) {
logger.error('Failed to get audit log:', error);
metrics.recordApiCall('audit.get', 'error');
res.status(500).json({
success: false,
error: 'Failed to get audit log'
});
}
});
/**
* Export audit logs
*/
router.post('/audit-export', auth(), async (req, res) => {
try {
const {
format = 'json',
startDate,
endDate,
category,
compress = false
} = req.body;
const criteria = {
accountId: req.user.accountId,
startDate: startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
endDate: endDate ? new Date(endDate) : new Date(),
category
};
const logs = await auditService.searchLogs(criteria);
let exportData;
let contentType;
let filename = `audit_export_${Date.now()}`;
switch (format) {
case 'csv':
exportData = convertToCSV(logs);
contentType = 'text/csv';
filename += '.csv';
break;
case 'json':
default:
exportData = JSON.stringify(logs, null, 2);
contentType = 'application/json';
filename += '.json';
break;
}
if (compress) {
// In production, implement compression
filename += '.gz';
contentType = 'application/gzip';
}
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(exportData);
metrics.recordApiCall('audit.export', 'success');
} catch (error) {
logger.error('Failed to export audit logs:', error);
metrics.recordApiCall('audit.export', 'error');
res.status(500).json({
success: false,
error: 'Failed to export audit logs'
});
}
});
/**
* Create manual audit log entry
*/
router.post('/audit-logs', auth(), async (req, res) => {
try {
const eventData = {
...req.body,
accountId: req.user.accountId,
actor: {
type: 'user',
id: req.user.id,
ip: req.ip,
userAgent: req.get('user-agent')
}
};
const auditLog = await auditService.logEvent(eventData);
res.status(201).json({
success: true,
data: auditLog
});
metrics.recordApiCall('audit.create', 'success');
} catch (error) {
logger.error('Failed to create audit log:', error);
metrics.recordApiCall('audit.create', 'error');
res.status(500).json({
success: false,
error: 'Failed to create audit log'
});
}
});
/**
* Helper function to convert to CSV
*/
function convertToCSV(logs) {
if (logs.length === 0) return '';
const headers = [
'Timestamp',
'Audit ID',
'Action',
'Category',
'Resource',
'Resource ID',
'Actor Type',
'Actor ID',
'Result',
'IP Address'
];
const rows = logs.map(log => [
log.timestamp,
log.auditId,
log.action,
log.category,
log.resource || '',
log.resourceId || '',
log.actor.type,
log.actor.id,
log.result.status,
log.actor.ip || ''
]);
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell =>
typeof cell === 'string' && cell.includes(',') ? `"${cell}"` : cell
).join(','))
].join('\n');
return csvContent;
}
export default router;

View File

@@ -0,0 +1,369 @@
import express from 'express';
import { ConsentRecord } from '../models/ConsentRecord.js';
import { AuditLog } from '../models/AuditLog.js';
import { auth } from '../utils/auth.js';
import { validateConsent } from '../utils/validation.js';
import { publishEvent } from '../services/messaging.js';
import { metrics } from '../utils/metrics.js';
import { logger } from '../utils/logger.js';
const router = express.Router();
/**
* Get consent records for a user
*/
router.get('/users/:userId/consents', auth(), async (req, res) => {
try {
const { userId } = req.params;
const { accountId, type, status } = req.query;
const query = { userId };
if (accountId) query.accountId = accountId;
if (type) query.type = type;
if (status) query.status = status;
const consents = await ConsentRecord.find(query)
.sort({ grantedAt: -1 })
.limit(100);
res.json({
success: true,
data: consents,
count: consents.length
});
metrics.recordApiCall('consent.list', 'success');
} catch (error) {
logger.error('Failed to get consent records:', error);
metrics.recordApiCall('consent.list', 'error');
res.status(500).json({
success: false,
error: 'Failed to retrieve consent records'
});
}
});
/**
* Get specific consent record
*/
router.get('/consents/:consentId', auth(), async (req, res) => {
try {
const { consentId } = req.params;
const consent = await ConsentRecord.findOne({ consentId });
if (!consent) {
return res.status(404).json({
success: false,
error: 'Consent record not found'
});
}
res.json({
success: true,
data: consent
});
metrics.recordApiCall('consent.get', 'success');
} catch (error) {
logger.error('Failed to get consent record:', error);
metrics.recordApiCall('consent.get', 'error');
res.status(500).json({
success: false,
error: 'Failed to retrieve consent record'
});
}
});
/**
* Grant consent
*/
router.post('/users/:userId/consents', auth(), validateConsent, async (req, res) => {
try {
const { userId } = req.params;
const consentData = {
...req.body,
userId,
accountId: req.user.accountId,
metadata: {
ipAddress: req.ip,
userAgent: req.get('user-agent'),
location: req.body.location // Should be determined by IP in production
}
};
// Check for existing consent
const existing = await ConsentRecord.findOne({
userId,
accountId: consentData.accountId,
type: consentData.type,
status: 'granted'
});
if (existing) {
return res.status(400).json({
success: false,
error: 'Consent already granted for this type'
});
}
// Create consent record
const consent = await ConsentRecord.grantConsent(consentData);
// Audit log
await AuditLog.log({
accountId: consentData.accountId,
userId,
action: 'consent_granted',
category: 'consent',
resource: 'consent_record',
resourceId: consent.consentId,
details: {
type: consent.type,
purposes: consent.purposes
},
result: { status: 'success' },
actor: {
type: 'user',
id: userId,
ip: req.ip,
userAgent: req.get('user-agent')
}
});
// Publish event
await publishEvent('consent.granted', {
consentId: consent.consentId,
userId,
type: consent.type
});
res.status(201).json({
success: true,
data: consent
});
metrics.recordApiCall('consent.grant', 'success');
} catch (error) {
logger.error('Failed to grant consent:', error);
metrics.recordApiCall('consent.grant', 'error');
res.status(500).json({
success: false,
error: 'Failed to grant consent'
});
}
});
/**
* Update consent
*/
router.put('/consents/:consentId', auth(), validateConsent, async (req, res) => {
try {
const { consentId } = req.params;
const consent = await ConsentRecord.findOne({ consentId });
if (!consent) {
return res.status(404).json({
success: false,
error: 'Consent record not found'
});
}
// Update consent
consent.update(req.body);
await consent.save();
// Audit log
await AuditLog.log({
accountId: consent.accountId,
userId: consent.userId,
action: 'consent_updated',
category: 'consent',
resource: 'consent_record',
resourceId: consent.consentId,
details: {
changes: req.body
},
result: { status: 'success' },
actor: {
type: 'user',
id: req.user.id,
ip: req.ip,
userAgent: req.get('user-agent')
}
});
res.json({
success: true,
data: consent
});
metrics.recordApiCall('consent.update', 'success');
} catch (error) {
logger.error('Failed to update consent:', error);
metrics.recordApiCall('consent.update', 'error');
res.status(500).json({
success: false,
error: 'Failed to update consent'
});
}
});
/**
* Withdraw consent
*/
router.delete('/consents/:consentId', auth(), async (req, res) => {
try {
const { consentId } = req.params;
const { reason } = req.body;
const consent = await ConsentRecord.findOne({ consentId });
if (!consent) {
return res.status(404).json({
success: false,
error: 'Consent record not found'
});
}
// Withdraw consent
consent.withdraw(reason || 'User requested withdrawal');
await consent.save();
// Audit log
await AuditLog.log({
accountId: consent.accountId,
userId: consent.userId,
action: 'consent_withdrawn',
category: 'consent',
resource: 'consent_record',
resourceId: consent.consentId,
details: {
reason: reason || 'User requested withdrawal'
},
result: { status: 'success' },
actor: {
type: 'user',
id: req.user.id,
ip: req.ip,
userAgent: req.get('user-agent')
}
});
// Publish event
await publishEvent('consent.withdrawn', {
consentId: consent.consentId,
userId: consent.userId,
type: consent.type
});
res.json({
success: true,
message: 'Consent withdrawn successfully'
});
metrics.recordApiCall('consent.withdraw', 'success');
} catch (error) {
logger.error('Failed to withdraw consent:', error);
metrics.recordApiCall('consent.withdraw', 'error');
res.status(500).json({
success: false,
error: 'Failed to withdraw consent'
});
}
});
/**
* Get consent history
*/
router.get('/users/:userId/consent-history', auth(), async (req, res) => {
try {
const { userId } = req.params;
const { startDate, endDate, type } = req.query;
const query = { userId };
if (type) query.type = type;
if (startDate || endDate) {
query.grantedAt = {};
if (startDate) query.grantedAt.$gte = new Date(startDate);
if (endDate) query.grantedAt.$lte = new Date(endDate);
}
const history = await ConsentRecord.find(query)
.sort({ grantedAt: -1 })
.limit(500);
const grouped = history.reduce((acc, consent) => {
const key = consent.type;
if (!acc[key]) acc[key] = [];
acc[key].push({
consentId: consent.consentId,
status: consent.status,
grantedAt: consent.grantedAt,
withdrawnAt: consent.withdrawnAt,
expiryDate: consent.expiryDate,
version: consent.version
});
return acc;
}, {});
res.json({
success: true,
data: grouped,
totalRecords: history.length
});
metrics.recordApiCall('consent.history', 'success');
} catch (error) {
logger.error('Failed to get consent history:', error);
metrics.recordApiCall('consent.history', 'error');
res.status(500).json({
success: false,
error: 'Failed to retrieve consent history'
});
}
});
/**
* Verify consent for specific purpose
*/
router.post('/verify-consent', auth(), async (req, res) => {
try {
const { userId, type, purpose } = req.body;
const consent = await ConsentRecord.findOne({
userId,
type,
status: 'granted'
});
if (!consent) {
return res.json({
success: true,
hasConsent: false,
reason: 'No consent record found'
});
}
const isValid = consent.isValid();
const hasPurpose = !purpose || consent.purposes.includes(purpose);
res.json({
success: true,
hasConsent: isValid && hasPurpose,
consentId: consent.consentId,
grantedAt: consent.grantedAt,
expiryDate: consent.expiryDate,
reason: !isValid ? 'Consent expired or withdrawn' : !hasPurpose ? 'Purpose not consented' : null
});
metrics.recordApiCall('consent.verify', 'success');
} catch (error) {
logger.error('Failed to verify consent:', error);
metrics.recordApiCall('consent.verify', 'error');
res.status(500).json({
success: false,
error: 'Failed to verify consent'
});
}
});
export default router;

View File

@@ -0,0 +1,312 @@
import express from 'express';
import { DataRequest } from '../models/DataRequest.js';
import { DataRequestProcessor } from '../services/dataRequestProcessor.js';
import { auth } from '../utils/auth.js';
import { validateDataRequest } from '../utils/validation.js';
import { metrics } from '../utils/metrics.js';
import { logger } from '../utils/logger.js';
const router = express.Router();
// Initialize data request processor
let dataRequestProcessor;
export const initializeDataRequestRoutes = (redisClient) => {
dataRequestProcessor = new DataRequestProcessor(redisClient);
return router;
};
/**
* Create data request
*/
router.post('/data-requests', auth(), validateDataRequest, async (req, res) => {
try {
const requestData = {
...req.body,
accountId: req.user.accountId,
audit: {
ipAddress: req.ip,
userAgent: req.get('user-agent')
}
};
const dataRequest = await dataRequestProcessor.createDataRequest(requestData);
res.status(201).json({
success: true,
data: dataRequest,
message: 'Data request created successfully'
});
metrics.recordApiCall('data_request.create', 'success');
} catch (error) {
logger.error('Failed to create data request:', error);
metrics.recordApiCall('data_request.create', 'error');
res.status(500).json({
success: false,
error: 'Failed to create data request'
});
}
});
/**
* Get data request by ID
*/
router.get('/data-requests/:requestId', auth(), async (req, res) => {
try {
const { requestId } = req.params;
const dataRequest = await DataRequest.findOne({ requestId });
if (!dataRequest) {
return res.status(404).json({
success: false,
error: 'Data request not found'
});
}
// Check access permissions
if (dataRequest.accountId !== req.user.accountId &&
dataRequest.userId !== req.user.id) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
res.json({
success: true,
data: dataRequest
});
metrics.recordApiCall('data_request.get', 'success');
} catch (error) {
logger.error('Failed to get data request:', error);
metrics.recordApiCall('data_request.get', 'error');
res.status(500).json({
success: false,
error: 'Failed to retrieve data request'
});
}
});
/**
* List data requests
*/
router.get('/data-requests', auth(), async (req, res) => {
try {
const { status, type, userId, startDate, endDate, page = 1, limit = 20 } = req.query;
const query = { accountId: req.user.accountId };
if (status) query.status = status;
if (type) query.type = type;
if (userId) query.userId = userId;
if (startDate || endDate) {
query.createdAt = {};
if (startDate) query.createdAt.$gte = new Date(startDate);
if (endDate) query.createdAt.$lte = new Date(endDate);
}
const skip = (page - 1) * limit;
const [requests, total] = await Promise.all([
DataRequest.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
DataRequest.countDocuments(query)
]);
res.json({
success: true,
data: requests,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
metrics.recordApiCall('data_request.list', 'success');
} catch (error) {
logger.error('Failed to list data requests:', error);
metrics.recordApiCall('data_request.list', 'error');
res.status(500).json({
success: false,
error: 'Failed to list data requests'
});
}
});
/**
* Verify data request
*/
router.post('/data-requests/:requestId/verify', auth(), async (req, res) => {
try {
const { requestId } = req.params;
const { method } = req.body;
const verificationData = {
verifiedBy: req.user.id,
method: method || 'manual'
};
const dataRequest = await dataRequestProcessor.verifyRequest(requestId, verificationData);
res.json({
success: true,
data: dataRequest,
message: 'Data request verified successfully'
});
metrics.recordApiCall('data_request.verify', 'success');
} catch (error) {
logger.error('Failed to verify data request:', error);
metrics.recordApiCall('data_request.verify', 'error');
res.status(500).json({
success: false,
error: error.message || 'Failed to verify data request'
});
}
});
/**
* Process data request
*/
router.post('/data-requests/:requestId/process', auth(), async (req, res) => {
try {
const { requestId } = req.params;
// Start processing asynchronously
dataRequestProcessor.processRequest(requestId)
.catch(error => {
logger.error('Data request processing failed:', error);
});
res.json({
success: true,
message: 'Data request processing started'
});
metrics.recordApiCall('data_request.process', 'success');
} catch (error) {
logger.error('Failed to start data request processing:', error);
metrics.recordApiCall('data_request.process', 'error');
res.status(500).json({
success: false,
error: 'Failed to start data request processing'
});
}
});
/**
* Download data
*/
router.get('/download/:token', async (req, res) => {
try {
const { token } = req.params;
// Retrieve data using token
const encryptedData = await dataRequestProcessor.redis.get(`download:${token}`);
if (!encryptedData) {
return res.status(404).json({
success: false,
error: 'Download link expired or invalid'
});
}
// Decrypt data
const { data, format, requestId } = dataRequestProcessor.dataProtection.decrypt(encryptedData);
// Set appropriate headers
const filename = `data_export_${requestId}.${format}`;
res.setHeader('Content-Type', getContentType(format));
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
// Send data
res.send(data);
// Delete token after download
await dataRequestProcessor.redis.del(`download:${token}`);
metrics.recordApiCall('data_request.download', 'success');
} catch (error) {
logger.error('Failed to download data:', error);
metrics.recordApiCall('data_request.download', 'error');
res.status(500).json({
success: false,
error: 'Failed to download data'
});
}
});
/**
* Get data request statistics
*/
router.get('/data-requests/stats', auth(), async (req, res) => {
try {
const { startDate, endDate } = req.query;
const stats = await DataRequest.getRequestStats(
req.user.accountId,
startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
endDate ? new Date(endDate) : new Date()
);
res.json({
success: true,
data: stats
});
metrics.recordApiCall('data_request.stats', 'success');
} catch (error) {
logger.error('Failed to get data request statistics:', error);
metrics.recordApiCall('data_request.stats', 'error');
res.status(500).json({
success: false,
error: 'Failed to get statistics'
});
}
});
/**
* Get overdue requests
*/
router.get('/data-requests/overdue', auth(), async (req, res) => {
try {
const overdueRequests = await DataRequest.findOverdue(req.user.accountId);
res.json({
success: true,
data: overdueRequests,
count: overdueRequests.length
});
metrics.recordApiCall('data_request.overdue', 'success');
} catch (error) {
logger.error('Failed to get overdue requests:', error);
metrics.recordApiCall('data_request.overdue', 'error');
res.status(500).json({
success: false,
error: 'Failed to get overdue requests'
});
}
});
/**
* Helper function to get content type
*/
function getContentType(format) {
switch (format) {
case 'json':
return 'application/json';
case 'csv':
return 'text/csv';
case 'xml':
return 'application/xml';
default:
return 'application/octet-stream';
}
}
export default router;

View File

@@ -0,0 +1,426 @@
import express from 'express';
import { ComplianceViolation } from '../models/ComplianceViolation.js';
import { AuditLog } from '../models/AuditLog.js';
import { auth } from '../utils/auth.js';
import { validateViolation } from '../utils/validation.js';
import { publishEvent } from '../services/messaging.js';
import { metrics } from '../utils/metrics.js';
import { logger } from '../utils/logger.js';
const router = express.Router();
/**
* Create violation
*/
router.post('/violations', auth(), validateViolation, async (req, res) => {
try {
const violationData = {
...req.body,
accountId: req.user.accountId,
violationId: `vio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timeline: {
detectedAt: new Date()
}
};
const violation = new ComplianceViolation(violationData);
violation.calculateRiskScore();
await violation.save();
// Audit log
await AuditLog.log({
accountId: req.user.accountId,
userId: req.user.id,
action: 'violation_created',
category: 'compliance',
resource: 'violation',
resourceId: violation.violationId,
details: {
type: violation.type,
severity: violation.severity,
riskScore: violation.risk.score
},
result: { status: 'success' },
actor: {
type: 'user',
id: req.user.id,
ip: req.ip
}
});
// Publish event
await publishEvent('violation.created', {
violationId: violation.violationId,
type: violation.type,
severity: violation.severity
});
res.status(201).json({
success: true,
data: violation
});
metrics.recordApiCall('violation.create', 'success');
} catch (error) {
logger.error('Failed to create violation:', error);
metrics.recordApiCall('violation.create', 'error');
res.status(500).json({
success: false,
error: 'Failed to create violation'
});
}
});
/**
* Get violation by ID
*/
router.get('/violations/:violationId', auth(), async (req, res) => {
try {
const { violationId } = req.params;
const violation = await ComplianceViolation.findOne({ violationId });
if (!violation) {
return res.status(404).json({
success: false,
error: 'Violation not found'
});
}
// Check access permissions
if (violation.accountId !== req.user.accountId) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
res.json({
success: true,
data: violation
});
metrics.recordApiCall('violation.get', 'success');
} catch (error) {
logger.error('Failed to get violation:', error);
metrics.recordApiCall('violation.get', 'error');
res.status(500).json({
success: false,
error: 'Failed to retrieve violation'
});
}
});
/**
* List violations
*/
router.get('/violations', auth(), async (req, res) => {
try {
const {
status,
severity,
type,
regulation,
startDate,
endDate,
page = 1,
limit = 20
} = req.query;
const query = { accountId: req.user.accountId };
if (status) query.status = status;
if (severity) query.severity = severity;
if (type) query.type = type;
if (regulation) query['details.regulation'] = regulation;
if (startDate || endDate) {
query['timeline.detectedAt'] = {};
if (startDate) query['timeline.detectedAt'].$gte = new Date(startDate);
if (endDate) query['timeline.detectedAt'].$lte = new Date(endDate);
}
const skip = (page - 1) * limit;
const [violations, total] = await Promise.all([
ComplianceViolation.find(query)
.sort({ 'timeline.detectedAt': -1 })
.skip(skip)
.limit(parseInt(limit)),
ComplianceViolation.countDocuments(query)
]);
res.json({
success: true,
data: violations,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
metrics.recordApiCall('violation.list', 'success');
} catch (error) {
logger.error('Failed to list violations:', error);
metrics.recordApiCall('violation.list', 'error');
res.status(500).json({
success: false,
error: 'Failed to list violations'
});
}
});
/**
* Update violation status
*/
router.put('/violations/:violationId/status', auth(), async (req, res) => {
try {
const { violationId } = req.params;
const { status, reason } = req.body;
const violation = await ComplianceViolation.findOne({ violationId });
if (!violation) {
return res.status(404).json({
success: false,
error: 'Violation not found'
});
}
// Check access permissions
if (violation.accountId !== req.user.accountId) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
const oldStatus = violation.status;
violation.status = status;
// Update timeline based on status
if (status === 'resolved') {
violation.timeline.resolvedAt = new Date();
violation.remediation.status = 'completed';
} else if (status === 'mitigated') {
violation.timeline.mitigatedAt = new Date();
}
await violation.save();
// Audit log
await AuditLog.log({
accountId: req.user.accountId,
userId: req.user.id,
action: 'violation_status_updated',
category: 'compliance',
resource: 'violation',
resourceId: violation.violationId,
details: {
oldStatus,
newStatus: status,
reason
},
result: { status: 'success' },
actor: {
type: 'user',
id: req.user.id,
ip: req.ip
}
});
res.json({
success: true,
data: violation
});
metrics.recordApiCall('violation.update_status', 'success');
} catch (error) {
logger.error('Failed to update violation status:', error);
metrics.recordApiCall('violation.update_status', 'error');
res.status(500).json({
success: false,
error: 'Failed to update violation status'
});
}
});
/**
* Add remediation action
*/
router.post('/violations/:violationId/remediation', auth(), async (req, res) => {
try {
const { violationId } = req.params;
const { action, description, targetDate } = req.body;
const violation = await ComplianceViolation.findOne({ violationId });
if (!violation) {
return res.status(404).json({
success: false,
error: 'Violation not found'
});
}
// Add remediation action
violation.remediation.actions.push({
action,
description,
targetDate: targetDate ? new Date(targetDate) : undefined,
createdAt: new Date(),
createdBy: req.user.id,
status: 'pending'
});
violation.remediation.status = 'in_progress';
await violation.save();
res.json({
success: true,
data: violation
});
metrics.recordApiCall('violation.add_remediation', 'success');
} catch (error) {
logger.error('Failed to add remediation action:', error);
metrics.recordApiCall('violation.add_remediation', 'error');
res.status(500).json({
success: false,
error: 'Failed to add remediation action'
});
}
});
/**
* Add notification record
*/
router.post('/violations/:violationId/notifications', auth(), async (req, res) => {
try {
const { violationId } = req.params;
const { type, recipients, method, content } = req.body;
const violation = await ComplianceViolation.findOne({ violationId });
if (!violation) {
return res.status(404).json({
success: false,
error: 'Violation not found'
});
}
// Add notification based on type
const notification = {
notifiedAt: new Date(),
method,
reference: `NOT-${violationId}-${Date.now()}`
};
if (type === 'users') {
violation.notifications.users.push({
...notification,
recipients,
content
});
} else if (type === 'authorities') {
violation.notifications.authorities.push({
...notification,
name: recipients.name || 'Data Protection Authority',
jurisdiction: recipients.jurisdiction
});
}
if (type === 'authorities') {
violation.timeline.notifiedAt = new Date();
}
await violation.save();
res.json({
success: true,
data: violation
});
metrics.recordApiCall('violation.add_notification', 'success');
} catch (error) {
logger.error('Failed to add notification record:', error);
metrics.recordApiCall('violation.add_notification', 'error');
res.status(500).json({
success: false,
error: 'Failed to add notification record'
});
}
});
/**
* Get violation statistics
*/
router.get('/violations/stats', auth(), async (req, res) => {
try {
const { days = 30 } = req.query;
const stats = await ComplianceViolation.getViolationStats(
req.user.accountId,
parseInt(days)
);
res.json({
success: true,
data: stats
});
metrics.recordApiCall('violation.stats', 'success');
} catch (error) {
logger.error('Failed to get violation statistics:', error);
metrics.recordApiCall('violation.stats', 'error');
res.status(500).json({
success: false,
error: 'Failed to get statistics'
});
}
});
/**
* Get active violations
*/
router.get('/violations/active', auth(), async (req, res) => {
try {
const activeViolations = await ComplianceViolation.findActive(req.user.accountId);
res.json({
success: true,
data: activeViolations,
count: activeViolations.length
});
metrics.recordApiCall('violation.active', 'success');
} catch (error) {
logger.error('Failed to get active violations:', error);
metrics.recordApiCall('violation.active', 'error');
res.status(500).json({
success: false,
error: 'Failed to get active violations'
});
}
});
/**
* Get violations requiring notification
*/
router.get('/violations/requiring-notification', auth(), async (req, res) => {
try {
const violations = await ComplianceViolation.findRequiringNotification(req.user.accountId);
res.json({
success: true,
data: violations,
count: violations.length
});
metrics.recordApiCall('violation.requiring_notification', 'success');
} catch (error) {
logger.error('Failed to get violations requiring notification:', error);
metrics.recordApiCall('violation.requiring_notification', 'error');
res.status(500).json({
success: false,
error: 'Failed to get violations requiring notification'
});
}
});
export default router;

View File

@@ -0,0 +1,364 @@
import { AuditLog } from '../models/AuditLog.js';
import { DataProtectionService } from './dataProtection.js';
import { logger } from '../utils/logger.js';
import { config } from '../config/index.js';
export class AuditService {
constructor() {
this.dataProtection = new DataProtectionService();
}
/**
* Log an audit event
*/
async logEvent(eventData) {
try {
// Add security metadata
const secureEventData = {
...eventData,
security: {
encrypted: true,
signed: true,
hash: this.dataProtection.hash(eventData),
signature: this.generateSignature(eventData)
}
};
// Create audit log
const auditLog = await AuditLog.log(secureEventData);
// Log to audit file (separate from application logs)
this.logToFile(auditLog);
return auditLog;
} catch (error) {
logger.error('Failed to create audit log:', error);
throw error;
}
}
/**
* Search audit logs
*/
async searchLogs(criteria) {
try {
const logs = await AuditLog.search(criteria);
// Decrypt sensitive data if needed
const decryptedLogs = logs.map(log => {
if (log.security?.encrypted && log.details) {
try {
// Decrypt only for authorized access
log.details = this.decryptAuditData(log.details);
} catch (error) {
logger.warn('Failed to decrypt audit log details:', error);
}
}
return log;
});
return decryptedLogs;
} catch (error) {
logger.error('Audit log search failed:', error);
throw error;
}
}
/**
* Get audit trail for a specific resource
*/
async getAuditTrail(resourceId, resourceType) {
const logs = await AuditLog.find({
$or: [
{ resourceId },
{ resource: resourceType, 'details.resourceId': resourceId }
]
}).sort({ timestamp: -1 });
return this.formatAuditTrail(logs);
}
/**
* Generate compliance report from audit logs
*/
async generateComplianceReport(accountId, startDate, endDate) {
const criteria = {
accountId,
startDate,
endDate
};
// Get various audit statistics
const [
activitySummary,
consentActions,
dataRequests,
securityEvents,
configChanges
] = await Promise.all([
AuditLog.getActivitySummary(accountId, Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))),
this.getConsentAuditStats(criteria),
this.getDataRequestAuditStats(criteria),
this.getSecurityAuditStats(criteria),
this.getConfigurationAuditStats(criteria)
]);
return {
period: {
start: startDate,
end: endDate
},
summary: {
totalEvents: this.calculateTotalEvents(activitySummary),
successRate: this.calculateSuccessRate(activitySummary),
topActions: this.getTopActions(activitySummary)
},
consent: consentActions,
dataRequests,
security: securityEvents,
configuration: configChanges,
compliance: {
gdprCompliant: this.checkGDPRCompliance(consentActions, dataRequests),
ccpaCompliant: this.checkCCPACompliance(consentActions, dataRequests)
}
};
}
/**
* Monitor suspicious activities
*/
async detectAnomalies(accountId, window = 3600000) { // 1 hour window
const recentLogs = await AuditLog.find({
accountId,
timestamp: { $gte: new Date(Date.now() - window) }
});
const anomalies = [];
// Check for unusual patterns
const activityByUser = {};
const activityByIP = {};
for (const log of recentLogs) {
// Track by user
const userId = log.userId || log.actor?.id;
if (userId) {
activityByUser[userId] = (activityByUser[userId] || 0) + 1;
}
// Track by IP
const ip = log.actor?.ip;
if (ip) {
activityByIP[ip] = (activityByIP[ip] || 0) + 1;
}
}
// Detect high-frequency actions
for (const [userId, count] of Object.entries(activityByUser)) {
if (count > 100) { // Threshold
anomalies.push({
type: 'high_frequency_user',
userId,
count,
severity: count > 500 ? 'high' : 'medium'
});
}
}
// Detect suspicious IPs
for (const [ip, count] of Object.entries(activityByIP)) {
if (count > 50) { // Threshold
anomalies.push({
type: 'high_frequency_ip',
ip,
count,
severity: count > 200 ? 'high' : 'medium'
});
}
}
// Check for failed authentication attempts
const failedAuth = recentLogs.filter(log =>
log.action.includes('login') && log.result.status === 'failure'
);
if (failedAuth.length > 10) {
anomalies.push({
type: 'multiple_failed_logins',
count: failedAuth.length,
severity: 'high'
});
}
return anomalies;
}
/**
* Helper methods
*/
generateSignature(data) {
// Generate digital signature for audit log integrity
return this.dataProtection.hash(JSON.stringify(data) + config.jwt.secret);
}
decryptAuditData(encryptedData) {
// Decrypt audit data (with proper authorization checks in production)
return this.dataProtection.decrypt(encryptedData);
}
formatAuditTrail(logs) {
return logs.map(log => ({
timestamp: log.timestamp,
action: log.action,
actor: `${log.actor.type}:${log.actor.id}`,
result: log.result.status,
changes: log.details?.changes || [],
metadata: {
ip: log.actor.ip,
userAgent: log.actor.userAgent
}
}));
}
async getConsentAuditStats(criteria) {
const consentLogs = await AuditLog.search({
...criteria,
category: 'consent'
});
return {
total: consentLogs.length,
granted: consentLogs.filter(l => l.action === 'consent_granted').length,
withdrawn: consentLogs.filter(l => l.action === 'consent_withdrawn').length,
updated: consentLogs.filter(l => l.action === 'consent_updated').length
};
}
async getDataRequestAuditStats(criteria) {
const requestLogs = await AuditLog.search({
...criteria,
category: 'data_access'
});
return {
total: requestLogs.length,
access: requestLogs.filter(l => l.action.includes('access')).length,
deletion: requestLogs.filter(l => l.action.includes('deletion')).length,
portability: requestLogs.filter(l => l.action.includes('portability')).length,
completed: requestLogs.filter(l => l.result.status === 'success').length
};
}
async getSecurityAuditStats(criteria) {
const securityLogs = await AuditLog.search({
...criteria,
category: 'security'
});
return {
total: securityLogs.length,
breaches: securityLogs.filter(l => l.action.includes('breach')).length,
violations: securityLogs.filter(l => l.action.includes('violation')).length,
authentications: securityLogs.filter(l => l.action.includes('auth')).length
};
}
async getConfigurationAuditStats(criteria) {
const configLogs = await AuditLog.search({
...criteria,
category: 'configuration'
});
return {
total: configLogs.length,
changes: configLogs.filter(l => l.result.status === 'success').length,
byResource: this.groupByResource(configLogs)
};
}
calculateTotalEvents(summary) {
return summary.reduce((total, day) => total + day.total, 0);
}
calculateSuccessRate(summary) {
const totals = summary.reduce((acc, day) => ({
total: acc.total + day.total,
success: acc.success + day.success
}), { total: 0, success: 0 });
return totals.total > 0 ? (totals.success / totals.total * 100).toFixed(2) : 0;
}
getTopActions(summary) {
const actionCounts = {};
summary.forEach(day => {
if (!actionCounts[day._id.category]) {
actionCounts[day._id.category] = 0;
}
actionCounts[day._id.category] += day.total;
});
return Object.entries(actionCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([action, count]) => ({ action, count }));
}
groupByResource(logs) {
const grouped = {};
logs.forEach(log => {
if (!grouped[log.resource]) {
grouped[log.resource] = 0;
}
grouped[log.resource]++;
});
return grouped;
}
checkGDPRCompliance(consentStats, requestStats) {
// Basic GDPR compliance checks
const consentRate = consentStats.total > 0
? (consentStats.granted / consentStats.total * 100)
: 0;
const requestCompletionRate = requestStats.total > 0
? (requestStats.completed / requestStats.total * 100)
: 100;
return {
consentManagement: consentRate > 0,
dataRequestHandling: requestCompletionRate > 95,
rightToAccess: requestStats.access > 0 || requestStats.total === 0,
rightToDeletion: requestStats.deletion > 0 || requestStats.total === 0,
overallCompliant: consentRate > 0 && requestCompletionRate > 95
};
}
checkCCPACompliance(consentStats, requestStats) {
// Basic CCPA compliance checks
return {
optOutAvailable: consentStats.withdrawn > 0 || consentStats.total === 0,
dataAccessProvided: requestStats.access > 0 || requestStats.total === 0,
deletionRequestsHandled: requestStats.deletion > 0 || requestStats.total === 0,
overallCompliant: true
};
}
logToFile(auditLog) {
// In production, this would write to a secure, tamper-proof audit file
const auditLine = JSON.stringify({
timestamp: auditLog.timestamp,
auditId: auditLog.auditId,
action: auditLog.action,
actor: auditLog.actor,
result: auditLog.result.status,
hash: auditLog.security.hash
});
// Append to audit file
logger.info(`AUDIT: ${auditLine}`);
}
}

View File

@@ -0,0 +1,486 @@
import cron from 'node-cron';
import geoip from 'geoip-lite';
import { ConsentRecord } from '../models/ConsentRecord.js';
import { DataRequest } from '../models/DataRequest.js';
import { ComplianceViolation } from '../models/ComplianceViolation.js';
import { AuditLog } from '../models/AuditLog.js';
import { publishEvent } from './messaging.js';
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';
export class ComplianceMonitor {
constructor(redisClient) {
this.redis = redisClient;
this.scheduledTasks = [];
this.violationThreshold = config.monitoring.violationThreshold;
this.checkInterval = config.monitoring.checkInterval;
}
/**
* Start compliance monitoring
*/
async start() {
logger.info('Starting compliance monitor');
// Schedule regular compliance checks
this.scheduleTask('*/5 * * * *', this.checkConsentExpiry.bind(this));
this.scheduleTask('*/10 * * * *', this.checkDataRequestDeadlines.bind(this));
this.scheduleTask('*/15 * * * *', this.checkRetentionPolicies.bind(this));
this.scheduleTask('0 * * * *', this.checkViolations.bind(this));
this.scheduleTask('0 0 * * *', this.generateComplianceReport.bind(this));
// Initial checks
await this.performInitialChecks();
}
/**
* Stop compliance monitoring
*/
async stop() {
logger.info('Stopping compliance monitor');
for (const task of this.scheduledTasks) {
task.stop();
}
this.scheduledTasks = [];
}
/**
* Schedule a monitoring task
*/
scheduleTask(cronPattern, taskFunction) {
const task = cron.schedule(cronPattern, async () => {
try {
await taskFunction();
} catch (error) {
logger.error('Scheduled task error:', error);
}
});
this.scheduledTasks.push(task);
}
/**
* Perform initial compliance checks
*/
async performInitialChecks() {
try {
await this.checkConsentExpiry();
await this.checkDataRequestDeadlines();
await this.checkViolations();
} catch (error) {
logger.error('Initial compliance check failed:', error);
}
}
/**
* Check for expired consents
*/
async checkConsentExpiry() {
try {
// Find consents expiring soon
const expiryThreshold = new Date();
expiryThreshold.setDate(expiryThreshold.getDate() + 30);
const expiringConsents = await ConsentRecord.find({
status: 'granted',
renewable: true,
expiryDate: {
$gte: new Date(),
$lte: expiryThreshold
}
});
for (const consent of expiringConsents) {
if (consent.needsRenewal()) {
await this.handleConsentExpiry(consent);
}
}
// Find already expired consents
const expiredConsents = await ConsentRecord.find({
status: 'granted',
expiryDate: { $lt: new Date() }
});
for (const consent of expiredConsents) {
await this.handleExpiredConsent(consent);
}
} catch (error) {
logger.error('Consent expiry check failed:', error);
}
}
/**
* Handle consent expiry
*/
async handleConsentExpiry(consent) {
// Send renewal reminder
await publishEvent('consent.expiring', {
consentId: consent.consentId,
userId: consent.userId,
type: consent.type,
expiryDate: consent.expiryDate,
daysRemaining: Math.ceil((consent.expiryDate - new Date()) / (1000 * 60 * 60 * 24))
});
// Log event
await AuditLog.log({
accountId: consent.accountId,
userId: consent.userId,
action: 'consent_expiry_notification',
category: 'consent',
resource: 'consent_record',
resourceId: consent.consentId,
details: {
type: consent.type,
expiryDate: consent.expiryDate
},
result: { status: 'success' },
actor: { type: 'system', id: 'compliance-monitor' }
});
}
/**
* Handle expired consent
*/
async handleExpiredConsent(consent) {
// Automatically withdraw expired consent
consent.withdraw('Consent expired');
await consent.save();
// Create violation if data is still being processed
await this.createViolation({
type: 'consent_expired',
severity: 'medium',
details: {
description: `Consent expired for user ${consent.userId}`,
affectedUsers: 1,
affectedData: [consent.type],
regulation: this.getRegulationByRegion(consent.metadata?.location?.country)
}
}, consent.accountId);
await publishEvent('consent.expired', {
consentId: consent.consentId,
userId: consent.userId,
type: consent.type
});
}
/**
* Check data request deadlines
*/
async checkDataRequestDeadlines() {
try {
const overdueRequests = await DataRequest.findOverdue();
for (const request of overdueRequests) {
await this.handleOverdueDataRequest(request);
}
// Check requests approaching deadline
const upcomingDeadlines = await DataRequest.find({
status: { $in: ['pending', 'processing'] },
'compliance.deadline': {
$gte: new Date(),
$lte: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) // 3 days
}
});
for (const request of upcomingDeadlines) {
await publishEvent('data_request.deadline_approaching', {
requestId: request.requestId,
type: request.type,
daysRemaining: request.getDaysRemaining()
});
}
} catch (error) {
logger.error('Data request deadline check failed:', error);
}
}
/**
* Handle overdue data request
*/
async handleOverdueDataRequest(request) {
await this.createViolation({
type: 'data_access_denied',
severity: 'high',
details: {
description: `Data request ${request.requestId} is overdue`,
affectedUsers: 1,
regulation: request.compliance.regulation,
articles: request.compliance.articles,
detectionMethod: 'automated'
}
}, request.accountId);
await publishEvent('data_request.overdue', {
requestId: request.requestId,
type: request.type,
userId: request.userId,
daysOverdue: Math.abs(request.getDaysRemaining())
});
}
/**
* Check data retention policies
*/
async checkRetentionPolicies() {
try {
// This would integrate with other services to check data retention
// For now, we'll check audit logs as an example
const deletedCount = await AuditLog.cleanupExpired();
if (deletedCount > 0) {
logger.info(`Cleaned up ${deletedCount} expired audit logs`);
}
} catch (error) {
logger.error('Retention policy check failed:', error);
}
}
/**
* Check for compliance violations
*/
async checkViolations() {
try {
const activeViolations = await ComplianceViolation.findActive();
for (const violation of activeViolations) {
// Update risk scores
violation.calculateRiskScore();
// Check if needs escalation
if (violation.needsEscalation() && violation.status !== 'escalated') {
await this.escalateViolation(violation);
}
// Check if overdue
if (violation.isOverdue()) {
await this.handleOverdueViolation(violation);
}
await violation.save();
}
// Check for violations requiring notification
const notificationRequired = await ComplianceViolation.findRequiringNotification();
for (const violation of notificationRequired) {
await this.notifyAuthorities(violation);
}
} catch (error) {
logger.error('Violation check failed:', error);
}
}
/**
* Create compliance violation
*/
async createViolation(violationData, accountId) {
const violation = new ComplianceViolation({
violationId: `vio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
accountId,
...violationData,
timeline: {
detectedAt: new Date()
}
});
// Calculate initial risk score
violation.calculateRiskScore();
// Set deadlines based on regulation
this.setViolationDeadlines(violation);
await violation.save();
await publishEvent('violation.detected', {
violationId: violation.violationId,
type: violation.type,
severity: violation.severity,
riskScore: violation.risk.score
});
return violation;
}
/**
* Set violation deadlines based on regulation
*/
setViolationDeadlines(violation) {
const regulation = violation.details.regulation;
switch (regulation) {
case 'gdpr':
// GDPR: 72 hours for breach notification
if (violation.type === 'security_breach') {
violation.timeline.deadlines.notification = new Date(Date.now() + 72 * 60 * 60 * 1000);
}
violation.timeline.deadlines.remediation = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
break;
case 'ccpa':
// CCPA: 30 days for data requests
violation.timeline.deadlines.remediation = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
break;
default:
// Default: 30 days
violation.timeline.deadlines.remediation = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
}
}
/**
* Escalate violation
*/
async escalateViolation(violation) {
violation.status = 'escalated';
await violation.save();
await publishEvent('violation.escalated', {
violationId: violation.violationId,
type: violation.type,
severity: violation.severity,
riskScore: violation.risk.score,
reason: 'High risk or overdue'
});
// Notify management
logger.error(`ESCALATED VIOLATION: ${violation.violationId} - ${violation.type}`);
}
/**
* Handle overdue violation
*/
async handleOverdueViolation(violation) {
// Update risk score
violation.risk.score = Math.min(10, violation.risk.score * 1.5);
await publishEvent('violation.overdue', {
violationId: violation.violationId,
type: violation.type,
daysOverdue: Math.ceil((new Date() - violation.timeline.deadlines.remediation) / (1000 * 60 * 60 * 24))
});
}
/**
* Notify authorities about violation
*/
async notifyAuthorities(violation) {
// In production, this would send actual notifications
logger.info(`Notifying authorities about violation ${violation.violationId}`);
violation.notifications.authorities.push({
name: 'Data Protection Authority',
notifiedAt: new Date(),
method: 'automated',
reference: `DPA-${violation.violationId}`
});
await violation.save();
await publishEvent('violation.authority_notified', {
violationId: violation.violationId,
authority: 'Data Protection Authority'
});
}
/**
* Generate compliance report
*/
async generateComplianceReport() {
try {
const report = {
date: new Date(),
period: 'daily',
violations: await ComplianceViolation.getViolationStats(null, 1),
dataRequests: await DataRequest.getRequestStats(null,
new Date(Date.now() - 24 * 60 * 60 * 1000),
new Date()
),
consents: await this.getConsentStats(),
auditActivity: await AuditLog.getActivitySummary(null, 1)
};
await publishEvent('compliance.report_generated', report);
logger.info('Daily compliance report generated');
} catch (error) {
logger.error('Report generation failed:', error);
}
}
/**
* Get consent statistics
*/
async getConsentStats() {
const stats = await ConsentRecord.aggregate([
{
$group: {
_id: {
type: '$type',
status: '$status'
},
count: { $sum: 1 }
}
},
{
$group: {
_id: '$_id.type',
statuses: {
$push: {
status: '$_id.status',
count: '$count'
}
},
total: { $sum: '$count' }
}
}
]);
return stats;
}
/**
* Get regulation by region
*/
getRegulationByRegion(country) {
if (!country) return 'other';
for (const [regulation, countries] of Object.entries(config.compliance.regions)) {
if (countries.includes(country)) {
return regulation;
}
}
return 'other';
}
/**
* Check user location for compliance
*/
checkUserLocation(ipAddress) {
const geo = geoip.lookup(ipAddress);
if (!geo) return { country: 'unknown', regulation: 'other' };
const country = geo.country;
const regulation = this.getRegulationByRegion(country);
return {
country,
region: geo.region,
city: geo.city,
regulation,
requiresConsent: ['gdpr', 'ccpa'].includes(regulation)
};
}
}

View File

@@ -0,0 +1,340 @@
import crypto from 'crypto';
import CryptoJS from 'crypto-js';
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';
export class DataProtectionService {
constructor() {
this.algorithm = config.encryption.algorithm;
this.keyDerivation = config.encryption.keyDerivation;
this.saltRounds = config.encryption.saltRounds;
this.masterKey = this.deriveMasterKey();
}
/**
* Derive master key from configuration
*/
deriveMasterKey() {
const salt = crypto.createHash('sha256').update('compliance-guard-salt').digest();
return crypto.pbkdf2Sync(
config.encryption.encryptionKey,
salt,
100000,
32,
'sha256'
);
}
/**
* Encrypt sensitive data
*/
encrypt(data, context = {}) {
try {
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
// Generate unique IV for each encryption
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.masterKey, iv);
let encrypted = cipher.update(dataStr, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Combine IV, authTag, and encrypted data
const result = {
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
data: encrypted,
algorithm: this.algorithm,
timestamp: new Date().toISOString(),
context
};
return Buffer.from(JSON.stringify(result)).toString('base64');
} catch (error) {
logger.error('Encryption failed:', error);
throw new Error('Failed to encrypt data');
}
}
/**
* Decrypt sensitive data
*/
decrypt(encryptedData) {
try {
const parsed = JSON.parse(Buffer.from(encryptedData, 'base64').toString());
const decipher = crypto.createDecipheriv(
parsed.algorithm || this.algorithm,
this.masterKey,
Buffer.from(parsed.iv, 'hex')
);
if (parsed.authTag) {
decipher.setAuthTag(Buffer.from(parsed.authTag, 'hex'));
}
let decrypted = decipher.update(parsed.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
try {
return JSON.parse(decrypted);
} catch {
return decrypted;
}
} catch (error) {
logger.error('Decryption failed:', error);
throw new Error('Failed to decrypt data');
}
}
/**
* Hash data for integrity checking
*/
hash(data) {
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
return crypto.createHash('sha256').update(dataStr).digest('hex');
}
/**
* Generate secure token
*/
generateToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
/**
* Anonymize personal data
*/
anonymize(data, fields = []) {
const anonymized = { ...data };
const defaultFields = ['email', 'phone', 'name', 'address', 'ip'];
const fieldsToAnonymize = fields.length > 0 ? fields : defaultFields;
for (const field of fieldsToAnonymize) {
if (anonymized[field]) {
anonymized[field] = this.anonymizeField(anonymized[field], field);
}
}
return anonymized;
}
/**
* Anonymize specific field based on type
*/
anonymizeField(value, fieldType) {
switch (fieldType) {
case 'email':
const [localPart, domain] = value.split('@');
const maskedLocal = localPart.substring(0, 2) + '***';
return `${maskedLocal}@${domain}`;
case 'phone':
return value.substring(0, 3) + '****' + value.substring(value.length - 2);
case 'name':
const parts = value.split(' ');
return parts.map(part => part.charAt(0) + '***').join(' ');
case 'ip':
const octets = value.split('.');
return octets.length === 4
? `${octets[0]}.${octets[1]}.XXX.XXX`
: 'XXX.XXX.XXX.XXX';
default:
// Generic masking
const visibleLength = Math.min(3, Math.floor(value.length * 0.3));
return value.substring(0, visibleLength) + '*'.repeat(value.length - visibleLength);
}
}
/**
* Pseudonymize data (reversible anonymization)
*/
pseudonymize(data, fields = []) {
const pseudonymized = { ...data };
const mapping = {};
for (const field of fields) {
if (pseudonymized[field]) {
const pseudonym = this.generatePseudonym(pseudonymized[field]);
mapping[pseudonym] = pseudonymized[field];
pseudonymized[field] = pseudonym;
}
}
return { data: pseudonymized, mapping };
}
/**
* Generate pseudonym
*/
generatePseudonym(value) {
const hash = crypto.createHash('sha256').update(value + this.masterKey).digest('hex');
return `PSEUDO_${hash.substring(0, 16)}`;
}
/**
* Tokenize sensitive data
*/
tokenize(data) {
const token = this.generateToken();
const encrypted = this.encrypt(data);
// Store token-data mapping (in production, use secure token vault)
return {
token,
encrypted,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
};
}
/**
* Check if data should be encrypted based on field type
*/
shouldEncrypt(fieldName) {
const sensitiveFields = [
'password', 'ssn', 'creditCard', 'bankAccount',
'medicalRecord', 'biometric', 'passport', 'driverLicense'
];
return sensitiveFields.some(field =>
fieldName.toLowerCase().includes(field.toLowerCase())
);
}
/**
* Secure erase data
*/
secureErase(data) {
if (typeof data === 'string') {
// Overwrite string in memory (limited effectiveness in JS)
const buffer = Buffer.from(data);
crypto.randomFillSync(buffer);
return true;
}
if (typeof data === 'object') {
for (const key in data) {
if (typeof data[key] === 'string') {
const buffer = Buffer.from(data[key]);
crypto.randomFillSync(buffer);
}
delete data[key];
}
return true;
}
return false;
}
/**
* Generate data retention policy
*/
generateRetentionPolicy(dataType, region) {
const policies = config.compliance.retentionPeriods;
let retentionDays = policies.default;
// Apply region-specific rules
if (config.compliance.regions.gdpr.includes(region)) {
retentionDays = policies.eu;
} else if (region === 'US') {
retentionDays = policies.us;
}
// Apply data type specific rules
if (dataType === 'marketing') {
retentionDays = Math.min(retentionDays, policies.marketing);
} else if (dataType === 'analytics') {
retentionDays = Math.min(retentionDays, policies.analytics);
}
return {
retentionDays,
expiryDate: new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000),
policy: `${dataType}_${region}_${retentionDays}d`
};
}
/**
* Export data in portable format
*/
exportUserData(userData, format = 'json') {
const exportData = {
exportDate: new Date().toISOString(),
format,
version: '1.0',
data: userData
};
switch (format) {
case 'json':
return JSON.stringify(exportData, null, 2);
case 'csv':
return this.convertToCSV(userData);
case 'xml':
return this.convertToXML(exportData);
default:
throw new Error(`Unsupported export format: ${format}`);
}
}
/**
* Convert data to CSV format
*/
convertToCSV(data) {
if (Array.isArray(data)) {
if (data.length === 0) return '';
const headers = Object.keys(data[0]);
const csvHeaders = headers.join(',');
const csvRows = data.map(row =>
headers.map(header => {
const value = row[header];
return typeof value === 'string' && value.includes(',')
? `"${value}"`
: value;
}).join(',')
);
return [csvHeaders, ...csvRows].join('\n');
}
// Convert single object to CSV
const headers = Object.keys(data);
const values = headers.map(h => data[h]);
return [headers.join(','), values.join(',')].join('\n');
}
/**
* Convert data to XML format
*/
convertToXML(data) {
const convert = (obj, rootName = 'root') => {
let xml = `<${rootName}>`;
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
xml += Array.isArray(value)
? value.map(item => convert(item, key)).join('')
: convert(value, key);
} else {
xml += `<${key}>${value}</${key}>`;
}
}
xml += `</${rootName}>`;
return xml;
};
return '<?xml version="1.0" encoding="UTF-8"?>' + convert(data, 'export');
}
}

View File

@@ -0,0 +1,554 @@
import { v4 as uuidv4 } from 'uuid';
import { DataRequest } from '../models/DataRequest.js';
import { AuditLog } from '../models/AuditLog.js';
import { DataProtectionService } from './dataProtection.js';
import { publishEvent } from './messaging.js';
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';
export class DataRequestProcessor {
constructor(redisClient) {
this.redis = redisClient;
this.dataProtection = new DataProtectionService();
this.processingQueue = [];
}
/**
* Create a new data request
*/
async createDataRequest(requestData) {
const requestId = `req_${Date.now()}_${uuidv4().substring(0, 8)}`;
// Determine SLA based on request type and regulation
const sla = this.determineSLA(requestData.type, requestData.regulation);
const dataRequest = new DataRequest({
requestId,
...requestData,
compliance: {
regulation: requestData.regulation || 'gdpr',
deadline: new Date(Date.now() + sla * 24 * 60 * 60 * 1000),
sla,
articles: this.getRelevantArticles(requestData.type, requestData.regulation)
}
});
await dataRequest.save();
// Log the request
await AuditLog.log({
accountId: requestData.accountId,
userId: requestData.userId,
action: 'data_request_created',
category: 'data_access',
resource: 'data_request',
resourceId: requestId,
details: {
type: requestData.type,
regulation: requestData.regulation
},
result: { status: 'success' },
actor: {
type: 'user',
id: requestData.userId,
ip: requestData.audit?.ipAddress
}
});
await publishEvent('data_request.created', {
requestId,
type: requestData.type,
userId: requestData.userId
});
return dataRequest;
}
/**
* Verify data request
*/
async verifyRequest(requestId, verificationData) {
const request = await DataRequest.findOne({ requestId });
if (!request) {
throw new Error('Data request not found');
}
request.verification.verified = true;
request.verification.verifiedAt = new Date();
request.verification.verifiedBy = verificationData.verifiedBy;
request.verification.method = verificationData.method;
await request.save();
await publishEvent('data_request.verified', {
requestId,
userId: request.userId
});
// Auto-process if verified
if (request.canProcess()) {
await this.processRequest(requestId);
}
return request;
}
/**
* Process data request
*/
async processRequest(requestId) {
const request = await DataRequest.findOne({ requestId });
if (!request) {
throw new Error('Data request not found');
}
if (!request.canProcess()) {
throw new Error('Request cannot be processed in current state');
}
try {
request.markProcessing('system');
await request.save();
// Collect data based on request type
const collectedData = await this.collectUserData(request);
// Process based on request type
let responseData;
switch (request.type) {
case 'access':
responseData = await this.handleAccessRequest(request, collectedData);
break;
case 'portability':
responseData = await this.handlePortabilityRequest(request, collectedData);
break;
case 'deletion':
responseData = await this.handleDeletionRequest(request, collectedData);
break;
case 'rectification':
responseData = await this.handleRectificationRequest(request, collectedData);
break;
case 'restriction':
responseData = await this.handleRestrictionRequest(request, collectedData);
break;
case 'objection':
responseData = await this.handleObjectionRequest(request, collectedData);
break;
default:
throw new Error(`Unknown request type: ${request.type}`);
}
request.markCompleted(responseData);
await request.save();
await publishEvent('data_request.completed', {
requestId,
type: request.type,
userId: request.userId
});
// Log completion
await AuditLog.log({
accountId: request.accountId,
userId: request.userId,
action: 'data_request_completed',
category: 'data_access',
resource: 'data_request',
resourceId: requestId,
details: {
type: request.type,
processingTime: request.processing.completedAt - request.processing.startedAt
},
result: { status: 'success' },
actor: { type: 'system', id: 'data-processor' }
});
} catch (error) {
logger.error('Request processing failed:', error);
request.processing.errors.push({
timestamp: new Date(),
message: error.message,
service: 'data-processor'
});
request.status = 'pending'; // Reset to pending
await request.save();
throw error;
}
}
/**
* Handle access request
*/
async handleAccessRequest(request, collectedData) {
// Format data for user access
const formattedData = this.formatDataForExport(
collectedData,
request.requestDetails.format
);
// Generate download URL
const downloadInfo = await this.generateDownloadLink(
formattedData,
request.requestDetails.format,
request.requestId
);
return {
method: 'download',
deliveredAt: new Date(),
downloadUrl: downloadInfo.url,
downloadExpiry: downloadInfo.expiry
};
}
/**
* Handle portability request
*/
async handlePortabilityRequest(request, collectedData) {
// Export in machine-readable format
const exportedData = this.dataProtection.exportUserData(
collectedData,
request.requestDetails.format
);
const downloadInfo = await this.generateDownloadLink(
exportedData,
request.requestDetails.format,
request.requestId
);
return {
method: 'download',
deliveredAt: new Date(),
downloadUrl: downloadInfo.url,
downloadExpiry: downloadInfo.expiry
};
}
/**
* Handle deletion request
*/
async handleDeletionRequest(request, collectedData) {
const deletionResults = [];
// Delete data from each service
for (const category of request.requestDetails.dataCategories || ['all']) {
try {
const result = await this.deleteUserData(
request.userId,
request.accountId,
category
);
deletionResults.push({
category,
status: 'deleted',
recordsDeleted: result.count
});
} catch (error) {
deletionResults.push({
category,
status: 'failed',
error: error.message
});
}
}
// Anonymize remaining data that cannot be deleted
await this.anonymizeRemainingData(request.userId, request.accountId);
return {
method: 'api',
deliveredAt: new Date(),
deletionResults
};
}
/**
* Handle rectification request
*/
async handleRectificationRequest(request, collectedData) {
// Apply corrections
const corrections = request.requestDetails.corrections || {};
const results = [];
for (const [field, newValue] of Object.entries(corrections)) {
try {
await this.updateUserData(
request.userId,
request.accountId,
field,
newValue
);
results.push({
field,
status: 'corrected',
oldValue: collectedData[field],
newValue
});
} catch (error) {
results.push({
field,
status: 'failed',
error: error.message
});
}
}
return {
method: 'api',
deliveredAt: new Date(),
corrections: results
};
}
/**
* Handle restriction request
*/
async handleRestrictionRequest(request, collectedData) {
// Restrict processing for specified purposes
const restrictions = request.requestDetails.restrictions || [];
for (const restriction of restrictions) {
await this.applyProcessingRestriction(
request.userId,
request.accountId,
restriction
);
}
return {
method: 'api',
deliveredAt: new Date(),
restrictionsApplied: restrictions
};
}
/**
* Handle objection request
*/
async handleObjectionRequest(request, collectedData) {
// Record objection and stop processing
const objections = request.requestDetails.objections || [];
for (const objection of objections) {
await this.recordProcessingObjection(
request.userId,
request.accountId,
objection
);
}
return {
method: 'api',
deliveredAt: new Date(),
objectionsRecorded: objections
};
}
/**
* Collect user data from all services
*/
async collectUserData(request) {
const { userId, accountId } = request;
const scope = request.requestDetails.scope;
const categories = request.requestDetails.dataCategories || ['all'];
// This would integrate with all services to collect data
const collectedData = {
profile: await this.getProfileData(userId, accountId),
activities: await this.getActivityData(userId, accountId),
messages: await this.getMessageData(userId, accountId),
analytics: await this.getAnalyticsData(userId, accountId),
consents: await this.getConsentData(userId, accountId),
metadata: {
collectionDate: new Date(),
userId,
accountId
}
};
// Filter by categories if specified
if (scope === 'specific' && !categories.includes('all')) {
const filteredData = {};
for (const category of categories) {
if (collectedData[category]) {
filteredData[category] = collectedData[category];
}
}
return filteredData;
}
return collectedData;
}
/**
* Mock data collection methods (would integrate with actual services)
*/
async getProfileData(userId, accountId) {
// Integrate with user service
return {
userId,
accountId,
createdAt: new Date(),
// ... other profile data
};
}
async getActivityData(userId, accountId) {
// Integrate with analytics service
return [];
}
async getMessageData(userId, accountId) {
// Integrate with messaging service
return [];
}
async getAnalyticsData(userId, accountId) {
// Integrate with analytics service
return {};
}
async getConsentData(userId, accountId) {
// Get from consent records
const ConsentRecord = require('../models/ConsentRecord').ConsentRecord;
return await ConsentRecord.find({ userId, accountId });
}
/**
* Format data for export
*/
formatDataForExport(data, format) {
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'csv':
return this.dataProtection.convertToCSV(data);
case 'xml':
return this.dataProtection.convertToXML(data);
default:
return data;
}
}
/**
* Generate download link
*/
async generateDownloadLink(data, format, requestId) {
// In production, this would upload to secure storage
const token = this.dataProtection.generateToken();
const expiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
// Store encrypted data with token
await this.redis.setex(
`download:${token}`,
7 * 24 * 60 * 60, // 7 days
this.dataProtection.encrypt({
data,
format,
requestId
})
);
return {
url: `/api/download/${token}`,
expiry,
token
};
}
/**
* Delete user data
*/
async deleteUserData(userId, accountId, category) {
// This would integrate with all services to delete data
logger.info(`Deleting ${category} data for user ${userId}`);
// Return mock result
return {
category,
count: Math.floor(Math.random() * 100) + 1
};
}
/**
* Anonymize remaining data
*/
async anonymizeRemainingData(userId, accountId) {
logger.info(`Anonymizing remaining data for user ${userId}`);
// This would integrate with services to anonymize data
}
/**
* Update user data
*/
async updateUserData(userId, accountId, field, newValue) {
logger.info(`Updating ${field} for user ${userId}`);
// This would integrate with services to update data
}
/**
* Apply processing restriction
*/
async applyProcessingRestriction(userId, accountId, restriction) {
logger.info(`Applying restriction ${restriction.type} for user ${userId}`);
// This would integrate with services to restrict processing
}
/**
* Record processing objection
*/
async recordProcessingObjection(userId, accountId, objection) {
logger.info(`Recording objection ${objection.type} for user ${userId}`);
// This would integrate with services to stop processing
}
/**
* Determine SLA based on request type and regulation
*/
determineSLA(type, regulation) {
const slaMap = {
gdpr: { default: 30, deletion: 30, access: 30 },
ccpa: { default: 45, deletion: 45, access: 45 },
lgpd: { default: 15, deletion: 20, access: 15 }
};
const regulationSLA = slaMap[regulation] || slaMap.gdpr;
return regulationSLA[type] || regulationSLA.default;
}
/**
* Get relevant articles based on request type
*/
getRelevantArticles(type, regulation) {
const articleMap = {
gdpr: {
access: ['Article 15'],
portability: ['Article 20'],
deletion: ['Article 17'],
rectification: ['Article 16'],
restriction: ['Article 18'],
objection: ['Article 21']
},
ccpa: {
access: ['Section 1798.100'],
deletion: ['Section 1798.105'],
opt_out: ['Section 1798.120']
}
};
const regulationArticles = articleMap[regulation] || articleMap.gdpr;
return regulationArticles[type] || [];
}
}

View File

@@ -0,0 +1,126 @@
import amqp from 'amqplib';
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';
let connection = null;
let channel = null;
/**
* Connect to RabbitMQ
*/
export const connectRabbitMQ = async () => {
try {
connection = await amqp.connect(config.rabbitmq.url);
channel = await connection.createChannel();
// Create exchange
await channel.assertExchange(config.rabbitmq.exchange, 'topic', {
durable: true
});
// Create queues
for (const [name, queueName] of Object.entries(config.rabbitmq.queues)) {
await channel.assertQueue(queueName, { durable: true });
}
logger.info('Connected to RabbitMQ');
// Handle connection events
connection.on('error', (err) => {
logger.error('RabbitMQ connection error:', err);
});
connection.on('close', () => {
logger.error('RabbitMQ connection closed, reconnecting...');
setTimeout(connectRabbitMQ, 5000);
});
return channel;
} catch (error) {
logger.error('Failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000);
throw error;
}
};
/**
* Publish event to RabbitMQ
*/
export const publishEvent = async (eventType, data) => {
try {
if (!channel) {
await connectRabbitMQ();
}
const message = {
eventType,
data,
timestamp: new Date().toISOString(),
service: 'compliance-guard'
};
const routingKey = `compliance.${eventType}`;
channel.publish(
config.rabbitmq.exchange,
routingKey,
Buffer.from(JSON.stringify(message)),
{ persistent: true }
);
logger.debug(`Published event: ${eventType}`, { data });
} catch (error) {
logger.error('Failed to publish event:', error);
}
};
/**
* Subscribe to events
*/
export const subscribeToEvents = async (patterns, handler) => {
try {
if (!channel) {
await connectRabbitMQ();
}
// Create exclusive queue for this consumer
const { queue } = await channel.assertQueue('', { exclusive: true });
// Bind patterns
for (const pattern of patterns) {
await channel.bindQueue(queue, config.rabbitmq.exchange, pattern);
}
// Consume messages
channel.consume(queue, async (msg) => {
if (!msg) return;
try {
const message = JSON.parse(msg.content.toString());
await handler(message);
channel.ack(msg);
} catch (error) {
logger.error('Error processing message:', error);
channel.nack(msg, false, false); // Don't requeue
}
});
logger.info(`Subscribed to patterns: ${patterns.join(', ')}`);
} catch (error) {
logger.error('Failed to subscribe to events:', error);
throw error;
}
};
/**
* Close RabbitMQ connection
*/
export const closeConnection = async () => {
try {
if (channel) await channel.close();
if (connection) await connection.close();
logger.info('RabbitMQ connection closed');
} catch (error) {
logger.error('Error closing RabbitMQ connection:', error);
}
};

View File

@@ -0,0 +1,217 @@
import jwt from 'jsonwebtoken';
import { config } from '../config/index.js';
import { logger } from './logger.js';
/**
* JWT authentication middleware
*/
export const auth = (requiredRole = null) => {
return async (req, res, next) => {
try {
// Extract token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'No token provided'
});
}
const token = authHeader.substring(7);
// Verify token
const decoded = jwt.verify(token, config.jwt.secret);
// Check token expiration
if (decoded.exp && decoded.exp < Date.now() / 1000) {
return res.status(401).json({
success: false,
error: 'Token expired'
});
}
// Check required role if specified
if (requiredRole && decoded.role !== requiredRole && decoded.role !== 'admin') {
return res.status(403).json({
success: false,
error: 'Insufficient permissions'
});
}
// Attach user info to request
req.user = {
id: decoded.userId,
accountId: decoded.accountId,
role: decoded.role,
permissions: decoded.permissions || []
};
next();
} catch (error) {
logger.error('Authentication error:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
error: 'Invalid token'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
error: 'Token expired'
});
}
return res.status(500).json({
success: false,
error: 'Authentication failed'
});
}
};
};
/**
* API key authentication middleware
*/
export const apiKeyAuth = () => {
return async (req, res, next) => {
try {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({
success: false,
error: 'API key required'
});
}
// In production, validate against database
if (apiKey !== config.apiKeys.internal) {
return res.status(401).json({
success: false,
error: 'Invalid API key'
});
}
// Set service context
req.service = {
name: 'internal-service',
permissions: ['read', 'write']
};
next();
} catch (error) {
logger.error('API key authentication error:', error);
return res.status(500).json({
success: false,
error: 'Authentication failed'
});
}
};
};
/**
* Permission check middleware
*/
export const requirePermission = (permission) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
const hasPermission = req.user.role === 'admin' ||
(req.user.permissions && req.user.permissions.includes(permission));
if (!hasPermission) {
return res.status(403).json({
success: false,
error: `Permission '${permission}' required`
});
}
next();
};
};
/**
* Rate limiting by user
*/
export const userRateLimit = (maxRequests = 100, windowMs = 60000) => {
const requests = new Map();
return (req, res, next) => {
if (!req.user) {
return next();
}
const userId = req.user.id;
const now = Date.now();
const userRequests = requests.get(userId) || [];
// Clean old requests
const validRequests = userRequests.filter(time => now - time < windowMs);
if (validRequests.length >= maxRequests) {
return res.status(429).json({
success: false,
error: 'Too many requests',
retryAfter: Math.ceil(windowMs / 1000)
});
}
validRequests.push(now);
requests.set(userId, validRequests);
next();
};
};
/**
* Generate JWT token
*/
export const generateToken = (payload, expiresIn = '24h') => {
return jwt.sign(payload, config.jwt.secret, {
expiresIn,
issuer: 'compliance-guard',
audience: 'marketing-agent'
});
};
/**
* Verify JWT token
*/
export const verifyToken = (token) => {
return jwt.verify(token, config.jwt.secret, {
issuer: 'compliance-guard',
audience: 'marketing-agent'
});
};
/**
* Hash API key
*/
export const hashApiKey = (apiKey) => {
const crypto = require('crypto');
return crypto.createHash('sha256').update(apiKey).digest('hex');
};
/**
* Validate request signature
*/
export const validateSignature = (payload, signature, secret) => {
const crypto = require('crypto');
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
};

View File

@@ -0,0 +1,162 @@
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import { config } from '../config/index.js';
const { combine, timestamp, json, printf, colorize, errors } = winston.format;
// Custom format for console output
const consoleFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
});
// Create transport for daily rotate file
const fileRotateTransport = new DailyRotateFile({
filename: 'logs/compliance-guard-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
format: combine(
timestamp(),
errors({ stack: true }),
json()
)
});
// Create transport for error logs
const errorFileTransport = new DailyRotateFile({
level: 'error',
filename: 'logs/compliance-guard-error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '30d',
format: combine(
timestamp(),
errors({ stack: true }),
json()
)
});
// Create transport for audit logs (separate from application logs)
const auditFileTransport = new DailyRotateFile({
filename: 'logs/audit-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '50m',
maxFiles: '90d', // Keep audit logs longer
format: combine(
timestamp(),
json()
)
});
// Create the logger
export const logger = winston.createLogger({
level: config.logging.level || 'info',
format: combine(
timestamp(),
errors({ stack: true }),
json()
),
defaultMeta: { service: 'compliance-guard' },
transports: [
fileRotateTransport,
errorFileTransport
]
});
// Create audit logger
export const auditLogger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
json()
),
defaultMeta: { service: 'compliance-guard-audit' },
transports: [
auditFileTransport
]
});
// Add console transport in non-production environments
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: combine(
colorize(),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
consoleFormat
)
}));
}
// Create a stream object for Morgan HTTP logger
logger.stream = {
write: (message) => {
logger.info(message.trim());
}
};
// Helper functions for structured logging
export const logError = (message, error, metadata = {}) => {
logger.error(message, {
error: {
message: error.message,
stack: error.stack,
code: error.code
},
...metadata
});
};
export const logWarning = (message, metadata = {}) => {
logger.warn(message, metadata);
};
export const logInfo = (message, metadata = {}) => {
logger.info(message, metadata);
};
export const logDebug = (message, metadata = {}) => {
logger.debug(message, metadata);
};
// Compliance-specific logging functions
export const logConsentAction = (action, consentData) => {
auditLogger.info('Consent action', {
action,
consentId: consentData.consentId,
userId: consentData.userId,
type: consentData.type,
timestamp: new Date().toISOString()
});
};
export const logDataRequest = (action, requestData) => {
auditLogger.info('Data request action', {
action,
requestId: requestData.requestId,
userId: requestData.userId,
type: requestData.type,
timestamp: new Date().toISOString()
});
};
export const logViolation = (violationData) => {
auditLogger.error('Compliance violation', {
violationId: violationData.violationId,
type: violationData.type,
severity: violationData.severity,
details: violationData.details,
timestamp: new Date().toISOString()
});
};
export const logSecurityEvent = (event, metadata = {}) => {
auditLogger.warn('Security event', {
event,
...metadata,
timestamp: new Date().toISOString()
});
};

View File

@@ -0,0 +1,319 @@
import promClient from 'prom-client';
import { logger } from './logger.js';
// Create a Registry
const register = new promClient.Registry();
// Add default metrics
promClient.collectDefaultMetrics({ register });
// Define custom metrics
const httpRequestDuration = new promClient.Histogram({
name: 'compliance_guard_http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
const apiCallCounter = new promClient.Counter({
name: 'compliance_guard_api_calls_total',
help: 'Total number of API calls',
labelNames: ['endpoint', 'status']
});
const consentCounter = new promClient.Counter({
name: 'compliance_guard_consent_total',
help: 'Total number of consent actions',
labelNames: ['type', 'action', 'status']
});
const dataRequestCounter = new promClient.Counter({
name: 'compliance_guard_data_requests_total',
help: 'Total number of data requests',
labelNames: ['type', 'status']
});
const violationCounter = new promClient.Counter({
name: 'compliance_guard_violations_total',
help: 'Total number of compliance violations',
labelNames: ['type', 'severity']
});
const activeDataRequestsGauge = new promClient.Gauge({
name: 'compliance_guard_active_data_requests',
help: 'Number of active data requests',
labelNames: ['type']
});
const pendingConsentRenewalsGauge = new promClient.Gauge({
name: 'compliance_guard_pending_consent_renewals',
help: 'Number of consents pending renewal'
});
const complianceScoreGauge = new promClient.Gauge({
name: 'compliance_guard_compliance_score',
help: 'Overall compliance score',
labelNames: ['regulation']
});
const dataProcessingLatency = new promClient.Histogram({
name: 'compliance_guard_data_processing_latency_seconds',
help: 'Latency of data processing operations',
labelNames: ['operation'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5, 10]
});
// Register metrics
register.registerMetric(httpRequestDuration);
register.registerMetric(apiCallCounter);
register.registerMetric(consentCounter);
register.registerMetric(dataRequestCounter);
register.registerMetric(violationCounter);
register.registerMetric(activeDataRequestsGauge);
register.registerMetric(pendingConsentRenewalsGauge);
register.registerMetric(complianceScoreGauge);
register.registerMetric(dataProcessingLatency);
// Metrics recording functions
export const metrics = {
/**
* Record HTTP request duration
*/
recordHttpRequest: (method, route, statusCode, duration) => {
httpRequestDuration
.labels(method, route, statusCode.toString())
.observe(duration);
},
/**
* Record API call
*/
recordApiCall: (endpoint, status) => {
apiCallCounter
.labels(endpoint, status)
.inc();
},
/**
* Record consent action
*/
recordConsentAction: (type, action, status) => {
consentCounter
.labels(type, action, status)
.inc();
},
/**
* Record data request
*/
recordDataRequest: (type, status) => {
dataRequestCounter
.labels(type, status)
.inc();
},
/**
* Record violation
*/
recordViolation: (type, severity) => {
violationCounter
.labels(type, severity)
.inc();
},
/**
* Update active data requests gauge
*/
updateActiveDataRequests: async (getCountsByType) => {
try {
const counts = await getCountsByType();
for (const [type, count] of Object.entries(counts)) {
activeDataRequestsGauge
.labels(type)
.set(count);
}
} catch (error) {
logger.error('Failed to update active data requests metric:', error);
}
},
/**
* Update pending consent renewals
*/
updatePendingConsentRenewals: async (getCount) => {
try {
const count = await getCount();
pendingConsentRenewalsGauge.set(count);
} catch (error) {
logger.error('Failed to update pending consent renewals metric:', error);
}
},
/**
* Update compliance score
*/
updateComplianceScore: (regulation, score) => {
complianceScoreGauge
.labels(regulation)
.set(score);
},
/**
* Record data processing latency
*/
recordDataProcessingLatency: (operation, duration) => {
dataProcessingLatency
.labels(operation)
.observe(duration);
},
/**
* Get metrics for Prometheus
*/
getMetrics: async () => {
return await register.metrics();
},
/**
* Get metrics in JSON format
*/
getMetricsJSON: async () => {
return await register.getMetricsAsJSON();
}
};
// Middleware to record HTTP metrics
export const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route ? req.route.path : 'unknown';
metrics.recordHttpRequest(
req.method,
route,
res.statusCode,
duration
);
});
next();
};
// Custom metrics for compliance monitoring
class ComplianceMetrics {
constructor() {
this.resetCounters();
}
resetCounters() {
this.counters = {
consentGrants: 0,
consentWithdrawals: 0,
dataRequests: 0,
dataRequestsCompleted: 0,
violations: 0,
violationsResolved: 0
};
}
incrementConsentGrants() {
this.counters.consentGrants++;
}
incrementConsentWithdrawals() {
this.counters.consentWithdrawals++;
}
incrementDataRequests() {
this.counters.dataRequests++;
}
incrementDataRequestsCompleted() {
this.counters.dataRequestsCompleted++;
}
incrementViolations() {
this.counters.violations++;
}
incrementViolationsResolved() {
this.counters.violationsResolved++;
}
getComplianceHealth() {
const health = {
status: 'healthy',
metrics: {
consentCompletionRate: this.counters.consentWithdrawals > 0
? (this.counters.consentGrants / (this.counters.consentGrants + this.counters.consentWithdrawals))
: 1,
dataRequestCompletionRate: this.counters.dataRequests > 0
? (this.counters.dataRequestsCompleted / this.counters.dataRequests)
: 1,
violationResolutionRate: this.counters.violations > 0
? (this.counters.violationsResolved / this.counters.violations)
: 1
}
};
// Determine overall health status
const rates = Object.values(health.metrics);
const avgRate = rates.reduce((a, b) => a + b, 0) / rates.length;
if (avgRate >= 0.95) {
health.status = 'healthy';
} else if (avgRate >= 0.80) {
health.status = 'warning';
} else {
health.status = 'critical';
}
return health;
}
}
export const complianceMetrics = new ComplianceMetrics();
// Periodic metrics update
export const startMetricsCollection = (models) => {
// Update metrics every minute
setInterval(async () => {
try {
// Update active data requests
if (models.DataRequest) {
const counts = await models.DataRequest.aggregate([
{ $match: { status: { $in: ['pending', 'processing'] } } },
{ $group: { _id: '$type', count: { $sum: 1 } } }
]);
const countsByType = counts.reduce((acc, item) => {
acc[item._id] = item.count;
return acc;
}, {});
await metrics.updateActiveDataRequests(async () => countsByType);
}
// Update pending consent renewals
if (models.ConsentRecord) {
await metrics.updatePendingConsentRenewals(async () => {
const thirtyDaysFromNow = new Date();
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
return await models.ConsentRecord.countDocuments({
status: 'granted',
renewable: true,
expiryDate: {
$gte: new Date(),
$lte: thirtyDaysFromNow
}
});
});
}
} catch (error) {
logger.error('Failed to update metrics:', error);
}
}, 60000); // Every minute
};

View File

@@ -0,0 +1,241 @@
import Joi from 'joi';
// Consent validation schemas
export const consentSchema = Joi.object({
type: Joi.string()
.valid('marketing', 'analytics', 'data_processing', 'third_party_sharing', 'cookies')
.required(),
purposes: Joi.array()
.items(Joi.string())
.min(1)
.required(),
duration: Joi.number()
.integer()
.min(1)
.max(730) // Max 2 years
.default(365),
renewable: Joi.boolean()
.default(true),
location: Joi.object({
country: Joi.string().length(2),
region: Joi.string(),
city: Joi.string()
}),
preferences: Joi.object({
channels: Joi.array().items(
Joi.string().valid('email', 'sms', 'push', 'in_app')
),
frequency: Joi.string().valid('always', 'daily', 'weekly', 'monthly'),
topics: Joi.array().items(Joi.string())
})
});
// Data request validation schemas
export const dataRequestSchema = Joi.object({
userId: Joi.string()
.required(),
type: Joi.string()
.valid('access', 'portability', 'deletion', 'rectification', 'restriction', 'objection')
.required(),
requestDetails: Joi.object({
scope: Joi.string()
.valid('all', 'specific')
.default('all'),
dataCategories: Joi.when('scope', {
is: 'specific',
then: Joi.array().items(Joi.string()).min(1).required(),
otherwise: Joi.array().items(Joi.string())
}),
format: Joi.string()
.valid('json', 'csv', 'xml')
.default('json'),
delivery: Joi.string()
.valid('download', 'email')
.default('download'),
corrections: Joi.when('type', {
is: 'rectification',
then: Joi.object().required(),
otherwise: Joi.object()
}),
restrictions: Joi.when('type', {
is: 'restriction',
then: Joi.array().items(Joi.object({
purpose: Joi.string().required(),
restriction: Joi.string().required()
})).required(),
otherwise: Joi.array()
}),
objections: Joi.when('type', {
is: 'objection',
then: Joi.array().items(Joi.object({
purpose: Joi.string().required(),
reason: Joi.string().required()
})).required(),
otherwise: Joi.array()
})
}).required(),
reason: Joi.string()
.max(500),
regulation: Joi.string()
.valid('gdpr', 'ccpa', 'lgpd', 'other')
.default('gdpr')
});
// Violation validation schemas
export const violationSchema = Joi.object({
type: Joi.string()
.valid(
'unauthorized_access',
'data_leak',
'consent_expired',
'retention_exceeded',
'unauthorized_sharing',
'security_breach',
'non_compliance',
'data_access_denied',
'notification_failure'
)
.required(),
severity: Joi.string()
.valid('low', 'medium', 'high', 'critical')
.required(),
details: Joi.object({
description: Joi.string()
.required(),
affectedUsers: Joi.number()
.integer()
.min(0)
.required(),
affectedData: Joi.array()
.items(Joi.string())
.required(),
regulation: Joi.string()
.valid('gdpr', 'ccpa', 'lgpd', 'other')
.required(),
articles: Joi.array()
.items(Joi.string()),
detectionMethod: Joi.string()
.valid('automated', 'manual', 'user_report', 'audit')
.required()
}).required(),
remediation: Joi.object({
status: Joi.string()
.valid('pending', 'in_progress', 'completed')
.default('pending'),
actions: Joi.array().items(Joi.object({
action: Joi.string().required(),
description: Joi.string(),
targetDate: Joi.date(),
status: Joi.string().valid('pending', 'completed').default('pending')
}))
})
});
// Validation middleware
export const validateConsent = (req, res, next) => {
const { error } = consentSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
error: 'Validation error',
details: error.details.map(d => d.message)
});
}
next();
};
export const validateDataRequest = (req, res, next) => {
const { error } = dataRequestSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
error: 'Validation error',
details: error.details.map(d => d.message)
});
}
next();
};
export const validateViolation = (req, res, next) => {
const { error } = violationSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
error: 'Validation error',
details: error.details.map(d => d.message)
});
}
next();
};
// Additional validation helpers
export const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
export const validatePhoneNumber = (phone) => {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
return phoneRegex.test(phone);
};
export const validateIPAddress = (ip) => {
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
const ipv6Regex = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
};
export const validateDateRange = (startDate, endDate) => {
const start = new Date(startDate);
const end = new Date(endDate);
return start <= end && start <= new Date();
};
export const validateRegulation = (regulation, country) => {
const regulationMap = {
gdpr: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR',
'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK',
'SI', 'ES', 'SE', 'GB', 'IS', 'LI', 'NO'],
ccpa: ['US-CA'],
lgpd: ['BR']
};
if (!regulationMap[regulation]) return false;
if (regulation === 'ccpa' && country === 'US') {
// CCPA only applies to California
return true;
}
return regulationMap[regulation].includes(country);
};
// Sanitization helpers
export const sanitizeInput = (input) => {
if (typeof input !== 'string') return input;
// Remove potential XSS vectors
return input
.replace(/[<>]/g, '')
.replace(/javascript:/gi, '')
.replace(/on\w+=/gi, '')
.trim();
};
export const sanitizeObject = (obj) => {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
sanitized[key] = sanitizeInput(value);
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = Array.isArray(value)
? value.map(item => typeof item === 'string' ? sanitizeInput(item) : item)
: sanitizeObject(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
};