@codaijs/keel 0.1.0

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 (116) hide show
  1. package/dist/__tests__/cli.test.d.ts +2 -0
  2. package/dist/__tests__/cli.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli.test.js +173 -0
  4. package/dist/__tests__/cli.test.js.map +1 -0
  5. package/dist/__tests__/registry.test.d.ts +2 -0
  6. package/dist/__tests__/registry.test.d.ts.map +1 -0
  7. package/dist/__tests__/registry.test.js +86 -0
  8. package/dist/__tests__/registry.test.js.map +1 -0
  9. package/dist/__tests__/sail-installer.test.d.ts +2 -0
  10. package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
  11. package/dist/__tests__/sail-installer.test.js +158 -0
  12. package/dist/__tests__/sail-installer.test.js.map +1 -0
  13. package/dist/create-runner.d.ts +11 -0
  14. package/dist/create-runner.d.ts.map +1 -0
  15. package/dist/create-runner.js +63 -0
  16. package/dist/create-runner.js.map +1 -0
  17. package/dist/create.d.ts +10 -0
  18. package/dist/create.d.ts.map +1 -0
  19. package/dist/create.js +15 -0
  20. package/dist/create.js.map +1 -0
  21. package/dist/manage.d.ts +24 -0
  22. package/dist/manage.d.ts.map +1 -0
  23. package/dist/manage.js +1461 -0
  24. package/dist/manage.js.map +1 -0
  25. package/dist/prompts.d.ts +36 -0
  26. package/dist/prompts.d.ts.map +1 -0
  27. package/dist/prompts.js +208 -0
  28. package/dist/prompts.js.map +1 -0
  29. package/dist/sail-installer.d.ts +37 -0
  30. package/dist/sail-installer.d.ts.map +1 -0
  31. package/dist/sail-installer.js +935 -0
  32. package/dist/sail-installer.js.map +1 -0
  33. package/dist/scaffold.d.ts +10 -0
  34. package/dist/scaffold.d.ts.map +1 -0
  35. package/dist/scaffold.js +297 -0
  36. package/dist/scaffold.js.map +1 -0
  37. package/package.json +57 -0
  38. package/sails/_template/addon.json +20 -0
  39. package/sails/_template/install.ts +402 -0
  40. package/sails/admin-dashboard/README.md +117 -0
  41. package/sails/admin-dashboard/addon.json +28 -0
  42. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
  43. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
  44. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
  45. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
  46. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
  47. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
  48. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
  49. package/sails/admin-dashboard/install.ts +305 -0
  50. package/sails/analytics/README.md +178 -0
  51. package/sails/analytics/addon.json +27 -0
  52. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
  53. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
  54. package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
  55. package/sails/analytics/install.ts +297 -0
  56. package/sails/file-uploads/README.md +191 -0
  57. package/sails/file-uploads/addon.json +30 -0
  58. package/sails/file-uploads/files/backend/routes/files.ts +198 -0
  59. package/sails/file-uploads/files/backend/schema/files.ts +36 -0
  60. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
  61. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
  62. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
  63. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
  64. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
  65. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
  66. package/sails/file-uploads/install.ts +466 -0
  67. package/sails/gdpr/README.md +174 -0
  68. package/sails/gdpr/addon.json +27 -0
  69. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
  70. package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
  71. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
  72. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
  73. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
  74. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
  75. package/sails/gdpr/install.ts +756 -0
  76. package/sails/google-oauth/README.md +121 -0
  77. package/sails/google-oauth/addon.json +22 -0
  78. package/sails/google-oauth/files/GoogleButton.tsx +50 -0
  79. package/sails/google-oauth/install.ts +252 -0
  80. package/sails/i18n/README.md +193 -0
  81. package/sails/i18n/addon.json +30 -0
  82. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
  83. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
  84. package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
  85. package/sails/i18n/files/frontend/locales/de/common.json +44 -0
  86. package/sails/i18n/files/frontend/locales/en/common.json +44 -0
  87. package/sails/i18n/install.ts +407 -0
  88. package/sails/push-notifications/README.md +163 -0
  89. package/sails/push-notifications/addon.json +31 -0
  90. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
  91. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
  92. package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
  93. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
  94. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
  95. package/sails/push-notifications/install.ts +384 -0
  96. package/sails/r2-storage/README.md +101 -0
  97. package/sails/r2-storage/addon.json +29 -0
  98. package/sails/r2-storage/files/backend/services/storage.ts +71 -0
  99. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
  100. package/sails/r2-storage/install.ts +412 -0
  101. package/sails/rate-limiting/README.md +145 -0
  102. package/sails/rate-limiting/addon.json +20 -0
  103. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
  104. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
  105. package/sails/rate-limiting/install.ts +300 -0
  106. package/sails/registry.json +107 -0
  107. package/sails/stripe/README.md +214 -0
  108. package/sails/stripe/addon.json +24 -0
  109. package/sails/stripe/files/backend/routes/stripe.ts +154 -0
  110. package/sails/stripe/files/backend/schema/stripe.ts +74 -0
  111. package/sails/stripe/files/backend/services/stripe.ts +224 -0
  112. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
  113. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
  114. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
  115. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
  116. package/sails/stripe/install.ts +378 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Template Sail Installer
3
+ *
4
+ * This file serves as a reference implementation for creating sail installers.
5
+ * Each sail should have its own install.ts that follows this wizard pattern.
6
+ *
7
+ * The installer is executed by the CLI tool or can be run standalone:
8
+ * npx tsx sails/<sail-name>/install.ts
9
+ *
10
+ * ============================================================================
11
+ * WIZARD PATTERN
12
+ * ============================================================================
13
+ *
14
+ * Every sail installer should follow this 8-step wizard flow:
15
+ *
16
+ * Step 1: Welcome - Explain what the sail does
17
+ * Step 2: Prerequisites - Check if the user has required accounts/setup
18
+ * If not, provide step-by-step guidance and wait
19
+ * Step 3: Credentials - Collect required API keys and secrets
20
+ * Validate format where possible (prefixes, etc.)
21
+ * Step 4: Summary - Show all files that will be created/modified
22
+ * Show collected env vars (masked)
23
+ * Step 5: Confirm - Ask user to confirm before making changes
24
+ * Step 6: Execute - Copy files, modify markers, update env
25
+ * Step 7: Dependencies - Install npm packages, run migrations
26
+ * Step 8: Next steps - Print what to do next and how to test
27
+ *
28
+ * Use @inquirer/prompts for all interactive prompts:
29
+ * - input() for text/credential entry with validation
30
+ * - confirm() for yes/no decisions
31
+ * - select() for choosing one option from a list
32
+ * - checkbox() for selecting multiple options
33
+ *
34
+ * ============================================================================
35
+ */
36
+
37
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
38
+ import { resolve, dirname, join } from "node:path";
39
+ import { execSync } from "node:child_process";
40
+ import { input, confirm, select } from "@inquirer/prompts";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ interface EnvVar {
47
+ key: string;
48
+ description: string;
49
+ }
50
+
51
+ interface SailManifest {
52
+ name: string;
53
+ displayName: string;
54
+ description: string;
55
+ version: string;
56
+ compatibility: string;
57
+ requiredEnvVars: EnvVar[];
58
+ dependencies: {
59
+ backend: Record<string, string>;
60
+ frontend: Record<string, string>;
61
+ };
62
+ modifies: {
63
+ backend: string[];
64
+ frontend: string[];
65
+ };
66
+ adds: {
67
+ backend: string[];
68
+ frontend: string[];
69
+ };
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
77
+ const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
78
+
79
+ function loadManifest(): SailManifest {
80
+ const raw = readFileSync(join(SAIL_DIR, "addon.json"), "utf-8");
81
+ return JSON.parse(raw) as SailManifest;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Step 1 -- Welcome message
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function printWelcome(manifest: SailManifest): void {
89
+ console.log("\n------------------------------------------------------------");
90
+ console.log(` ${manifest.displayName} Installer (v${manifest.version})`);
91
+ console.log("------------------------------------------------------------");
92
+ console.log();
93
+ console.log(` ${manifest.description}`);
94
+ console.log();
95
+
96
+ const pkgPath = join(PROJECT_ROOT, "package.json");
97
+ if (existsSync(pkgPath)) {
98
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
99
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
100
+ console.log(` Required compatibility: ${manifest.compatibility}`);
101
+ console.log();
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Step 2 -- Prerequisites check
107
+ // ---------------------------------------------------------------------------
108
+
109
+ async function checkPrerequisites(): Promise<void> {
110
+ const hasPrereqs = await confirm({
111
+ message: "Do you have the required external service account set up?",
112
+ default: false,
113
+ });
114
+
115
+ if (!hasPrereqs) {
116
+ console.log();
117
+ console.log(" Follow these steps to set up the required service:");
118
+ console.log();
119
+ console.log(" 1. Go to <service dashboard URL>");
120
+ console.log(" 2. Create an account or sign in");
121
+ console.log(" 3. Create the required resources (API keys, projects, etc.)");
122
+ console.log(" 4. Note down the credentials you will need");
123
+ console.log();
124
+
125
+ await confirm({
126
+ message: "I have completed the setup and have my credentials ready",
127
+ default: false,
128
+ });
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Step 3 -- Collect credentials
134
+ // ---------------------------------------------------------------------------
135
+
136
+ async function collectCredentials(
137
+ envVars: EnvVar[]
138
+ ): Promise<Record<string, string>> {
139
+ const values: Record<string, string> = {};
140
+
141
+ console.log();
142
+ for (const v of envVars) {
143
+ const value = await input({
144
+ message: `${v.description}:`,
145
+ validate: (val) => {
146
+ if (!val || val.trim().length === 0) {
147
+ return `${v.key} is required.`;
148
+ }
149
+ return true;
150
+ },
151
+ });
152
+ values[v.key] = value.trim();
153
+ }
154
+
155
+ return values;
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Step 4 -- Show summary
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function showSummary(
163
+ manifest: SailManifest,
164
+ envValues: Record<string, string>
165
+ ): void {
166
+ console.log();
167
+ console.log(" Summary of changes:");
168
+ console.log(" -------------------");
169
+
170
+ if (manifest.adds.backend.length > 0 || manifest.adds.frontend.length > 0) {
171
+ console.log(" Files to create:");
172
+ for (const f of manifest.adds.backend) {
173
+ console.log(` + packages/backend/${f}`);
174
+ }
175
+ for (const f of manifest.adds.frontend) {
176
+ console.log(` + packages/frontend/${f}`);
177
+ }
178
+ console.log();
179
+ }
180
+
181
+ if (manifest.modifies.backend.length > 0 || manifest.modifies.frontend.length > 0) {
182
+ console.log(" Files to modify:");
183
+ for (const f of manifest.modifies.backend) {
184
+ console.log(` ~ packages/backend/${f}`);
185
+ }
186
+ for (const f of manifest.modifies.frontend) {
187
+ console.log(` ~ packages/frontend/${f}`);
188
+ }
189
+ console.log();
190
+ }
191
+
192
+ console.log(" Environment variables:");
193
+ for (const [key, val] of Object.entries(envValues)) {
194
+ const masked = val.length > 8 ? val.slice(0, 8) + "..." : val;
195
+ console.log(` ${key}=${masked}`);
196
+ }
197
+ console.log();
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Step 5 -- Confirm
202
+ // ---------------------------------------------------------------------------
203
+
204
+ async function confirmInstallation(): Promise<void> {
205
+ const proceed = await confirm({
206
+ message: "Proceed with installation?",
207
+ default: true,
208
+ });
209
+
210
+ if (!proceed) {
211
+ console.log("\n Installation cancelled.\n");
212
+ process.exit(0);
213
+ }
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Step 6 -- Execute
218
+ // ---------------------------------------------------------------------------
219
+
220
+ function copySailFiles(manifest: SailManifest): void {
221
+ const filesDir = join(SAIL_DIR, "files");
222
+ if (!existsSync(filesDir)) {
223
+ console.log(" No files/ directory -- skipping file copy.");
224
+ return;
225
+ }
226
+
227
+ console.log(" Copying files...");
228
+
229
+ for (const file of manifest.adds.backend) {
230
+ const src = join(filesDir, "backend", file.replace(/^src\//, ""));
231
+ const dest = join(PROJECT_ROOT, "packages/backend", file);
232
+ if (existsSync(src)) {
233
+ mkdirSync(dirname(dest), { recursive: true });
234
+ copyFileSync(src, dest);
235
+ console.log(` Copied -> ${file}`);
236
+ }
237
+ }
238
+
239
+ for (const file of manifest.adds.frontend) {
240
+ const src = join(filesDir, "frontend", file.replace(/^src\//, ""));
241
+ const dest = join(PROJECT_ROOT, "packages/frontend", file);
242
+ if (existsSync(src)) {
243
+ mkdirSync(dirname(dest), { recursive: true });
244
+ copyFileSync(src, dest);
245
+ console.log(` Copied -> ${file}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ function insertAtMarker(
251
+ filePath: string,
252
+ marker: string,
253
+ insertion: string
254
+ ): void {
255
+ if (!existsSync(filePath)) {
256
+ console.warn(` Warning: File not found: ${filePath} -- skipping.`);
257
+ return;
258
+ }
259
+
260
+ const content = readFileSync(filePath, "utf-8");
261
+
262
+ if (!content.includes(marker)) {
263
+ console.warn(` Warning: Marker "${marker}" not found in ${filePath}.`);
264
+ return;
265
+ }
266
+
267
+ if (content.includes(insertion.trim())) {
268
+ console.log(` Skipped (already present) -> ${filePath}`);
269
+ return;
270
+ }
271
+
272
+ const updated = content.replace(marker, `${marker}\n${insertion}`);
273
+ writeFileSync(filePath, updated, "utf-8");
274
+ console.log(` Inserted at ${marker} -> ${filePath}`);
275
+ }
276
+
277
+ function updateEnvFiles(
278
+ sectionName: string,
279
+ envVars: Record<string, string>
280
+ ): void {
281
+ console.log(" Updating environment files...");
282
+
283
+ for (const envFile of [".env.example", ".env"]) {
284
+ const envPath = join(PROJECT_ROOT, envFile);
285
+ if (!existsSync(envPath)) continue;
286
+
287
+ let content = readFileSync(envPath, "utf-8");
288
+ const additions: string[] = [];
289
+ for (const [key, value] of Object.entries(envVars)) {
290
+ if (!content.includes(key)) {
291
+ additions.push(`${key}=${value}`);
292
+ }
293
+ }
294
+
295
+ if (additions.length > 0) {
296
+ content += `\n# ${sectionName}\n${additions.join("\n")}\n`;
297
+ writeFileSync(envPath, content, "utf-8");
298
+ console.log(` Updated ${envFile}`);
299
+ }
300
+ }
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // Step 7 -- Install dependencies and run migrations
305
+ // ---------------------------------------------------------------------------
306
+
307
+ function installDependencies(
308
+ deps: Record<string, string>,
309
+ workspace: string
310
+ ): void {
311
+ const entries = Object.entries(deps);
312
+ if (entries.length === 0) return;
313
+
314
+ const packages = entries.map(([name, version]) => `${name}@${version}`).join(" ");
315
+ const cmd = `npm install ${packages} --workspace=${workspace}`;
316
+ console.log(` Running: ${cmd}`);
317
+ execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
318
+ }
319
+
320
+ function generateMigrations(): void {
321
+ console.log(" Running: npx drizzle-kit generate");
322
+ try {
323
+ execSync("npx drizzle-kit generate", {
324
+ cwd: join(PROJECT_ROOT, "packages/backend"),
325
+ stdio: "inherit",
326
+ });
327
+ } catch {
328
+ console.warn(" Warning: Could not generate migrations. Run manually:");
329
+ console.warn(" cd packages/backend && npx drizzle-kit generate");
330
+ }
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Step 8 -- Print next steps
335
+ // ---------------------------------------------------------------------------
336
+
337
+ function printNextSteps(manifest: SailManifest): void {
338
+ console.log();
339
+ console.log("------------------------------------------------------------");
340
+ console.log(` ${manifest.displayName} installed successfully!`);
341
+ console.log("------------------------------------------------------------");
342
+ console.log();
343
+ console.log(" Next steps:");
344
+ console.log(" 1. Fill in any placeholder env vars in .env");
345
+ console.log(" 2. Run database migrations: npm run db:migrate");
346
+ console.log(" 3. Review the modified files listed in addon.json");
347
+ console.log(" 4. Read the sail README for provider-specific setup");
348
+ console.log();
349
+ console.log(" Testing:");
350
+ console.log(" 1. Start your dev server: npm run dev");
351
+ console.log(" 2. Test the new functionality in the browser");
352
+ console.log(" 3. Check server logs for any configuration errors");
353
+ console.log();
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Main
358
+ // ---------------------------------------------------------------------------
359
+
360
+ async function main(): Promise<void> {
361
+ const manifest = loadManifest();
362
+
363
+ printWelcome(manifest);
364
+ await checkPrerequisites();
365
+ const envValues = await collectCredentials(manifest.requiredEnvVars);
366
+ showSummary(manifest, envValues);
367
+ await confirmInstallation();
368
+
369
+ console.log();
370
+ console.log(" Installing...");
371
+ console.log();
372
+ copySailFiles(manifest);
373
+
374
+ console.log();
375
+ console.log(" Modifying existing files...");
376
+ // Sail specific marker insertions go here. Example:
377
+ // insertAtMarker(
378
+ // join(PROJECT_ROOT, "packages/backend/src/index.ts"),
379
+ // "// [SAIL_IMPORTS]",
380
+ // 'import { myRouter } from "./routes/my-route";'
381
+ // );
382
+
383
+ updateEnvFiles(manifest.displayName, envValues);
384
+
385
+ console.log();
386
+ console.log(" Installing dependencies...");
387
+ installDependencies(manifest.dependencies.backend, "packages/backend");
388
+ installDependencies(manifest.dependencies.frontend, "packages/frontend");
389
+
390
+ if (manifest.adds.backend.some((f) => f.includes("schema"))) {
391
+ console.log();
392
+ console.log(" Generating database migrations...");
393
+ generateMigrations();
394
+ }
395
+
396
+ printNextSteps(manifest);
397
+ }
398
+
399
+ main().catch((err) => {
400
+ console.error("Installation failed:", err);
401
+ process.exit(1);
402
+ });
@@ -0,0 +1,117 @@
1
+ # Admin Dashboard Sail
2
+
3
+ Adds a user management and metrics dashboard to your keel application. Access is restricted to email addresses listed in the `ADMIN_EMAILS` environment variable.
4
+
5
+ ## Features
6
+
7
+ - Dashboard with stats cards (total users, new this week/month, active sessions)
8
+ - User signup chart (last 30 days) powered by recharts
9
+ - Users table with search, sorting, and pagination
10
+ - User detail view with admin actions
11
+ - Admin actions: verify email, delete user
12
+ - Access controlled via `ADMIN_EMAILS` environment variable
13
+ - Admin middleware for backend route protection
14
+
15
+ ## Prerequisites
16
+
17
+ - A working keel project with BetterAuth authentication
18
+ - At least one user account with an email you want to use as admin
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npx tsx sails/admin-dashboard/install.ts
24
+ ```
25
+
26
+ The installer will prompt for admin email addresses and configure everything automatically.
27
+
28
+ ## Manual Setup
29
+
30
+ ### 1. Environment Variables
31
+
32
+ Add the following to your `.env`:
33
+
34
+ ```env
35
+ ADMIN_EMAILS=admin@example.com,another-admin@example.com
36
+ ```
37
+
38
+ Multiple emails are separated by commas.
39
+
40
+ ### 2. Access the Dashboard
41
+
42
+ 1. Start your dev server: `npm run dev`
43
+ 2. Log in with an admin email address
44
+ 3. Navigate to `/admin`
45
+
46
+ ## Architecture
47
+
48
+ ### Backend
49
+
50
+ **Admin Middleware** (`src/middleware/admin.ts`)
51
+
52
+ Checks if the authenticated user's email is in the `ADMIN_EMAILS` list. Returns 403 if not. Must be used after `requireAuth`.
53
+
54
+ **Admin Routes** (`src/routes/admin.ts`)
55
+
56
+ | Method | Path | Description |
57
+ |--------|------|-------------|
58
+ | GET | /api/admin/users | List users (paginated, searchable) |
59
+ | GET | /api/admin/users/:id | Get user details |
60
+ | PATCH | /api/admin/users/:id | Update user (name, emailVerified) |
61
+ | DELETE | /api/admin/users/:id | Delete a user |
62
+ | GET | /api/admin/stats | Dashboard statistics |
63
+
64
+ All routes require authentication + admin privileges.
65
+
66
+ ### Frontend
67
+
68
+ **Dashboard** (`/admin`)
69
+
70
+ The main admin page showing:
71
+ - Stats cards with key metrics
72
+ - Line chart of user signups over the last 30 days
73
+ - Searchable, sortable users table
74
+
75
+ **User Detail** (`/admin/users/:id`)
76
+
77
+ Detailed view of a single user with:
78
+ - User profile information
79
+ - Active session count
80
+ - Admin actions (verify email, delete user)
81
+
82
+ ### Components
83
+
84
+ - `StatsCard` - Reusable card for displaying a metric with optional trend
85
+ - `UsersTable` - Table with search, sort, and pagination
86
+
87
+ ### Hooks
88
+
89
+ - `useAdminStats()` - Fetch dashboard statistics
90
+ - `useAdminUsers(page, search)` - Fetch paginated user list
91
+ - `fetchUser(id)` - Get single user details
92
+ - `updateUser(id, data)` - Update user fields
93
+ - `deleteUser(id)` - Delete a user
94
+
95
+ ## Customization
96
+
97
+ ### Adding Admin Link to Header
98
+
99
+ Add a link to the admin dashboard in your Header component for admin users:
100
+
101
+ ```tsx
102
+ {isAuthenticated && isAdmin && (
103
+ <Link to="/admin" className="text-sm font-medium text-keel-gray-400 hover:text-white">
104
+ Admin
105
+ </Link>
106
+ )}
107
+ ```
108
+
109
+ You can check admin status by comparing the user's email against a list fetched from the backend, or by adding an admin check API endpoint.
110
+
111
+ ### Extending the Dashboard
112
+
113
+ Add new stats cards by modifying `Dashboard.tsx` and adding corresponding backend queries in `routes/admin.ts`.
114
+
115
+ ### Adding More Admin Actions
116
+
117
+ Extend the `PATCH /api/admin/users/:id` endpoint to support additional fields, or add new endpoints for other admin operations (e.g., ban user, reset password, impersonate).
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "admin-dashboard",
3
+ "displayName": "Admin Dashboard",
4
+ "description": "Admin dashboard for user management, metrics, and basic analytics",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [
8
+ { "key": "ADMIN_EMAILS", "description": "Comma-separated list of admin email addresses" }
9
+ ],
10
+ "dependencies": {
11
+ "backend": {},
12
+ "frontend": { "recharts": "^2.15.0" }
13
+ },
14
+ "modifies": {
15
+ "backend": ["src/index.ts", "src/env.ts"],
16
+ "frontend": ["src/router.tsx"]
17
+ },
18
+ "adds": {
19
+ "backend": ["src/middleware/admin.ts", "src/routes/admin.ts"],
20
+ "frontend": [
21
+ "src/pages/admin/Dashboard.tsx",
22
+ "src/pages/admin/UserDetail.tsx",
23
+ "src/components/admin/StatsCard.tsx",
24
+ "src/components/admin/UsersTable.tsx",
25
+ "src/hooks/useAdmin.ts"
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,34 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ import { env } from "../env.js";
3
+
4
+ /**
5
+ * Middleware that checks whether the authenticated user is an admin.
6
+ * Admin status is determined by the ADMIN_EMAILS environment variable
7
+ * which contains a comma-separated list of admin email addresses.
8
+ *
9
+ * Must be used AFTER requireAuth so that req.user is populated.
10
+ */
11
+ export function requireAdmin(
12
+ req: Request,
13
+ res: Response,
14
+ next: NextFunction,
15
+ ): void {
16
+ const user = req.user;
17
+
18
+ if (!user) {
19
+ res.status(401).json({ error: "Unauthorized" });
20
+ return;
21
+ }
22
+
23
+ const adminEmails = (env.ADMIN_EMAILS ?? "")
24
+ .split(",")
25
+ .map((e) => e.trim().toLowerCase())
26
+ .filter(Boolean);
27
+
28
+ if (!adminEmails.includes(user.email.toLowerCase())) {
29
+ res.status(403).json({ error: "Forbidden: admin access required" });
30
+ return;
31
+ }
32
+
33
+ next();
34
+ }