@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.
Files changed (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. 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
+ }