23b233bda3
Build pitch-deck / build-push-deploy (push) Successful in 1m30s
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 29s
CI / test-python-voice (push) Successful in 29s
CI / test-bqas (push) Successful in 30s
- New POST /api/admin/investors/[id]/generate-link endpoint: creates a magic link without sending email, returns the URL for the admin to copy and share manually (for when email is filtered) - Adds 'Copy Link' button (emerald) to investor list and detail pages; link is copied to clipboard on click - New lib/masking.ts: maskOverdueInvestors() UPDATE that anonymizes email/name/company → revokes sessions 72h after first investor login - first_activity_at recorded on first verify (COALESCE, set once only) - migration 004 adds first_activity_at + data_masked_at columns with partial index; also wired into /api/admin/migrate for one-shot apply - Admin UI shows 'anonymized' badge, expiry countdown, and masked state; Copy Link + Resend are disabled for anonymized investors - verify route returns 410 if data_masked_at is set (belt-and-suspenders alongside the revoked status check) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
31 lines
838 B
TypeScript
31 lines
838 B
TypeScript
import pool from '@/lib/db'
|
|
|
|
const MASKING_HOURS = parseInt(process.env.DATA_MASKING_HOURS || '72')
|
|
|
|
export async function maskOverdueInvestors(): Promise<void> {
|
|
const cutoff = new Date(Date.now() - MASKING_HOURS * 60 * 60 * 1000)
|
|
|
|
const { rows } = 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 first_activity_at IS NOT NULL
|
|
AND first_activity_at < $1
|
|
AND data_masked_at IS NULL
|
|
RETURNING id`,
|
|
[cutoff],
|
|
)
|
|
|
|
if (rows.length > 0) {
|
|
await pool.query(
|
|
`UPDATE pitch_sessions SET revoked = true
|
|
WHERE investor_id = ANY($1::uuid[]) AND NOT revoked`,
|
|
[rows.map((r) => r.id)],
|
|
)
|
|
}
|
|
}
|