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
14import { runInProjectDatabase } from '../db/data-plane.js';
15
16export 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
37export 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}