@codaijs/keel 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- package/sails/stripe/install.ts +378 -378
|
@@ -1,293 +1,293 @@
|
|
|
1
|
-
import { eq, and, isNull, lte } from "drizzle-orm";
|
|
2
|
-
import { db } from "../db/index.js";
|
|
3
|
-
import { env } from "../env.js";
|
|
4
|
-
import {
|
|
5
|
-
users,
|
|
6
|
-
sessions,
|
|
7
|
-
accounts,
|
|
8
|
-
consentRecords,
|
|
9
|
-
deletionRequests,
|
|
10
|
-
} from "../db/schema/index.js";
|
|
11
|
-
import {
|
|
12
|
-
sendDeletionRequestedEmail,
|
|
13
|
-
sendDeletionCompletedEmail,
|
|
14
|
-
sendDeletionCancelledEmail,
|
|
15
|
-
sendDataExportReadyEmail,
|
|
16
|
-
sendConsentUpdatedEmail,
|
|
17
|
-
} from "../auth/email.js";
|
|
18
|
-
|
|
19
|
-
export async function exportUserData(userId: string) {
|
|
20
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
21
|
-
const userSessions = await db
|
|
22
|
-
.select()
|
|
23
|
-
.from(sessions)
|
|
24
|
-
.where(eq(sessions.userId, userId));
|
|
25
|
-
const userAccounts = await db
|
|
26
|
-
.select({
|
|
27
|
-
id: accounts.id,
|
|
28
|
-
accountId: accounts.accountId,
|
|
29
|
-
providerId: accounts.providerId,
|
|
30
|
-
scope: accounts.scope,
|
|
31
|
-
createdAt: accounts.createdAt,
|
|
32
|
-
})
|
|
33
|
-
.from(accounts)
|
|
34
|
-
.where(eq(accounts.userId, userId));
|
|
35
|
-
const userConsents = await db
|
|
36
|
-
.select()
|
|
37
|
-
.from(consentRecords)
|
|
38
|
-
.where(eq(consentRecords.userId, userId));
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
profile: user
|
|
42
|
-
? {
|
|
43
|
-
id: user.id,
|
|
44
|
-
name: user.name,
|
|
45
|
-
email: user.email,
|
|
46
|
-
emailVerified: user.emailVerified,
|
|
47
|
-
image: user.image,
|
|
48
|
-
createdAt: user.createdAt,
|
|
49
|
-
updatedAt: user.updatedAt,
|
|
50
|
-
}
|
|
51
|
-
: null,
|
|
52
|
-
sessions: userSessions.map((s) => ({
|
|
53
|
-
id: s.id,
|
|
54
|
-
ipAddress: s.ipAddress,
|
|
55
|
-
userAgent: s.userAgent,
|
|
56
|
-
createdAt: s.createdAt,
|
|
57
|
-
expiresAt: s.expiresAt,
|
|
58
|
-
})),
|
|
59
|
-
accounts: userAccounts,
|
|
60
|
-
consents: userConsents,
|
|
61
|
-
exportedAt: new Date().toISOString(),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function requestDeletion(userId: string, reason?: string) {
|
|
66
|
-
// Check for an existing pending deletion request
|
|
67
|
-
const [existing] = await db
|
|
68
|
-
.select()
|
|
69
|
-
.from(deletionRequests)
|
|
70
|
-
.where(
|
|
71
|
-
and(
|
|
72
|
-
eq(deletionRequests.userId, userId),
|
|
73
|
-
eq(deletionRequests.status, "pending"),
|
|
74
|
-
),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
if (existing) {
|
|
78
|
-
return existing;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const scheduledDeletionAt = new Date();
|
|
82
|
-
scheduledDeletionAt.setDate(scheduledDeletionAt.getDate() + 30);
|
|
83
|
-
|
|
84
|
-
const [request] = await db
|
|
85
|
-
.insert(deletionRequests)
|
|
86
|
-
.values({
|
|
87
|
-
userId,
|
|
88
|
-
reason: reason ?? null,
|
|
89
|
-
scheduledDeletionAt,
|
|
90
|
-
})
|
|
91
|
-
.returning();
|
|
92
|
-
|
|
93
|
-
// Send deletion requested email
|
|
94
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
95
|
-
if (user) {
|
|
96
|
-
const formattedDate = scheduledDeletionAt.toLocaleDateString("en-US", {
|
|
97
|
-
year: "numeric",
|
|
98
|
-
month: "long",
|
|
99
|
-
day: "numeric",
|
|
100
|
-
});
|
|
101
|
-
const cancelUrl = `${env.FRONTEND_URL}/settings/cancel-deletion?requestId=${request.id}`;
|
|
102
|
-
await sendDeletionRequestedEmail(
|
|
103
|
-
user.email,
|
|
104
|
-
user.name,
|
|
105
|
-
formattedDate,
|
|
106
|
-
cancelUrl,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return request;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function cancelDeletion(userId: string) {
|
|
114
|
-
const [updated] = await db
|
|
115
|
-
.update(deletionRequests)
|
|
116
|
-
.set({
|
|
117
|
-
status: "cancelled",
|
|
118
|
-
cancelledAt: new Date(),
|
|
119
|
-
})
|
|
120
|
-
.where(
|
|
121
|
-
and(
|
|
122
|
-
eq(deletionRequests.userId, userId),
|
|
123
|
-
eq(deletionRequests.status, "pending"),
|
|
124
|
-
),
|
|
125
|
-
)
|
|
126
|
-
.returning();
|
|
127
|
-
|
|
128
|
-
if (!updated) return null;
|
|
129
|
-
|
|
130
|
-
// Send deletion cancelled email
|
|
131
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
132
|
-
if (user) {
|
|
133
|
-
await sendDeletionCancelledEmail(
|
|
134
|
-
user.email,
|
|
135
|
-
user.name,
|
|
136
|
-
`${env.FRONTEND_URL}/dashboard`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return updated;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function processPendingDeletions() {
|
|
144
|
-
const now = new Date();
|
|
145
|
-
|
|
146
|
-
const expiredRequests = await db
|
|
147
|
-
.select()
|
|
148
|
-
.from(deletionRequests)
|
|
149
|
-
.where(
|
|
150
|
-
and(
|
|
151
|
-
eq(deletionRequests.status, "pending"),
|
|
152
|
-
lte(deletionRequests.scheduledDeletionAt, now),
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const results: Array<{ userId: string; success: boolean; error?: string }> =
|
|
157
|
-
[];
|
|
158
|
-
|
|
159
|
-
for (const request of expiredRequests) {
|
|
160
|
-
try {
|
|
161
|
-
// Fetch user info before deletion so we can send the email
|
|
162
|
-
const [user] = await db
|
|
163
|
-
.select()
|
|
164
|
-
.from(users)
|
|
165
|
-
.where(eq(users.id, request.userId));
|
|
166
|
-
|
|
167
|
-
// Delete user data (cascades handle related records)
|
|
168
|
-
await db.delete(users).where(eq(users.id, request.userId));
|
|
169
|
-
|
|
170
|
-
// Mark deletion as completed
|
|
171
|
-
await db
|
|
172
|
-
.update(deletionRequests)
|
|
173
|
-
.set({
|
|
174
|
-
status: "completed",
|
|
175
|
-
completedAt: new Date(),
|
|
176
|
-
})
|
|
177
|
-
.where(eq(deletionRequests.id, request.id));
|
|
178
|
-
|
|
179
|
-
// Send deletion completed email
|
|
180
|
-
if (user) {
|
|
181
|
-
await sendDeletionCompletedEmail(user.email, user.name);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
results.push({ userId: request.userId, success: true });
|
|
185
|
-
} catch (error) {
|
|
186
|
-
const message =
|
|
187
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
188
|
-
results.push({ userId: request.userId, success: false, error: message });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return results;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export async function sendExportReadyNotification(
|
|
196
|
-
userId: string,
|
|
197
|
-
downloadUrl: string,
|
|
198
|
-
expiresIn: string,
|
|
199
|
-
) {
|
|
200
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
201
|
-
if (user) {
|
|
202
|
-
await sendDataExportReadyEmail(user.email, user.name, downloadUrl, expiresIn);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export async function recordConsent(
|
|
207
|
-
userId: string,
|
|
208
|
-
input: { consentType: string; granted: boolean; version: string },
|
|
209
|
-
ip?: string,
|
|
210
|
-
userAgent?: string,
|
|
211
|
-
) {
|
|
212
|
-
const [record] = await db
|
|
213
|
-
.insert(consentRecords)
|
|
214
|
-
.values({
|
|
215
|
-
userId,
|
|
216
|
-
consentType: input.consentType,
|
|
217
|
-
granted: input.granted,
|
|
218
|
-
version: input.version,
|
|
219
|
-
ipAddress: ip ?? null,
|
|
220
|
-
userAgent: userAgent ?? null,
|
|
221
|
-
})
|
|
222
|
-
.returning();
|
|
223
|
-
|
|
224
|
-
// Send consent updated email
|
|
225
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
226
|
-
if (user) {
|
|
227
|
-
await sendConsentUpdatedEmail(user.email, user.name, [
|
|
228
|
-
{
|
|
229
|
-
type: input.consentType,
|
|
230
|
-
action: input.granted ? "granted" : "revoked",
|
|
231
|
-
},
|
|
232
|
-
]);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return record;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export async function revokeConsent(userId: string, consentType: string) {
|
|
239
|
-
const [updated] = await db
|
|
240
|
-
.update(consentRecords)
|
|
241
|
-
.set({ revokedAt: new Date() })
|
|
242
|
-
.where(
|
|
243
|
-
and(
|
|
244
|
-
eq(consentRecords.userId, userId),
|
|
245
|
-
eq(consentRecords.consentType, consentType),
|
|
246
|
-
isNull(consentRecords.revokedAt),
|
|
247
|
-
),
|
|
248
|
-
)
|
|
249
|
-
.returning();
|
|
250
|
-
|
|
251
|
-
if (!updated) return null;
|
|
252
|
-
|
|
253
|
-
// Send consent updated email
|
|
254
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
255
|
-
if (user) {
|
|
256
|
-
await sendConsentUpdatedEmail(user.email, user.name, [
|
|
257
|
-
{ type: consentType, action: "revoked" },
|
|
258
|
-
]);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return updated;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export async function getUserConsents(userId: string) {
|
|
265
|
-
return db
|
|
266
|
-
.select()
|
|
267
|
-
.from(consentRecords)
|
|
268
|
-
.where(
|
|
269
|
-
and(
|
|
270
|
-
eq(consentRecords.userId, userId),
|
|
271
|
-
isNull(consentRecords.revokedAt),
|
|
272
|
-
),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export async function immediatelyDeleteUser(userId: string) {
|
|
277
|
-
// Fetch user info before deletion
|
|
278
|
-
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
279
|
-
if (!user) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const email = user.email;
|
|
284
|
-
const name = user.name;
|
|
285
|
-
|
|
286
|
-
// Delete user data (cascades handle related records)
|
|
287
|
-
await db.delete(users).where(eq(users.id, userId));
|
|
288
|
-
|
|
289
|
-
// Send deletion completed email
|
|
290
|
-
await sendDeletionCompletedEmail(email, name);
|
|
291
|
-
|
|
292
|
-
return { userId, email };
|
|
293
|
-
}
|
|
1
|
+
import { eq, and, isNull, lte } from "drizzle-orm";
|
|
2
|
+
import { db } from "../db/index.js";
|
|
3
|
+
import { env } from "../env.js";
|
|
4
|
+
import {
|
|
5
|
+
users,
|
|
6
|
+
sessions,
|
|
7
|
+
accounts,
|
|
8
|
+
consentRecords,
|
|
9
|
+
deletionRequests,
|
|
10
|
+
} from "../db/schema/index.js";
|
|
11
|
+
import {
|
|
12
|
+
sendDeletionRequestedEmail,
|
|
13
|
+
sendDeletionCompletedEmail,
|
|
14
|
+
sendDeletionCancelledEmail,
|
|
15
|
+
sendDataExportReadyEmail,
|
|
16
|
+
sendConsentUpdatedEmail,
|
|
17
|
+
} from "../auth/email.js";
|
|
18
|
+
|
|
19
|
+
export async function exportUserData(userId: string) {
|
|
20
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
21
|
+
const userSessions = await db
|
|
22
|
+
.select()
|
|
23
|
+
.from(sessions)
|
|
24
|
+
.where(eq(sessions.userId, userId));
|
|
25
|
+
const userAccounts = await db
|
|
26
|
+
.select({
|
|
27
|
+
id: accounts.id,
|
|
28
|
+
accountId: accounts.accountId,
|
|
29
|
+
providerId: accounts.providerId,
|
|
30
|
+
scope: accounts.scope,
|
|
31
|
+
createdAt: accounts.createdAt,
|
|
32
|
+
})
|
|
33
|
+
.from(accounts)
|
|
34
|
+
.where(eq(accounts.userId, userId));
|
|
35
|
+
const userConsents = await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(consentRecords)
|
|
38
|
+
.where(eq(consentRecords.userId, userId));
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
profile: user
|
|
42
|
+
? {
|
|
43
|
+
id: user.id,
|
|
44
|
+
name: user.name,
|
|
45
|
+
email: user.email,
|
|
46
|
+
emailVerified: user.emailVerified,
|
|
47
|
+
image: user.image,
|
|
48
|
+
createdAt: user.createdAt,
|
|
49
|
+
updatedAt: user.updatedAt,
|
|
50
|
+
}
|
|
51
|
+
: null,
|
|
52
|
+
sessions: userSessions.map((s) => ({
|
|
53
|
+
id: s.id,
|
|
54
|
+
ipAddress: s.ipAddress,
|
|
55
|
+
userAgent: s.userAgent,
|
|
56
|
+
createdAt: s.createdAt,
|
|
57
|
+
expiresAt: s.expiresAt,
|
|
58
|
+
})),
|
|
59
|
+
accounts: userAccounts,
|
|
60
|
+
consents: userConsents,
|
|
61
|
+
exportedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function requestDeletion(userId: string, reason?: string) {
|
|
66
|
+
// Check for an existing pending deletion request
|
|
67
|
+
const [existing] = await db
|
|
68
|
+
.select()
|
|
69
|
+
.from(deletionRequests)
|
|
70
|
+
.where(
|
|
71
|
+
and(
|
|
72
|
+
eq(deletionRequests.userId, userId),
|
|
73
|
+
eq(deletionRequests.status, "pending"),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (existing) {
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const scheduledDeletionAt = new Date();
|
|
82
|
+
scheduledDeletionAt.setDate(scheduledDeletionAt.getDate() + 30);
|
|
83
|
+
|
|
84
|
+
const [request] = await db
|
|
85
|
+
.insert(deletionRequests)
|
|
86
|
+
.values({
|
|
87
|
+
userId,
|
|
88
|
+
reason: reason ?? null,
|
|
89
|
+
scheduledDeletionAt,
|
|
90
|
+
})
|
|
91
|
+
.returning();
|
|
92
|
+
|
|
93
|
+
// Send deletion requested email
|
|
94
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
95
|
+
if (user) {
|
|
96
|
+
const formattedDate = scheduledDeletionAt.toLocaleDateString("en-US", {
|
|
97
|
+
year: "numeric",
|
|
98
|
+
month: "long",
|
|
99
|
+
day: "numeric",
|
|
100
|
+
});
|
|
101
|
+
const cancelUrl = `${env.FRONTEND_URL}/settings/cancel-deletion?requestId=${request.id}`;
|
|
102
|
+
await sendDeletionRequestedEmail(
|
|
103
|
+
user.email,
|
|
104
|
+
user.name,
|
|
105
|
+
formattedDate,
|
|
106
|
+
cancelUrl,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return request;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function cancelDeletion(userId: string) {
|
|
114
|
+
const [updated] = await db
|
|
115
|
+
.update(deletionRequests)
|
|
116
|
+
.set({
|
|
117
|
+
status: "cancelled",
|
|
118
|
+
cancelledAt: new Date(),
|
|
119
|
+
})
|
|
120
|
+
.where(
|
|
121
|
+
and(
|
|
122
|
+
eq(deletionRequests.userId, userId),
|
|
123
|
+
eq(deletionRequests.status, "pending"),
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
.returning();
|
|
127
|
+
|
|
128
|
+
if (!updated) return null;
|
|
129
|
+
|
|
130
|
+
// Send deletion cancelled email
|
|
131
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
132
|
+
if (user) {
|
|
133
|
+
await sendDeletionCancelledEmail(
|
|
134
|
+
user.email,
|
|
135
|
+
user.name,
|
|
136
|
+
`${env.FRONTEND_URL}/dashboard`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return updated;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function processPendingDeletions() {
|
|
144
|
+
const now = new Date();
|
|
145
|
+
|
|
146
|
+
const expiredRequests = await db
|
|
147
|
+
.select()
|
|
148
|
+
.from(deletionRequests)
|
|
149
|
+
.where(
|
|
150
|
+
and(
|
|
151
|
+
eq(deletionRequests.status, "pending"),
|
|
152
|
+
lte(deletionRequests.scheduledDeletionAt, now),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const results: Array<{ userId: string; success: boolean; error?: string }> =
|
|
157
|
+
[];
|
|
158
|
+
|
|
159
|
+
for (const request of expiredRequests) {
|
|
160
|
+
try {
|
|
161
|
+
// Fetch user info before deletion so we can send the email
|
|
162
|
+
const [user] = await db
|
|
163
|
+
.select()
|
|
164
|
+
.from(users)
|
|
165
|
+
.where(eq(users.id, request.userId));
|
|
166
|
+
|
|
167
|
+
// Delete user data (cascades handle related records)
|
|
168
|
+
await db.delete(users).where(eq(users.id, request.userId));
|
|
169
|
+
|
|
170
|
+
// Mark deletion as completed
|
|
171
|
+
await db
|
|
172
|
+
.update(deletionRequests)
|
|
173
|
+
.set({
|
|
174
|
+
status: "completed",
|
|
175
|
+
completedAt: new Date(),
|
|
176
|
+
})
|
|
177
|
+
.where(eq(deletionRequests.id, request.id));
|
|
178
|
+
|
|
179
|
+
// Send deletion completed email
|
|
180
|
+
if (user) {
|
|
181
|
+
await sendDeletionCompletedEmail(user.email, user.name);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
results.push({ userId: request.userId, success: true });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const message =
|
|
187
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
188
|
+
results.push({ userId: request.userId, success: false, error: message });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function sendExportReadyNotification(
|
|
196
|
+
userId: string,
|
|
197
|
+
downloadUrl: string,
|
|
198
|
+
expiresIn: string,
|
|
199
|
+
) {
|
|
200
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
201
|
+
if (user) {
|
|
202
|
+
await sendDataExportReadyEmail(user.email, user.name, downloadUrl, expiresIn);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function recordConsent(
|
|
207
|
+
userId: string,
|
|
208
|
+
input: { consentType: string; granted: boolean; version: string },
|
|
209
|
+
ip?: string,
|
|
210
|
+
userAgent?: string,
|
|
211
|
+
) {
|
|
212
|
+
const [record] = await db
|
|
213
|
+
.insert(consentRecords)
|
|
214
|
+
.values({
|
|
215
|
+
userId,
|
|
216
|
+
consentType: input.consentType,
|
|
217
|
+
granted: input.granted,
|
|
218
|
+
version: input.version,
|
|
219
|
+
ipAddress: ip ?? null,
|
|
220
|
+
userAgent: userAgent ?? null,
|
|
221
|
+
})
|
|
222
|
+
.returning();
|
|
223
|
+
|
|
224
|
+
// Send consent updated email
|
|
225
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
226
|
+
if (user) {
|
|
227
|
+
await sendConsentUpdatedEmail(user.email, user.name, [
|
|
228
|
+
{
|
|
229
|
+
type: input.consentType,
|
|
230
|
+
action: input.granted ? "granted" : "revoked",
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return record;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function revokeConsent(userId: string, consentType: string) {
|
|
239
|
+
const [updated] = await db
|
|
240
|
+
.update(consentRecords)
|
|
241
|
+
.set({ revokedAt: new Date() })
|
|
242
|
+
.where(
|
|
243
|
+
and(
|
|
244
|
+
eq(consentRecords.userId, userId),
|
|
245
|
+
eq(consentRecords.consentType, consentType),
|
|
246
|
+
isNull(consentRecords.revokedAt),
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
.returning();
|
|
250
|
+
|
|
251
|
+
if (!updated) return null;
|
|
252
|
+
|
|
253
|
+
// Send consent updated email
|
|
254
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
255
|
+
if (user) {
|
|
256
|
+
await sendConsentUpdatedEmail(user.email, user.name, [
|
|
257
|
+
{ type: consentType, action: "revoked" },
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return updated;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function getUserConsents(userId: string) {
|
|
265
|
+
return db
|
|
266
|
+
.select()
|
|
267
|
+
.from(consentRecords)
|
|
268
|
+
.where(
|
|
269
|
+
and(
|
|
270
|
+
eq(consentRecords.userId, userId),
|
|
271
|
+
isNull(consentRecords.revokedAt),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function immediatelyDeleteUser(userId: string) {
|
|
277
|
+
// Fetch user info before deletion
|
|
278
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
|
279
|
+
if (!user) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const email = user.email;
|
|
284
|
+
const name = user.name;
|
|
285
|
+
|
|
286
|
+
// Delete user data (cascades handle related records)
|
|
287
|
+
await db.delete(users).where(eq(users.id, userId));
|
|
288
|
+
|
|
289
|
+
// Send deletion completed email
|
|
290
|
+
await sendDeletionCompletedEmail(email, name);
|
|
291
|
+
|
|
292
|
+
return { userId, email };
|
|
293
|
+
}
|