@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.
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- 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
|
+
}
|