@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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostHog Analytics Sail Installer
|
|
3
|
+
*
|
|
4
|
+
* Adds privacy-friendly analytics with PostHog — automatic page views,
|
|
5
|
+
* user identification, and custom event tracking.
|
|
6
|
+
*
|
|
7
|
+
* PostHog can be used as a cloud service or self-hosted.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx sails/analytics/install.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
readFileSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
copyFileSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { resolve, dirname, join } from "node:path";
|
|
21
|
+
import { execSync } from "node:child_process";
|
|
22
|
+
import { input, confirm, select } from "@inquirer/prompts";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Paths
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
29
|
+
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
30
|
+
const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
interface SailManifest {
|
|
37
|
+
name: string;
|
|
38
|
+
displayName: string;
|
|
39
|
+
version: string;
|
|
40
|
+
requiredEnvVars: { key: string; description: string }[];
|
|
41
|
+
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadManifest(): SailManifest {
|
|
45
|
+
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function copyFile(src: string, dest: string, label: string): void {
|
|
49
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
50
|
+
copyFileSync(src, dest);
|
|
51
|
+
console.log(` Copied -> ${label}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendToEnvFiles(section: string, entries: Record<string, string>): void {
|
|
55
|
+
for (const envFile of [".env.example", ".env"]) {
|
|
56
|
+
const envPath = join(PROJECT_ROOT, envFile);
|
|
57
|
+
if (!existsSync(envPath)) continue;
|
|
58
|
+
let content = readFileSync(envPath, "utf-8");
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
61
|
+
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
62
|
+
}
|
|
63
|
+
if (lines.length > 0) {
|
|
64
|
+
content += `\n# ${section}\n${lines.join("\n")}\n`;
|
|
65
|
+
writeFileSync(envPath, content, "utf-8");
|
|
66
|
+
console.log(` Updated ${envFile}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
72
|
+
const entries = Object.entries(deps);
|
|
73
|
+
if (entries.length === 0) return;
|
|
74
|
+
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
75
|
+
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
76
|
+
console.log(` Running: ${cmd}`);
|
|
77
|
+
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Main
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
async function main(): Promise<void> {
|
|
85
|
+
const manifest = loadManifest();
|
|
86
|
+
|
|
87
|
+
// -- Step 1: Welcome --------------------------------------------------------
|
|
88
|
+
console.log("\n------------------------------------------------------------");
|
|
89
|
+
console.log(` PostHog Analytics Sail Installer (v${manifest.version})`);
|
|
90
|
+
console.log("------------------------------------------------------------");
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(" This sail adds PostHog analytics to your app:");
|
|
93
|
+
console.log(" - Automatic page view tracking on SPA route changes");
|
|
94
|
+
console.log(" - User identification tied to your auth system");
|
|
95
|
+
console.log(" - Custom event tracking for feature usage");
|
|
96
|
+
console.log(" - Session recording and heatmaps (configurable in PostHog)");
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(" PostHog is privacy-friendly, open-source, and GDPR-compatible.");
|
|
99
|
+
console.log(" You can use PostHog Cloud or self-host it.");
|
|
100
|
+
console.log();
|
|
101
|
+
|
|
102
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
103
|
+
if (existsSync(pkgPath)) {
|
|
104
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
105
|
+
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -- Step 2: Hosting choice -------------------------------------------------
|
|
110
|
+
const hostingChoice = await select({
|
|
111
|
+
message: "How will you use PostHog?",
|
|
112
|
+
choices: [
|
|
113
|
+
{
|
|
114
|
+
name: "PostHog Cloud (US)",
|
|
115
|
+
value: "cloud-us",
|
|
116
|
+
description: "Hosted by PostHog at us.i.posthog.com — easiest setup",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "PostHog Cloud (EU)",
|
|
120
|
+
value: "cloud-eu",
|
|
121
|
+
description: "Hosted by PostHog at eu.i.posthog.com — EU data residency",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "Self-hosted",
|
|
125
|
+
value: "self-hosted",
|
|
126
|
+
description: "Your own PostHog instance — full data control",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let posthogHost: string;
|
|
132
|
+
|
|
133
|
+
if (hostingChoice === "cloud-us") {
|
|
134
|
+
posthogHost = "https://us.i.posthog.com";
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(" To get your API key:");
|
|
137
|
+
console.log(" 1. Sign up or log in at https://app.posthog.com");
|
|
138
|
+
console.log(" 2. Create a project (or select existing)");
|
|
139
|
+
console.log(" 3. Go to Project Settings");
|
|
140
|
+
console.log(" 4. Copy the Project API Key");
|
|
141
|
+
console.log();
|
|
142
|
+
} else if (hostingChoice === "cloud-eu") {
|
|
143
|
+
posthogHost = "https://eu.i.posthog.com";
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(" To get your API key:");
|
|
146
|
+
console.log(" 1. Sign up or log in at https://eu.posthog.com");
|
|
147
|
+
console.log(" 2. Create a project (or select existing)");
|
|
148
|
+
console.log(" 3. Go to Project Settings");
|
|
149
|
+
console.log(" 4. Copy the Project API Key");
|
|
150
|
+
console.log();
|
|
151
|
+
} else {
|
|
152
|
+
console.log();
|
|
153
|
+
posthogHost = await input({
|
|
154
|
+
message: "PostHog instance URL (e.g., https://posthog.yourdomain.com):",
|
|
155
|
+
validate: (value) => {
|
|
156
|
+
if (!value || value.trim().length === 0) return "Host URL is required.";
|
|
157
|
+
if (!value.startsWith("http")) return "Should start with http:// or https://";
|
|
158
|
+
return true;
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
posthogHost = posthogHost.replace(/\/+$/, ""); // Remove trailing slashes
|
|
162
|
+
console.log();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -- Step 3: Collect API key ------------------------------------------------
|
|
166
|
+
const posthogKey = await input({
|
|
167
|
+
message: "PostHog Project API Key:",
|
|
168
|
+
validate: (value) => {
|
|
169
|
+
if (!value || value.trim().length === 0) return "API key is required.";
|
|
170
|
+
if (value.startsWith("phx_")) return "This looks like a personal API key. Use the Project API key (starts with 'phc_').";
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// -- Step 4: Summary --------------------------------------------------------
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(" Summary of changes:");
|
|
178
|
+
console.log(" -------------------");
|
|
179
|
+
console.log(" Files to create (frontend):");
|
|
180
|
+
console.log(" + packages/frontend/src/lib/analytics.ts");
|
|
181
|
+
console.log(" + packages/frontend/src/hooks/useAnalytics.ts");
|
|
182
|
+
console.log(" + packages/frontend/src/components/AnalyticsProvider.tsx");
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(" Files to modify:");
|
|
185
|
+
console.log(" ~ packages/frontend/src/App.tsx");
|
|
186
|
+
console.log(" ~ .env.example / .env");
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(" Environment variables:");
|
|
189
|
+
console.log(` VITE_POSTHOG_KEY=${posthogKey.slice(0, 12)}...`);
|
|
190
|
+
console.log(` VITE_POSTHOG_HOST=${posthogHost}`);
|
|
191
|
+
console.log();
|
|
192
|
+
|
|
193
|
+
// -- Step 5: Confirm --------------------------------------------------------
|
|
194
|
+
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
195
|
+
if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
|
|
196
|
+
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(" Installing...");
|
|
199
|
+
console.log();
|
|
200
|
+
|
|
201
|
+
// -- Step 6: Copy files and modify App.tsx ----------------------------------
|
|
202
|
+
console.log(" Copying frontend files...");
|
|
203
|
+
const frontendFiles = [
|
|
204
|
+
{ src: "frontend/lib/analytics.ts", dest: "src/lib/analytics.ts" },
|
|
205
|
+
{ src: "frontend/hooks/useAnalytics.ts", dest: "src/hooks/useAnalytics.ts" },
|
|
206
|
+
{ src: "frontend/components/AnalyticsProvider.tsx", dest: "src/components/AnalyticsProvider.tsx" },
|
|
207
|
+
];
|
|
208
|
+
for (const f of frontendFiles) {
|
|
209
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(" Modifying App.tsx...");
|
|
214
|
+
|
|
215
|
+
const appPath = join(FRONTEND_ROOT, "src/App.tsx");
|
|
216
|
+
if (existsSync(appPath)) {
|
|
217
|
+
let appContent = readFileSync(appPath, "utf-8");
|
|
218
|
+
|
|
219
|
+
if (!appContent.includes("AnalyticsProvider")) {
|
|
220
|
+
// Add import
|
|
221
|
+
const importLine = 'import { AnalyticsProvider } from "./components/AnalyticsProvider.js";';
|
|
222
|
+
// Find the last import line and add after it
|
|
223
|
+
const lastImport = appContent.lastIndexOf("import ");
|
|
224
|
+
const importLineEnd = appContent.indexOf("\n", lastImport);
|
|
225
|
+
appContent =
|
|
226
|
+
appContent.slice(0, importLineEnd + 1) +
|
|
227
|
+
importLine + "\n" +
|
|
228
|
+
appContent.slice(importLineEnd + 1);
|
|
229
|
+
|
|
230
|
+
// Wrap <AppRouter /> with <AnalyticsProvider>
|
|
231
|
+
appContent = appContent.replace(
|
|
232
|
+
"<AppRouter />",
|
|
233
|
+
"<AnalyticsProvider>\n <AppRouter />\n </AnalyticsProvider>",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
writeFileSync(appPath, appContent, "utf-8");
|
|
237
|
+
console.log(" Modified -> App.tsx");
|
|
238
|
+
} else {
|
|
239
|
+
console.log(" Skipped (already present) -> App.tsx");
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
console.warn(" Warning: App.tsx not found. Wrap your app with <AnalyticsProvider> manually.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(" Installing dependencies...");
|
|
247
|
+
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
248
|
+
|
|
249
|
+
console.log();
|
|
250
|
+
console.log(" Updating environment files...");
|
|
251
|
+
appendToEnvFiles("PostHog Analytics", {
|
|
252
|
+
VITE_POSTHOG_KEY: posthogKey.trim(),
|
|
253
|
+
VITE_POSTHOG_HOST: posthogHost,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// -- Step 7: Next steps ------------------------------------------------------
|
|
257
|
+
console.log();
|
|
258
|
+
console.log("------------------------------------------------------------");
|
|
259
|
+
console.log(" PostHog Analytics installed successfully!");
|
|
260
|
+
console.log("------------------------------------------------------------");
|
|
261
|
+
console.log();
|
|
262
|
+
console.log(" Next steps:");
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(" 1. Start your dev server:");
|
|
265
|
+
console.log(" npm run dev");
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(" 2. Verify analytics is working:");
|
|
268
|
+
console.log(" - Open your app in the browser");
|
|
269
|
+
console.log(" - Log in and navigate between pages");
|
|
270
|
+
console.log(" - Check PostHog dashboard for events");
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(" 3. Configure PostHog features (optional):");
|
|
273
|
+
console.log(" - Session Recording: enable in PostHog project settings");
|
|
274
|
+
console.log(" - Feature Flags: use posthog.isFeatureEnabled('flag-name')");
|
|
275
|
+
console.log(" - Surveys: create in-app surveys from the PostHog dashboard");
|
|
276
|
+
console.log();
|
|
277
|
+
console.log(" 4. Track custom events in your components:");
|
|
278
|
+
console.log();
|
|
279
|
+
console.log(" import { useAnalytics } from '@/hooks/useAnalytics';");
|
|
280
|
+
console.log();
|
|
281
|
+
console.log(" function MyComponent() {");
|
|
282
|
+
console.log(" const { trackEvent } = useAnalytics();");
|
|
283
|
+
console.log(" return (");
|
|
284
|
+
console.log(" <button onClick={() => trackEvent('button_clicked', { label: 'cta' })}>");
|
|
285
|
+
console.log(" Click me");
|
|
286
|
+
console.log(" </button>");
|
|
287
|
+
console.log(" );");
|
|
288
|
+
console.log(" }");
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(" GDPR note:");
|
|
291
|
+
console.log(" PostHog supports cookie-less tracking and consent management.");
|
|
292
|
+
console.log(" If you have the GDPR sail installed, consider integrating");
|
|
293
|
+
console.log(" analytics consent with your consent tracking system.");
|
|
294
|
+
console.log();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# File Uploads Sail
|
|
2
|
+
|
|
3
|
+
Generic file upload system with S3-compatible storage. Works with Cloudflare R2, AWS S3, MinIO, and any other S3-compatible provider.
|
|
4
|
+
|
|
5
|
+
## What this sail adds
|
|
6
|
+
|
|
7
|
+
### Backend
|
|
8
|
+
- **`src/services/file-storage.ts`** -- S3 client with helpers for presigned URLs, deletion, and listing
|
|
9
|
+
- **`src/routes/files.ts`** -- File management API (all auth-protected):
|
|
10
|
+
- `POST /api/files/upload-url` -- generate a presigned upload URL
|
|
11
|
+
- `GET /api/files` -- list the current user's files
|
|
12
|
+
- `GET /api/files/:fileId` -- get file metadata and download URL
|
|
13
|
+
- `DELETE /api/files/:fileId` -- delete a file
|
|
14
|
+
- **`src/db/schema/files.ts`** -- Drizzle schema for the `files` table
|
|
15
|
+
|
|
16
|
+
### Frontend
|
|
17
|
+
- **`src/hooks/useFileUpload.ts`** -- React hook for uploading files with progress tracking
|
|
18
|
+
- **`src/hooks/useFiles.ts`** -- React hook for listing, downloading, and deleting files
|
|
19
|
+
- **`src/components/files/FileUploadButton.tsx`** -- Upload button with drag-and-drop support
|
|
20
|
+
- **`src/components/files/FileList.tsx`** -- File browser with download and delete actions
|
|
21
|
+
- **`src/pages/Files.tsx`** -- Full files page combining upload and browser
|
|
22
|
+
|
|
23
|
+
### Environment variables
|
|
24
|
+
|
|
25
|
+
| Variable | Description | Default |
|
|
26
|
+
|----------|-------------|---------|
|
|
27
|
+
| `S3_ENDPOINT` | S3-compatible endpoint URL | (required) |
|
|
28
|
+
| `S3_ACCESS_KEY_ID` | S3 access key ID | (required) |
|
|
29
|
+
| `S3_SECRET_ACCESS_KEY` | S3 secret access key | (required) |
|
|
30
|
+
| `S3_BUCKET_NAME` | Bucket name | (required) |
|
|
31
|
+
| `S3_PUBLIC_URL` | Public URL for serving files | `""` |
|
|
32
|
+
| `S3_REGION` | S3 region | `auto` |
|
|
33
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
|
|
36
|
+
1. An S3-compatible storage account (Cloudflare R2, AWS S3, MinIO, etc.)
|
|
37
|
+
2. A bucket created in your storage provider
|
|
38
|
+
3. API credentials with read/write permissions
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
### Run the installer
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx tsx cli/sails/file-uploads/install.ts
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or use the CLI:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @codaijs/keel sail add file-uploads
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The installer will guide you through:
|
|
55
|
+
1. Choosing your storage provider
|
|
56
|
+
2. Entering credentials
|
|
57
|
+
3. Configuring max file size
|
|
58
|
+
4. Copying files and modifying markers
|
|
59
|
+
5. Installing dependencies and generating migrations
|
|
60
|
+
|
|
61
|
+
### After installation
|
|
62
|
+
|
|
63
|
+
1. Run database migrations:
|
|
64
|
+
```bash
|
|
65
|
+
npm run db:migrate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. Configure CORS on your storage bucket (see below)
|
|
69
|
+
|
|
70
|
+
3. Start the dev server:
|
|
71
|
+
```bash
|
|
72
|
+
npm run dev
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
4. Navigate to `/files` to test
|
|
76
|
+
|
|
77
|
+
## Upload flow
|
|
78
|
+
|
|
79
|
+
1. The frontend calls `POST /api/files/upload-url` with the file name and content type
|
|
80
|
+
2. The backend generates a presigned PUT URL and creates a file record in the database
|
|
81
|
+
3. The frontend uploads the file directly to storage using the presigned URL
|
|
82
|
+
4. The file is now tracked in the database and available for listing, downloading, and deletion
|
|
83
|
+
|
|
84
|
+
Files never pass through your server -- they go directly from the browser to storage.
|
|
85
|
+
|
|
86
|
+
## CORS configuration
|
|
87
|
+
|
|
88
|
+
Your storage bucket must allow PUT and GET requests from your frontend origin.
|
|
89
|
+
|
|
90
|
+
### Cloudflare R2
|
|
91
|
+
|
|
92
|
+
In your bucket settings, add a CORS policy:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
"AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],
|
|
98
|
+
"AllowedMethods": ["GET", "PUT"],
|
|
99
|
+
"AllowedHeaders": ["Content-Type"],
|
|
100
|
+
"MaxAgeSeconds": 3600
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### AWS S3
|
|
106
|
+
|
|
107
|
+
In bucket permissions, add a CORS configuration:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
[
|
|
111
|
+
{
|
|
112
|
+
"AllowedHeaders": ["Content-Type"],
|
|
113
|
+
"AllowedMethods": ["GET", "PUT"],
|
|
114
|
+
"AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],
|
|
115
|
+
"MaxAgeSeconds": 3600
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### MinIO
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
mc admin config set local api cors_allow_origin=http://localhost:5173
|
|
124
|
+
mc admin service restart local
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Usage in your own components
|
|
128
|
+
|
|
129
|
+
### Upload a file
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { useFileUpload } from "@/hooks/useFileUpload";
|
|
133
|
+
|
|
134
|
+
function MyComponent() {
|
|
135
|
+
const { upload, isUploading, progress, error } = useFileUpload();
|
|
136
|
+
|
|
137
|
+
const handleUpload = async (file: File) => {
|
|
138
|
+
const result = await upload(file);
|
|
139
|
+
if (result) {
|
|
140
|
+
console.log("Uploaded:", result.id, result.fileName);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### List and manage files
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useFiles } from "@/hooks/useFiles";
|
|
150
|
+
|
|
151
|
+
function MyComponent() {
|
|
152
|
+
const { files, isLoading, deleteFile, getDownloadUrl, refresh } = useFiles();
|
|
153
|
+
|
|
154
|
+
const handleDownload = async (id: string) => {
|
|
155
|
+
const url = await getDownloadUrl(id);
|
|
156
|
+
if (url) window.open(url);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Custom upload button
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { FileUploadButton } from "@/components/files/FileUploadButton";
|
|
165
|
+
|
|
166
|
+
<FileUploadButton
|
|
167
|
+
accept="image/*,.pdf"
|
|
168
|
+
maxSize={10 * 1024 * 1024} // 10 MB
|
|
169
|
+
onUploadComplete={(file) => console.log("Uploaded:", file)}
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Difference from the r2-storage sail
|
|
174
|
+
|
|
175
|
+
The **r2-storage** sail is focused specifically on Cloudflare R2 and profile picture uploads. This **file-uploads** sail is a more general-purpose system:
|
|
176
|
+
|
|
177
|
+
- Works with any S3-compatible provider (R2, S3, MinIO)
|
|
178
|
+
- Supports any file type, not just images
|
|
179
|
+
- Tracks files in a database table
|
|
180
|
+
- Includes a full file browser UI
|
|
181
|
+
- Supports listing, downloading, and deleting files
|
|
182
|
+
|
|
183
|
+
If you only need profile picture uploads with R2, use the r2-storage sail instead.
|
|
184
|
+
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
- **CORS errors**: Make sure your bucket CORS policy includes your frontend origin and allows PUT/GET methods
|
|
188
|
+
- **403 Forbidden**: Verify your API credentials have the correct permissions
|
|
189
|
+
- **Upload succeeds but file not in list**: Check the server logs for database insert errors
|
|
190
|
+
- **Presigned URL expired**: URLs expire after 10 minutes -- upload should happen immediately
|
|
191
|
+
- **Large files timing out**: Increase the presigned URL expiry in `file-storage.ts` or your reverse proxy timeout
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "file-uploads",
|
|
3
|
+
"displayName": "File Uploads",
|
|
4
|
+
"description": "Generic file upload system with S3-compatible storage (R2, S3, MinIO). Includes file management API, upload hooks, and a file browser component.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"compatibility": ">=1.0.0",
|
|
7
|
+
"requiredEnvVars": [
|
|
8
|
+
{ "key": "S3_ENDPOINT", "description": "S3-compatible endpoint URL" },
|
|
9
|
+
{ "key": "S3_ACCESS_KEY_ID", "description": "S3 access key ID" },
|
|
10
|
+
{ "key": "S3_SECRET_ACCESS_KEY", "description": "S3 secret access key" },
|
|
11
|
+
{ "key": "S3_BUCKET_NAME", "description": "S3 bucket name" },
|
|
12
|
+
{ "key": "S3_PUBLIC_URL", "description": "Public URL for serving files" },
|
|
13
|
+
{ "key": "S3_REGION", "description": "S3 region (defaults to auto)" }
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"backend": {
|
|
17
|
+
"@aws-sdk/client-s3": "^3.700.0",
|
|
18
|
+
"@aws-sdk/s3-request-presigner": "^3.700.0"
|
|
19
|
+
},
|
|
20
|
+
"frontend": {}
|
|
21
|
+
},
|
|
22
|
+
"modifies": {
|
|
23
|
+
"backend": ["src/index.ts", "src/db/schema/index.ts", "src/env.ts"],
|
|
24
|
+
"frontend": ["src/router.tsx"]
|
|
25
|
+
},
|
|
26
|
+
"adds": {
|
|
27
|
+
"backend": ["src/services/file-storage.ts", "src/routes/files.ts", "src/db/schema/files.ts"],
|
|
28
|
+
"frontend": ["src/hooks/useFileUpload.ts", "src/hooks/useFiles.ts", "src/components/files/FileUploadButton.tsx", "src/components/files/FileList.tsx", "src/pages/Files.tsx"]
|
|
29
|
+
}
|
|
30
|
+
}
|