@codaijs/keel 0.2.3 → 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,140 +1,140 @@
|
|
|
1
|
-
import { Router, type Request, type Response } from "express";
|
|
2
|
-
import { eq, and } from "drizzle-orm";
|
|
3
|
-
import { verifyPassword } from "better-auth/crypto";
|
|
4
|
-
import { consentInputSchema, CONSENT_TYPES } from "@keel/shared";
|
|
5
|
-
import { requireAuth } from "../middleware/auth.js";
|
|
6
|
-
import { env } from "../env.js";
|
|
7
|
-
import { db } from "../db/index.js";
|
|
8
|
-
import { accounts } from "../db/schema/index.js";
|
|
9
|
-
import {
|
|
10
|
-
exportUserData,
|
|
11
|
-
requestDeletion,
|
|
12
|
-
cancelDeletion,
|
|
13
|
-
processPendingDeletions,
|
|
14
|
-
recordConsent,
|
|
15
|
-
revokeConsent,
|
|
16
|
-
getUserConsents,
|
|
17
|
-
immediatelyDeleteUser,
|
|
18
|
-
} from "../services/gdpr.js";
|
|
19
|
-
|
|
20
|
-
const router = Router();
|
|
21
|
-
|
|
22
|
-
// POST /process-deletions — internal endpoint for cron job
|
|
23
|
-
router.post("/process-deletions", async (req: Request, res: Response) => {
|
|
24
|
-
const secret = req.headers["x-cron-secret"];
|
|
25
|
-
if (secret !== env.DELETION_CRON_SECRET) {
|
|
26
|
-
res.status(403).json({ error: "Forbidden" });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const results = await processPendingDeletions();
|
|
31
|
-
res.json({ results });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// All routes below require authentication
|
|
35
|
-
router.use(requireAuth);
|
|
36
|
-
|
|
37
|
-
// GET /export — export all user data
|
|
38
|
-
router.get("/export", async (req: Request, res: Response) => {
|
|
39
|
-
const data = await exportUserData(req.user!.id);
|
|
40
|
-
res.json(data);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// POST /deletion — request account deletion
|
|
44
|
-
router.post("/deletion", async (req: Request, res: Response) => {
|
|
45
|
-
const { reason } = req.body as { reason?: string };
|
|
46
|
-
const request = await requestDeletion(req.user!.id, reason);
|
|
47
|
-
res.json({ deletionRequest: request });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// POST /deletion/cancel — cancel pending deletion
|
|
51
|
-
router.post("/deletion/cancel", async (req: Request, res: Response) => {
|
|
52
|
-
const result = await cancelDeletion(req.user!.id);
|
|
53
|
-
if (!result) {
|
|
54
|
-
res.status(404).json({ error: "No pending deletion request found" });
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
res.json({ deletionRequest: result });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// DELETE /account — immediately and permanently delete user account (no grace period)
|
|
61
|
-
router.delete("/account", async (req: Request, res: Response) => {
|
|
62
|
-
const { password } = req.body as { password?: string };
|
|
63
|
-
|
|
64
|
-
if (!password) {
|
|
65
|
-
res.status(400).json({ error: "Password confirmation is required" });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Look up the credential account and verify the password hash directly
|
|
70
|
-
// (avoids creating a new session just to check the password)
|
|
71
|
-
const account = await db.query.accounts.findFirst({
|
|
72
|
-
where: and(
|
|
73
|
-
eq(accounts.userId, req.user!.id),
|
|
74
|
-
eq(accounts.providerId, "credential"),
|
|
75
|
-
),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!account?.password) {
|
|
79
|
-
res.status(401).json({ error: "Invalid password" });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const valid = await verifyPassword({
|
|
84
|
-
hash: account.password,
|
|
85
|
-
password,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
if (!valid) {
|
|
89
|
-
res.status(401).json({ error: "Invalid password" });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = await immediatelyDeleteUser(req.user!.id);
|
|
94
|
-
if (!result) {
|
|
95
|
-
res.status(404).json({ error: "User not found" });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
res.json({ message: "Account has been permanently deleted" });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// GET /consents — get all user consents
|
|
103
|
-
router.get("/consents", async (req: Request, res: Response) => {
|
|
104
|
-
const consents = await getUserConsents(req.user!.id);
|
|
105
|
-
res.json({ consents });
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// POST /consents — record a new consent
|
|
109
|
-
router.post("/consents", async (req: Request, res: Response) => {
|
|
110
|
-
const parsed = consentInputSchema.safeParse(req.body);
|
|
111
|
-
if (!parsed.success) {
|
|
112
|
-
res.status(400).json({ error: parsed.error.flatten() });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const ip = req.ip ?? undefined;
|
|
117
|
-
const userAgent = req.headers["user-agent"] ?? undefined;
|
|
118
|
-
|
|
119
|
-
const record = await recordConsent(req.user!.id, parsed.data, ip, userAgent);
|
|
120
|
-
res.status(201).json({ consent: record });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// DELETE /consents/:consentType — revoke a specific consent
|
|
124
|
-
router.delete("/consents/:consentType", async (req: Request, res: Response) => {
|
|
125
|
-
const { consentType } = req.params;
|
|
126
|
-
|
|
127
|
-
if (!consentType || !(CONSENT_TYPES as readonly string[]).includes(consentType)) {
|
|
128
|
-
res.status(400).json({ error: `Invalid consentType. Allowed values: ${CONSENT_TYPES.join(", ")}` });
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const result = await revokeConsent(req.user!.id, consentType);
|
|
133
|
-
if (!result) {
|
|
134
|
-
res.status(404).json({ error: "No active consent found for this type" });
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
res.json({ consent: result });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
export default router;
|
|
1
|
+
import { Router, type Request, type Response } from "express";
|
|
2
|
+
import { eq, and } from "drizzle-orm";
|
|
3
|
+
import { verifyPassword } from "better-auth/crypto";
|
|
4
|
+
import { consentInputSchema, CONSENT_TYPES } from "@keel/shared";
|
|
5
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
6
|
+
import { env } from "../env.js";
|
|
7
|
+
import { db } from "../db/index.js";
|
|
8
|
+
import { accounts } from "../db/schema/index.js";
|
|
9
|
+
import {
|
|
10
|
+
exportUserData,
|
|
11
|
+
requestDeletion,
|
|
12
|
+
cancelDeletion,
|
|
13
|
+
processPendingDeletions,
|
|
14
|
+
recordConsent,
|
|
15
|
+
revokeConsent,
|
|
16
|
+
getUserConsents,
|
|
17
|
+
immediatelyDeleteUser,
|
|
18
|
+
} from "../services/gdpr.js";
|
|
19
|
+
|
|
20
|
+
const router = Router();
|
|
21
|
+
|
|
22
|
+
// POST /process-deletions — internal endpoint for cron job
|
|
23
|
+
router.post("/process-deletions", async (req: Request, res: Response) => {
|
|
24
|
+
const secret = req.headers["x-cron-secret"];
|
|
25
|
+
if (secret !== env.DELETION_CRON_SECRET) {
|
|
26
|
+
res.status(403).json({ error: "Forbidden" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const results = await processPendingDeletions();
|
|
31
|
+
res.json({ results });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// All routes below require authentication
|
|
35
|
+
router.use(requireAuth);
|
|
36
|
+
|
|
37
|
+
// GET /export — export all user data
|
|
38
|
+
router.get("/export", async (req: Request, res: Response) => {
|
|
39
|
+
const data = await exportUserData(req.user!.id);
|
|
40
|
+
res.json(data);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// POST /deletion — request account deletion
|
|
44
|
+
router.post("/deletion", async (req: Request, res: Response) => {
|
|
45
|
+
const { reason } = req.body as { reason?: string };
|
|
46
|
+
const request = await requestDeletion(req.user!.id, reason);
|
|
47
|
+
res.json({ deletionRequest: request });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// POST /deletion/cancel — cancel pending deletion
|
|
51
|
+
router.post("/deletion/cancel", async (req: Request, res: Response) => {
|
|
52
|
+
const result = await cancelDeletion(req.user!.id);
|
|
53
|
+
if (!result) {
|
|
54
|
+
res.status(404).json({ error: "No pending deletion request found" });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
res.json({ deletionRequest: result });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// DELETE /account — immediately and permanently delete user account (no grace period)
|
|
61
|
+
router.delete("/account", async (req: Request, res: Response) => {
|
|
62
|
+
const { password } = req.body as { password?: string };
|
|
63
|
+
|
|
64
|
+
if (!password) {
|
|
65
|
+
res.status(400).json({ error: "Password confirmation is required" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Look up the credential account and verify the password hash directly
|
|
70
|
+
// (avoids creating a new session just to check the password)
|
|
71
|
+
const account = await db.query.accounts.findFirst({
|
|
72
|
+
where: and(
|
|
73
|
+
eq(accounts.userId, req.user!.id),
|
|
74
|
+
eq(accounts.providerId, "credential"),
|
|
75
|
+
),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!account?.password) {
|
|
79
|
+
res.status(401).json({ error: "Invalid password" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const valid = await verifyPassword({
|
|
84
|
+
hash: account.password,
|
|
85
|
+
password,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!valid) {
|
|
89
|
+
res.status(401).json({ error: "Invalid password" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await immediatelyDeleteUser(req.user!.id);
|
|
94
|
+
if (!result) {
|
|
95
|
+
res.status(404).json({ error: "User not found" });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
res.json({ message: "Account has been permanently deleted" });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// GET /consents — get all user consents
|
|
103
|
+
router.get("/consents", async (req: Request, res: Response) => {
|
|
104
|
+
const consents = await getUserConsents(req.user!.id);
|
|
105
|
+
res.json({ consents });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// POST /consents — record a new consent
|
|
109
|
+
router.post("/consents", async (req: Request, res: Response) => {
|
|
110
|
+
const parsed = consentInputSchema.safeParse(req.body);
|
|
111
|
+
if (!parsed.success) {
|
|
112
|
+
res.status(400).json({ error: parsed.error.flatten() });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ip = req.ip ?? undefined;
|
|
117
|
+
const userAgent = req.headers["user-agent"] ?? undefined;
|
|
118
|
+
|
|
119
|
+
const record = await recordConsent(req.user!.id, parsed.data, ip, userAgent);
|
|
120
|
+
res.status(201).json({ consent: record });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// DELETE /consents/:consentType — revoke a specific consent
|
|
124
|
+
router.delete("/consents/:consentType", async (req: Request, res: Response) => {
|
|
125
|
+
const { consentType } = req.params;
|
|
126
|
+
|
|
127
|
+
if (!consentType || !(CONSENT_TYPES as readonly string[]).includes(consentType)) {
|
|
128
|
+
res.status(400).json({ error: `Invalid consentType. Allowed values: ${CONSENT_TYPES.join(", ")}` });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = await revokeConsent(req.user!.id, consentType);
|
|
133
|
+
if (!result) {
|
|
134
|
+
res.status(404).json({ error: "No active consent found for this type" });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
res.json({ consent: result });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export default router;
|