@lastbrain/app 2.0.1 → 2.0.2
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/config/version.d.ts +7 -0
- package/dist/config/version.d.ts.map +1 -0
- package/dist/config/version.js +25 -0
- package/dist/src/__tests__/module-registry.test.d.ts +2 -0
- package/dist/src/__tests__/module-registry.test.d.ts.map +1 -0
- package/dist/src/__tests__/module-registry.test.js +53 -0
- package/dist/src/app-shell/(admin)/layout.d.ts +4 -0
- package/dist/src/app-shell/(admin)/layout.d.ts.map +1 -0
- package/dist/src/app-shell/(admin)/layout.js +5 -0
- package/dist/src/app-shell/(auth)/layout.d.ts +4 -0
- package/dist/src/app-shell/(auth)/layout.d.ts.map +1 -0
- package/dist/src/app-shell/(auth)/layout.js +5 -0
- package/dist/src/app-shell/(public)/page.d.ts +2 -0
- package/dist/src/app-shell/(public)/page.d.ts.map +1 -0
- package/dist/src/app-shell/(public)/page.js +5 -0
- package/dist/src/app-shell/layout.d.ts +3 -0
- package/dist/src/app-shell/layout.d.ts.map +1 -0
- package/dist/src/app-shell/layout.js +3 -0
- package/dist/src/app-shell/not-found.d.ts +2 -0
- package/dist/src/app-shell/not-found.d.ts.map +1 -0
- package/dist/src/app-shell/not-found.js +10 -0
- package/dist/src/auth/authHelpers.d.ts +7 -0
- package/dist/src/auth/authHelpers.d.ts.map +1 -0
- package/dist/src/auth/authHelpers.js +19 -0
- package/dist/src/auth/useAuthSession.d.ts +7 -0
- package/dist/src/auth/useAuthSession.d.ts.map +1 -0
- package/dist/src/auth/useAuthSession.js +49 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +143 -0
- package/dist/src/components/NotificationContainer.d.ts +2 -0
- package/dist/src/components/NotificationContainer.d.ts.map +1 -0
- package/dist/src/components/NotificationContainer.js +8 -0
- package/dist/src/hooks/useNotifications.d.ts +30 -0
- package/dist/src/hooks/useNotifications.d.ts.map +1 -0
- package/dist/src/hooks/useNotifications.js +165 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +22 -0
- package/dist/src/layouts/AdminLayout.d.ts +4 -0
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayout.js +4 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +10 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.js +62 -0
- package/dist/src/layouts/AppProviders.d.ts +27 -0
- package/dist/src/layouts/AppProviders.d.ts.map +1 -0
- package/dist/src/layouts/AppProviders.js +48 -0
- package/dist/src/layouts/AuthLayout.d.ts +4 -0
- package/dist/src/layouts/AuthLayout.d.ts.map +1 -0
- package/dist/src/layouts/AuthLayout.js +4 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +12 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.js +60 -0
- package/dist/src/layouts/PublicLayout.d.ts +8 -0
- package/dist/src/layouts/PublicLayout.d.ts.map +1 -0
- package/dist/src/layouts/PublicLayout.js +6 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +9 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.js +60 -0
- package/dist/src/layouts/RootLayout.d.ts +6 -0
- package/dist/src/layouts/RootLayout.d.ts.map +1 -0
- package/dist/src/layouts/RootLayout.js +9 -0
- package/dist/src/modules/module-loader.d.ts +5 -0
- package/dist/src/modules/module-loader.d.ts.map +1 -0
- package/dist/src/modules/module-loader.js +10 -0
- package/dist/src/scripts/db-init.d.ts +2 -0
- package/dist/src/scripts/db-init.d.ts.map +1 -0
- package/dist/src/scripts/db-init.js +300 -0
- package/dist/src/scripts/db-migrations-sync.d.ts +2 -0
- package/dist/src/scripts/db-migrations-sync.d.ts.map +1 -0
- package/dist/src/scripts/db-migrations-sync.js +84 -0
- package/dist/src/scripts/dev-sync.d.ts +2 -0
- package/dist/src/scripts/dev-sync.d.ts.map +1 -0
- package/dist/src/scripts/dev-sync.js +194 -0
- package/dist/src/scripts/init-app.d.ts +12 -0
- package/dist/src/scripts/init-app.d.ts.map +1 -0
- package/dist/src/scripts/init-app.js +2175 -0
- package/dist/src/scripts/module-add.d.ts +2 -0
- package/dist/src/scripts/module-add.d.ts.map +1 -0
- package/dist/src/scripts/module-add.js +232 -0
- package/dist/src/scripts/module-build.d.ts +2 -0
- package/dist/src/scripts/module-build.d.ts.map +1 -0
- package/dist/src/scripts/module-build.js +1280 -0
- package/dist/src/scripts/module-create.d.ts +28 -0
- package/dist/src/scripts/module-create.d.ts.map +1 -0
- package/dist/src/scripts/module-create.js +1429 -0
- package/dist/src/scripts/module-delete.d.ts +6 -0
- package/dist/src/scripts/module-delete.d.ts.map +1 -0
- package/dist/src/scripts/module-delete.js +147 -0
- package/dist/src/scripts/module-list.d.ts +2 -0
- package/dist/src/scripts/module-list.d.ts.map +1 -0
- package/dist/src/scripts/module-list.js +61 -0
- package/dist/src/scripts/module-remove.d.ts +2 -0
- package/dist/src/scripts/module-remove.d.ts.map +1 -0
- package/dist/src/scripts/module-remove.js +311 -0
- package/dist/src/scripts/readme-build.d.ts +2 -0
- package/dist/src/scripts/readme-build.d.ts.map +1 -0
- package/dist/src/scripts/readme-build.js +39 -0
- package/dist/src/scripts/script-runner.d.ts +5 -0
- package/dist/src/scripts/script-runner.d.ts.map +1 -0
- package/dist/src/scripts/script-runner.js +25 -0
- package/dist/src/templates/AuthGuidePage.d.ts +2 -0
- package/dist/src/templates/AuthGuidePage.d.ts.map +1 -0
- package/dist/src/templates/AuthGuidePage.js +9 -0
- package/dist/src/templates/DefaultDoc.d.ts +2 -0
- package/dist/src/templates/DefaultDoc.d.ts.map +1 -0
- package/dist/src/templates/DefaultDoc.js +240 -0
- package/dist/src/templates/DocPage.d.ts +17 -0
- package/dist/src/templates/DocPage.d.ts.map +1 -0
- package/dist/src/templates/DocPage.js +193 -0
- package/dist/src/templates/DocsPageWithModules.d.ts +2 -0
- package/dist/src/templates/DocsPageWithModules.d.ts.map +1 -0
- package/dist/src/templates/DocsPageWithModules.js +8 -0
- package/dist/src/templates/MigrationsGuidePage.d.ts +2 -0
- package/dist/src/templates/MigrationsGuidePage.d.ts.map +1 -0
- package/dist/src/templates/MigrationsGuidePage.js +11 -0
- package/dist/src/templates/ModuleGuidePage.d.ts +2 -0
- package/dist/src/templates/ModuleGuidePage.d.ts.map +1 -0
- package/dist/src/templates/ModuleGuidePage.js +14 -0
- package/dist/src/templates/SimpleDocPage.d.ts +2 -0
- package/dist/src/templates/SimpleDocPage.d.ts.map +1 -0
- package/dist/src/templates/SimpleDocPage.js +28 -0
- package/dist/src/templates/SimpleHomePage.d.ts +6 -0
- package/dist/src/templates/SimpleHomePage.d.ts.map +1 -0
- package/dist/src/templates/SimpleHomePage.js +7 -0
- package/dist/src/types/menu.d.ts +23 -0
- package/dist/src/types/menu.d.ts.map +1 -0
- package/dist/src/types/menu.js +1 -0
- package/package.json +4 -5
- package/src/scripts/init-app.ts +7 -76
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { AVAILABLE_MODULES } from "@lastbrain/core/config/modules";
|
|
8
|
+
import { PACKAGE_VERSIONS } from "../../config/version.js";
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
export async function initApp(options) {
|
|
12
|
+
const { targetDir, force, projectName, useHeroUI, interactive = true, } = options;
|
|
13
|
+
let { withAuth = false, isMonorepoProject = false } = options;
|
|
14
|
+
console.log(chalk.blue("\n🚀 LastBrain App Init\n"));
|
|
15
|
+
console.log(chalk.gray(`📁 Dossier cible: ${targetDir}`));
|
|
16
|
+
if (useHeroUI) {
|
|
17
|
+
console.log(chalk.magenta(`🎨 Mode: HeroUI\n`));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(chalk.gray(`🎨 Mode: Tailwind CSS only\n`));
|
|
21
|
+
}
|
|
22
|
+
// Sélection interactive des modules
|
|
23
|
+
const selectedModules = [];
|
|
24
|
+
if (interactive && !withAuth) {
|
|
25
|
+
console.log(chalk.blue("📦 Sélection des modules:\n"));
|
|
26
|
+
const answers = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: "checkbox",
|
|
29
|
+
name: "modules",
|
|
30
|
+
message: "Quels modules voulez-vous installer ?",
|
|
31
|
+
choices: AVAILABLE_MODULES.map((module) => ({
|
|
32
|
+
name: `${module.emoji} ${module.name.charAt(0).toUpperCase() + module.name.slice(1)} - ${module.description}`,
|
|
33
|
+
value: module.name,
|
|
34
|
+
checked: false,
|
|
35
|
+
})),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "rawlist",
|
|
39
|
+
name: "projectType",
|
|
40
|
+
message: "Type de projet ?",
|
|
41
|
+
choices: [
|
|
42
|
+
{
|
|
43
|
+
name: "📦 Indépendant (créer une nouvelle BDD Supabase)",
|
|
44
|
+
value: "independent",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "🏢 Monorepo (utiliser la BDD existante)",
|
|
48
|
+
value: "monorepo",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
default: "independent",
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
selectedModules.push(...answers.modules);
|
|
55
|
+
withAuth = selectedModules.includes("auth");
|
|
56
|
+
isMonorepoProject = answers.projectType === "monorepo";
|
|
57
|
+
if (isMonorepoProject) {
|
|
58
|
+
console.log(chalk.cyan("\n💡 Mode monorepo activé:"), chalk.gray("Votre app utilisera la base de données centralisée du monorepo."));
|
|
59
|
+
console.log(chalk.gray(" Aucune configuration Supabase locale ne sera créée.\n"));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log(chalk.cyan("\n💡 Mode projet indépendant activé:"), chalk.gray("Votre app aura sa propre instance Supabase locale."));
|
|
63
|
+
console.log(chalk.gray(" Configuration complète sera créée.\n"));
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
else if (!interactive) {
|
|
68
|
+
// En mode non-interactif, installer auth et ai par défaut
|
|
69
|
+
selectedModules.push("auth", "ai");
|
|
70
|
+
withAuth = true;
|
|
71
|
+
isMonorepoProject = false;
|
|
72
|
+
console.log(chalk.blue("📦 Modules installés par défaut : Authentication, AI Generation\n"));
|
|
73
|
+
}
|
|
74
|
+
// Créer le dossier s'il n'existe pas
|
|
75
|
+
await fs.ensureDir(targetDir);
|
|
76
|
+
// 1. Vérifier/créer package.json
|
|
77
|
+
await ensurePackageJson(targetDir, projectName);
|
|
78
|
+
// 2. Installer les dépendances
|
|
79
|
+
await addDependencies(targetDir, useHeroUI, withAuth, selectedModules);
|
|
80
|
+
// 3. Créer la structure Next.js
|
|
81
|
+
await createNextStructure(targetDir, force, useHeroUI, withAuth);
|
|
82
|
+
// 4. Créer les fichiers de configuration
|
|
83
|
+
await createConfigFiles(targetDir, force, useHeroUI, projectName);
|
|
84
|
+
// 5. Créer le système de proxy storage
|
|
85
|
+
await createStorageProxy(targetDir, force);
|
|
86
|
+
// 6. Créer .gitignore et .env.local.example
|
|
87
|
+
await createGitIgnore(targetDir, force);
|
|
88
|
+
await createEnvExample(targetDir, force);
|
|
89
|
+
await createEnvLocal(targetDir, force, isMonorepoProject);
|
|
90
|
+
await createEnvProd(targetDir, force);
|
|
91
|
+
// 7. Créer la structure Supabase avec migrations (seulement pour projets indépendants)
|
|
92
|
+
if (!isMonorepoProject) {
|
|
93
|
+
console.log(chalk.blue("🗄️ Création de la structure Supabase locale...\n"));
|
|
94
|
+
await createSupabaseStructure(targetDir, force);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(chalk.yellow("⏭️ Projet monorepo détecté - pas de configuration Supabase locale requise\n"));
|
|
98
|
+
}
|
|
99
|
+
// 8. Ajouter les scripts NPM
|
|
100
|
+
await addScriptsToPackageJson(targetDir);
|
|
101
|
+
// 9. Enregistrer les modules sélectionnés
|
|
102
|
+
if (withAuth && !selectedModules.includes("auth")) {
|
|
103
|
+
selectedModules.push("auth");
|
|
104
|
+
}
|
|
105
|
+
if (selectedModules.length > 0) {
|
|
106
|
+
await saveModulesConfig(targetDir, selectedModules, withAuth);
|
|
107
|
+
}
|
|
108
|
+
console.log(chalk.green("\n✅ Application LastBrain initialisée avec succès!\n"));
|
|
109
|
+
const relativePath = path.relative(process.cwd(), targetDir);
|
|
110
|
+
// Demander si l'utilisateur veut lancer l'installation et le dev server
|
|
111
|
+
const { launchNow } = await inquirer.prompt([
|
|
112
|
+
{
|
|
113
|
+
type: "confirm",
|
|
114
|
+
name: "launchNow",
|
|
115
|
+
message: "Voulez-vous installer les dépendances et lancer le serveur de développement maintenant ?",
|
|
116
|
+
default: true,
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
if (launchNow) {
|
|
120
|
+
console.log(chalk.yellow("\n📦 Installation des dépendances...\n"));
|
|
121
|
+
try {
|
|
122
|
+
execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
|
|
123
|
+
console.log(chalk.green("\n✓ Dépendances installées\n"));
|
|
124
|
+
console.log(chalk.yellow("🔧 Génération des routes des modules...\n"));
|
|
125
|
+
execSync("pnpm build:modules", { cwd: targetDir, stdio: "inherit" });
|
|
126
|
+
console.log(chalk.green("\n✓ Routes des modules générées\n"));
|
|
127
|
+
console.log(chalk.yellow("📜 Synchronisation des migrations des modules...\n"));
|
|
128
|
+
try {
|
|
129
|
+
execSync("pnpm db:migrations:sync", {
|
|
130
|
+
cwd: targetDir,
|
|
131
|
+
stdio: "inherit",
|
|
132
|
+
});
|
|
133
|
+
console.log(chalk.green("\n✓ Migrations synchronisées\n"));
|
|
134
|
+
}
|
|
135
|
+
catch (_error) {
|
|
136
|
+
console.log(chalk.yellow("\n⚠️ Erreur de synchronisation des migrations\n"));
|
|
137
|
+
}
|
|
138
|
+
// Ne pas initialiser la BDD en mode monorepo
|
|
139
|
+
if (!isMonorepoProject) {
|
|
140
|
+
console.log(chalk.yellow("🗄️ Initialisation de la base de données...\n"));
|
|
141
|
+
try {
|
|
142
|
+
execSync("pnpm db:init", { cwd: targetDir, stdio: "inherit" });
|
|
143
|
+
console.log(chalk.green("\n✓ Base de données initialisée\n"));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
console.log(chalk.yellow("\n⚠️ Erreur d'initialisation de la DB (normal si Supabase pas configuré)\n"));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(chalk.cyan("⏭️ Mode monorepo détecté - BDD centralisée, pas d'initialisation locale requise\n"));
|
|
151
|
+
}
|
|
152
|
+
// Détecter le port (par défaut 3000 pour Next.js)
|
|
153
|
+
const port = 3000;
|
|
154
|
+
const url = `http://127.0.0.1:${port}`;
|
|
155
|
+
console.log(chalk.yellow("🚀 Lancement du serveur de développement...\n"));
|
|
156
|
+
console.log(chalk.cyan(`📱 Ouvrez votre navigateur sur : ${url}\n`));
|
|
157
|
+
console.log(chalk.blue("\n📋 Prochaines étapes après le démarrage :"));
|
|
158
|
+
console.log(chalk.white(" 1. Cliquez sur 'Get Started' sur la page d'accueil"));
|
|
159
|
+
if (isMonorepoProject) {
|
|
160
|
+
console.log(chalk.white(" 2. La BDD est centralisée dans le monorepo\n"));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(chalk.white(" 2. Lancez Docker Desktop"));
|
|
164
|
+
console.log(chalk.white(" 3. Installez Supabase CLI si nécessaire : brew install supabase/tap/supabase"));
|
|
165
|
+
console.log(chalk.white(" 4. Exécutez : pnpm db:init"));
|
|
166
|
+
console.log(chalk.white(" 5. Rechargez la page\n"));
|
|
167
|
+
}
|
|
168
|
+
// Ouvrir le navigateur
|
|
169
|
+
const openCommand = process.platform === "darwin"
|
|
170
|
+
? "open"
|
|
171
|
+
: process.platform === "win32"
|
|
172
|
+
? "start"
|
|
173
|
+
: "xdg-open";
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
try {
|
|
176
|
+
execSync(`${openCommand} ${url}`, { stdio: "ignore" });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Ignorer les erreurs d'ouverture du navigateur
|
|
180
|
+
}
|
|
181
|
+
}, 2000);
|
|
182
|
+
// Lancer pnpm dev
|
|
183
|
+
execSync("pnpm dev:local", { cwd: targetDir, stdio: "inherit" });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
console.error(chalk.red("\n❌ Erreur lors du lancement\n"));
|
|
187
|
+
console.log(chalk.cyan("\nVous pouvez lancer manuellement avec :"));
|
|
188
|
+
console.log(chalk.white(` cd ${relativePath}`));
|
|
189
|
+
console.log(chalk.white(" pnpm install"));
|
|
190
|
+
console.log(chalk.white(" pnpm build:modules"));
|
|
191
|
+
console.log(chalk.white(" pnpm db:migrations:sync"));
|
|
192
|
+
if (!isMonorepoProject) {
|
|
193
|
+
console.log(chalk.white(" pnpm db:init"));
|
|
194
|
+
}
|
|
195
|
+
console.log(chalk.white(" pnpm dev:local (ou pnpm dev:prod)\n"));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.log(chalk.cyan("\n📋 Prochaines étapes:"));
|
|
200
|
+
console.log(chalk.white(" 1. cd " + relativePath));
|
|
201
|
+
console.log(chalk.white(" 2. pnpm install (installer les dépendances)"));
|
|
202
|
+
console.log(chalk.white(" 3. pnpm build:modules (générer les routes des modules)"));
|
|
203
|
+
console.log(chalk.white(" 4. pnpm db:migrations:sync (synchroniser les migrations)"));
|
|
204
|
+
console.log(chalk.white(" 5. pnpm db:init (initialiser la base de données)"));
|
|
205
|
+
console.log(chalk.white(" 6. pnpm dev:local (ou pnpm dev:prod)\n"));
|
|
206
|
+
console.log(chalk.cyan("\n💡 Scripts disponibles:"));
|
|
207
|
+
console.log(chalk.white(" • pnpm dev:local - Lance avec .env.local"));
|
|
208
|
+
console.log(chalk.white(" • pnpm dev:prod - Lance avec .env.prod"));
|
|
209
|
+
console.log(chalk.white(" • pnpm dev - Lance avec .env actuel\n"));
|
|
210
|
+
console.log(chalk.gray("Prérequis pour Supabase :"));
|
|
211
|
+
console.log(chalk.white(" - Docker Desktop installé et lancé"));
|
|
212
|
+
console.log(chalk.white(" - Supabase CLI : brew install supabase/tap/supabase\n"));
|
|
213
|
+
// Afficher la commande cd pour faciliter la copie
|
|
214
|
+
console.log(chalk.gray("Pour vous déplacer dans le projet :"));
|
|
215
|
+
console.log(chalk.cyan(` cd ${relativePath}\n`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function ensurePackageJson(targetDir, projectName) {
|
|
219
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
220
|
+
if (!fs.existsSync(pkgPath)) {
|
|
221
|
+
console.log(chalk.yellow("📦 Création de package.json..."));
|
|
222
|
+
const pkg = {
|
|
223
|
+
name: projectName || path.basename(targetDir),
|
|
224
|
+
version: "0.1.0",
|
|
225
|
+
private: true,
|
|
226
|
+
type: "module",
|
|
227
|
+
scripts: {},
|
|
228
|
+
dependencies: {},
|
|
229
|
+
devDependencies: {},
|
|
230
|
+
};
|
|
231
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
232
|
+
console.log(chalk.green("✓ package.json créé"));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.log(chalk.green("✓ package.json existe"));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Récupère les versions actuelles des packages LastBrain depuis npm ou le monorepo
|
|
240
|
+
*/
|
|
241
|
+
function getLastBrainVersions(targetDir) {
|
|
242
|
+
const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
|
|
243
|
+
fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
|
|
244
|
+
if (targetIsInMonorepo) {
|
|
245
|
+
// Dans le monorepo, utiliser workspace:*
|
|
246
|
+
return {
|
|
247
|
+
app: "workspace:*",
|
|
248
|
+
core: "workspace:*",
|
|
249
|
+
ui: "workspace:*",
|
|
250
|
+
moduleAuth: "workspace:*",
|
|
251
|
+
moduleAi: "workspace:*",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Hors monorepo, utiliser les versions publiées
|
|
255
|
+
return {
|
|
256
|
+
app: PACKAGE_VERSIONS["@lastbrain/app"],
|
|
257
|
+
core: PACKAGE_VERSIONS["@lastbrain/core"],
|
|
258
|
+
ui: PACKAGE_VERSIONS["@lastbrain/ui"],
|
|
259
|
+
moduleAuth: PACKAGE_VERSIONS["@lastbrain/module-auth"],
|
|
260
|
+
moduleAi: PACKAGE_VERSIONS["@lastbrain/module-ai"],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function addDependencies(targetDir, useHeroUI, withAuth, selectedModules = []) {
|
|
264
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
265
|
+
const pkg = await fs.readJson(pkgPath);
|
|
266
|
+
console.log(chalk.yellow("\n📦 Configuration des dépendances..."));
|
|
267
|
+
const versions = getLastBrainVersions(targetDir);
|
|
268
|
+
// Dependencies
|
|
269
|
+
const requiredDeps = {
|
|
270
|
+
next: "^15.0.3",
|
|
271
|
+
react: "^18.3.1",
|
|
272
|
+
"react-dom": "^18.3.1",
|
|
273
|
+
"@lastbrain/app": versions.app,
|
|
274
|
+
"@lastbrain/core": versions.core,
|
|
275
|
+
"@lastbrain/ui": versions.ui,
|
|
276
|
+
"next-themes": "^0.4.6",
|
|
277
|
+
pino: "^10.1.0",
|
|
278
|
+
"pino-pretty": "^13.1.2",
|
|
279
|
+
};
|
|
280
|
+
// Ajouter module-auth si demandé
|
|
281
|
+
if (withAuth) {
|
|
282
|
+
requiredDeps["@lastbrain/module-auth"] = versions.moduleAuth;
|
|
283
|
+
}
|
|
284
|
+
// Ajouter les autres modules sélectionnés
|
|
285
|
+
for (const moduleName of selectedModules) {
|
|
286
|
+
const moduleInfo = AVAILABLE_MODULES.find((m) => m.name === moduleName);
|
|
287
|
+
if (moduleInfo && moduleInfo.package !== "@lastbrain/module-auth") {
|
|
288
|
+
// Utiliser les vraies versions au lieu de "latest"
|
|
289
|
+
if (moduleInfo.package === "@lastbrain/module-ai") {
|
|
290
|
+
requiredDeps[moduleInfo.package] = versions.moduleAi;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Pour les futurs modules, utiliser "latest" en attendant
|
|
294
|
+
requiredDeps[moduleInfo.package] = "latest";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Ajouter les dépendances HeroUI si nécessaire
|
|
299
|
+
if (useHeroUI) {
|
|
300
|
+
// Tous les packages HeroUI nécessaires (car @lastbrain/ui les ré-exporte)
|
|
301
|
+
requiredDeps["@heroui/system"] = "^2.4.23";
|
|
302
|
+
requiredDeps["@heroui/theme"] = "^2.4.23";
|
|
303
|
+
requiredDeps["@heroui/accordion"] = "^2.2.24";
|
|
304
|
+
requiredDeps["@heroui/alert"] = "^2.2.27";
|
|
305
|
+
requiredDeps["@heroui/autocomplete"] = "^2.3.29";
|
|
306
|
+
requiredDeps["@heroui/avatar"] = "^2.2.22";
|
|
307
|
+
requiredDeps["@heroui/badge"] = "^2.2.17";
|
|
308
|
+
requiredDeps["@heroui/breadcrumbs"] = "^2.2.22";
|
|
309
|
+
requiredDeps["@heroui/button"] = "^2.2.27";
|
|
310
|
+
requiredDeps["@heroui/card"] = "^2.2.25";
|
|
311
|
+
requiredDeps["@heroui/checkbox"] = "^2.3.27";
|
|
312
|
+
requiredDeps["@heroui/chip"] = "^2.2.22";
|
|
313
|
+
requiredDeps["@heroui/code"] = "^2.2.21";
|
|
314
|
+
requiredDeps["@heroui/divider"] = "^2.2.20";
|
|
315
|
+
requiredDeps["@heroui/drawer"] = "^2.2.24";
|
|
316
|
+
requiredDeps["@heroui/dropdown"] = "^2.3.27";
|
|
317
|
+
requiredDeps["@heroui/form"] = "^2.1.27";
|
|
318
|
+
requiredDeps["@heroui/input"] = "^2.4.28";
|
|
319
|
+
requiredDeps["@heroui/link"] = "^2.2.23";
|
|
320
|
+
requiredDeps["@heroui/listbox"] = "^2.3.26";
|
|
321
|
+
requiredDeps["@heroui/modal"] = "^2.2.24";
|
|
322
|
+
requiredDeps["@heroui/navbar"] = "^2.2.25";
|
|
323
|
+
requiredDeps["@heroui/pagination"] = "^2.2.24";
|
|
324
|
+
requiredDeps["@heroui/popover"] = "^2.3.27";
|
|
325
|
+
requiredDeps["@heroui/progress"] = "^2.2.22";
|
|
326
|
+
requiredDeps["@heroui/radio"] = "^2.3.27";
|
|
327
|
+
requiredDeps["@heroui/scroll-shadow"] = "^2.3.18";
|
|
328
|
+
requiredDeps["@heroui/select"] = "^2.4.28";
|
|
329
|
+
requiredDeps["@heroui/skeleton"] = "^2.2.17";
|
|
330
|
+
requiredDeps["@heroui/snippet"] = "^2.2.28";
|
|
331
|
+
requiredDeps["@heroui/spacer"] = "^2.2.21";
|
|
332
|
+
requiredDeps["@heroui/spinner"] = "^2.2.24";
|
|
333
|
+
requiredDeps["@heroui/switch"] = "^2.2.24";
|
|
334
|
+
requiredDeps["@heroui/table"] = "^2.2.27";
|
|
335
|
+
requiredDeps["@heroui/tabs"] = "^2.2.24";
|
|
336
|
+
requiredDeps["@heroui/toast"] = "^2.0.17";
|
|
337
|
+
requiredDeps["@heroui/tooltip"] = "^2.2.24";
|
|
338
|
+
requiredDeps["@heroui/user"] = "^2.2.22";
|
|
339
|
+
// Dependencies
|
|
340
|
+
requiredDeps["lucide-react"] = "^0.554.0";
|
|
341
|
+
requiredDeps["framer-motion"] = "^11.18.2";
|
|
342
|
+
requiredDeps["clsx"] = "^2.1.1";
|
|
343
|
+
requiredDeps["env-cmd"] = "^11.0.0";
|
|
344
|
+
}
|
|
345
|
+
// DevDependencies
|
|
346
|
+
const requiredDevDeps = {
|
|
347
|
+
tailwindcss: "^4.1.0",
|
|
348
|
+
"@tailwindcss/postcss": "^4.1.0",
|
|
349
|
+
postcss: "^8.4.0",
|
|
350
|
+
typescript: "^5.4.0",
|
|
351
|
+
"@types/node": "^20.0.0",
|
|
352
|
+
"@types/react": "^18.3.0",
|
|
353
|
+
"@types/react-dom": "^18.3.0",
|
|
354
|
+
};
|
|
355
|
+
pkg.dependencies = { ...pkg.dependencies, ...requiredDeps };
|
|
356
|
+
pkg.devDependencies = { ...pkg.devDependencies, ...requiredDevDeps };
|
|
357
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
358
|
+
console.log(chalk.green("✓ Dépendances configurées"));
|
|
359
|
+
}
|
|
360
|
+
async function createNextStructure(targetDir, force, useHeroUI, withAuth) {
|
|
361
|
+
console.log(chalk.yellow("\n📁 Création de la structure Next.js..."));
|
|
362
|
+
const appDir = path.join(targetDir, "app");
|
|
363
|
+
const stylesDir = path.join(targetDir, "styles");
|
|
364
|
+
await fs.ensureDir(appDir);
|
|
365
|
+
await fs.ensureDir(stylesDir);
|
|
366
|
+
// Générer le layout principal
|
|
367
|
+
const layoutDest = path.join(appDir, "layout.tsx");
|
|
368
|
+
if (!fs.existsSync(layoutDest) || force) {
|
|
369
|
+
let layoutContent = "";
|
|
370
|
+
if (useHeroUI) {
|
|
371
|
+
// Layout avec HeroUI - Server Component
|
|
372
|
+
layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
|
|
373
|
+
// Server Component pour permettre le SSR des pages enfants
|
|
374
|
+
|
|
375
|
+
import "../styles/globals.css";
|
|
376
|
+
import { ClientLayout } from "../components/ClientLayout";
|
|
377
|
+
import type { PropsWithChildren } from "react";
|
|
378
|
+
|
|
379
|
+
export default function RootLayout({ children }: PropsWithChildren<{}>) {
|
|
380
|
+
return (
|
|
381
|
+
<html lang="fr" suppressHydrationWarning>
|
|
382
|
+
<body className="min-h-screen">
|
|
383
|
+
<ClientLayout>{children}</ClientLayout>
|
|
384
|
+
</body>
|
|
385
|
+
</html>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// Layout Tailwind CSS uniquement - Server Component
|
|
392
|
+
layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
|
|
393
|
+
// Server Component pour permettre le SSR des pages enfants
|
|
394
|
+
|
|
395
|
+
import "../styles/globals.css";
|
|
396
|
+
import { ClientLayout } from "../components/ClientLayout";
|
|
397
|
+
import type { PropsWithChildren } from "react";
|
|
398
|
+
|
|
399
|
+
export default function RootLayout({ children }: PropsWithChildren<{}>) {
|
|
400
|
+
return (
|
|
401
|
+
<html lang="fr" suppressHydrationWarning>
|
|
402
|
+
<body className="min-h-screen">
|
|
403
|
+
<ClientLayout>{children}</ClientLayout>
|
|
404
|
+
</body>
|
|
405
|
+
</html>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
await fs.writeFile(layoutDest, layoutContent);
|
|
411
|
+
console.log(chalk.green("✓ app/layout.tsx créé"));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
console.log(chalk.gray(" app/layout.tsx existe déjà (utilisez --force pour écraser)"));
|
|
415
|
+
}
|
|
416
|
+
// Créer globals.css
|
|
417
|
+
const globalsPath = path.join(stylesDir, "globals.css");
|
|
418
|
+
if (!fs.existsSync(globalsPath) || force) {
|
|
419
|
+
const globalsContent = `@import "tailwindcss";
|
|
420
|
+
|
|
421
|
+
@config "../tailwind.config.mjs";
|
|
422
|
+
`;
|
|
423
|
+
await fs.writeFile(globalsPath, globalsContent);
|
|
424
|
+
console.log(chalk.green("✓ styles/globals.css créé"));
|
|
425
|
+
}
|
|
426
|
+
// Créer la page d'accueil publique (racine)
|
|
427
|
+
const homePagePath = path.join(appDir, "page.tsx");
|
|
428
|
+
if (!fs.existsSync(homePagePath) || force) {
|
|
429
|
+
const homePageContent = `// GENERATED BY LASTBRAIN APP-SHELL
|
|
430
|
+
|
|
431
|
+
import { SimpleHomePage } from "@lastbrain/app";
|
|
432
|
+
import { Footer } from "@lastbrain/ui";
|
|
433
|
+
import { footerConfig } from "../config/footer";
|
|
434
|
+
|
|
435
|
+
export default function RootPage() {
|
|
436
|
+
return (
|
|
437
|
+
<>
|
|
438
|
+
<SimpleHomePage showAuth={${withAuth}} />
|
|
439
|
+
<Footer config={footerConfig} />
|
|
440
|
+
</>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
`;
|
|
444
|
+
await fs.writeFile(homePagePath, homePageContent);
|
|
445
|
+
console.log(chalk.green("✓ app/page.tsx créé"));
|
|
446
|
+
}
|
|
447
|
+
// Créer la page not-found.tsx
|
|
448
|
+
const notFoundPath = path.join(appDir, "not-found.tsx");
|
|
449
|
+
if (!fs.existsSync(notFoundPath) || force) {
|
|
450
|
+
const notFoundContent = `"use client";
|
|
451
|
+
import { Button } from "@lastbrain/ui";
|
|
452
|
+
import { useRouter } from "next/navigation";
|
|
453
|
+
|
|
454
|
+
export default function NotFound() {
|
|
455
|
+
const router = useRouter();
|
|
456
|
+
return (
|
|
457
|
+
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
458
|
+
<div className="mx-auto max-w-md text-center">
|
|
459
|
+
<h1 className="mb-4 text-6xl font-bold text-foreground">404</h1>
|
|
460
|
+
<h2 className="mb-4 text-2xl font-semibold text-foreground">
|
|
461
|
+
Page non trouvée
|
|
462
|
+
</h2>
|
|
463
|
+
<p className="mb-8 text-muted-foreground">
|
|
464
|
+
La page que vous recherchez n'existe pas ou a été déplacée.
|
|
465
|
+
</p>
|
|
466
|
+
<Button
|
|
467
|
+
onPress={() => {
|
|
468
|
+
router.back();
|
|
469
|
+
}}
|
|
470
|
+
className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
|
471
|
+
>
|
|
472
|
+
Retour à l'accueil
|
|
473
|
+
</Button>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
`;
|
|
479
|
+
await fs.writeFile(notFoundPath, notFoundContent);
|
|
480
|
+
console.log(chalk.green("✓ app/not-found.tsx créé"));
|
|
481
|
+
}
|
|
482
|
+
// Créer les routes avec leurs layouts
|
|
483
|
+
await createRoute(appDir, "admin", "admin", force);
|
|
484
|
+
await createRoute(appDir, "auth", "auth", force);
|
|
485
|
+
await createRoute(appDir, "docs", "public", force);
|
|
486
|
+
// Créer le composant ClientLayout (wrapper client avec providers)
|
|
487
|
+
await createClientLayout(targetDir, force, useHeroUI);
|
|
488
|
+
// Créer le composant AppHeader
|
|
489
|
+
await createAppHeader(targetDir, force);
|
|
490
|
+
// Créer le composant AppAside
|
|
491
|
+
await createAppAside(targetDir, force);
|
|
492
|
+
// Créer le wrapper AppProviders avec configuration realtime
|
|
493
|
+
await createAppProvidersWrapper(targetDir, force);
|
|
494
|
+
}
|
|
495
|
+
async function createClientLayout(targetDir, force, useHeroUI) {
|
|
496
|
+
const componentsDir = path.join(targetDir, "components");
|
|
497
|
+
await fs.ensureDir(componentsDir);
|
|
498
|
+
const clientLayoutPath = path.join(componentsDir, "ClientLayout.tsx");
|
|
499
|
+
if (!fs.existsSync(clientLayoutPath) || force) {
|
|
500
|
+
let clientLayoutContent = "";
|
|
501
|
+
if (useHeroUI) {
|
|
502
|
+
// ClientLayout avec HeroUI
|
|
503
|
+
clientLayoutContent = `"use client";
|
|
504
|
+
|
|
505
|
+
import { HeroUIProvider } from "@heroui/system";
|
|
506
|
+
import { ThemeProvider } from "next-themes";
|
|
507
|
+
import { useRouter } from "next/navigation";
|
|
508
|
+
import { AppProviders } from "./AppProviders";
|
|
509
|
+
import { AppHeader } from "./AppHeader";
|
|
510
|
+
import type { ReactNode } from "react";
|
|
511
|
+
|
|
512
|
+
export function ClientLayout({ children }: { children: ReactNode }) {
|
|
513
|
+
const router = useRouter();
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<HeroUIProvider navigate={router.push}>
|
|
517
|
+
<ThemeProvider
|
|
518
|
+
attribute="class"
|
|
519
|
+
defaultTheme="dark"
|
|
520
|
+
enableSystem={false}
|
|
521
|
+
storageKey="lastbrain-theme"
|
|
522
|
+
>
|
|
523
|
+
<AppProviders>
|
|
524
|
+
<AppHeader />
|
|
525
|
+
<div className="min-h-screen text-foreground bg-background">
|
|
526
|
+
{children}
|
|
527
|
+
</div>
|
|
528
|
+
</AppProviders>
|
|
529
|
+
</ThemeProvider>
|
|
530
|
+
</HeroUIProvider>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// ClientLayout Tailwind CSS uniquement
|
|
537
|
+
clientLayoutContent = `"use client";
|
|
538
|
+
|
|
539
|
+
import { ThemeProvider } from "next-themes";
|
|
540
|
+
import { AppProviders } from "./AppProviders";
|
|
541
|
+
import { AppHeader } from "./AppHeader";
|
|
542
|
+
import type { ReactNode } from "react";
|
|
543
|
+
|
|
544
|
+
export function ClientLayout({ children }: { children: ReactNode }) {
|
|
545
|
+
return (
|
|
546
|
+
<ThemeProvider
|
|
547
|
+
attribute="class"
|
|
548
|
+
defaultTheme="light"
|
|
549
|
+
enableSystem={false}
|
|
550
|
+
storageKey="lastbrain-theme"
|
|
551
|
+
>
|
|
552
|
+
<AppProviders>
|
|
553
|
+
<AppHeader />
|
|
554
|
+
<div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-white">
|
|
555
|
+
{children}
|
|
556
|
+
</div>
|
|
557
|
+
</AppProviders>
|
|
558
|
+
</ThemeProvider>
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
await fs.writeFile(clientLayoutPath, clientLayoutContent);
|
|
564
|
+
console.log(chalk.green("✓ components/ClientLayout.tsx créé"));
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
console.log(chalk.gray(" components/ClientLayout.tsx existe déjà (utilisez --force pour écraser)"));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function createRoute(appDir, routeName, layoutType, force) {
|
|
571
|
+
const routeDir = path.join(appDir, routeName);
|
|
572
|
+
await fs.ensureDir(routeDir);
|
|
573
|
+
// Layout pour la route
|
|
574
|
+
const layoutPath = path.join(routeDir, "layout.tsx");
|
|
575
|
+
if (!fs.existsSync(layoutPath) || force) {
|
|
576
|
+
let layoutContent = "";
|
|
577
|
+
if (routeName === "admin") {
|
|
578
|
+
// Layout admin avec sidebar
|
|
579
|
+
layoutContent = `import { AdminLayoutWithSidebar } from "@lastbrain/app";
|
|
580
|
+
import { menuConfig } from "../../config/menu";
|
|
581
|
+
|
|
582
|
+
export default function AdminLayout({
|
|
583
|
+
children,
|
|
584
|
+
}: {
|
|
585
|
+
children: React.ReactNode;
|
|
586
|
+
}) {
|
|
587
|
+
return (
|
|
588
|
+
<AdminLayoutWithSidebar menuConfig={menuConfig}>
|
|
589
|
+
{children}
|
|
590
|
+
</AdminLayoutWithSidebar>
|
|
591
|
+
);
|
|
592
|
+
}`;
|
|
593
|
+
}
|
|
594
|
+
else if (routeName === "auth") {
|
|
595
|
+
// Layout auth avec sidebar
|
|
596
|
+
layoutContent = `import { AuthLayoutWithSidebar } from "@lastbrain/app";
|
|
597
|
+
import { menuConfig } from "../../config/menu";
|
|
598
|
+
|
|
599
|
+
export default function AuthLayout({
|
|
600
|
+
children,
|
|
601
|
+
}: {
|
|
602
|
+
children: React.ReactNode;
|
|
603
|
+
}) {
|
|
604
|
+
return (
|
|
605
|
+
<AuthLayoutWithSidebar menuConfig={menuConfig}>
|
|
606
|
+
{children}
|
|
607
|
+
</AuthLayoutWithSidebar>
|
|
608
|
+
);
|
|
609
|
+
}`;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// Layout standard pour les autres routes (ex: docs = public)
|
|
613
|
+
const layoutComponent = layoutType.charAt(0).toUpperCase() + layoutType.slice(1) + "Layout";
|
|
614
|
+
// Layout docs avec footer
|
|
615
|
+
layoutContent = `import { ${layoutComponent} } from "@lastbrain/app";
|
|
616
|
+
import { footerConfig } from "../../config/footer";
|
|
617
|
+
|
|
618
|
+
export default function DocsLayout({
|
|
619
|
+
children,
|
|
620
|
+
}: {
|
|
621
|
+
children: React.ReactNode;
|
|
622
|
+
}) {
|
|
623
|
+
return <${layoutComponent} footerConfig={footerConfig}>{children}</${layoutComponent}>;
|
|
624
|
+
}`;
|
|
625
|
+
}
|
|
626
|
+
await fs.writeFile(layoutPath, layoutContent);
|
|
627
|
+
console.log(chalk.green(`✓ app/${routeName}/layout.tsx créé`));
|
|
628
|
+
}
|
|
629
|
+
// Page par défaut
|
|
630
|
+
const pagePath = path.join(routeDir, "page.tsx");
|
|
631
|
+
if (!fs.existsSync(pagePath) || force) {
|
|
632
|
+
let templateImport = "";
|
|
633
|
+
let componentName = null;
|
|
634
|
+
// Choisir le template approprié selon la route
|
|
635
|
+
switch (routeName) {
|
|
636
|
+
case "admin":
|
|
637
|
+
templateImport = 'import { ModuleGuidePage } from "@lastbrain/app";';
|
|
638
|
+
componentName = "ModuleGuidePage";
|
|
639
|
+
break;
|
|
640
|
+
case "docs":
|
|
641
|
+
templateImport = 'import { SimpleDocPage } from "@lastbrain/app";';
|
|
642
|
+
componentName = "SimpleDocPage";
|
|
643
|
+
break;
|
|
644
|
+
default:
|
|
645
|
+
// Template générique pour les autres routes
|
|
646
|
+
templateImport = `// Generic page for ${routeName}`;
|
|
647
|
+
componentName = null;
|
|
648
|
+
}
|
|
649
|
+
const pageContent = componentName
|
|
650
|
+
? `// GENERATED BY LASTBRAIN APP-SHELL
|
|
651
|
+
|
|
652
|
+
${templateImport}
|
|
653
|
+
|
|
654
|
+
export default function ${routeName.charAt(0).toUpperCase() + routeName.slice(1)}Page() {
|
|
655
|
+
return <${componentName} />;
|
|
656
|
+
}
|
|
657
|
+
`
|
|
658
|
+
: `// GENERATED BY LASTBRAIN APP-SHELL
|
|
659
|
+
|
|
660
|
+
export default function ${routeName.charAt(0).toUpperCase() + routeName.slice(1)}Page() {
|
|
661
|
+
return (
|
|
662
|
+
<div className="container mx-auto px-4 py-8">
|
|
663
|
+
<h1 className="text-3xl font-bold mb-4">${routeName.charAt(0).toUpperCase() + routeName.slice(1)} Page</h1>
|
|
664
|
+
<p className="text-slate-600 dark:text-slate-400">
|
|
665
|
+
Cette page a été générée par LastBrain Init.
|
|
666
|
+
</p>
|
|
667
|
+
<p className="text-sm text-slate-500 dark:text-slate-500 mt-4">
|
|
668
|
+
Route: /${routeName}
|
|
669
|
+
</p>
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
`;
|
|
674
|
+
await fs.writeFile(pagePath, pageContent);
|
|
675
|
+
console.log(chalk.green(`✓ app/${routeName}/page.tsx créé`));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async function createAppHeader(targetDir, force) {
|
|
679
|
+
const componentsDir = path.join(targetDir, "components");
|
|
680
|
+
await fs.ensureDir(componentsDir);
|
|
681
|
+
const headerPath = path.join(componentsDir, "AppHeader.tsx");
|
|
682
|
+
if (!fs.existsSync(headerPath) || force) {
|
|
683
|
+
const headerContent = `"use client";
|
|
684
|
+
|
|
685
|
+
import { Header, type MenuItem } from "@lastbrain/ui";
|
|
686
|
+
import { menuConfig } from "../config/menu";
|
|
687
|
+
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
688
|
+
import { useRouter } from "next/navigation";
|
|
689
|
+
import { useAuthSession, useNotificationsContext } from "@lastbrain/app";
|
|
690
|
+
import { useState, useEffect } from "react";
|
|
691
|
+
|
|
692
|
+
interface MenuIgnored {
|
|
693
|
+
public: { title: string; path: string }[];
|
|
694
|
+
auth: { title: string; path: string }[];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function AppHeader() {
|
|
698
|
+
const router = useRouter();
|
|
699
|
+
const { user, isSuperAdmin } = useAuthSession();
|
|
700
|
+
const { data, loading, markAsRead, markAllAsRead, deleteNotification } =
|
|
701
|
+
useNotificationsContext();
|
|
702
|
+
const [menuIgnored, setMenuIgnored] = useState<MenuIgnored | undefined>();
|
|
703
|
+
const [menuCustom, setMenuCustom] = useState<MenuItem[] | undefined>();
|
|
704
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
705
|
+
|
|
706
|
+
// Charger menuIgnored et menuCustom s'ils existent
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
let loadedIgnored = false;
|
|
709
|
+
let loadedCustom = false;
|
|
710
|
+
|
|
711
|
+
// Charger menu-ignored
|
|
712
|
+
import("../config/menu-ignored")
|
|
713
|
+
.then((mod) => setMenuIgnored(mod.menuIgnored))
|
|
714
|
+
.catch(() => setMenuIgnored(undefined))
|
|
715
|
+
.finally(() => {
|
|
716
|
+
loadedIgnored = true;
|
|
717
|
+
if (loadedIgnored && loadedCustom) setIsLoading(false);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Charger menu-custom
|
|
721
|
+
import("../config/menu-custom")
|
|
722
|
+
.then((mod) => {
|
|
723
|
+
// Combiner les menus custom publics et auth
|
|
724
|
+
const customMenus: MenuItem[] = [];
|
|
725
|
+
if (mod.menuCustom?.public) customMenus.push(...mod.menuCustom.public);
|
|
726
|
+
if (mod.menuCustom?.auth) customMenus.push(...mod.menuCustom.auth);
|
|
727
|
+
setMenuCustom(customMenus.length > 0 ? customMenus : undefined);
|
|
728
|
+
})
|
|
729
|
+
.catch(() => setMenuCustom(undefined))
|
|
730
|
+
.finally(() => {
|
|
731
|
+
loadedCustom = true;
|
|
732
|
+
if (loadedIgnored && loadedCustom) setIsLoading(false);
|
|
733
|
+
});
|
|
734
|
+
}, []);
|
|
735
|
+
|
|
736
|
+
const handleLogout = async () => {
|
|
737
|
+
await supabaseBrowserClient.auth.signOut();
|
|
738
|
+
router.push("/");
|
|
739
|
+
router.refresh();
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
return (
|
|
743
|
+
<Header
|
|
744
|
+
user={user}
|
|
745
|
+
onLogout={handleLogout}
|
|
746
|
+
menuConfig={menuConfig}
|
|
747
|
+
accountMenu={menuConfig.account}
|
|
748
|
+
brandName="LastBrain App"
|
|
749
|
+
brandHref="/"
|
|
750
|
+
isSuperAdmin={isSuperAdmin}
|
|
751
|
+
notifications={data.notifications}
|
|
752
|
+
unreadCount={data.unreadCount}
|
|
753
|
+
notificationsLoading={loading}
|
|
754
|
+
onMarkAsRead={markAsRead}
|
|
755
|
+
onMarkAllAsRead={markAllAsRead}
|
|
756
|
+
onDeleteNotification={deleteNotification}
|
|
757
|
+
{...(menuIgnored ? { menuIgnored } : {})}
|
|
758
|
+
{...(menuCustom ? { menuCustom } : {})}
|
|
759
|
+
{...{ isLoadingMenus: isLoading }}
|
|
760
|
+
/>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
`;
|
|
764
|
+
await fs.writeFile(headerPath, headerContent);
|
|
765
|
+
console.log(chalk.green("✓ components/AppHeader.tsx créé"));
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
console.log(chalk.gray(" components/AppHeader.tsx existe déjà (utilisez --force pour écraser)"));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function createAppAside(targetDir, force) {
|
|
772
|
+
const componentsDir = path.join(targetDir, "components");
|
|
773
|
+
await fs.ensureDir(componentsDir);
|
|
774
|
+
const asidePath = path.join(componentsDir, "AppAside.tsx");
|
|
775
|
+
if (!fs.existsSync(asidePath) || force) {
|
|
776
|
+
const asideContent = `"use client";
|
|
777
|
+
|
|
778
|
+
import { AppAside as UIAppAside } from "@lastbrain/app";
|
|
779
|
+
import { useAuthSession } from "@lastbrain/app";
|
|
780
|
+
import { menuConfig } from "../config/menu";
|
|
781
|
+
|
|
782
|
+
interface AppAsideProps {
|
|
783
|
+
className?: string;
|
|
784
|
+
isVisible?: boolean;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
|
|
788
|
+
const { isSuperAdmin } = useAuthSession();
|
|
789
|
+
|
|
790
|
+
return (
|
|
791
|
+
<UIAppAside
|
|
792
|
+
className={className}
|
|
793
|
+
menuConfig={menuConfig}
|
|
794
|
+
isSuperAdmin={isSuperAdmin}
|
|
795
|
+
isVisible={isVisible}
|
|
796
|
+
/>
|
|
797
|
+
);
|
|
798
|
+
}`;
|
|
799
|
+
await fs.writeFile(asidePath, asideContent);
|
|
800
|
+
console.log(chalk.green("✓ components/AppAside.tsx créé"));
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
console.log(chalk.gray(" components/AppAside.tsx existe déjà (utilisez --force pour écraser)"));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async function createAppProvidersWrapper(targetDir, force) {
|
|
807
|
+
const componentsDir = path.join(targetDir, "components");
|
|
808
|
+
await fs.ensureDir(componentsDir);
|
|
809
|
+
const providersPath = path.join(componentsDir, "AppProviders.tsx");
|
|
810
|
+
if (!fs.existsSync(providersPath) || force) {
|
|
811
|
+
const providersContent = `"use client";
|
|
812
|
+
|
|
813
|
+
import { AppProviders as BaseAppProviders } from "@lastbrain/app";
|
|
814
|
+
import { realtimeConfig } from "../config/realtime";
|
|
815
|
+
import type { ReactNode } from "react";
|
|
816
|
+
|
|
817
|
+
export function AppProviders({ children }: { children: ReactNode }) {
|
|
818
|
+
return (
|
|
819
|
+
<BaseAppProviders realtimeConfig={realtimeConfig}>
|
|
820
|
+
{children}
|
|
821
|
+
</BaseAppProviders>
|
|
822
|
+
);
|
|
823
|
+
}`;
|
|
824
|
+
await fs.writeFile(providersPath, providersContent);
|
|
825
|
+
console.log(chalk.green("✓ components/AppProviders.tsx créé"));
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
console.log(chalk.gray(" components/AppProviders.tsx existe déjà (utilisez --force pour écraser)"));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function createConfigFiles(targetDir, force, useHeroUI, projectName) {
|
|
832
|
+
console.log(chalk.yellow("\n⚙️ Création des fichiers de configuration..."));
|
|
833
|
+
// middleware.ts - Protection des routes /auth/*, /admin/* et /api/admin/*
|
|
834
|
+
const middlewarePath = path.join(targetDir, "middleware.ts");
|
|
835
|
+
if (!fs.existsSync(middlewarePath) || force) {
|
|
836
|
+
const middleware = `import { type NextRequest, NextResponse } from "next/server";
|
|
837
|
+
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Crée un client Supabase pour le middleware
|
|
841
|
+
*/
|
|
842
|
+
function createMiddlewareClient(request: NextRequest) {
|
|
843
|
+
let response = NextResponse.next({
|
|
844
|
+
request: {
|
|
845
|
+
headers: request.headers,
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
const supabase = createServerClient(
|
|
850
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
851
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
852
|
+
{
|
|
853
|
+
cookies: {
|
|
854
|
+
get(name: string) {
|
|
855
|
+
return request.cookies.get(name)?.value;
|
|
856
|
+
},
|
|
857
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
858
|
+
request.cookies.set({
|
|
859
|
+
name,
|
|
860
|
+
value,
|
|
861
|
+
...options,
|
|
862
|
+
});
|
|
863
|
+
response = NextResponse.next({
|
|
864
|
+
request: {
|
|
865
|
+
headers: request.headers,
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
response.cookies.set({
|
|
869
|
+
name,
|
|
870
|
+
value,
|
|
871
|
+
...options,
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
remove(name: string, options: CookieOptions) {
|
|
875
|
+
request.cookies.delete(name);
|
|
876
|
+
response = NextResponse.next({
|
|
877
|
+
request: {
|
|
878
|
+
headers: request.headers,
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
response.cookies.delete(name);
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
return { supabase, response };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export async function middleware(request: NextRequest) {
|
|
891
|
+
const { pathname } = request.nextUrl;
|
|
892
|
+
const isApi = pathname.startsWith("/api/");
|
|
893
|
+
|
|
894
|
+
// Pages publiques d'authentification (ne pas protéger)
|
|
895
|
+
const publicAuthPages = [
|
|
896
|
+
"/signin",
|
|
897
|
+
"/signup",
|
|
898
|
+
"/reset-password",
|
|
899
|
+
"/forgot-password",
|
|
900
|
+
"/callback",
|
|
901
|
+
];
|
|
902
|
+
|
|
903
|
+
if(process.env.MAINTENANCE_MODE === "true" && !pathname.startsWith("/maintenance")) {
|
|
904
|
+
const redirectUrl = new URL("/maintenance", request.url);
|
|
905
|
+
return NextResponse.redirect(redirectUrl);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const isPublicAuthPage = publicAuthPages.some((page) =>
|
|
909
|
+
pathname.startsWith(page)
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
// Ne pas protéger les pages publiques d'authentification
|
|
913
|
+
if (isPublicAuthPage) {
|
|
914
|
+
return NextResponse.next();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Protéger les routes /auth/* (espace membre)
|
|
918
|
+
if (pathname.startsWith("/auth")) {
|
|
919
|
+
try {
|
|
920
|
+
const { supabase, response } = createMiddlewareClient(request);
|
|
921
|
+
const {
|
|
922
|
+
data: { session },
|
|
923
|
+
} = await supabase.auth.getSession();
|
|
924
|
+
|
|
925
|
+
// Pas de session → nettoyage des cookies + redirection vers /signin
|
|
926
|
+
if (!session) {
|
|
927
|
+
const redirectUrl = new URL("/signin", request.url);
|
|
928
|
+
redirectUrl.searchParams.set("redirect", pathname);
|
|
929
|
+
const res = NextResponse.redirect(redirectUrl);
|
|
930
|
+
res.cookies.delete("sb-access-token");
|
|
931
|
+
res.cookies.delete("sb-refresh-token");
|
|
932
|
+
res.cookies.delete("sb:token");
|
|
933
|
+
res.cookies.delete("sb:refresh-token");
|
|
934
|
+
return res;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return response;
|
|
938
|
+
} catch (error) {
|
|
939
|
+
console.error("Middleware auth error:", error);
|
|
940
|
+
const res = NextResponse.redirect(new URL("/signin", request.url));
|
|
941
|
+
res.cookies.delete("sb-access-token");
|
|
942
|
+
res.cookies.delete("sb-refresh-token");
|
|
943
|
+
res.cookies.delete("sb:token");
|
|
944
|
+
res.cookies.delete("sb:refresh-token");
|
|
945
|
+
return res;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Protéger les routes /admin/* et /api/admin/* (superadmin uniquement)
|
|
950
|
+
if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) {
|
|
951
|
+
try {
|
|
952
|
+
const { supabase, response } = createMiddlewareClient(request);
|
|
953
|
+
const {
|
|
954
|
+
data: { session },
|
|
955
|
+
} = await supabase.auth.getSession();
|
|
956
|
+
|
|
957
|
+
// Pas de session → 401 JSON pour API, sinon redirection vers /signin avec nettoyage cookies
|
|
958
|
+
if (!session) {
|
|
959
|
+
if (isApi) {
|
|
960
|
+
const res = NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
961
|
+
res.cookies.delete("sb-access-token");
|
|
962
|
+
res.cookies.delete("sb-refresh-token");
|
|
963
|
+
res.cookies.delete("sb:token");
|
|
964
|
+
res.cookies.delete("sb:refresh-token");
|
|
965
|
+
return res;
|
|
966
|
+
}
|
|
967
|
+
const redirectUrl = new URL("/signin", request.url);
|
|
968
|
+
redirectUrl.searchParams.set("redirect", pathname);
|
|
969
|
+
const res = NextResponse.redirect(redirectUrl);
|
|
970
|
+
res.cookies.delete("sb-access-token");
|
|
971
|
+
res.cookies.delete("sb-refresh-token");
|
|
972
|
+
res.cookies.delete("sb:token");
|
|
973
|
+
res.cookies.delete("sb:refresh-token");
|
|
974
|
+
return res;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Vérifier si l'utilisateur est superadmin
|
|
978
|
+
const { data: isSuperAdmin, error } = await supabase.rpc(
|
|
979
|
+
"is_superadmin",
|
|
980
|
+
{ user_id: session.user.id }
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
if (error || !isSuperAdmin) {
|
|
984
|
+
console.error("Access denied: not a superadmin", error);
|
|
985
|
+
if (isApi) {
|
|
986
|
+
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
|
987
|
+
}
|
|
988
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return response;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.error("Middleware admin error:", error);
|
|
994
|
+
if (isApi) {
|
|
995
|
+
return NextResponse.json({ error: "Erreur middleware" }, { status: 500 });
|
|
996
|
+
}
|
|
997
|
+
const res = NextResponse.redirect(new URL("/", request.url));
|
|
998
|
+
res.cookies.delete("sb-access-token");
|
|
999
|
+
res.cookies.delete("sb-refresh-token");
|
|
1000
|
+
res.cookies.delete("sb:token");
|
|
1001
|
+
res.cookies.delete("sb:refresh-token");
|
|
1002
|
+
return res;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return NextResponse.next();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export const config = {
|
|
1010
|
+
matcher: [
|
|
1011
|
+
/*
|
|
1012
|
+
* Match all request paths except:
|
|
1013
|
+
* - _next/static (static files)
|
|
1014
|
+
* - _next/image (image optimization files)
|
|
1015
|
+
* - favicon.ico (favicon file)
|
|
1016
|
+
* - public folder
|
|
1017
|
+
*/
|
|
1018
|
+
"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
1019
|
+
],
|
|
1020
|
+
};
|
|
1021
|
+
`;
|
|
1022
|
+
await fs.writeFile(middlewarePath, middleware);
|
|
1023
|
+
console.log(chalk.green("✓ middleware.ts créé (protection /auth/*, /admin/* et /api/admin/*)"));
|
|
1024
|
+
}
|
|
1025
|
+
// next.config.mjs
|
|
1026
|
+
const nextConfigPath = path.join(targetDir, "next.config.mjs");
|
|
1027
|
+
if (!fs.existsSync(nextConfigPath) || force) {
|
|
1028
|
+
const nextConfig = `/** @type {import('next').NextConfig} */
|
|
1029
|
+
const nextConfig = {
|
|
1030
|
+
reactStrictMode: true,
|
|
1031
|
+
devIndicators: {
|
|
1032
|
+
position: 'bottom-right',
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
export default nextConfig;
|
|
1037
|
+
`;
|
|
1038
|
+
await fs.writeFile(nextConfigPath, nextConfig);
|
|
1039
|
+
console.log(chalk.green("✓ next.config.mjs créé"));
|
|
1040
|
+
}
|
|
1041
|
+
// tailwind.config.mjs
|
|
1042
|
+
const tailwindConfigPath = path.join(targetDir, "tailwind.config.mjs");
|
|
1043
|
+
if (!fs.existsSync(tailwindConfigPath) || force) {
|
|
1044
|
+
let tailwindConfig = "";
|
|
1045
|
+
if (useHeroUI) {
|
|
1046
|
+
// Configuration avec HeroUI - complète avec thèmes
|
|
1047
|
+
tailwindConfig = `import { heroui } from "@heroui/theme";
|
|
1048
|
+
|
|
1049
|
+
/** @type {import('tailwindcss').Config} */
|
|
1050
|
+
const config = {
|
|
1051
|
+
content: [
|
|
1052
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
1053
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
1054
|
+
"./node_modules/@lastbrain/*/src/**/*.{js,jsx,ts,tsx}",
|
|
1055
|
+
"./node_modules/@heroui/theme/dist/**/*.{js,mjs}",
|
|
1056
|
+
],
|
|
1057
|
+
darkMode: "class",
|
|
1058
|
+
plugins: [
|
|
1059
|
+
heroui({
|
|
1060
|
+
themes: {
|
|
1061
|
+
light: {
|
|
1062
|
+
colors: {
|
|
1063
|
+
default: {
|
|
1064
|
+
950: "#0d0d0e",
|
|
1065
|
+
900: "#19191c",
|
|
1066
|
+
800: "#26262a",
|
|
1067
|
+
700: "#323238",
|
|
1068
|
+
600: "#3f3f46",
|
|
1069
|
+
500: "#65656b",
|
|
1070
|
+
400: "#8c8c90",
|
|
1071
|
+
300: "#b2b2b5",
|
|
1072
|
+
200: "#d9d9da",
|
|
1073
|
+
100: "#ffffff",
|
|
1074
|
+
foreground: "#000000",
|
|
1075
|
+
DEFAULT: "#d4d4d8",
|
|
1076
|
+
},
|
|
1077
|
+
primary: {
|
|
1078
|
+
50: "#eef2ff",
|
|
1079
|
+
100: "#e0e7ff",
|
|
1080
|
+
200: "#c7d2fe",
|
|
1081
|
+
300: "#a5b4fc",
|
|
1082
|
+
400: "#818cf8",
|
|
1083
|
+
500: "#6366f1",
|
|
1084
|
+
600: "#4f46e5",
|
|
1085
|
+
700: "#4338ca",
|
|
1086
|
+
800: "#3730a3",
|
|
1087
|
+
900: "#312e81",
|
|
1088
|
+
foreground: "#ffffff",
|
|
1089
|
+
DEFAULT: "#6366f1",
|
|
1090
|
+
},
|
|
1091
|
+
secondary: {
|
|
1092
|
+
50: "#f5e8ff",
|
|
1093
|
+
100: "#e8ccff",
|
|
1094
|
+
200: "#d7a6ff",
|
|
1095
|
+
300: "#c57fff",
|
|
1096
|
+
400: "#b359ff",
|
|
1097
|
+
500: "#9b37ff",
|
|
1098
|
+
600: "#7d1fcc",
|
|
1099
|
+
700: "#5f1799",
|
|
1100
|
+
800: "#410f66",
|
|
1101
|
+
900: "#240833",
|
|
1102
|
+
foreground: "#ffffff",
|
|
1103
|
+
DEFAULT: "#9b37ff",
|
|
1104
|
+
},
|
|
1105
|
+
success: {
|
|
1106
|
+
50: "#e7fff7",
|
|
1107
|
+
100: "#c3ffec",
|
|
1108
|
+
200: "#9affdf",
|
|
1109
|
+
300: "#70ffd2",
|
|
1110
|
+
400: "#4bffca",
|
|
1111
|
+
500: "#22e6b0",
|
|
1112
|
+
600: "#18b38a",
|
|
1113
|
+
700: "#108066",
|
|
1114
|
+
800: "#074d40",
|
|
1115
|
+
900: "#012b22",
|
|
1116
|
+
foreground: "#000",
|
|
1117
|
+
DEFAULT: "#22e6b0",
|
|
1118
|
+
},
|
|
1119
|
+
warning: {
|
|
1120
|
+
50: "#fff8e1",
|
|
1121
|
+
100: "#ffecb3",
|
|
1122
|
+
200: "#ffe082",
|
|
1123
|
+
300: "#ffd54f",
|
|
1124
|
+
400: "#ffca28",
|
|
1125
|
+
500: "#ffc107",
|
|
1126
|
+
600: "#ffb300",
|
|
1127
|
+
700: "#ffa000",
|
|
1128
|
+
800: "#ff8f00",
|
|
1129
|
+
900: "#ff6f00",
|
|
1130
|
+
foreground: "#000",
|
|
1131
|
+
DEFAULT: "#ffca28",
|
|
1132
|
+
},
|
|
1133
|
+
danger: {
|
|
1134
|
+
50: "#ffe6e9",
|
|
1135
|
+
100: "#ffbac1",
|
|
1136
|
+
200: "#ff8f99",
|
|
1137
|
+
300: "#ff6471",
|
|
1138
|
+
400: "#ff3949",
|
|
1139
|
+
500: "#ff112a",
|
|
1140
|
+
600: "#d80c22",
|
|
1141
|
+
700: "#b1081b",
|
|
1142
|
+
800: "#890514",
|
|
1143
|
+
900: "#62020d",
|
|
1144
|
+
foreground: "#fff",
|
|
1145
|
+
DEFAULT: "#ff112a",
|
|
1146
|
+
},
|
|
1147
|
+
background: "#f7f8fc",
|
|
1148
|
+
foreground: "#0e0e10",
|
|
1149
|
+
content1: "#ffffff",
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
dark: {
|
|
1153
|
+
colors: {
|
|
1154
|
+
default: {
|
|
1155
|
+
50: "#1a1c1f",
|
|
1156
|
+
100: "#232529",
|
|
1157
|
+
200: "#2c2f34",
|
|
1158
|
+
300: "#363940",
|
|
1159
|
+
400: "#40444d",
|
|
1160
|
+
500: "#4b4f59",
|
|
1161
|
+
600: "#6c707b",
|
|
1162
|
+
700: "#8d919b",
|
|
1163
|
+
800: "#afb2bb",
|
|
1164
|
+
900: "#d1d3d9",
|
|
1165
|
+
foreground: "#ffffff",
|
|
1166
|
+
DEFAULT: "#363940",
|
|
1167
|
+
},
|
|
1168
|
+
primary: {
|
|
1169
|
+
50: "#312e81",
|
|
1170
|
+
100: "#3730a3",
|
|
1171
|
+
200: "#4338ca",
|
|
1172
|
+
300: "#4f46e5",
|
|
1173
|
+
400: "#6366f1",
|
|
1174
|
+
500: "#818cf8",
|
|
1175
|
+
600: "#a5b4fc",
|
|
1176
|
+
700: "#c7d2fe",
|
|
1177
|
+
800: "#e0e7ff",
|
|
1178
|
+
900: "#eef2ff",
|
|
1179
|
+
foreground: "#ffffff",
|
|
1180
|
+
DEFAULT: "#6366f1",
|
|
1181
|
+
},
|
|
1182
|
+
secondary: {
|
|
1183
|
+
50: "#240833",
|
|
1184
|
+
100: "#410f66",
|
|
1185
|
+
200: "#5f1799",
|
|
1186
|
+
300: "#7d1fcc",
|
|
1187
|
+
400: "#9b37ff",
|
|
1188
|
+
500: "#b359ff",
|
|
1189
|
+
600: "#c57fff",
|
|
1190
|
+
700: "#d7a6ff",
|
|
1191
|
+
800: "#e8ccff",
|
|
1192
|
+
900: "#f5e8ff",
|
|
1193
|
+
foreground: "#ffffff",
|
|
1194
|
+
DEFAULT: "#9b37ff",
|
|
1195
|
+
},
|
|
1196
|
+
success: {
|
|
1197
|
+
50: "#012b22",
|
|
1198
|
+
100: "#074d40",
|
|
1199
|
+
200: "#108066",
|
|
1200
|
+
300: "#18b38a",
|
|
1201
|
+
400: "#22e6b0",
|
|
1202
|
+
500: "#4bffca",
|
|
1203
|
+
600: "#70ffd2",
|
|
1204
|
+
700: "#9affdf",
|
|
1205
|
+
800: "#c3ffec",
|
|
1206
|
+
900: "#e7fff7",
|
|
1207
|
+
foreground: "#0e0e0e",
|
|
1208
|
+
DEFAULT: "#22e6b0",
|
|
1209
|
+
},
|
|
1210
|
+
warning: {
|
|
1211
|
+
50: "#3a2c00",
|
|
1212
|
+
100: "#5e4700",
|
|
1213
|
+
200: "#836300",
|
|
1214
|
+
300: "#a77e00",
|
|
1215
|
+
400: "#cc9900",
|
|
1216
|
+
500: "#ffc107",
|
|
1217
|
+
600: "#ffd54f",
|
|
1218
|
+
700: "#ffe082",
|
|
1219
|
+
800: "#ffecb3",
|
|
1220
|
+
900: "#fff8e1",
|
|
1221
|
+
foreground: "#000",
|
|
1222
|
+
DEFAULT: "#ffc107",
|
|
1223
|
+
},
|
|
1224
|
+
danger: {
|
|
1225
|
+
50: "#4a000a",
|
|
1226
|
+
100: "#6e000f",
|
|
1227
|
+
200: "#920014",
|
|
1228
|
+
300: "#b60019",
|
|
1229
|
+
400: "#da001f",
|
|
1230
|
+
500: "#ff112a",
|
|
1231
|
+
600: "#ff3949",
|
|
1232
|
+
700: "#ff6471",
|
|
1233
|
+
800: "#ff8f99",
|
|
1234
|
+
900: "#ffbac1",
|
|
1235
|
+
foreground: "#ffffff",
|
|
1236
|
+
DEFAULT: "#ff112a",
|
|
1237
|
+
},
|
|
1238
|
+
background: "#0f1114",
|
|
1239
|
+
foreground: "#f8f8f8",
|
|
1240
|
+
content1: "#15181d",
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
}),
|
|
1245
|
+
],
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
export default config;
|
|
1249
|
+
`;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
// Configuration Tailwind CSS uniquement
|
|
1253
|
+
tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
1254
|
+
const config = {
|
|
1255
|
+
content: [
|
|
1256
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
1257
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
1258
|
+
"./node_modules/@lastbrain/*/src/**/*.{js,jsx,ts,tsx}",
|
|
1259
|
+
],
|
|
1260
|
+
theme: {
|
|
1261
|
+
extend: {
|
|
1262
|
+
colors: {
|
|
1263
|
+
primary: {
|
|
1264
|
+
50: "#eff6ff",
|
|
1265
|
+
100: "#dbeafe",
|
|
1266
|
+
200: "#bfdbfe",
|
|
1267
|
+
300: "#93c5fd",
|
|
1268
|
+
400: "#60a5fa",
|
|
1269
|
+
500: "#3b82f6",
|
|
1270
|
+
600: "#2563eb",
|
|
1271
|
+
700: "#1d4ed8",
|
|
1272
|
+
800: "#1e40af",
|
|
1273
|
+
900: "#1e3a8a",
|
|
1274
|
+
950: "#172554",
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
plugins: [],
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
export default config;
|
|
1283
|
+
`;
|
|
1284
|
+
}
|
|
1285
|
+
await fs.writeFile(tailwindConfigPath, tailwindConfig);
|
|
1286
|
+
console.log(chalk.green("✓ tailwind.config.mjs créé"));
|
|
1287
|
+
}
|
|
1288
|
+
// postcss.config.mjs
|
|
1289
|
+
const postcssConfigPath = path.join(targetDir, "postcss.config.mjs");
|
|
1290
|
+
if (!fs.existsSync(postcssConfigPath) || force) {
|
|
1291
|
+
const postcssConfig = `export default {
|
|
1292
|
+
plugins: {
|
|
1293
|
+
'@tailwindcss/postcss': {},
|
|
1294
|
+
},
|
|
1295
|
+
};
|
|
1296
|
+
`;
|
|
1297
|
+
await fs.writeFile(postcssConfigPath, postcssConfig);
|
|
1298
|
+
console.log(chalk.green("✓ postcss.config.mjs créé"));
|
|
1299
|
+
}
|
|
1300
|
+
// tsconfig.json
|
|
1301
|
+
const tsconfigPath = path.join(targetDir, "tsconfig.json");
|
|
1302
|
+
if (!fs.existsSync(tsconfigPath) || force) {
|
|
1303
|
+
// Détecter si le projet cible est dans un monorepo
|
|
1304
|
+
const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
|
|
1305
|
+
fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
|
|
1306
|
+
// Générer les chemins en fonction du contexte
|
|
1307
|
+
const paths = {
|
|
1308
|
+
"@/*": ["./*"],
|
|
1309
|
+
};
|
|
1310
|
+
if (targetIsInMonorepo) {
|
|
1311
|
+
// En monorepo, pointer sur les sources locales
|
|
1312
|
+
paths["@lastbrain/ui"] = ["../../packages/ui/src/index.ts"];
|
|
1313
|
+
paths["@lastbrain/ui/*"] = ["../../packages/ui/src/*"];
|
|
1314
|
+
paths["@lastbrain/core"] = ["../../packages/core/src/index.ts"];
|
|
1315
|
+
paths["@lastbrain/core/*"] = ["../../packages/core/src/*"];
|
|
1316
|
+
paths["@lastbrain/app"] = ["../../packages/app/src/index.ts"];
|
|
1317
|
+
paths["@lastbrain/app/*"] = ["../../packages/app/src/*"];
|
|
1318
|
+
paths["@lastbrain/module-auth"] = [
|
|
1319
|
+
"../../packages/module-auth/src/index.ts",
|
|
1320
|
+
];
|
|
1321
|
+
paths["@lastbrain/module-auth/*"] = ["../../packages/module-auth/src/*"];
|
|
1322
|
+
paths["@lastbrain/module-ai"] = ["../../packages/module-ai/src/index.ts"];
|
|
1323
|
+
paths["@lastbrain/module-ai/*"] = ["../../packages/module-ai/src/*"];
|
|
1324
|
+
paths["@lastbrain/module-legal"] = [
|
|
1325
|
+
"../../packages/module-legal/src/index.ts",
|
|
1326
|
+
];
|
|
1327
|
+
paths["@lastbrain/module-legal/*"] = [
|
|
1328
|
+
"../../packages/module-legal/src/*",
|
|
1329
|
+
];
|
|
1330
|
+
paths["@lastbrain/module-project-board"] = [
|
|
1331
|
+
"../../packages/module-project-board/src/index.ts",
|
|
1332
|
+
];
|
|
1333
|
+
paths["@lastbrain/module-project-board/*"] = [
|
|
1334
|
+
"../../packages/module-project-board/src/*",
|
|
1335
|
+
];
|
|
1336
|
+
paths["@lastbrain/module-tasks"] = [
|
|
1337
|
+
"../../packages/module-tasks/src/index.ts",
|
|
1338
|
+
];
|
|
1339
|
+
paths["@lastbrain/module-tasks/*"] = [
|
|
1340
|
+
"../../packages/module-tasks/src/*",
|
|
1341
|
+
];
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// Hors monorepo (npm install), ne pas utiliser de paths aliases
|
|
1345
|
+
delete paths["@/*"];
|
|
1346
|
+
}
|
|
1347
|
+
const tsconfig = {
|
|
1348
|
+
compilerOptions: {
|
|
1349
|
+
target: "ES2020",
|
|
1350
|
+
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
|
1351
|
+
jsx: "preserve",
|
|
1352
|
+
module: "ESNext",
|
|
1353
|
+
moduleResolution: "bundler",
|
|
1354
|
+
resolveJsonModule: true,
|
|
1355
|
+
allowJs: true,
|
|
1356
|
+
strict: true,
|
|
1357
|
+
noEmit: true,
|
|
1358
|
+
esModuleInterop: true,
|
|
1359
|
+
skipLibCheck: true,
|
|
1360
|
+
forceConsistentCasingInFileNames: true,
|
|
1361
|
+
incremental: true,
|
|
1362
|
+
isolatedModules: true,
|
|
1363
|
+
plugins: [{ name: "next" }],
|
|
1364
|
+
paths,
|
|
1365
|
+
types: [],
|
|
1366
|
+
},
|
|
1367
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
1368
|
+
exclude: ["node_modules", "dist", ".next", "out"],
|
|
1369
|
+
};
|
|
1370
|
+
// Ajouter extends seulement en monorepo
|
|
1371
|
+
if (targetIsInMonorepo) {
|
|
1372
|
+
tsconfig.extends = "../../tsconfig.base.json";
|
|
1373
|
+
}
|
|
1374
|
+
await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
|
|
1375
|
+
console.log(chalk.green("✓ tsconfig.json créé"));
|
|
1376
|
+
}
|
|
1377
|
+
// config/menu.ts
|
|
1378
|
+
const configDir = path.join(targetDir, "config");
|
|
1379
|
+
await fs.ensureDir(configDir);
|
|
1380
|
+
const menuConfigPath = path.join(configDir, "menu.ts");
|
|
1381
|
+
if (!fs.existsSync(menuConfigPath) || force) {
|
|
1382
|
+
const menuConfig = `import type { MenuConfig } from "@lastbrain/ui";
|
|
1383
|
+
|
|
1384
|
+
export const menuConfig: MenuConfig = {
|
|
1385
|
+
public: [
|
|
1386
|
+
{ label: "Accueil", href: "/" },
|
|
1387
|
+
{ label: "Documentation", href: "/docs" },
|
|
1388
|
+
],
|
|
1389
|
+
auth: [
|
|
1390
|
+
{ label: "Dashboard", href: "/auth/dashboard" },
|
|
1391
|
+
{ label: "Profil", href: "/auth/profile" },
|
|
1392
|
+
],
|
|
1393
|
+
admin: [
|
|
1394
|
+
{ label: "Admin", href: "/admin" },
|
|
1395
|
+
{ label: "Utilisateurs", href: "/admin/users" },
|
|
1396
|
+
],
|
|
1397
|
+
};
|
|
1398
|
+
`;
|
|
1399
|
+
await fs.writeFile(menuConfigPath, menuConfig);
|
|
1400
|
+
console.log(chalk.green("✓ config/menu.ts créé"));
|
|
1401
|
+
}
|
|
1402
|
+
// config/menu-ignored.ts - Optional file to hide menus and block routes
|
|
1403
|
+
const menuIgnoredPath = path.join(configDir, "menu-ignored.ts");
|
|
1404
|
+
if (!fs.existsSync(menuIgnoredPath) || force) {
|
|
1405
|
+
const menuIgnored = `import type { MenuIgnored } from "@lastbrain/app";
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Menu items to hide and routes to block
|
|
1409
|
+
* This file is optional - if it doesn't exist, all menus are shown
|
|
1410
|
+
*
|
|
1411
|
+
* Example:
|
|
1412
|
+
* export const menuIgnored: MenuIgnored = {
|
|
1413
|
+
* public: [
|
|
1414
|
+
* { title: "Documentation", path: "/docs" }
|
|
1415
|
+
* ],
|
|
1416
|
+
* auth: [
|
|
1417
|
+
* { title: "Profile", path: "/auth/profile" }
|
|
1418
|
+
* ]
|
|
1419
|
+
* };
|
|
1420
|
+
*/
|
|
1421
|
+
|
|
1422
|
+
export const menuIgnored: MenuIgnored = {
|
|
1423
|
+
public: [],
|
|
1424
|
+
auth: [],
|
|
1425
|
+
};
|
|
1426
|
+
`;
|
|
1427
|
+
await fs.writeFile(menuIgnoredPath, menuIgnored);
|
|
1428
|
+
console.log(chalk.green("✓ config/menu-ignored.ts créé"));
|
|
1429
|
+
}
|
|
1430
|
+
// config/menu-custom.ts - Optional file to add custom menus
|
|
1431
|
+
const menuCustomPath = path.join(configDir, "menu-custom.ts");
|
|
1432
|
+
if (!fs.existsSync(menuCustomPath) || force) {
|
|
1433
|
+
const menuCustom = `import type { MenuCustom } from "@lastbrain/app";
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Custom menu items to add without creating a module
|
|
1437
|
+
* This file is optional - if it doesn't exist, no custom menus are added
|
|
1438
|
+
*
|
|
1439
|
+
* Example:
|
|
1440
|
+
* export const menuCustom: MenuCustom = {
|
|
1441
|
+
* public: [
|
|
1442
|
+
* { label: "Blog", href: "/blog" },
|
|
1443
|
+
* { label: "Pricing", href: "/pricing" }
|
|
1444
|
+
* ],
|
|
1445
|
+
* auth: [
|
|
1446
|
+
* { label: "Invoices", href: "/auth/invoices" }
|
|
1447
|
+
* ],
|
|
1448
|
+
* admin: [
|
|
1449
|
+
* { label: "Reports", href: "/admin/reports" }
|
|
1450
|
+
* ]
|
|
1451
|
+
* };
|
|
1452
|
+
*/
|
|
1453
|
+
|
|
1454
|
+
export const menuCustom: MenuCustom = {
|
|
1455
|
+
public: [],
|
|
1456
|
+
auth: [],
|
|
1457
|
+
admin: [],
|
|
1458
|
+
};
|
|
1459
|
+
`;
|
|
1460
|
+
await fs.writeFile(menuCustomPath, menuCustom);
|
|
1461
|
+
console.log(chalk.green("✓ config/menu-custom.ts créé"));
|
|
1462
|
+
}
|
|
1463
|
+
// config/footer.ts
|
|
1464
|
+
const footerConfigPath = path.join(configDir, "footer.ts");
|
|
1465
|
+
if (!fs.existsSync(footerConfigPath) || force) {
|
|
1466
|
+
const footerConfig = `// Auto-generated footer configuration
|
|
1467
|
+
// Run "node ../../scripts/generate-footer-config.js ./apps/[your-app]" to regenerate
|
|
1468
|
+
"use client";
|
|
1469
|
+
|
|
1470
|
+
import type { FooterConfig } from "@lastbrain/ui";
|
|
1471
|
+
|
|
1472
|
+
export const footerConfig: FooterConfig = {
|
|
1473
|
+
companyName: "${projectName}",
|
|
1474
|
+
companyDescription: "Application LastBrain",
|
|
1475
|
+
links: [],
|
|
1476
|
+
social: [],
|
|
1477
|
+
};
|
|
1478
|
+
`;
|
|
1479
|
+
await fs.writeFile(footerConfigPath, footerConfig);
|
|
1480
|
+
console.log(chalk.green("✓ config/footer.ts créé"));
|
|
1481
|
+
}
|
|
1482
|
+
// config/user-tabs.ts - User detail page tabs configuration
|
|
1483
|
+
const userTabsConfigPath = path.join(configDir, "user-tabs.ts");
|
|
1484
|
+
if (!fs.existsSync(userTabsConfigPath) || force) {
|
|
1485
|
+
const userTabsConfig = `// User tabs configuration
|
|
1486
|
+
// This file is generated and can be regenerated by running: pnpm build:modules
|
|
1487
|
+
"use client";
|
|
1488
|
+
|
|
1489
|
+
import type React from "react";
|
|
1490
|
+
|
|
1491
|
+
export interface ModuleUserTab {
|
|
1492
|
+
id: string;
|
|
1493
|
+
label: string;
|
|
1494
|
+
component: React.ComponentType<{ userId: string }>;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Module user tabs - dynamically imported from active modules
|
|
1498
|
+
// To add tabs from modules, import them here and add to the array below
|
|
1499
|
+
export const moduleUserTabs: ModuleUserTab[] = [];
|
|
1500
|
+
`;
|
|
1501
|
+
await fs.writeFile(userTabsConfigPath, userTabsConfig);
|
|
1502
|
+
console.log(chalk.green("✓ config/user-tabs.ts créé"));
|
|
1503
|
+
}
|
|
1504
|
+
// Créer les hooks
|
|
1505
|
+
await createHooksDirectory(targetDir, force);
|
|
1506
|
+
}
|
|
1507
|
+
async function createHooksDirectory(targetDir, force) {
|
|
1508
|
+
const hooksDir = path.join(targetDir, "hooks");
|
|
1509
|
+
await fs.ensureDir(hooksDir);
|
|
1510
|
+
// Créer le hook useNotifications
|
|
1511
|
+
const useNotificationsPath = path.join(hooksDir, "useNotifications.ts");
|
|
1512
|
+
if (!fs.existsSync(useNotificationsPath) || force) {
|
|
1513
|
+
const hookContent = `"use client";
|
|
1514
|
+
|
|
1515
|
+
import { useEffect, useState } from "react";
|
|
1516
|
+
import { useNotificationRealtime } from "@lastbrain/core";
|
|
1517
|
+
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
1518
|
+
import { useAuth } from "@lastbrain/app";
|
|
1519
|
+
|
|
1520
|
+
export interface Notification {
|
|
1521
|
+
id: string;
|
|
1522
|
+
title: string;
|
|
1523
|
+
message: string;
|
|
1524
|
+
read: boolean;
|
|
1525
|
+
created_at: string;
|
|
1526
|
+
owner_id: string;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
export function useNotifications() {
|
|
1530
|
+
const { user } = useAuth();
|
|
1531
|
+
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
1532
|
+
const [loading, setLoading] = useState(true);
|
|
1533
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1534
|
+
|
|
1535
|
+
// Écouter les changements en temps réel
|
|
1536
|
+
const realtimeSignal = useNotificationRealtime();
|
|
1537
|
+
|
|
1538
|
+
// Charger les notifications initiales
|
|
1539
|
+
const fetchNotifications = async () => {
|
|
1540
|
+
if (!user?.id) return;
|
|
1541
|
+
|
|
1542
|
+
try {
|
|
1543
|
+
setLoading(true);
|
|
1544
|
+
const { data, error } = await supabaseBrowserClient
|
|
1545
|
+
.from("user_notifications")
|
|
1546
|
+
.select("*")
|
|
1547
|
+
.eq("owner_id", user.id)
|
|
1548
|
+
.order("created_at", { ascending: false });
|
|
1549
|
+
|
|
1550
|
+
if (error) throw error;
|
|
1551
|
+
setNotifications(data || []);
|
|
1552
|
+
setError(null);
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
setError(err as Error);
|
|
1555
|
+
} finally {
|
|
1556
|
+
setLoading(false);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
// Charger au montage et quand l'utilisateur change
|
|
1561
|
+
useEffect(() => {
|
|
1562
|
+
fetchNotifications();
|
|
1563
|
+
}, [user?.id]);
|
|
1564
|
+
|
|
1565
|
+
// Recharger quand les données changent en temps réel
|
|
1566
|
+
useEffect(() => {
|
|
1567
|
+
if (realtimeSignal.hasUpdates && user?.id) {
|
|
1568
|
+
console.log("🔔 Rechargement des notifications suite à un changement");
|
|
1569
|
+
fetchNotifications();
|
|
1570
|
+
}
|
|
1571
|
+
}, [realtimeSignal.tick, user?.id]);
|
|
1572
|
+
|
|
1573
|
+
return {
|
|
1574
|
+
notifications,
|
|
1575
|
+
loading,
|
|
1576
|
+
error,
|
|
1577
|
+
refetch: fetchNotifications,
|
|
1578
|
+
hasUpdates: realtimeSignal.hasUpdates,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
`;
|
|
1582
|
+
await fs.writeFile(useNotificationsPath, hookContent);
|
|
1583
|
+
console.log(chalk.green("✓ hooks/useNotifications.ts créé"));
|
|
1584
|
+
}
|
|
1585
|
+
// Créer un placeholder pour realtime.ts qui sera généré par build:modules
|
|
1586
|
+
const realtimePath = path.join(targetDir, "config", "realtime.ts");
|
|
1587
|
+
if (!fs.existsSync(realtimePath) || force) {
|
|
1588
|
+
const realtimeContent = `import type { ModuleRealtimeConfig } from "@lastbrain/core";
|
|
1589
|
+
|
|
1590
|
+
// GENERATED FILE - DO NOT EDIT MANUALLY
|
|
1591
|
+
// Ce fichier sera automatiquement généré lors du build:modules
|
|
1592
|
+
// Exécutez "pnpm build:modules" pour créer la configuration realtime
|
|
1593
|
+
|
|
1594
|
+
export const realtimeConfig: ModuleRealtimeConfig[] = [];
|
|
1595
|
+
|
|
1596
|
+
export default realtimeConfig;
|
|
1597
|
+
`;
|
|
1598
|
+
await fs.writeFile(realtimePath, realtimeContent);
|
|
1599
|
+
console.log(chalk.green("✓ config/realtime.ts placeholder créé"));
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async function createGitIgnore(targetDir, force) {
|
|
1603
|
+
const gitignorePath = path.join(targetDir, ".gitignore");
|
|
1604
|
+
if (!fs.existsSync(gitignorePath) || force) {
|
|
1605
|
+
console.log(chalk.yellow("\n📝 Création de .gitignore..."));
|
|
1606
|
+
// Copier le fichier .gitignore depuis le template
|
|
1607
|
+
const templateGitignorePath = path.join(__dirname, "../templates/gitignore/.gitignore");
|
|
1608
|
+
if (fs.existsSync(templateGitignorePath)) {
|
|
1609
|
+
await fs.copyFile(templateGitignorePath, gitignorePath);
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
// Fallback si le template n'existe pas
|
|
1613
|
+
const gitignoreContent = `# ===========================================
|
|
1614
|
+
# GENERATED BY LASTBRAIN
|
|
1615
|
+
# ===========================================
|
|
1616
|
+
|
|
1617
|
+
# Node
|
|
1618
|
+
node_modules/
|
|
1619
|
+
npm-debug.log*
|
|
1620
|
+
pnpm-debug.log*
|
|
1621
|
+
yarn-debug.log*
|
|
1622
|
+
yarn-error.log*
|
|
1623
|
+
|
|
1624
|
+
# Environment
|
|
1625
|
+
.env
|
|
1626
|
+
.env.*
|
|
1627
|
+
!.env.example
|
|
1628
|
+
|
|
1629
|
+
# Next.js
|
|
1630
|
+
.next/
|
|
1631
|
+
out/
|
|
1632
|
+
dist/
|
|
1633
|
+
build/
|
|
1634
|
+
|
|
1635
|
+
# Supabase
|
|
1636
|
+
supabase/.temp/
|
|
1637
|
+
supabase/functions/.netlify/
|
|
1638
|
+
supabase/functions/node_modules/
|
|
1639
|
+
supabase/.branches/
|
|
1640
|
+
|
|
1641
|
+
# Logs
|
|
1642
|
+
logs
|
|
1643
|
+
*.log
|
|
1644
|
+
*.log.*
|
|
1645
|
+
|
|
1646
|
+
# OS / Editor
|
|
1647
|
+
.DS_Store
|
|
1648
|
+
Thumbs.db
|
|
1649
|
+
.idea/
|
|
1650
|
+
.vscode/
|
|
1651
|
+
*.swp
|
|
1652
|
+
|
|
1653
|
+
# Local backups
|
|
1654
|
+
*.sql
|
|
1655
|
+
*.sql.zip
|
|
1656
|
+
backup/
|
|
1657
|
+
tmp/
|
|
1658
|
+
|
|
1659
|
+
# Coverage
|
|
1660
|
+
coverage/
|
|
1661
|
+
*.lcov
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
`;
|
|
1665
|
+
await fs.writeFile(gitignorePath, gitignoreContent);
|
|
1666
|
+
}
|
|
1667
|
+
console.log(chalk.green("✓ .gitignore créé"));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
async function createEnvExample(targetDir, force) {
|
|
1671
|
+
const envExamplePath = path.join(targetDir, ".env.local.example");
|
|
1672
|
+
if (!fs.existsSync(envExamplePath) || force) {
|
|
1673
|
+
console.log(chalk.yellow("\n🔐 Création de .env.local.example..."));
|
|
1674
|
+
const envContent = `# Supabase Configuration
|
|
1675
|
+
# Exécutez 'pnpm db:init' pour initialiser Supabase local et générer le vrai .env.local
|
|
1676
|
+
|
|
1677
|
+
# Supabase Local (par défaut)
|
|
1678
|
+
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
|
1679
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_LOCAL_ANON_KEY
|
|
1680
|
+
SUPABASE_SERVICE_ROLE_KEY=YOUR_LOCAL_SERVICE_ROLE_KEY
|
|
1681
|
+
|
|
1682
|
+
# Supabase Production
|
|
1683
|
+
# NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
|
1684
|
+
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your_production_anon_key
|
|
1685
|
+
# SUPABASE_SERVICE_ROLE_KEY=your_production_service_role_key
|
|
1686
|
+
`;
|
|
1687
|
+
await fs.writeFile(envExamplePath, envContent);
|
|
1688
|
+
console.log(chalk.green("✓ .env.local.example créé"));
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async function createEnvLocal(targetDir, force, isMonorepoProject = false) {
|
|
1692
|
+
const envLocalPath = path.join(targetDir, ".env.local");
|
|
1693
|
+
if (!fs.existsSync(envLocalPath) || force) {
|
|
1694
|
+
console.log(chalk.yellow("\n🔐 Création de .env.local..."));
|
|
1695
|
+
let envContent;
|
|
1696
|
+
if (isMonorepoProject) {
|
|
1697
|
+
// Pour les projets monorepo, utiliser les variables du monorepo
|
|
1698
|
+
envContent = `# Supabase Configuration (Monorepo - Centralisé)
|
|
1699
|
+
# Les variables Supabase sont gérées au niveau du monorepo
|
|
1700
|
+
|
|
1701
|
+
# OpenAI Configuration (clé factice pour éviter les erreurs de build)
|
|
1702
|
+
OPENAI_API_KEY=sk-fake-openai-key-for-development-replace-with-real-key
|
|
1703
|
+
|
|
1704
|
+
# Note: Les variables Supabase sont fournies par le monorepo parent
|
|
1705
|
+
`;
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
// Pour les projets indépendants
|
|
1709
|
+
envContent = `# Supabase Configuration
|
|
1710
|
+
# Valeurs par défaut pour le développement local
|
|
1711
|
+
|
|
1712
|
+
# Supabase Local (par défaut)
|
|
1713
|
+
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
|
1714
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
|
|
1715
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
|
|
1716
|
+
|
|
1717
|
+
# OpenAI Configuration (clé factice pour éviter les erreurs de build)
|
|
1718
|
+
OPENAI_API_KEY=sk-fake-openai-key-for-development-replace-with-real-key
|
|
1719
|
+
`;
|
|
1720
|
+
}
|
|
1721
|
+
await fs.writeFile(envLocalPath, envContent);
|
|
1722
|
+
console.log(chalk.green("✓ .env.local créé"));
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async function createEnvProd(targetDir, force) {
|
|
1726
|
+
const envProdPath = path.join(targetDir, ".env.prod");
|
|
1727
|
+
if (!fs.existsSync(envProdPath) || force) {
|
|
1728
|
+
console.log(chalk.yellow("\n🔐 Création de .env.prod..."));
|
|
1729
|
+
const envContent = `# Production Environment Configuration
|
|
1730
|
+
# Copy your production values here
|
|
1731
|
+
|
|
1732
|
+
# Supabase Production
|
|
1733
|
+
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
|
1734
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-prod-anon-key
|
|
1735
|
+
SUPABASE_SERVICE_ROLE_KEY=your-prod-service-role-key
|
|
1736
|
+
|
|
1737
|
+
# OpenAI Production
|
|
1738
|
+
OPENAI_API_KEY=sk-proj-your-prod-api-key
|
|
1739
|
+
|
|
1740
|
+
# Note: Update these values with your actual production credentials
|
|
1741
|
+
`;
|
|
1742
|
+
await fs.writeFile(envProdPath, envContent);
|
|
1743
|
+
console.log(chalk.green("✓ .env.prod créé"));
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function createSupabaseStructure(targetDir, force) {
|
|
1747
|
+
console.log(chalk.yellow("\n🗄️ Création de la structure Supabase..."));
|
|
1748
|
+
const supabaseDir = path.join(targetDir, "supabase");
|
|
1749
|
+
const migrationsDir = path.join(supabaseDir, "migrations");
|
|
1750
|
+
await fs.ensureDir(migrationsDir);
|
|
1751
|
+
// Copier le fichier de migration initial depuis le template
|
|
1752
|
+
const templateMigrationPath = path.join(__dirname, "../templates/migrations/20201010100000_app_base.sql");
|
|
1753
|
+
const migrationDestPath = path.join(migrationsDir, "20201010100000_app_base.sql");
|
|
1754
|
+
if (!fs.existsSync(migrationDestPath) || force) {
|
|
1755
|
+
try {
|
|
1756
|
+
if (fs.existsSync(templateMigrationPath)) {
|
|
1757
|
+
await fs.copy(templateMigrationPath, migrationDestPath);
|
|
1758
|
+
console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé"));
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
console.log(chalk.yellow("⚠ Template de migration introuvable, création d'un fichier vide"));
|
|
1762
|
+
await fs.writeFile(migrationDestPath, "-- Initial migration\n-- Add your database schema here\n");
|
|
1763
|
+
console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé (vide)"));
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
catch (_error) {
|
|
1767
|
+
console.error(chalk.red("✗ Erreur lors de la création de la migration:"), _error);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
else {
|
|
1771
|
+
console.log(chalk.gray(" supabase/migrations/20201010100000_app_base.sql existe déjà"));
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async function addScriptsToPackageJson(targetDir) {
|
|
1775
|
+
console.log(chalk.yellow("\n🔧 Ajout des scripts NPM..."));
|
|
1776
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
1777
|
+
const pkg = await fs.readJson(pkgPath);
|
|
1778
|
+
// Détecter si le projet cible est dans un workspace
|
|
1779
|
+
const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
|
|
1780
|
+
fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
|
|
1781
|
+
// Utiliser le CLI depuis le monorepo si présent, sinon depuis node_modules
|
|
1782
|
+
const scriptsPrefix = targetIsInMonorepo
|
|
1783
|
+
? "node ../../packages/app/dist/cli.js"
|
|
1784
|
+
: "node node_modules/@lastbrain/app/dist/cli.js";
|
|
1785
|
+
const scripts = {
|
|
1786
|
+
predev: targetIsInMonorepo
|
|
1787
|
+
? "pnpm --filter @lastbrain/core build && pnpm --filter @lastbrain/ui build && pnpm --filter @lastbrain/app build && pnpm --filter @lastbrain/module-auth build && pnpm --filter @lastbrain/module-ai build"
|
|
1788
|
+
: "echo 'No prebuild needed'",
|
|
1789
|
+
dev: "next dev",
|
|
1790
|
+
"dev:local": "env-cmd -f .env.local next dev",
|
|
1791
|
+
"dev:prod": "env-cmd -f .env.prod next dev",
|
|
1792
|
+
build: "next build",
|
|
1793
|
+
start: "next start",
|
|
1794
|
+
lint: "next lint",
|
|
1795
|
+
lastbrain: scriptsPrefix,
|
|
1796
|
+
"build:modules": `${scriptsPrefix} module:build`,
|
|
1797
|
+
"db:migrations:sync": `${scriptsPrefix} db:migrations:sync`,
|
|
1798
|
+
"db:init": `${scriptsPrefix} db:init`,
|
|
1799
|
+
"readme:create": `${scriptsPrefix} readme:create`,
|
|
1800
|
+
};
|
|
1801
|
+
pkg.scripts = { ...pkg.scripts, ...scripts };
|
|
1802
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
1803
|
+
console.log(chalk.green("✓ Scripts ajoutés au package.json"));
|
|
1804
|
+
}
|
|
1805
|
+
async function saveModulesConfig(targetDir, selectedModules, withAuth) {
|
|
1806
|
+
const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
|
|
1807
|
+
await fs.ensureDir(path.dirname(modulesConfigPath));
|
|
1808
|
+
const modules = [];
|
|
1809
|
+
// Ajouter TOUS les modules disponibles
|
|
1810
|
+
for (const availableModule of AVAILABLE_MODULES) {
|
|
1811
|
+
const isSelected = selectedModules.includes(availableModule.name) ||
|
|
1812
|
+
(withAuth && availableModule.name === "auth");
|
|
1813
|
+
// Vérifier si le module a des migrations
|
|
1814
|
+
const modulePath = path.join(targetDir, "node_modules", ...availableModule.package.split("/"));
|
|
1815
|
+
const migrationsDir = path.join(modulePath, "supabase", "migrations");
|
|
1816
|
+
const moduleConfig = {
|
|
1817
|
+
package: availableModule.package,
|
|
1818
|
+
active: isSelected,
|
|
1819
|
+
};
|
|
1820
|
+
if (fs.existsSync(migrationsDir)) {
|
|
1821
|
+
const migrationFiles = fs
|
|
1822
|
+
.readdirSync(migrationsDir)
|
|
1823
|
+
.filter((f) => f.endsWith(".sql"));
|
|
1824
|
+
if (migrationFiles.length > 0) {
|
|
1825
|
+
moduleConfig.migrations = isSelected ? migrationFiles : [];
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
modules.push(moduleConfig);
|
|
1829
|
+
}
|
|
1830
|
+
await fs.writeJson(modulesConfigPath, { modules }, { spaces: 2 });
|
|
1831
|
+
console.log(chalk.green("✓ Configuration des modules sauvegardée"));
|
|
1832
|
+
}
|
|
1833
|
+
async function createStorageProxy(targetDir, force) {
|
|
1834
|
+
console.log(chalk.yellow("\n🗂️ Création du système de proxy storage..."));
|
|
1835
|
+
// Créer le dossier lib
|
|
1836
|
+
const libDir = path.join(targetDir, "lib");
|
|
1837
|
+
await fs.ensureDir(libDir);
|
|
1838
|
+
// 1. Créer lib/bucket-config.ts
|
|
1839
|
+
const bucketConfigPath = path.join(libDir, "bucket-config.ts");
|
|
1840
|
+
if (!fs.existsSync(bucketConfigPath) || force) {
|
|
1841
|
+
const bucketConfigContent = `/**
|
|
1842
|
+
* Storage configuration for buckets and access control
|
|
1843
|
+
*/
|
|
1844
|
+
|
|
1845
|
+
export interface BucketConfig {
|
|
1846
|
+
name: string;
|
|
1847
|
+
isPublic: boolean;
|
|
1848
|
+
description: string;
|
|
1849
|
+
allowedFileTypes?: string[];
|
|
1850
|
+
maxFileSize?: number; // in bytes
|
|
1851
|
+
customAccessControl?: (userId: string, filePath: string) => boolean;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
|
|
1855
|
+
avatar: {
|
|
1856
|
+
name: "avatar",
|
|
1857
|
+
isPublic: true,
|
|
1858
|
+
description: "User profile pictures and avatars",
|
|
1859
|
+
allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1860
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
1861
|
+
},
|
|
1862
|
+
app: {
|
|
1863
|
+
name: "app",
|
|
1864
|
+
isPublic: false,
|
|
1865
|
+
description: "Private user files and documents",
|
|
1866
|
+
maxFileSize: 100 * 1024 * 1024, // 100MB
|
|
1867
|
+
customAccessControl: (userId: string, filePath: string) => {
|
|
1868
|
+
// Users can only access files in their own folder (app/{userId}/...)
|
|
1869
|
+
return filePath.startsWith(\`\${userId}/\`);
|
|
1870
|
+
},
|
|
1871
|
+
},
|
|
1872
|
+
recipes: {
|
|
1873
|
+
name: "recipes",
|
|
1874
|
+
isPublic: true,
|
|
1875
|
+
description: "Public recipe images accessible by slug",
|
|
1876
|
+
allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1877
|
+
maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
1878
|
+
},
|
|
1879
|
+
// Example for future buckets:
|
|
1880
|
+
// public: {
|
|
1881
|
+
// name: "public",
|
|
1882
|
+
// isPublic: true,
|
|
1883
|
+
// description: "Publicly accessible files like logos, banners",
|
|
1884
|
+
// allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
|
|
1885
|
+
// maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
1886
|
+
// },
|
|
1887
|
+
// documents: {
|
|
1888
|
+
// name: "documents",
|
|
1889
|
+
// isPublic: false,
|
|
1890
|
+
// description: "Private documents requiring authentication",
|
|
1891
|
+
// allowedFileTypes: ["application/pdf", "application/msword", "text/plain"],
|
|
1892
|
+
// maxFileSize: 25 * 1024 * 1024, // 25MB
|
|
1893
|
+
// },
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Get bucket configuration
|
|
1898
|
+
*/
|
|
1899
|
+
export function getBucketConfig(bucketName: string): BucketConfig | null {
|
|
1900
|
+
return BUCKET_CONFIGS[bucketName] || null;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Check if bucket is public
|
|
1905
|
+
*/
|
|
1906
|
+
export function isPublicBucket(bucketName: string): boolean {
|
|
1907
|
+
const config = getBucketConfig(bucketName);
|
|
1908
|
+
return config?.isPublic ?? false;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* Check if user has access to a specific file
|
|
1913
|
+
*/
|
|
1914
|
+
export function hasFileAccess(bucketName: string, userId: string, filePath: string): boolean {
|
|
1915
|
+
const config = getBucketConfig(bucketName);
|
|
1916
|
+
if (!config) return false;
|
|
1917
|
+
|
|
1918
|
+
// Public buckets are accessible to everyone
|
|
1919
|
+
if (config.isPublic) return true;
|
|
1920
|
+
|
|
1921
|
+
// Private buckets require authentication
|
|
1922
|
+
if (!userId) return false;
|
|
1923
|
+
|
|
1924
|
+
// Apply custom access control if defined
|
|
1925
|
+
if (config.customAccessControl) {
|
|
1926
|
+
return config.customAccessControl(userId, filePath);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Validate file type for bucket
|
|
1934
|
+
*/
|
|
1935
|
+
export function isValidFileType(bucketName: string, contentType: string): boolean {
|
|
1936
|
+
const config = getBucketConfig(bucketName);
|
|
1937
|
+
if (!config || !config.allowedFileTypes) return true;
|
|
1938
|
+
|
|
1939
|
+
return config.allowedFileTypes.includes(contentType);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/**
|
|
1943
|
+
* Check if file size is within bucket limits
|
|
1944
|
+
*/
|
|
1945
|
+
export function isValidFileSize(bucketName: string, fileSize: number): boolean {
|
|
1946
|
+
const config = getBucketConfig(bucketName);
|
|
1947
|
+
if (!config || !config.maxFileSize) return true;
|
|
1948
|
+
|
|
1949
|
+
return fileSize <= config.maxFileSize;
|
|
1950
|
+
}`;
|
|
1951
|
+
await fs.writeFile(bucketConfigPath, bucketConfigContent);
|
|
1952
|
+
console.log(chalk.green("✓ lib/bucket-config.ts créé"));
|
|
1953
|
+
}
|
|
1954
|
+
// 2. Créer lib/storage.ts
|
|
1955
|
+
const storagePath = path.join(libDir, "storage.ts");
|
|
1956
|
+
if (!fs.existsSync(storagePath) || force) {
|
|
1957
|
+
const storageContent = `/**
|
|
1958
|
+
* Build storage proxy URL for files in Supabase buckets
|
|
1959
|
+
*
|
|
1960
|
+
* @param bucket - The bucket name (e.g., "avatar", "app")
|
|
1961
|
+
* @param path - The file path within the bucket
|
|
1962
|
+
* @returns Proxied URL (e.g., "/api/storage/avatar/user_128_123456.webp")
|
|
1963
|
+
*/
|
|
1964
|
+
export function buildStorageUrl(bucket: string, path: string): string {
|
|
1965
|
+
// Remove leading slash if present
|
|
1966
|
+
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
1967
|
+
|
|
1968
|
+
// Remove bucket prefix from path if present (e.g., "avatar/file.jpg" -> "file.jpg")
|
|
1969
|
+
const pathWithoutBucket = cleanPath.startsWith(bucket + "/")
|
|
1970
|
+
? cleanPath.slice(bucket.length + 1)
|
|
1971
|
+
: cleanPath;
|
|
1972
|
+
|
|
1973
|
+
return \`/api/storage/\${bucket}/\${pathWithoutBucket}\`;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/**
|
|
1977
|
+
* Extract bucket and path from a storage URL
|
|
1978
|
+
*
|
|
1979
|
+
* @param url - Storage URL (can be proxied URL or Supabase public URL)
|
|
1980
|
+
* @returns Object with bucket and path, or null if not a valid storage URL
|
|
1981
|
+
*/
|
|
1982
|
+
export function parseStorageUrl(url: string): { bucket: string; path: string } | null {
|
|
1983
|
+
// Handle proxy URLs like "/api/storage/avatar/file.jpg"
|
|
1984
|
+
const proxyMatch = url.match(/^\\/api\\/storage\\/([^\\/]+)\\/(.+)$/);
|
|
1985
|
+
if (proxyMatch) {
|
|
1986
|
+
return {
|
|
1987
|
+
bucket: proxyMatch[1],
|
|
1988
|
+
path: proxyMatch[2]
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// Handle Supabase public URLs
|
|
1993
|
+
const supabaseMatch = url.match(/\\/storage\\/v1\\/object\\/public\\/([^\\/]+)\\/(.+)$/);
|
|
1994
|
+
if (supabaseMatch) {
|
|
1995
|
+
return {
|
|
1996
|
+
bucket: supabaseMatch[1],
|
|
1997
|
+
path: supabaseMatch[2]
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Convert a Supabase storage path to proxy URL
|
|
2006
|
+
*
|
|
2007
|
+
* @param storagePath - Path like "avatar/file.jpg" or "app/user/file.pdf"
|
|
2008
|
+
* @returns Proxied URL
|
|
2009
|
+
*/
|
|
2010
|
+
export function storagePathToProxyUrl(storagePath: string): string {
|
|
2011
|
+
const parts = storagePath.split("/");
|
|
2012
|
+
if (parts.length < 2) {
|
|
2013
|
+
throw new Error("Invalid storage path format");
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
const bucket = parts[0];
|
|
2017
|
+
const path = parts.slice(1).join("/");
|
|
2018
|
+
|
|
2019
|
+
return buildStorageUrl(bucket, path);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* List of public buckets that don't require authentication
|
|
2024
|
+
*/
|
|
2025
|
+
export const PUBLIC_BUCKETS = ["avatar"];
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* List of private buckets that require authentication
|
|
2029
|
+
*/
|
|
2030
|
+
export const PRIVATE_BUCKETS = ["app"];
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* Check if a bucket is public
|
|
2034
|
+
*/
|
|
2035
|
+
export function isPublicBucket(bucket: string): boolean {
|
|
2036
|
+
return PUBLIC_BUCKETS.includes(bucket);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Check if a bucket is private
|
|
2041
|
+
*/
|
|
2042
|
+
export function isPrivateBucket(bucket: string): boolean {
|
|
2043
|
+
return PRIVATE_BUCKETS.includes(bucket);
|
|
2044
|
+
}`;
|
|
2045
|
+
await fs.writeFile(storagePath, storageContent);
|
|
2046
|
+
console.log(chalk.green("✓ lib/storage.ts créé"));
|
|
2047
|
+
}
|
|
2048
|
+
// 3. Créer app/api/storage/[bucket]/[...path]/route.ts
|
|
2049
|
+
const apiStorageDir = path.join(targetDir, "app", "api", "storage", "[bucket]", "[...path]");
|
|
2050
|
+
await fs.ensureDir(apiStorageDir);
|
|
2051
|
+
const routePath = path.join(apiStorageDir, "route.ts");
|
|
2052
|
+
if (!fs.existsSync(routePath) || force) {
|
|
2053
|
+
const routeContent = `import { NextRequest, NextResponse } from "next/server";
|
|
2054
|
+
import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
2055
|
+
import { getBucketConfig, hasFileAccess } from "@/lib/bucket-config";
|
|
2056
|
+
|
|
2057
|
+
/**
|
|
2058
|
+
* GET /api/storage/[bucket]/[...path]
|
|
2059
|
+
* Proxy for Supabase Storage files with clean URLs and access control
|
|
2060
|
+
*
|
|
2061
|
+
* Examples:
|
|
2062
|
+
* - /api/storage/avatar/user_128_123456.webp
|
|
2063
|
+
* - /api/storage/app/user/documents/file.pdf
|
|
2064
|
+
*/
|
|
2065
|
+
export async function GET(
|
|
2066
|
+
request: NextRequest,
|
|
2067
|
+
props: { params: Promise<{ bucket: string; path: string[] }> }
|
|
2068
|
+
) {
|
|
2069
|
+
try {
|
|
2070
|
+
const { bucket, path } = await props.params;
|
|
2071
|
+
const filePath = path.join("/");
|
|
2072
|
+
|
|
2073
|
+
// Check if bucket exists in our configuration
|
|
2074
|
+
const bucketConfig = getBucketConfig(bucket);
|
|
2075
|
+
if (!bucketConfig) {
|
|
2076
|
+
return new NextResponse("Bucket not allowed", { status: 403 });
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const supabase = await getSupabaseServerClient();
|
|
2080
|
+
let userId: string | null = null;
|
|
2081
|
+
|
|
2082
|
+
// Get user for private buckets or custom access control
|
|
2083
|
+
if (!bucketConfig.isPublic) {
|
|
2084
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
2085
|
+
|
|
2086
|
+
if (authError || !user) {
|
|
2087
|
+
return new NextResponse("Unauthorized", { status: 401 });
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
userId = user.id;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// Check file access permissions
|
|
2094
|
+
if (!hasFileAccess(bucket, userId || "", filePath)) {
|
|
2095
|
+
return new NextResponse("Forbidden - Access denied to this file", { status: 403 });
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Get file from Supabase Storage
|
|
2099
|
+
const { data: file, error } = await supabase.storage
|
|
2100
|
+
.from(bucket)
|
|
2101
|
+
.download(filePath);
|
|
2102
|
+
|
|
2103
|
+
if (error) {
|
|
2104
|
+
console.error("Storage download error:", error);
|
|
2105
|
+
if (error.message.includes("not found")) {
|
|
2106
|
+
return new NextResponse("File not found", { status: 404 });
|
|
2107
|
+
}
|
|
2108
|
+
return new NextResponse("Storage error", { status: 500 });
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
if (!file) {
|
|
2112
|
+
return new NextResponse("File not found", { status: 404 });
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Convert blob to array buffer
|
|
2116
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
2117
|
+
|
|
2118
|
+
// Determine content type from file extension
|
|
2119
|
+
const getContentType = (filename: string): string => {
|
|
2120
|
+
const ext = filename.toLowerCase().split(".").pop();
|
|
2121
|
+
const mimeTypes: Record<string, string> = {
|
|
2122
|
+
// Images
|
|
2123
|
+
jpg: "image/jpeg",
|
|
2124
|
+
jpeg: "image/jpeg",
|
|
2125
|
+
png: "image/png",
|
|
2126
|
+
gif: "image/gif",
|
|
2127
|
+
webp: "image/webp",
|
|
2128
|
+
svg: "image/svg+xml",
|
|
2129
|
+
// Documents
|
|
2130
|
+
pdf: "application/pdf",
|
|
2131
|
+
doc: "application/msword",
|
|
2132
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2133
|
+
xls: "application/vnd.ms-excel",
|
|
2134
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2135
|
+
// Text
|
|
2136
|
+
txt: "text/plain",
|
|
2137
|
+
csv: "text/csv",
|
|
2138
|
+
// Videos
|
|
2139
|
+
mp4: "video/mp4",
|
|
2140
|
+
avi: "video/x-msvideo",
|
|
2141
|
+
mov: "video/quicktime",
|
|
2142
|
+
// Audio
|
|
2143
|
+
mp3: "audio/mpeg",
|
|
2144
|
+
wav: "audio/wav",
|
|
2145
|
+
// Archives
|
|
2146
|
+
zip: "application/zip",
|
|
2147
|
+
rar: "application/x-rar-compressed",
|
|
2148
|
+
};
|
|
2149
|
+
return mimeTypes[ext || ""] || "application/octet-stream";
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
const contentType = getContentType(filePath);
|
|
2153
|
+
|
|
2154
|
+
// Create response with proper headers
|
|
2155
|
+
const response = new NextResponse(arrayBuffer, {
|
|
2156
|
+
status: 200,
|
|
2157
|
+
headers: {
|
|
2158
|
+
"Content-Type": contentType,
|
|
2159
|
+
"Cache-Control": "public, max-age=31536000, immutable", // Cache for 1 year
|
|
2160
|
+
"Content-Length": arrayBuffer.byteLength.toString(),
|
|
2161
|
+
},
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
return response;
|
|
2165
|
+
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
console.error("Storage proxy error:", error);
|
|
2168
|
+
return new NextResponse("Internal server error", { status: 500 });
|
|
2169
|
+
}
|
|
2170
|
+
}`;
|
|
2171
|
+
await fs.writeFile(routePath, routeContent);
|
|
2172
|
+
console.log(chalk.green("✓ app/api/storage/[bucket]/[...path]/route.ts créé"));
|
|
2173
|
+
}
|
|
2174
|
+
console.log(chalk.green("✓ Système de proxy storage configuré"));
|
|
2175
|
+
}
|