← back to Watches
middleware/gdpr.js
548 lines
/**
* GDPR Compliance Middleware
* Implements EU General Data Protection Regulation requirements
* - Cookie consent management
* - Data retention policies
* - Right to access
* - Right to be forgotten
* - Data portability
* - Privacy by design
*/
import crypto from 'crypto';
// ============================================================================
// COOKIE CONSENT MANAGEMENT
// ============================================================================
class CookieConsentManager {
constructor() {
this.consents = new Map(); // userId -> consent data
}
/**
* Record user's cookie consent preferences
*/
recordConsent(userId, preferences) {
const consent = {
userId,
timestamp: new Date().toISOString(),
version: '1.0',
preferences: {
necessary: true, // Always true - required for functionality
analytics: preferences.analytics || false,
marketing: preferences.marketing || false,
preferences: preferences.preferences || false
},
ip: preferences.ip,
userAgent: preferences.userAgent
};
this.consents.set(userId, consent);
return consent;
}
/**
* Get user's consent status
*/
getConsent(userId) {
return this.consents.get(userId);
}
/**
* Check if specific cookie category is allowed
*/
isAllowed(userId, category) {
const consent = this.consents.get(userId);
if (!consent) return false;
return consent.preferences[category] || false;
}
/**
* Revoke consent
*/
revokeConsent(userId) {
this.consents.delete(userId);
}
}
export const cookieConsent = new CookieConsentManager();
/**
* Middleware to check cookie consent before setting analytics cookies
*/
export const checkCookieConsent = (req, res, next) => {
const userId = req.cookies?.userId || req.headers['x-user-id'];
if (!userId) {
// First-time visitor - set necessary cookies only
res.cookie('session', crypto.randomUUID(), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
req.cookieConsentRequired = true;
} else {
const consent = cookieConsent.getConsent(userId);
req.cookieConsent = consent;
}
next();
};
// ============================================================================
// DATA RETENTION POLICIES
// ============================================================================
class DataRetentionManager {
constructor() {
this.policies = {
viewCounts: 90, // days
watchlists: 365, // days
securityLogs: 90, // days
userPreferences: 730, // days (2 years)
anonymousData: 30 // days
};
}
/**
* Check if data should be deleted based on retention policy
*/
shouldDelete(dataType, createdAt) {
const retentionDays = this.policies[dataType];
if (!retentionDays) return false;
const ageInDays = (Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24);
return ageInDays > retentionDays;
}
/**
* Clean up expired data
*/
cleanupExpiredData(dataStore, dataType) {
const expired = [];
for (const [key, value] of Object.entries(dataStore)) {
if (this.shouldDelete(dataType, value.createdAt)) {
expired.push(key);
}
}
expired.forEach(key => delete dataStore[key]);
return {
cleaned: expired.length,
dataType,
timestamp: new Date().toISOString()
};
}
/**
* Get retention policy information
*/
getPolicies() {
return { ...this.policies };
}
}
export const dataRetention = new DataRetentionManager();
// ============================================================================
// USER DATA MANAGEMENT
// ============================================================================
class UserDataManager {
constructor() {
this.userData = new Map(); // userId -> user data
}
/**
* Store user data with timestamp
*/
storeData(userId, data) {
const existing = this.userData.get(userId) || {};
this.userData.set(userId, {
...existing,
...data,
updatedAt: new Date().toISOString(),
createdAt: existing.createdAt || new Date().toISOString()
});
}
/**
* Get all data for a user (Right to Access - Art. 15 GDPR)
*/
getUserData(userId) {
return this.userData.get(userId) || null;
}
/**
* Export user data in machine-readable format (Right to Data Portability - Art. 20 GDPR)
*/
exportUserData(userId) {
const data = this.getUserData(userId);
if (!data) {
return null;
}
return {
userId,
exportDate: new Date().toISOString(),
format: 'JSON',
data: {
profile: data.profile || {},
watchlists: data.watchlists || [],
preferences: data.preferences || {},
viewHistory: data.viewHistory || [],
cookieConsent: cookieConsent.getConsent(userId)
},
metadata: {
createdAt: data.createdAt,
updatedAt: data.updatedAt,
dataRetentionPolicy: dataRetention.getPolicies()
}
};
}
/**
* Delete all user data (Right to be Forgotten - Art. 17 GDPR)
*/
deleteUserData(userId) {
const hadData = this.userData.has(userId);
// Delete from all data stores
this.userData.delete(userId);
cookieConsent.revokeConsent(userId);
return {
userId,
deleted: hadData,
timestamp: new Date().toISOString(),
dataTypes: ['profile', 'watchlists', 'preferences', 'cookieConsent', 'viewHistory']
};
}
/**
* Anonymize user data (Alternative to deletion when legal basis exists)
*/
anonymizeUserData(userId) {
const data = this.userData.get(userId);
if (!data) {
return null;
}
// Create anonymized version
const anonymousId = crypto.randomUUID();
const anonymizedData = {
...data,
userId: anonymousId,
anonymized: true,
originalUserId: null, // Cannot be linked back
anonymizedAt: new Date().toISOString()
};
// Remove original
this.userData.delete(userId);
// Store anonymized version
this.userData.set(anonymousId, anonymizedData);
return {
success: true,
newId: anonymousId,
timestamp: new Date().toISOString()
};
}
/**
* Restrict processing (Right to Restriction - Art. 18 GDPR)
*/
restrictProcessing(userId, reason) {
const data = this.userData.get(userId);
if (!data) {
return null;
}
data.processingRestricted = true;
data.restrictionReason = reason;
data.restrictedAt = new Date().toISOString();
this.userData.set(userId, data);
return {
userId,
restricted: true,
reason,
timestamp: new Date().toISOString()
};
}
/**
* Check if processing is restricted
*/
isProcessingRestricted(userId) {
const data = this.userData.get(userId);
return data?.processingRestricted || false;
}
}
export const userDataManager = new UserDataManager();
// ============================================================================
// PRIVACY MIDDLEWARE
// ============================================================================
/**
* Middleware to check if user has restricted processing
*/
export const checkProcessingRestriction = (req, res, next) => {
const userId = req.cookies?.userId || req.headers['x-user-id'];
if (userId && userDataManager.isProcessingRestricted(userId)) {
return res.status(403).json({
error: 'Processing restricted',
message: 'Data processing is currently restricted for this user',
contact: 'privacy@omegawatches.local'
});
}
next();
};
/**
* Middleware to anonymize IP addresses (Privacy by Design)
*/
export const anonymizeIP = (req, res, next) => {
if (req.ip) {
// Anonymize last octet of IPv4 or last 80 bits of IPv6
const parts = req.ip.split('.');
if (parts.length === 4) {
req.anonymizedIP = `${parts[0]}.${parts[1]}.${parts[2]}.0`;
} else {
// IPv6 - anonymize last 80 bits
const ipv6Parts = req.ip.split(':');
req.anonymizedIP = ipv6Parts.slice(0, 3).join(':') + '::0';
}
}
next();
};
/**
* Middleware to log data access for audit trail
*/
export const logDataAccess = (req, res, next) => {
const userId = req.cookies?.userId || req.headers['x-user-id'];
if (userId && req.path.includes('/api/watchlist') || req.path.includes('/api/user')) {
// Log personal data access
console.log(`[GDPR AUDIT] Data access: ${req.method} ${req.path} by ${userId} at ${new Date().toISOString()}`);
}
next();
};
// ============================================================================
// GDPR API ENDPOINTS HELPERS
// ============================================================================
/**
* Handle GDPR data access request
*/
export const handleDataAccessRequest = (req, res) => {
const userId = req.params.userId || req.cookies?.userId;
if (!userId) {
return res.status(400).json({
error: 'User ID required',
message: 'Please provide a valid user ID'
});
}
const userData = userDataManager.exportUserData(userId);
if (!userData) {
return res.status(404).json({
error: 'No data found',
message: 'No personal data found for this user ID'
});
}
// Set headers for download
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename="personal-data-${userId}.json"`);
res.json(userData);
};
/**
* Handle GDPR data deletion request (Right to be Forgotten)
*/
export const handleDataDeletionRequest = (req, res) => {
const userId = req.params.userId || req.body.userId;
if (!userId) {
return res.status(400).json({
error: 'User ID required',
message: 'Please provide a valid user ID'
});
}
const result = userDataManager.deleteUserData(userId);
res.json({
success: true,
message: 'All personal data has been permanently deleted',
details: result,
confirmationId: crypto.randomUUID()
});
};
/**
* Handle cookie consent
*/
export const handleConsentUpdate = (req, res) => {
const userId = req.body.userId || crypto.randomUUID();
const preferences = req.body.preferences || {};
const consent = cookieConsent.recordConsent(userId, {
...preferences,
ip: req.anonymizedIP || req.ip,
userAgent: req.get('user-agent')
});
// Set cookie with user ID
res.cookie('userId', userId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 365 * 24 * 60 * 60 * 1000 // 1 year
});
res.json({
success: true,
consent,
message: 'Cookie preferences updated successfully'
});
};
/**
* Generate privacy policy text
*/
export const getPrivacyPolicyData = () => {
return {
version: '1.0',
effectiveDate: '2024-01-01',
lastUpdated: new Date().toISOString(),
dataController: {
name: 'Omega Watch Price History Platform',
contact: 'privacy@omegawatches.local',
address: 'Server: 45.61.58.125:7600'
},
dataCollected: [
{
type: 'Technical Data',
description: 'IP address (anonymized), browser type, device type',
purpose: 'Service functionality and security',
legalBasis: 'Legitimate interest (Art. 6(1)(f) GDPR)',
retention: '90 days'
},
{
type: 'Usage Data',
description: 'Pages viewed, watch preferences, search queries',
purpose: 'Service improvement and analytics',
legalBasis: 'Consent (Art. 6(1)(a) GDPR)',
retention: '1 year with consent'
},
{
type: 'Watchlist Data',
description: 'Saved watches and preferences',
purpose: 'Provide personalized experience',
legalBasis: 'Consent (Art. 6(1)(a) GDPR)',
retention: '1 year with consent'
}
],
userRights: [
'Right to access (Art. 15 GDPR)',
'Right to rectification (Art. 16 GDPR)',
'Right to erasure (Art. 17 GDPR)',
'Right to restriction of processing (Art. 18 GDPR)',
'Right to data portability (Art. 20 GDPR)',
'Right to object (Art. 21 GDPR)',
'Right to withdraw consent (Art. 7(3) GDPR)'
],
cookiePolicy: {
necessary: {
description: 'Essential for website functionality',
canDisable: false,
examples: ['Session management', 'Security tokens']
},
analytics: {
description: 'Help us understand how you use the site',
canDisable: true,
examples: ['Page views', 'Popular watches']
},
preferences: {
description: 'Remember your settings and preferences',
canDisable: true,
examples: ['Dark mode', 'Language preference']
}
},
dataRetentionPolicies: dataRetention.getPolicies(),
contact: {
dataProtectionOfficer: 'privacy@omegawatches.local',
supervisoryAuthority: 'Your local data protection authority'
}
};
};
/**
* Middleware to inject GDPR headers
*/
export const gdprHeaders = (req, res, next) => {
res.setHeader('X-Privacy-Policy', '/privacy-policy');
res.setHeader('X-Data-Request', '/api/gdpr/data-request');
next();
};
// ============================================================================
// SCHEDULED CLEANUP
// ============================================================================
/**
* Run scheduled data cleanup (should be called via cron job)
*/
export const runScheduledCleanup = () => {
console.log('[GDPR] Running scheduled data cleanup...');
const results = {
timestamp: new Date().toISOString(),
cleaned: []
};
// This would clean actual data stores in production
console.log('[GDPR] Cleanup completed:', results);
return results;
};
export default {
cookieConsent,
dataRetention,
userDataManager,
checkCookieConsent,
checkProcessingRestriction,
anonymizeIP,
logDataAccess,
handleDataAccessRequest,
handleDataDeletionRequest,
handleConsentUpdate,
getPrivacyPolicyData,
gdprHeaders,
runScheduledCleanup
};