auth-gdpr-export.ts178 lines · main
| 1 | /** |
| 2 | * GDPR data export — Gap Fix #15. |
| 3 | * |
| 4 | * Returns a structured JSON dump of every record that belongs to a |
| 5 | * specific user across all briven auth tables in the project's tenant |
| 6 | * database. This is used for the "right to data portability" and |
| 7 | * "right of access" flows, and is called before account deletion so |
| 8 | * the admin can provide the user with a copy of their data. |
| 9 | * |
| 10 | * PII that is normally redacted in admin views (raw IPs, full emails |
| 11 | * of other users) is NOT included — only the target user's own data. |
| 12 | */ |
| 13 | |
| 14 | import { runInProjectDatabase } from '../db/data-plane.js'; |
| 15 | |
| 16 | export interface UserDataExport { |
| 17 | user: unknown; |
| 18 | sessions: unknown[]; |
| 19 | accounts: unknown[]; |
| 20 | twoFactors: unknown[]; |
| 21 | passkeys: unknown[]; |
| 22 | security: unknown | null; |
| 23 | metadata: unknown | null; |
| 24 | emails: unknown[]; |
| 25 | usernames: unknown[]; |
| 26 | devices: unknown[]; |
| 27 | orgMemberships: unknown[]; |
| 28 | orgInvites: unknown[]; |
| 29 | membershipRequests: unknown[]; |
| 30 | auditLog: unknown[]; |
| 31 | ssoSessions: unknown[]; |
| 32 | impersonationSessions: unknown[]; |
| 33 | passwordHistory: unknown[]; |
| 34 | exportedAt: string; |
| 35 | } |
| 36 | |
| 37 | export async function exportUserData( |
| 38 | projectId: string, |
| 39 | userId: string, |
| 40 | ): Promise<UserDataExport> { |
| 41 | return runInProjectDatabase<UserDataExport>(projectId, async (tx) => { |
| 42 | const user = ( |
| 43 | (await tx.unsafe( |
| 44 | `SELECT id, name, email, email_verified, image, two_factor_enabled, created_at, updated_at |
| 45 | FROM "_briven_auth_users" WHERE id = $1 LIMIT 1`, |
| 46 | [userId] as never, |
| 47 | )) as unknown[] |
| 48 | )[0]; |
| 49 | |
| 50 | const sessions = await tx.unsafe( |
| 51 | `SELECT id, token, expires_at, user_agent, created_at, updated_at |
| 52 | FROM "_briven_auth_sessions" WHERE user_id = $1`, |
| 53 | [userId] as never, |
| 54 | ); |
| 55 | |
| 56 | const accounts = await tx.unsafe( |
| 57 | `SELECT id, account_id, provider_id, scope, created_at, updated_at |
| 58 | FROM "_briven_auth_accounts" WHERE user_id = $1`, |
| 59 | [userId] as never, |
| 60 | ); |
| 61 | |
| 62 | const twoFactors = await tx.unsafe( |
| 63 | `SELECT id, backup_codes, verified, created_at, updated_at |
| 64 | FROM "_briven_auth_two_factors" WHERE user_id = $1`, |
| 65 | [userId] as never, |
| 66 | ); |
| 67 | |
| 68 | const passkeys = await tx.unsafe( |
| 69 | `SELECT id, name, credential_id, counter, created_at, updated_at |
| 70 | FROM "_briven_auth_passkeys" WHERE user_id = $1`, |
| 71 | [userId] as never, |
| 72 | ); |
| 73 | |
| 74 | const security = ( |
| 75 | (await tx.unsafe( |
| 76 | `SELECT suspended_at, suspended_reason, banned_at, banned_reason, ban_expires_at, created_at, updated_at |
| 77 | FROM "_briven_auth_user_security" WHERE user_id = $1 LIMIT 1`, |
| 78 | [userId] as never, |
| 79 | )) as unknown[] |
| 80 | )[0] ?? null; |
| 81 | |
| 82 | const metadata = ( |
| 83 | (await tx.unsafe( |
| 84 | `SELECT public_metadata, private_metadata, created_at, updated_at |
| 85 | FROM "_briven_auth_user_metadata" WHERE user_id = $1 LIMIT 1`, |
| 86 | [userId] as never, |
| 87 | )) as unknown[] |
| 88 | )[0] ?? null; |
| 89 | |
| 90 | const emails = await tx.unsafe( |
| 91 | `SELECT id, email, verified, primary, created_at, updated_at |
| 92 | FROM "_briven_auth_user_emails" WHERE user_id = $1`, |
| 93 | [userId] as never, |
| 94 | ); |
| 95 | |
| 96 | const usernames = await tx.unsafe( |
| 97 | `SELECT id, username, created_at, updated_at |
| 98 | FROM "_briven_auth_user_usernames" WHERE user_id = $1`, |
| 99 | [userId] as never, |
| 100 | ); |
| 101 | |
| 102 | const devices = await tx.unsafe( |
| 103 | `SELECT id, fingerprint, user_agent, created_at, updated_at |
| 104 | FROM "_briven_auth_devices" WHERE user_id = $1`, |
| 105 | [userId] as never, |
| 106 | ); |
| 107 | |
| 108 | const orgMemberships = await tx.unsafe( |
| 109 | `SELECT id, org_id, role, created_at, updated_at |
| 110 | FROM "_briven_auth_org_members" WHERE user_id = $1`, |
| 111 | [userId] as never, |
| 112 | ); |
| 113 | |
| 114 | const orgInvites = await tx.unsafe( |
| 115 | `SELECT id, org_id, email, role, token, expires_at, accepted_at, created_at, updated_at |
| 116 | FROM "_briven_auth_org_invites" WHERE invited_by = $1`, |
| 117 | [userId] as never, |
| 118 | ); |
| 119 | |
| 120 | const membershipRequests = await tx.unsafe( |
| 121 | `SELECT id, org_id, status, message, requested_at, resolved_at, resolved_by, created_at, updated_at |
| 122 | FROM "_briven_auth_org_membership_requests" WHERE user_id = $1`, |
| 123 | [userId] as never, |
| 124 | ); |
| 125 | |
| 126 | const auditLog = await tx.unsafe( |
| 127 | `SELECT id, action, metadata, occurred_at |
| 128 | FROM "_briven_auth_audit_log" WHERE user_id = $1 |
| 129 | ORDER BY occurred_at DESC |
| 130 | LIMIT 1000`, |
| 131 | [userId] as never, |
| 132 | ); |
| 133 | |
| 134 | const ssoSessions = await tx.unsafe( |
| 135 | `SELECT s.id, s.session_id, s.connection_id, s.created_at, s.updated_at |
| 136 | FROM "_briven_auth_sso_sessions" s |
| 137 | JOIN "_briven_auth_sessions" sess ON s.session_id = sess.id |
| 138 | WHERE sess.user_id = $1`, |
| 139 | [userId] as never, |
| 140 | ); |
| 141 | |
| 142 | const impersonationSessions = await tx.unsafe( |
| 143 | `SELECT i.id, i.session_id, i.impersonated_by, i.target_user_id, i.stopped_at, i.created_at, i.updated_at |
| 144 | FROM "_briven_auth_impersonation_sessions" i |
| 145 | JOIN "_briven_auth_sessions" sess ON i.session_id = sess.id |
| 146 | WHERE sess.user_id = $1 OR i.target_user_id = $1`, |
| 147 | [userId] as never, |
| 148 | ); |
| 149 | |
| 150 | const passwordHistory = await tx.unsafe( |
| 151 | `SELECT id, password_hash, created_at |
| 152 | FROM "_briven_auth_password_history" WHERE user_id = $1 |
| 153 | ORDER BY created_at DESC`, |
| 154 | [userId] as never, |
| 155 | ); |
| 156 | |
| 157 | return { |
| 158 | user: user ?? null, |
| 159 | sessions: sessions as unknown[], |
| 160 | accounts: accounts as unknown[], |
| 161 | twoFactors: twoFactors as unknown[], |
| 162 | passkeys: passkeys as unknown[], |
| 163 | security, |
| 164 | metadata, |
| 165 | emails: emails as unknown[], |
| 166 | usernames: usernames as unknown[], |
| 167 | devices: devices as unknown[], |
| 168 | orgMemberships: orgMemberships as unknown[], |
| 169 | orgInvites: orgInvites as unknown[], |
| 170 | membershipRequests: membershipRequests as unknown[], |
| 171 | auditLog: auditLog as unknown[], |
| 172 | ssoSessions: ssoSessions as unknown[], |
| 173 | impersonationSessions: impersonationSessions as unknown[], |
| 174 | passwordHistory: passwordHistory as unknown[], |
| 175 | exportedAt: new Date().toISOString(), |
| 176 | }; |
| 177 | }); |
| 178 | } |