@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.
@@ -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-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() {
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;AA8BnD,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,2CAsUrE"}
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.23",
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
+ }
@@ -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 .gitignore et .env.local.example
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
- // 6. Créer la structure Supabase avec migrations
85
+ // 7. Créer la structure Supabase avec migrations
83
86
  await createSupabaseStructure(targetDir, force);
84
87
 
85
- // 7. Ajouter les scripts NPM
88
+ // 8. Ajouter les scripts NPM
86
89
  await addScriptsToPackageJson(targetDir);
87
90
 
88
- // 8. Enregistrer les modules sélectionnés
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: targetIsInMonorepo
1066
- ? "pnpm exec lastbrain"
1067
- : "node node_modules/@lastbrain/app/dist/cli.js",
1068
- "build:modules": targetIsInMonorepo
1069
- ? "pnpm exec lastbrain module:build"
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
+ }