@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.
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,198 +1,198 @@
1
- /**
2
- * File management API routes.
3
- *
4
- * All routes require authentication. Files are scoped per user.
5
- *
6
- * Endpoints:
7
- * POST /upload-url -- generate a presigned upload URL
8
- * GET / -- list the current user's files
9
- * GET /:fileId -- get file metadata + download URL
10
- * DELETE /:fileId -- delete a file
11
- */
12
-
13
- import { Router, type Request, type Response } from "express";
14
- import { eq, and } from "drizzle-orm";
15
- import { db } from "../db/index.js";
16
- import { files } from "../db/schema/files.js";
17
- import { requireAuth } from "../middleware/auth.js";
18
- import {
19
- generateUploadUrl,
20
- generateDownloadUrl,
21
- deleteFile as deleteFromStorage,
22
- } from "../services/file-storage.js";
23
-
24
- export const filesRouter = Router();
25
-
26
- // All file routes require authentication.
27
- filesRouter.use(requireAuth);
28
-
29
- // ---------------------------------------------------------------------------
30
- // POST /upload-url -- generate presigned upload URL
31
- // ---------------------------------------------------------------------------
32
-
33
- filesRouter.post("/upload-url", async (req: Request, res: Response) => {
34
- try {
35
- const { fileName, contentType, maxSize } = req.body as {
36
- fileName?: string;
37
- contentType?: string;
38
- maxSize?: number;
39
- };
40
-
41
- if (!fileName || typeof fileName !== "string") {
42
- res.status(400).json({ error: "fileName is required" });
43
- return;
44
- }
45
-
46
- if (!contentType || typeof contentType !== "string") {
47
- res.status(400).json({ error: "contentType is required" });
48
- return;
49
- }
50
-
51
- const userId = req.user!.id;
52
- const { uploadUrl, key } = await generateUploadUrl(
53
- userId,
54
- fileName,
55
- contentType,
56
- maxSize,
57
- );
58
-
59
- // Create a file record in the database so we can track it.
60
- const [fileRecord] = await db
61
- .insert(files)
62
- .values({
63
- id: crypto.randomUUID(),
64
- userId,
65
- key,
66
- fileName,
67
- contentType,
68
- sizeBytes: maxSize ?? null,
69
- })
70
- .returning();
71
-
72
- res.json({
73
- uploadUrl,
74
- file: {
75
- id: fileRecord.id,
76
- key: fileRecord.key,
77
- fileName: fileRecord.fileName,
78
- },
79
- });
80
- } catch (error) {
81
- console.error("Error generating upload URL:", error);
82
- res.status(500).json({ error: "Failed to generate upload URL" });
83
- }
84
- });
85
-
86
- // ---------------------------------------------------------------------------
87
- // GET / -- list user's files
88
- // ---------------------------------------------------------------------------
89
-
90
- filesRouter.get("/", async (req: Request, res: Response) => {
91
- try {
92
- const userId = req.user!.id;
93
- const prefix = req.query.prefix as string | undefined;
94
-
95
- let query = db
96
- .select()
97
- .from(files)
98
- .where(eq(files.userId, userId))
99
- .$dynamic();
100
-
101
- if (prefix) {
102
- query = query.where(
103
- and(eq(files.userId, userId)),
104
- );
105
- }
106
-
107
- const userFiles = await db
108
- .select()
109
- .from(files)
110
- .where(eq(files.userId, userId))
111
- .orderBy(files.createdAt);
112
-
113
- res.json({
114
- files: userFiles.map((f) => ({
115
- id: f.id,
116
- fileName: f.fileName,
117
- contentType: f.contentType,
118
- sizeBytes: f.sizeBytes,
119
- createdAt: f.createdAt,
120
- })),
121
- });
122
- } catch (error) {
123
- console.error("Error listing files:", error);
124
- res.status(500).json({ error: "Failed to list files" });
125
- }
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // GET /:fileId -- get file metadata + download URL
130
- // ---------------------------------------------------------------------------
131
-
132
- filesRouter.get("/:fileId", async (req: Request, res: Response) => {
133
- try {
134
- const userId = req.user!.id;
135
- const { fileId } = req.params;
136
-
137
- const file = await db.query.files.findFirst({
138
- where: and(eq(files.id, fileId), eq(files.userId, userId)),
139
- });
140
-
141
- if (!file) {
142
- res.status(404).json({ error: "File not found" });
143
- return;
144
- }
145
-
146
- const downloadUrl = await generateDownloadUrl(file.key);
147
-
148
- res.json({
149
- file: {
150
- id: file.id,
151
- fileName: file.fileName,
152
- contentType: file.contentType,
153
- sizeBytes: file.sizeBytes,
154
- createdAt: file.createdAt,
155
- downloadUrl,
156
- },
157
- });
158
- } catch (error) {
159
- console.error("Error getting file:", error);
160
- res.status(500).json({ error: "Failed to get file" });
161
- }
162
- });
163
-
164
- // ---------------------------------------------------------------------------
165
- // DELETE /:fileId -- delete a file
166
- // ---------------------------------------------------------------------------
167
-
168
- filesRouter.delete("/:fileId", async (req: Request, res: Response) => {
169
- try {
170
- const userId = req.user!.id;
171
- const { fileId } = req.params;
172
-
173
- const file = await db.query.files.findFirst({
174
- where: and(eq(files.id, fileId), eq(files.userId, userId)),
175
- });
176
-
177
- if (!file) {
178
- res.status(404).json({ error: "File not found" });
179
- return;
180
- }
181
-
182
- // Delete from S3-compatible storage.
183
- try {
184
- await deleteFromStorage(file.key);
185
- } catch (err) {
186
- console.error("Warning: failed to delete file from storage:", err);
187
- // Continue with DB deletion even if storage deletion fails.
188
- }
189
-
190
- // Delete from database.
191
- await db.delete(files).where(eq(files.id, fileId));
192
-
193
- res.json({ success: true });
194
- } catch (error) {
195
- console.error("Error deleting file:", error);
196
- res.status(500).json({ error: "Failed to delete file" });
197
- }
198
- });
1
+ /**
2
+ * File management API routes.
3
+ *
4
+ * All routes require authentication. Files are scoped per user.
5
+ *
6
+ * Endpoints:
7
+ * POST /upload-url -- generate a presigned upload URL
8
+ * GET / -- list the current user's files
9
+ * GET /:fileId -- get file metadata + download URL
10
+ * DELETE /:fileId -- delete a file
11
+ */
12
+
13
+ import { Router, type Request, type Response } from "express";
14
+ import { eq, and } from "drizzle-orm";
15
+ import { db } from "../db/index.js";
16
+ import { files } from "../db/schema/files.js";
17
+ import { requireAuth } from "../middleware/auth.js";
18
+ import {
19
+ generateUploadUrl,
20
+ generateDownloadUrl,
21
+ deleteFile as deleteFromStorage,
22
+ } from "../services/file-storage.js";
23
+
24
+ export const filesRouter = Router();
25
+
26
+ // All file routes require authentication.
27
+ filesRouter.use(requireAuth);
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // POST /upload-url -- generate presigned upload URL
31
+ // ---------------------------------------------------------------------------
32
+
33
+ filesRouter.post("/upload-url", async (req: Request, res: Response) => {
34
+ try {
35
+ const { fileName, contentType, maxSize } = req.body as {
36
+ fileName?: string;
37
+ contentType?: string;
38
+ maxSize?: number;
39
+ };
40
+
41
+ if (!fileName || typeof fileName !== "string") {
42
+ res.status(400).json({ error: "fileName is required" });
43
+ return;
44
+ }
45
+
46
+ if (!contentType || typeof contentType !== "string") {
47
+ res.status(400).json({ error: "contentType is required" });
48
+ return;
49
+ }
50
+
51
+ const userId = req.user!.id;
52
+ const { uploadUrl, key } = await generateUploadUrl(
53
+ userId,
54
+ fileName,
55
+ contentType,
56
+ maxSize,
57
+ );
58
+
59
+ // Create a file record in the database so we can track it.
60
+ const [fileRecord] = await db
61
+ .insert(files)
62
+ .values({
63
+ id: crypto.randomUUID(),
64
+ userId,
65
+ key,
66
+ fileName,
67
+ contentType,
68
+ sizeBytes: maxSize ?? null,
69
+ })
70
+ .returning();
71
+
72
+ res.json({
73
+ uploadUrl,
74
+ file: {
75
+ id: fileRecord.id,
76
+ key: fileRecord.key,
77
+ fileName: fileRecord.fileName,
78
+ },
79
+ });
80
+ } catch (error) {
81
+ console.error("Error generating upload URL:", error);
82
+ res.status(500).json({ error: "Failed to generate upload URL" });
83
+ }
84
+ });
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // GET / -- list user's files
88
+ // ---------------------------------------------------------------------------
89
+
90
+ filesRouter.get("/", async (req: Request, res: Response) => {
91
+ try {
92
+ const userId = req.user!.id;
93
+ const prefix = req.query.prefix as string | undefined;
94
+
95
+ let query = db
96
+ .select()
97
+ .from(files)
98
+ .where(eq(files.userId, userId))
99
+ .$dynamic();
100
+
101
+ if (prefix) {
102
+ query = query.where(
103
+ and(eq(files.userId, userId)),
104
+ );
105
+ }
106
+
107
+ const userFiles = await db
108
+ .select()
109
+ .from(files)
110
+ .where(eq(files.userId, userId))
111
+ .orderBy(files.createdAt);
112
+
113
+ res.json({
114
+ files: userFiles.map((f) => ({
115
+ id: f.id,
116
+ fileName: f.fileName,
117
+ contentType: f.contentType,
118
+ sizeBytes: f.sizeBytes,
119
+ createdAt: f.createdAt,
120
+ })),
121
+ });
122
+ } catch (error) {
123
+ console.error("Error listing files:", error);
124
+ res.status(500).json({ error: "Failed to list files" });
125
+ }
126
+ });
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // GET /:fileId -- get file metadata + download URL
130
+ // ---------------------------------------------------------------------------
131
+
132
+ filesRouter.get("/:fileId", async (req: Request, res: Response) => {
133
+ try {
134
+ const userId = req.user!.id;
135
+ const { fileId } = req.params;
136
+
137
+ const file = await db.query.files.findFirst({
138
+ where: and(eq(files.id, fileId), eq(files.userId, userId)),
139
+ });
140
+
141
+ if (!file) {
142
+ res.status(404).json({ error: "File not found" });
143
+ return;
144
+ }
145
+
146
+ const downloadUrl = await generateDownloadUrl(file.key);
147
+
148
+ res.json({
149
+ file: {
150
+ id: file.id,
151
+ fileName: file.fileName,
152
+ contentType: file.contentType,
153
+ sizeBytes: file.sizeBytes,
154
+ createdAt: file.createdAt,
155
+ downloadUrl,
156
+ },
157
+ });
158
+ } catch (error) {
159
+ console.error("Error getting file:", error);
160
+ res.status(500).json({ error: "Failed to get file" });
161
+ }
162
+ });
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // DELETE /:fileId -- delete a file
166
+ // ---------------------------------------------------------------------------
167
+
168
+ filesRouter.delete("/:fileId", async (req: Request, res: Response) => {
169
+ try {
170
+ const userId = req.user!.id;
171
+ const { fileId } = req.params;
172
+
173
+ const file = await db.query.files.findFirst({
174
+ where: and(eq(files.id, fileId), eq(files.userId, userId)),
175
+ });
176
+
177
+ if (!file) {
178
+ res.status(404).json({ error: "File not found" });
179
+ return;
180
+ }
181
+
182
+ // Delete from S3-compatible storage.
183
+ try {
184
+ await deleteFromStorage(file.key);
185
+ } catch (err) {
186
+ console.error("Warning: failed to delete file from storage:", err);
187
+ // Continue with DB deletion even if storage deletion fails.
188
+ }
189
+
190
+ // Delete from database.
191
+ await db.delete(files).where(eq(files.id, fileId));
192
+
193
+ res.json({ success: true });
194
+ } catch (error) {
195
+ console.error("Error deleting file:", error);
196
+ res.status(500).json({ error: "Failed to delete file" });
197
+ }
198
+ });
@@ -1,36 +1,36 @@
1
- import {
2
- pgTable,
3
- text,
4
- varchar,
5
- integer,
6
- timestamp,
7
- } from "drizzle-orm/pg-core";
8
- import { relations } from "drizzle-orm";
9
- import { users } from "./users.js";
10
-
11
- /**
12
- * Files table.
13
- *
14
- * Tracks uploaded files and their S3-compatible storage keys. Each file
15
- * belongs to a user and is deleted when the user is deleted (cascade).
16
- */
17
- export const files = pgTable("files", {
18
- id: text("id").primaryKey(),
19
- userId: text("user_id")
20
- .notNull()
21
- .references(() => users.id, { onDelete: "cascade" }),
22
- key: text("key").notNull(),
23
- fileName: text("file_name").notNull(),
24
- contentType: varchar("content_type", { length: 100 }),
25
- sizeBytes: integer("size_bytes"),
26
- createdAt: timestamp("created_at", { withTimezone: true })
27
- .notNull()
28
- .defaultNow(),
29
- });
30
-
31
- export const filesRelations = relations(files, ({ one }) => ({
32
- user: one(users, {
33
- fields: [files.userId],
34
- references: [users.id],
35
- }),
36
- }));
1
+ import {
2
+ pgTable,
3
+ text,
4
+ varchar,
5
+ integer,
6
+ timestamp,
7
+ } from "drizzle-orm/pg-core";
8
+ import { relations } from "drizzle-orm";
9
+ import { users } from "./users.js";
10
+
11
+ /**
12
+ * Files table.
13
+ *
14
+ * Tracks uploaded files and their S3-compatible storage keys. Each file
15
+ * belongs to a user and is deleted when the user is deleted (cascade).
16
+ */
17
+ export const files = pgTable("files", {
18
+ id: text("id").primaryKey(),
19
+ userId: text("user_id")
20
+ .notNull()
21
+ .references(() => users.id, { onDelete: "cascade" }),
22
+ key: text("key").notNull(),
23
+ fileName: text("file_name").notNull(),
24
+ contentType: varchar("content_type", { length: 100 }),
25
+ sizeBytes: integer("size_bytes"),
26
+ createdAt: timestamp("created_at", { withTimezone: true })
27
+ .notNull()
28
+ .defaultNow(),
29
+ });
30
+
31
+ export const filesRelations = relations(files, ({ one }) => ({
32
+ user: one(users, {
33
+ fields: [files.userId],
34
+ references: [users.id],
35
+ }),
36
+ }));