@lastbrain/app 0.1.23 → 0.1.24
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/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +348 -19
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +16 -1
- package/dist/templates/DocPage.d.ts.map +1 -1
- package/dist/templates/DocPage.js +9 -3
- package/package.json +19 -20
- package/src/scripts/init-app.ts +359 -19
- package/src/templates/DefaultDoc.tsx +127 -0
- package/src/templates/DocPage.tsx +9 -3
|
@@ -24,7 +24,22 @@ set raw_app_meta_data = jsonb_set(
|
|
|
24
24
|
'{roles}',
|
|
25
25
|
'["admin"]'::jsonb
|
|
26
26
|
)
|
|
27
|
-
where email = 'votre@email.com';` }) })] }), _jsxs("li", { className: "text-lg", children: [_jsx("strong", { children: "Acc\u00E9der \u00E0 l'interface admin" }), _jsxs("p", { className: "text-slate-600 dark:text-slate-400 ml-6", children: ["Vous pouvez maintenant acc\u00E9der \u00E0", " ", _jsx("code", { className: "px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded", children: "/admin" })] })] })] }) })] }), _jsxs(Card, { id: "section-development", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Rocket, { size: 24 }), "D\u00E9veloppement"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Lancer le serveur de d\u00E9veloppement" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm dev" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "G\u00E9n\u00E9rer les routes des modules" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: "\u00C0 ex\u00E9cuter apr\u00E8s avoir ajout\u00E9 ou modifi\u00E9 des modules" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build:modules" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Build de production" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "D\u00E9velopper un module" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-2", children: "cd packages/module-auth" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm dev" })] })] })] }), _jsxs(Card, { id: "section-routes", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Shield, { size: 24 }), "Routes Prot\u00E9g\u00E9es"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400 mb-4", children: "Le middleware prot\u00E8ge automatiquement vos routes selon ces r\u00E8gles :" }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "p-3 bg-slate-50 dark:bg-slate-900 rounded", children: [_jsx("code", { className: "text-blue-600 dark:text-blue-400", children: "/auth/*" }), _jsx("span", { className: "ml-2", children: "\u2192 Authentification requise" })] }), _jsxs("div", { className: "p-3 bg-slate-50 dark:bg-slate-900 rounded", children: [_jsx("code", { className: "text-purple-600 dark:text-purple-400", children: "/admin/*" }), _jsx("span", { className: "ml-2", children: "\u2192 Superadmin uniquement" })] }), _jsxs("div", { className: "p-3 bg-slate-50 dark:bg-slate-900 rounded", children: [_jsx("code", { className: "text-green-600 dark:text-green-400", children: "/docs/*" }), _jsx("span", { className: "ml-2", children: "\u2192 Acc\u00E8s public" })] })] }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mt-4", children: ["Le middleware se trouve dans", " ", _jsx("code", { className: "px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded", children: "middleware.ts" })] })] })] }), _jsxs(Card, { id: "section-workflow", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Zap, { size: 24 }), "Workflow de D\u00E9veloppement"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "1. Modifier un module" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: ["\u00C9ditez les fichiers dans", " ", _jsx("code", { className: "px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded", children: "packages/module-*/src" })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "2. Compiler le module" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-2", children: "cd packages/module-auth" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "3. R\u00E9g\u00E9n\u00E9rer les pages" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-2", children: "cd apps/my-app" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build:modules" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "4. Appliquer les migrations" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "supabase migration up" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "5. Tester" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm dev" })] })] })] }), _jsxs(Card, { id: "section-ui", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Palette, { size: 24 }), "Interface utilisateur"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Composants NextUI" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-3", children: ["Le package", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "@lastbrain/ui" }), "r\u00E9exporte les composants NextUI avec une configuration Tailwind personnalis\u00E9e."] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Th\u00E8mes" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: ["Le mode sombre/clair est g\u00E9r\u00E9 par", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "next-themes" }), ". Le switch est disponible dans le header de l'application."] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Tailwind CSS v4" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: ["Le projet utilise Tailwind CSS v4 avec un preset personnalis\u00E9 partag\u00E9 via", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "@lastbrain/ui/tailwind.preset" }), "."] })] })] })] }), _jsxs(Card, { id: "section-
|
|
27
|
+
where email = 'votre@email.com';` }) })] }), _jsxs("li", { className: "text-lg", children: [_jsx("strong", { children: "Acc\u00E9der \u00E0 l'interface admin" }), _jsxs("p", { className: "text-slate-600 dark:text-slate-400 ml-6", children: ["Vous pouvez maintenant acc\u00E9der \u00E0", " ", _jsx("code", { className: "px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded", children: "/admin" })] })] })] }) })] }), _jsxs(Card, { id: "section-development", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Rocket, { size: 24 }), "D\u00E9veloppement"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Lancer le serveur de d\u00E9veloppement" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm dev" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "G\u00E9n\u00E9rer les routes des modules" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: "\u00C0 ex\u00E9cuter apr\u00E8s avoir ajout\u00E9 ou modifi\u00E9 des modules" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build:modules" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Build de production" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "D\u00E9velopper un module" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-2", children: "cd packages/module-auth" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm dev" })] })] })] }), _jsxs(Card, { id: "section-routes", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Shield, { size: 24 }), "Routes Prot\u00E9g\u00E9es"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400 mb-4", children: "Le middleware prot\u00E8ge automatiquement vos routes selon ces r\u00E8gles :" }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "p-3 bg-slate-50 dark:bg-slate-900 rounded", children: [_jsx("code", { className: "text-blue-600 dark:text-blue-400", children: "/auth/*" }), _jsx("span", { className: "ml-2", children: "\u2192 Authentification requise" })] }), _jsxs("div", { className: "p-3 bg-slate-50 dark:bg-slate-900 rounded", children: [_jsx("code", { className: "text-purple-600 dark:text-purple-400", children: "/admin/*" }), _jsx("span", { className: "ml-2", children: "\u2192 Superadmin uniquement" })] }), _jsxs("div", { className: "p-3 bg-slate-50 dark:bg-slate-900 rounded", children: [_jsx("code", { className: "text-green-600 dark:text-green-400", children: "/docs/*" }), _jsx("span", { className: "ml-2", children: "\u2192 Acc\u00E8s public" })] })] }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mt-4", children: ["Le middleware se trouve dans", " ", _jsx("code", { className: "px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded", children: "middleware.ts" })] })] })] }), _jsxs(Card, { id: "section-workflow", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Zap, { size: 24 }), "Workflow de D\u00E9veloppement"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "1. Modifier un module" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: ["\u00C9ditez les fichiers dans", " ", _jsx("code", { className: "px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded", children: "packages/module-*/src" })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "2. Compiler le module" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-2", children: "cd packages/module-auth" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "3. R\u00E9g\u00E9n\u00E9rer les pages" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm mb-2", children: "cd apps/my-app" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm build:modules" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "4. Appliquer les migrations" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "supabase migration up" })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "5. Tester" }), _jsx(Snippet, { symbol: "", hideSymbol: true, className: "text-sm", children: "pnpm dev" })] })] })] }), _jsxs(Card, { id: "section-ui", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Palette, { size: 24 }), "Interface utilisateur"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Composants NextUI" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-3", children: ["Le package", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "@lastbrain/ui" }), "r\u00E9exporte les composants NextUI avec une configuration Tailwind personnalis\u00E9e."] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Th\u00E8mes" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: ["Le mode sombre/clair est g\u00E9r\u00E9 par", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "next-themes" }), ". Le switch est disponible dans le header de l'application."] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Tailwind CSS v4" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: ["Le projet utilise Tailwind CSS v4 avec un preset personnalis\u00E9 partag\u00E9 via", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "@lastbrain/ui/tailwind.preset" }), "."] })] })] })] }), _jsxs(Card, { id: "section-storage", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(Database, { size: 24 }), "Syst\u00E8me de Proxy Storage"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400", children: "LastBrain int\u00E8gre un syst\u00E8me de proxy pour les fichiers Supabase Storage qui permet d'utiliser des URLs propres avec contr\u00F4le d'acc\u00E8s granulaire." }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "URLs Proxy vs URLs Supabase" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "bg-red-50 dark:bg-red-950 p-3 rounded-lg", children: [_jsx("p", { className: "text-sm font-medium text-red-700 dark:text-red-300 mb-1", children: "\u274C URL Supabase classique (longue)" }), _jsx("code", { className: "text-xs text-red-600 dark:text-red-400 break-all", children: "https://xxx.supabase.co/storage/v1/object/public/avatar/user_128_123456.webp" })] }), _jsxs("div", { className: "bg-green-50 dark:bg-green-950 p-3 rounded-lg", children: [_jsx("p", { className: "text-sm font-medium text-green-700 dark:text-green-300 mb-1", children: "\u2705 URL Proxy (propre)" }), _jsx("code", { className: "text-xs text-green-600 dark:text-green-400", children: "/api/storage/avatar/user_128_123456.webp" })] })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Configuration des Buckets" }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "border-l-4 border-green-500 pl-4", children: [_jsx("h4", { className: "font-semibold text-green-600 dark:text-green-400", children: "\uD83D\uDCC2 avatar (Public)" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: "Photos de profil et avatars \u2022 10MB max \u2022 Types: JPEG, PNG, WebP, GIF" }), _jsx("code", { className: "text-xs bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "/api/storage/avatar/user_128_123456.webp" })] }), _jsxs("div", { className: "border-l-4 border-blue-500 pl-4", children: [_jsx("h4", { className: "font-semibold text-blue-600 dark:text-blue-400", children: "\uD83D\uDD12 app (Priv\u00E9)" }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400", children: "Fichiers priv\u00E9s des utilisateurs \u2022 100MB max \u2022 Authentification requise" }), _jsx("code", { className: "text-xs bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "/api/storage/app/user-id/documents/file.pdf" })] })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Utilisation dans le code" }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `// Upload d'un fichier
|
|
28
|
+
import { uploadFile } from "@/api/storage";
|
|
29
|
+
|
|
30
|
+
const proxyUrl = await uploadFile(
|
|
31
|
+
"avatar",
|
|
32
|
+
"user_128_123456.webp",
|
|
33
|
+
file,
|
|
34
|
+
"image/webp"
|
|
35
|
+
);
|
|
36
|
+
// Retourne: "/api/storage/avatar/user_128_123456.webp"
|
|
37
|
+
|
|
38
|
+
// Conversion de chemin storage
|
|
39
|
+
import { storagePathToProxyUrl } from "@/lib/storage";
|
|
40
|
+
|
|
41
|
+
const proxyUrl = storagePathToProxyUrl("avatar/user_128_123456.webp");
|
|
42
|
+
// Retourne: "/api/storage/avatar/user_128_123456.webp"` }) })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Avantages du syst\u00E8me" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "URLs plus courtes et lisibles" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Contr\u00F4le d'acc\u00E8s granulaire" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Cache optimis\u00E9 (1 an)" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "S\u00E9curit\u00E9 am\u00E9lior\u00E9e" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "D\u00E9tection auto des types MIME" })] }), _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "text-green-600", children: "\u2705" }), _jsx("span", { children: "Extensible facilement" })] })] })] })] })] }), _jsxs(Card, { id: "section-module-docs", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsxs("h2", { className: "text-2xl font-semibold flex items-center gap-2", children: [_jsx(BookOpen, { size: 24 }), "Documenter ses modules"] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400", children: "Chaque module peut exporter un composant de documentation qui sera automatiquement int\u00E9gr\u00E9 dans cette page." }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-2", children: "Cr\u00E9er une documentation" }), _jsxs("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: ["Dans votre module, cr\u00E9ez", " ", _jsx("code", { className: "text-sm bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded", children: "src/components/Doc.tsx" }), " ", ":"] }), _jsx("div", { className: "bg-slate-50 dark:bg-slate-900 p-4 rounded-lg text-sm font-mono overflow-x-auto", children: _jsx("pre", { children: `export function MonModuleDoc() {
|
|
28
43
|
return (
|
|
29
44
|
<div>
|
|
30
45
|
<h1>Mon Module</h1>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocPage.d.ts","sourceRoot":"","sources":["../../src/templates/DocPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"DocPage.d.ts","sourceRoot":"","sources":["../../src/templates/DocPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AA+BnD,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EACF,SAAS,GACT,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,SAAS,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAClC;AAED,wBAAgB,OAAO,CAAC,EAAE,OAAY,EAAE,cAAc,EAAE,EAAE,YAAY,2CA2UrE"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { useState, useEffect } from "react";
|
|
5
5
|
import { Card, CardBody, CardHeader, Listbox, ListboxItem, Chip, Button, Drawer, DrawerContent, DrawerHeader, DrawerBody, Snippet, } from "@lastbrain/ui";
|
|
6
|
-
import { Menu, Home, Sparkles, Rocket, Building2, Package, Database, Palette, BookOpen, Link, Blocks, } from "lucide-react";
|
|
6
|
+
import { Menu, Home, Sparkles, Rocket, Building2, Package, Database, Palette, BookOpen, Link, Blocks, HardDrive, } from "lucide-react";
|
|
7
7
|
import { DefaultDocumentation } from "./DefaultDoc.js";
|
|
8
8
|
export function DocPage({ modules = [], defaultContent }) {
|
|
9
9
|
const [selectedModule, setSelectedModule] = useState("default");
|
|
@@ -18,13 +18,13 @@ export function DocPage({ modules = [], defaultContent }) {
|
|
|
18
18
|
"section-architecture",
|
|
19
19
|
"section-create-module",
|
|
20
20
|
"section-database",
|
|
21
|
+
"section-storage",
|
|
21
22
|
"section-ui",
|
|
22
23
|
"section-module-docs",
|
|
23
24
|
"section-links",
|
|
24
25
|
...(modules.length > 0 ? ["section-modules"] : []),
|
|
25
26
|
...modules.map((m) => `module-${m.id}`),
|
|
26
|
-
];
|
|
27
|
-
// Trouver la section actuellement visible
|
|
27
|
+
]; // Trouver la section actuellement visible
|
|
28
28
|
let currentSection = "default";
|
|
29
29
|
// Si on est tout en haut de la page
|
|
30
30
|
if (window.scrollY < 100) {
|
|
@@ -109,6 +109,12 @@ export function DocPage({ modules = [], defaultContent }) {
|
|
|
109
109
|
description: "",
|
|
110
110
|
icon: Database,
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
id: "section-storage",
|
|
114
|
+
name: "Proxy Storage",
|
|
115
|
+
description: "Gestion des fichiers",
|
|
116
|
+
icon: HardDrive,
|
|
117
|
+
},
|
|
112
118
|
{
|
|
113
119
|
id: "section-ui",
|
|
114
120
|
name: "Interface utilisateur",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastbrain/app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "Framework modulaire Next.js avec CLI et système de modules",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -30,24 +30,6 @@
|
|
|
30
30
|
"dist",
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
|
-
"scripts": {
|
|
34
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
35
|
-
"build": "tsc -p tsconfig.json && npm run build:css && npm run copy:templates && chmod +x dist/cli.js",
|
|
36
|
-
"build:css": "NODE_ENV=production pnpm exec postcss ./src/styles.css -o ./dist/styles.css",
|
|
37
|
-
"copy:templates": "mkdir -p dist/templates/migrations dist/templates/env.example dist/templates/gitignore && cp src/templates/migrations/* dist/templates/migrations/ 2>/dev/null || true && cp src/templates/env.example/.env.example dist/templates/env.example/ 2>/dev/null || true && cp src/templates/gitignore/.gitignore dist/templates/gitignore/ 2>/dev/null || true",
|
|
38
|
-
"prepublishOnly": "npm run build",
|
|
39
|
-
"module:build": "node dist/scripts/module-build.js",
|
|
40
|
-
"module:create": "node dist/scripts/module-create.js",
|
|
41
|
-
"module:add": "node dist/scripts/module-add.js",
|
|
42
|
-
"module:remove": "node dist/scripts/module-remove.js",
|
|
43
|
-
"module:list": "node dist/scripts/module-list.js",
|
|
44
|
-
"build:modules": "node dist/scripts/module-build.js",
|
|
45
|
-
"db:migrations:sync": "node dist/scripts/db-migrations-sync.js",
|
|
46
|
-
"db:init": "node dist/scripts/db-init.js",
|
|
47
|
-
"readme:create": "node dist/scripts/readme-build.js",
|
|
48
|
-
"dev:shell": "next dev ./src/app-shell -p 3001",
|
|
49
|
-
"dev:sync": "node dist/scripts/dev-sync.js"
|
|
50
|
-
},
|
|
51
33
|
"dependencies": {
|
|
52
34
|
"@lastbrain/core": "^0.1.0",
|
|
53
35
|
"@lastbrain/module-auth": "^0.1.2",
|
|
@@ -76,5 +58,22 @@
|
|
|
76
58
|
"postcss-cli": "^11.0.0",
|
|
77
59
|
"tailwindcss": "^4.1.17",
|
|
78
60
|
"typescript": "^5.4.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
64
|
+
"build": "tsc -p tsconfig.json && npm run build:css && npm run copy:templates && chmod +x dist/cli.js",
|
|
65
|
+
"build:css": "NODE_ENV=production pnpm exec postcss ./src/styles.css -o ./dist/styles.css",
|
|
66
|
+
"copy:templates": "mkdir -p dist/templates/migrations dist/templates/env.example dist/templates/gitignore && cp src/templates/migrations/* dist/templates/migrations/ 2>/dev/null || true && cp src/templates/env.example/.env.example dist/templates/env.example/ 2>/dev/null || true && cp src/templates/gitignore/.gitignore dist/templates/gitignore/ 2>/dev/null || true",
|
|
67
|
+
"module:build": "node dist/scripts/module-build.js",
|
|
68
|
+
"module:create": "node dist/scripts/module-create.js",
|
|
69
|
+
"module:add": "node dist/scripts/module-add.js",
|
|
70
|
+
"module:remove": "node dist/scripts/module-remove.js",
|
|
71
|
+
"module:list": "node dist/scripts/module-list.js",
|
|
72
|
+
"build:modules": "node dist/scripts/module-build.js",
|
|
73
|
+
"db:migrations:sync": "node dist/scripts/db-migrations-sync.js",
|
|
74
|
+
"db:init": "node dist/scripts/db-init.js",
|
|
75
|
+
"readme:create": "node dist/scripts/readme-build.js",
|
|
76
|
+
"dev:shell": "next dev ./src/app-shell -p 3001",
|
|
77
|
+
"dev:sync": "node dist/scripts/dev-sync.js"
|
|
79
78
|
}
|
|
80
|
-
}
|
|
79
|
+
}
|
package/src/scripts/init-app.ts
CHANGED
|
@@ -75,17 +75,20 @@ export async function initApp(options: InitAppOptions) {
|
|
|
75
75
|
// 4. Créer les fichiers de configuration
|
|
76
76
|
await createConfigFiles(targetDir, force, useHeroUI);
|
|
77
77
|
|
|
78
|
-
// 5. Créer
|
|
78
|
+
// 5. Créer le système de proxy storage
|
|
79
|
+
await createStorageProxy(targetDir, force);
|
|
80
|
+
|
|
81
|
+
// 6. Créer .gitignore et .env.local.example
|
|
79
82
|
await createGitIgnore(targetDir, force);
|
|
80
83
|
await createEnvExample(targetDir, force);
|
|
81
84
|
|
|
82
|
-
//
|
|
85
|
+
// 7. Créer la structure Supabase avec migrations
|
|
83
86
|
await createSupabaseStructure(targetDir, force);
|
|
84
87
|
|
|
85
|
-
//
|
|
88
|
+
// 8. Ajouter les scripts NPM
|
|
86
89
|
await addScriptsToPackageJson(targetDir);
|
|
87
90
|
|
|
88
|
-
//
|
|
91
|
+
// 9. Enregistrer les modules sélectionnés
|
|
89
92
|
if (withAuth || selectedModules.length > 0) {
|
|
90
93
|
await saveModulesConfig(targetDir, selectedModules, withAuth);
|
|
91
94
|
}
|
|
@@ -323,6 +326,7 @@ async function addDependencies(
|
|
|
323
326
|
requiredDeps["@heroui/switch"] = "^2.2.24";
|
|
324
327
|
requiredDeps["@heroui/table"] = "^2.2.27";
|
|
325
328
|
requiredDeps["@heroui/tabs"] = "^2.2.24";
|
|
329
|
+
requiredDeps["@heroui/system"] = "^2.4.23"; // Ajout pour HeroUIProvider
|
|
326
330
|
requiredDeps["@heroui/toast"] = "^2.0.17";
|
|
327
331
|
requiredDeps["@heroui/tooltip"] = "^2.2.24";
|
|
328
332
|
requiredDeps["@heroui/user"] = "^2.2.22";
|
|
@@ -1062,21 +1066,11 @@ async function addScriptsToPackageJson(targetDir: string) {
|
|
|
1062
1066
|
build: "next build",
|
|
1063
1067
|
start: "next start",
|
|
1064
1068
|
lint: "next lint",
|
|
1065
|
-
lastbrain:
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
"
|
|
1069
|
-
|
|
1070
|
-
: "node node_modules/@lastbrain/app/dist/scripts/module-build.js",
|
|
1071
|
-
"db:migrations:sync": targetIsInMonorepo
|
|
1072
|
-
? "pnpm exec lastbrain db:migrations:sync"
|
|
1073
|
-
: "node node_modules/@lastbrain/app/dist/scripts/db-migrations-sync.js",
|
|
1074
|
-
"db:init": targetIsInMonorepo
|
|
1075
|
-
? "pnpm exec lastbrain db:init"
|
|
1076
|
-
: "node node_modules/@lastbrain/app/dist/scripts/db-init.js",
|
|
1077
|
-
"readme:create": targetIsInMonorepo
|
|
1078
|
-
? "pnpm exec lastbrain readme:create"
|
|
1079
|
-
: "node node_modules/@lastbrain/app/dist/scripts/readme-build.js",
|
|
1069
|
+
lastbrain: "node node_modules/@lastbrain/app/dist/cli.js",
|
|
1070
|
+
"build:modules": "node node_modules/@lastbrain/app/dist/scripts/module-build.js",
|
|
1071
|
+
"db:migrations:sync": "node node_modules/@lastbrain/app/dist/scripts/db-migrations-sync.js",
|
|
1072
|
+
"db:init": "node node_modules/@lastbrain/app/dist/scripts/db-init.js",
|
|
1073
|
+
"readme:create": "node node_modules/@lastbrain/app/dist/scripts/readme-build.js",
|
|
1080
1074
|
};
|
|
1081
1075
|
|
|
1082
1076
|
pkg.scripts = { ...pkg.scripts, ...scripts };
|
|
@@ -1128,3 +1122,349 @@ async function saveModulesConfig(
|
|
|
1128
1122
|
await fs.writeJson(modulesConfigPath, { modules }, { spaces: 2 });
|
|
1129
1123
|
console.log(chalk.green("✓ Configuration des modules sauvegardée"));
|
|
1130
1124
|
}
|
|
1125
|
+
|
|
1126
|
+
async function createStorageProxy(targetDir: string, force: boolean) {
|
|
1127
|
+
console.log(chalk.yellow("\n🗂️ Création du système de proxy storage..."));
|
|
1128
|
+
|
|
1129
|
+
// Créer le dossier lib
|
|
1130
|
+
const libDir = path.join(targetDir, "lib");
|
|
1131
|
+
await fs.ensureDir(libDir);
|
|
1132
|
+
|
|
1133
|
+
// 1. Créer lib/bucket-config.ts
|
|
1134
|
+
const bucketConfigPath = path.join(libDir, "bucket-config.ts");
|
|
1135
|
+
if (!fs.existsSync(bucketConfigPath) || force) {
|
|
1136
|
+
const bucketConfigContent = `/**
|
|
1137
|
+
* Storage configuration for buckets and access control
|
|
1138
|
+
*/
|
|
1139
|
+
|
|
1140
|
+
export interface BucketConfig {
|
|
1141
|
+
name: string;
|
|
1142
|
+
isPublic: boolean;
|
|
1143
|
+
description: string;
|
|
1144
|
+
allowedFileTypes?: string[];
|
|
1145
|
+
maxFileSize?: number; // in bytes
|
|
1146
|
+
customAccessControl?: (userId: string, filePath: string) => boolean;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
|
|
1150
|
+
avatar: {
|
|
1151
|
+
name: "avatar",
|
|
1152
|
+
isPublic: true,
|
|
1153
|
+
description: "User profile pictures and avatars",
|
|
1154
|
+
allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1155
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
1156
|
+
},
|
|
1157
|
+
app: {
|
|
1158
|
+
name: "app",
|
|
1159
|
+
isPublic: false,
|
|
1160
|
+
description: "Private user files and documents",
|
|
1161
|
+
maxFileSize: 100 * 1024 * 1024, // 100MB
|
|
1162
|
+
customAccessControl: (userId: string, filePath: string) => {
|
|
1163
|
+
// Users can only access files in their own folder (app/{userId}/...)
|
|
1164
|
+
return filePath.startsWith(\`\${userId}/\`);
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
// Example for future buckets:
|
|
1168
|
+
// public: {
|
|
1169
|
+
// name: "public",
|
|
1170
|
+
// isPublic: true,
|
|
1171
|
+
// description: "Publicly accessible files like logos, banners",
|
|
1172
|
+
// allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
|
|
1173
|
+
// maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
1174
|
+
// },
|
|
1175
|
+
// documents: {
|
|
1176
|
+
// name: "documents",
|
|
1177
|
+
// isPublic: false,
|
|
1178
|
+
// description: "Private documents requiring authentication",
|
|
1179
|
+
// allowedFileTypes: ["application/pdf", "application/msword", "text/plain"],
|
|
1180
|
+
// maxFileSize: 25 * 1024 * 1024, // 25MB
|
|
1181
|
+
// },
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Get bucket configuration
|
|
1186
|
+
*/
|
|
1187
|
+
export function getBucketConfig(bucketName: string): BucketConfig | null {
|
|
1188
|
+
return BUCKET_CONFIGS[bucketName] || null;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Check if bucket is public
|
|
1193
|
+
*/
|
|
1194
|
+
export function isPublicBucket(bucketName: string): boolean {
|
|
1195
|
+
const config = getBucketConfig(bucketName);
|
|
1196
|
+
return config?.isPublic ?? false;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Check if user has access to a specific file
|
|
1201
|
+
*/
|
|
1202
|
+
export function hasFileAccess(bucketName: string, userId: string, filePath: string): boolean {
|
|
1203
|
+
const config = getBucketConfig(bucketName);
|
|
1204
|
+
if (!config) return false;
|
|
1205
|
+
|
|
1206
|
+
// Public buckets are accessible to everyone
|
|
1207
|
+
if (config.isPublic) return true;
|
|
1208
|
+
|
|
1209
|
+
// Private buckets require authentication
|
|
1210
|
+
if (!userId) return false;
|
|
1211
|
+
|
|
1212
|
+
// Apply custom access control if defined
|
|
1213
|
+
if (config.customAccessControl) {
|
|
1214
|
+
return config.customAccessControl(userId, filePath);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return true;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Validate file type for bucket
|
|
1222
|
+
*/
|
|
1223
|
+
export function isValidFileType(bucketName: string, contentType: string): boolean {
|
|
1224
|
+
const config = getBucketConfig(bucketName);
|
|
1225
|
+
if (!config || !config.allowedFileTypes) return true;
|
|
1226
|
+
|
|
1227
|
+
return config.allowedFileTypes.includes(contentType);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Check if file size is within bucket limits
|
|
1232
|
+
*/
|
|
1233
|
+
export function isValidFileSize(bucketName: string, fileSize: number): boolean {
|
|
1234
|
+
const config = getBucketConfig(bucketName);
|
|
1235
|
+
if (!config || !config.maxFileSize) return true;
|
|
1236
|
+
|
|
1237
|
+
return fileSize <= config.maxFileSize;
|
|
1238
|
+
}`;
|
|
1239
|
+
|
|
1240
|
+
await fs.writeFile(bucketConfigPath, bucketConfigContent);
|
|
1241
|
+
console.log(chalk.green("✓ lib/bucket-config.ts créé"));
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// 2. Créer lib/storage.ts
|
|
1245
|
+
const storagePath = path.join(libDir, "storage.ts");
|
|
1246
|
+
if (!fs.existsSync(storagePath) || force) {
|
|
1247
|
+
const storageContent = `/**
|
|
1248
|
+
* Build storage proxy URL for files in Supabase buckets
|
|
1249
|
+
*
|
|
1250
|
+
* @param bucket - The bucket name (e.g., "avatar", "app")
|
|
1251
|
+
* @param path - The file path within the bucket
|
|
1252
|
+
* @returns Proxied URL (e.g., "/api/storage/avatar/user_128_123456.webp")
|
|
1253
|
+
*/
|
|
1254
|
+
export function buildStorageUrl(bucket: string, path: string): string {
|
|
1255
|
+
// Remove leading slash if present
|
|
1256
|
+
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
1257
|
+
|
|
1258
|
+
// Remove bucket prefix from path if present (e.g., "avatar/file.jpg" -> "file.jpg")
|
|
1259
|
+
const pathWithoutBucket = cleanPath.startsWith(bucket + "/")
|
|
1260
|
+
? cleanPath.slice(bucket.length + 1)
|
|
1261
|
+
: cleanPath;
|
|
1262
|
+
|
|
1263
|
+
return \`/api/storage/\${bucket}/\${pathWithoutBucket}\`;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Extract bucket and path from a storage URL
|
|
1268
|
+
*
|
|
1269
|
+
* @param url - Storage URL (can be proxied URL or Supabase public URL)
|
|
1270
|
+
* @returns Object with bucket and path, or null if not a valid storage URL
|
|
1271
|
+
*/
|
|
1272
|
+
export function parseStorageUrl(url: string): { bucket: string; path: string } | null {
|
|
1273
|
+
// Handle proxy URLs like "/api/storage/avatar/file.jpg"
|
|
1274
|
+
const proxyMatch = url.match(/^\\/api\\/storage\\/([^\\/]+)\\/(.+)$/);
|
|
1275
|
+
if (proxyMatch) {
|
|
1276
|
+
return {
|
|
1277
|
+
bucket: proxyMatch[1],
|
|
1278
|
+
path: proxyMatch[2]
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Handle Supabase public URLs
|
|
1283
|
+
const supabaseMatch = url.match(/\\/storage\\/v1\\/object\\/public\\/([^\\/]+)\\/(.+)$/);
|
|
1284
|
+
if (supabaseMatch) {
|
|
1285
|
+
return {
|
|
1286
|
+
bucket: supabaseMatch[1],
|
|
1287
|
+
path: supabaseMatch[2]
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Convert a Supabase storage path to proxy URL
|
|
1296
|
+
*
|
|
1297
|
+
* @param storagePath - Path like "avatar/file.jpg" or "app/user/file.pdf"
|
|
1298
|
+
* @returns Proxied URL
|
|
1299
|
+
*/
|
|
1300
|
+
export function storagePathToProxyUrl(storagePath: string): string {
|
|
1301
|
+
const parts = storagePath.split("/");
|
|
1302
|
+
if (parts.length < 2) {
|
|
1303
|
+
throw new Error("Invalid storage path format");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const bucket = parts[0];
|
|
1307
|
+
const path = parts.slice(1).join("/");
|
|
1308
|
+
|
|
1309
|
+
return buildStorageUrl(bucket, path);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* List of public buckets that don't require authentication
|
|
1314
|
+
*/
|
|
1315
|
+
export const PUBLIC_BUCKETS = ["avatar"];
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* List of private buckets that require authentication
|
|
1319
|
+
*/
|
|
1320
|
+
export const PRIVATE_BUCKETS = ["app"];
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Check if a bucket is public
|
|
1324
|
+
*/
|
|
1325
|
+
export function isPublicBucket(bucket: string): boolean {
|
|
1326
|
+
return PUBLIC_BUCKETS.includes(bucket);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Check if a bucket is private
|
|
1331
|
+
*/
|
|
1332
|
+
export function isPrivateBucket(bucket: string): boolean {
|
|
1333
|
+
return PRIVATE_BUCKETS.includes(bucket);
|
|
1334
|
+
}`;
|
|
1335
|
+
|
|
1336
|
+
await fs.writeFile(storagePath, storageContent);
|
|
1337
|
+
console.log(chalk.green("✓ lib/storage.ts créé"));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// 3. Créer app/api/storage/[bucket]/[...path]/route.ts
|
|
1341
|
+
const apiStorageDir = path.join(targetDir, "app", "api", "storage", "[bucket]", "[...path]");
|
|
1342
|
+
await fs.ensureDir(apiStorageDir);
|
|
1343
|
+
|
|
1344
|
+
const routePath = path.join(apiStorageDir, "route.ts");
|
|
1345
|
+
if (!fs.existsSync(routePath) || force) {
|
|
1346
|
+
const routeContent = `import { NextRequest, NextResponse } from "next/server";
|
|
1347
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
1348
|
+
import { getBucketConfig, hasFileAccess } from "@/lib/bucket-config";
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* GET /api/storage/[bucket]/[...path]
|
|
1352
|
+
* Proxy for Supabase Storage files with clean URLs and access control
|
|
1353
|
+
*
|
|
1354
|
+
* Examples:
|
|
1355
|
+
* - /api/storage/avatar/user_128_123456.webp
|
|
1356
|
+
* - /api/storage/app/user/documents/file.pdf
|
|
1357
|
+
*/
|
|
1358
|
+
export async function GET(
|
|
1359
|
+
request: NextRequest,
|
|
1360
|
+
props: { params: Promise<{ bucket: string; path: string[] }> }
|
|
1361
|
+
) {
|
|
1362
|
+
try {
|
|
1363
|
+
const { bucket, path } = await props.params;
|
|
1364
|
+
const filePath = path.join("/");
|
|
1365
|
+
|
|
1366
|
+
// Check if bucket exists in our configuration
|
|
1367
|
+
const bucketConfig = getBucketConfig(bucket);
|
|
1368
|
+
if (!bucketConfig) {
|
|
1369
|
+
return new NextResponse("Bucket not allowed", { status: 403 });
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const supabase = await getSupabaseServerClient();
|
|
1373
|
+
let userId: string | null = null;
|
|
1374
|
+
|
|
1375
|
+
// Get user for private buckets or custom access control
|
|
1376
|
+
if (!bucketConfig.isPublic) {
|
|
1377
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
1378
|
+
|
|
1379
|
+
if (authError || !user) {
|
|
1380
|
+
return new NextResponse("Unauthorized", { status: 401 });
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
userId = user.id;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Check file access permissions
|
|
1387
|
+
if (!hasFileAccess(bucket, userId || "", filePath)) {
|
|
1388
|
+
return new NextResponse("Forbidden - Access denied to this file", { status: 403 });
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Get file from Supabase Storage
|
|
1392
|
+
const { data: file, error } = await supabase.storage
|
|
1393
|
+
.from(bucket)
|
|
1394
|
+
.download(filePath);
|
|
1395
|
+
|
|
1396
|
+
if (error) {
|
|
1397
|
+
console.error("Storage download error:", error);
|
|
1398
|
+
if (error.message.includes("not found")) {
|
|
1399
|
+
return new NextResponse("File not found", { status: 404 });
|
|
1400
|
+
}
|
|
1401
|
+
return new NextResponse("Storage error", { status: 500 });
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (!file) {
|
|
1405
|
+
return new NextResponse("File not found", { status: 404 });
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Convert blob to array buffer
|
|
1409
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
1410
|
+
|
|
1411
|
+
// Determine content type from file extension
|
|
1412
|
+
const getContentType = (filename: string): string => {
|
|
1413
|
+
const ext = filename.toLowerCase().split(".").pop();
|
|
1414
|
+
const mimeTypes: Record<string, string> = {
|
|
1415
|
+
// Images
|
|
1416
|
+
jpg: "image/jpeg",
|
|
1417
|
+
jpeg: "image/jpeg",
|
|
1418
|
+
png: "image/png",
|
|
1419
|
+
gif: "image/gif",
|
|
1420
|
+
webp: "image/webp",
|
|
1421
|
+
svg: "image/svg+xml",
|
|
1422
|
+
// Documents
|
|
1423
|
+
pdf: "application/pdf",
|
|
1424
|
+
doc: "application/msword",
|
|
1425
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1426
|
+
xls: "application/vnd.ms-excel",
|
|
1427
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1428
|
+
// Text
|
|
1429
|
+
txt: "text/plain",
|
|
1430
|
+
csv: "text/csv",
|
|
1431
|
+
// Videos
|
|
1432
|
+
mp4: "video/mp4",
|
|
1433
|
+
avi: "video/x-msvideo",
|
|
1434
|
+
mov: "video/quicktime",
|
|
1435
|
+
// Audio
|
|
1436
|
+
mp3: "audio/mpeg",
|
|
1437
|
+
wav: "audio/wav",
|
|
1438
|
+
// Archives
|
|
1439
|
+
zip: "application/zip",
|
|
1440
|
+
rar: "application/x-rar-compressed",
|
|
1441
|
+
};
|
|
1442
|
+
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
const contentType = getContentType(filePath);
|
|
1446
|
+
|
|
1447
|
+
// Create response with proper headers
|
|
1448
|
+
const response = new NextResponse(arrayBuffer, {
|
|
1449
|
+
status: 200,
|
|
1450
|
+
headers: {
|
|
1451
|
+
"Content-Type": contentType,
|
|
1452
|
+
"Cache-Control": "public, max-age=31536000, immutable", // Cache for 1 year
|
|
1453
|
+
"Content-Length": arrayBuffer.byteLength.toString(),
|
|
1454
|
+
},
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
return response;
|
|
1458
|
+
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
console.error("Storage proxy error:", error);
|
|
1461
|
+
return new NextResponse("Internal server error", { status: 500 });
|
|
1462
|
+
}
|
|
1463
|
+
}`;
|
|
1464
|
+
|
|
1465
|
+
await fs.writeFile(routePath, routeContent);
|
|
1466
|
+
console.log(chalk.green("✓ app/api/storage/[bucket]/[...path]/route.ts créé"));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
console.log(chalk.green("✓ Système de proxy storage configuré"));
|
|
1470
|
+
}
|