5946aa47d5
Build pitch-deck / build-push-deploy (push) Successful in 1m37s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 38s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 30s
- runDataCleanup() replaces maskOverdueInvestors(): now also anonymizes never-activated invites after 90 days, deletes sessions + magic links older than 30 days, NULLs IPs in audit logs older than 30 days, and redacts email from audit log details JSONB for masked investors - New /api/admin/cleanup POST endpoint for scheduled invocation - New .gitea/workflows/pitch-cleanup.yml: daily cron at 02:00 UTC calls the cleanup endpoint so anonymization is genuinely automatic, not lazy - Switch masking window from first_activity_at to last_login_at (30 days of inactivity; resets on each login) - Both auth pages: DSGVO footer now covers all Art. 13 requirements — data categories, retention cutoffs, Art. 15–21 rights, contact address, LfDI Baden-Württemberg as supervisory authority Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import pool from '@/lib/db'
|
|
|
|
const MASKING_DAYS = parseInt(process.env.DATA_MASKING_DAYS || '30')
|
|
const NEVER_ACTIVATED_DAYS = parseInt(process.env.NEVER_ACTIVATED_DAYS || '90')
|
|
|
|
export interface CleanupStats {
|
|
investors_masked: number
|
|
sessions_deleted: number
|
|
audit_ips_anonymized: number
|
|
audit_details_redacted: number
|
|
magic_links_deleted: number
|
|
}
|
|
|
|
export async function runDataCleanup(): Promise<CleanupStats> {
|
|
const stats: CleanupStats = {
|
|
investors_masked: 0,
|
|
sessions_deleted: 0,
|
|
audit_ips_anonymized: 0,
|
|
audit_details_redacted: 0,
|
|
magic_links_deleted: 0,
|
|
}
|
|
|
|
const activeCutoff = new Date(Date.now() - MASKING_DAYS * 24 * 60 * 60 * 1000)
|
|
const neverActivatedCutoff = new Date(Date.now() - NEVER_ACTIVATED_DAYS * 24 * 60 * 60 * 1000)
|
|
|
|
// 1. Mask investors inactive for MASKING_DAYS
|
|
const { rows: maskedActive } = await pool.query<{ id: string }>(
|
|
`UPDATE pitch_investors
|
|
SET email = 'anon.' || id::text,
|
|
name = NULL, company = NULL,
|
|
status = 'revoked', data_masked_at = NOW(), updated_at = NOW()
|
|
WHERE last_login_at IS NOT NULL
|
|
AND last_login_at < $1
|
|
AND data_masked_at IS NULL
|
|
RETURNING id`,
|
|
[activeCutoff],
|
|
)
|
|
|
|
// 2. Mask investors invited but never activated after NEVER_ACTIVATED_DAYS
|
|
const { rows: maskedNever } = await pool.query<{ id: string }>(
|
|
`UPDATE pitch_investors
|
|
SET email = 'anon.' || id::text,
|
|
name = NULL, company = NULL,
|
|
status = 'revoked', data_masked_at = NOW(), updated_at = NOW()
|
|
WHERE last_login_at IS NULL
|
|
AND created_at < $1
|
|
AND data_masked_at IS NULL
|
|
RETURNING id`,
|
|
[neverActivatedCutoff],
|
|
)
|
|
|
|
const allMaskedIds = [...maskedActive, ...maskedNever].map((r) => r.id)
|
|
stats.investors_masked = allMaskedIds.length
|
|
|
|
if (allMaskedIds.length > 0) {
|
|
// 3. Revoke sessions for newly masked investors
|
|
await pool.query(
|
|
`UPDATE pitch_sessions SET revoked = true
|
|
WHERE investor_id = ANY($1::uuid[]) AND NOT revoked`,
|
|
[allMaskedIds],
|
|
)
|
|
|
|
// 4. Redact email field from audit log details for masked investors
|
|
const { rowCount } = await pool.query(
|
|
`UPDATE pitch_audit_logs
|
|
SET details = details - 'email'
|
|
WHERE investor_id = ANY($1::uuid[]) AND details ? 'email'`,
|
|
[allMaskedIds],
|
|
)
|
|
stats.audit_details_redacted = rowCount ?? 0
|
|
}
|
|
|
|
// 5. Delete sessions older than MASKING_DAYS (expired, contain IP/UA)
|
|
const { rowCount: sessionsDeleted } = await pool.query(
|
|
`DELETE FROM pitch_sessions WHERE created_at < $1`,
|
|
[activeCutoff],
|
|
)
|
|
stats.sessions_deleted = sessionsDeleted ?? 0
|
|
|
|
// 6. Anonymize IP addresses in audit logs older than MASKING_DAYS
|
|
const { rowCount: logsAnonymized } = await pool.query(
|
|
`UPDATE pitch_audit_logs
|
|
SET ip_address = NULL
|
|
WHERE created_at < $1 AND ip_address IS NOT NULL`,
|
|
[activeCutoff],
|
|
)
|
|
stats.audit_ips_anonymized = logsAnonymized ?? 0
|
|
|
|
// 7. Delete magic links older than MASKING_DAYS
|
|
const { rowCount: linksDeleted } = await pool.query(
|
|
`DELETE FROM pitch_magic_links WHERE created_at < $1`,
|
|
[activeCutoff],
|
|
)
|
|
stats.magic_links_deleted = linksDeleted ?? 0
|
|
|
|
return stats
|
|
}
|