@lastbrain/app 0.1.39 → 0.1.42
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/cli.js +6 -1
- package/dist/hooks/useNotifications.d.ts +1 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -1
- package/dist/layouts/PublicLayout.js +1 -1
- package/dist/scripts/init-app.js +54 -12
- package/dist/scripts/module-add.d.ts.map +1 -1
- package/dist/scripts/module-add.js +28 -10
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +432 -48
- package/dist/scripts/module-list.d.ts.map +1 -1
- package/dist/scripts/module-list.js +38 -8
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +2 -2
- package/package.json +2 -2
- package/src/cli.ts +6 -1
- package/src/hooks/useNotifications.ts +1 -0
- package/src/layouts/PublicLayout.tsx +1 -1
- package/src/scripts/init-app.ts +56 -12
- package/src/scripts/module-add.ts +44 -11
- package/src/scripts/module-build.ts +499 -54
- package/src/scripts/module-list.ts +45 -8
- package/src/templates/DefaultDoc.tsx +325 -12
|
@@ -19,6 +19,9 @@ const monorepoRoot = projectRoot.includes("/apps/")
|
|
|
19
19
|
: projectRoot;
|
|
20
20
|
const appDirectory = path.join(projectRoot, "app");
|
|
21
21
|
|
|
22
|
+
// Mode debug activé via --debug
|
|
23
|
+
const isDebugMode = process.argv.includes("--debug");
|
|
24
|
+
|
|
22
25
|
// Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
|
|
23
26
|
const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
24
27
|
|
|
@@ -161,7 +164,9 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
|
161
164
|
.replace(/^@lastbrain\/module-/, "")
|
|
162
165
|
.toLowerCase();
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
if (isDebugMode) {
|
|
168
|
+
console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
|
|
169
|
+
}
|
|
165
170
|
|
|
166
171
|
// Ajouter le préfixe du module au path pour les sections admin et auth,
|
|
167
172
|
// MAIS seulement quand la section ne correspond PAS au module lui-même
|
|
@@ -173,11 +178,17 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
|
173
178
|
// Éviter les doublons si le préfixe est déjà présent
|
|
174
179
|
if (!page.path.startsWith(`/${modulePrefix}/`)) {
|
|
175
180
|
effectivePath = `/${modulePrefix}${page.path}`;
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
if (isDebugMode) {
|
|
182
|
+
console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
|
|
183
|
+
}
|
|
184
|
+
} else if (isDebugMode) {
|
|
178
185
|
console.log(`✅ Module prefix already present: ${page.path}`);
|
|
179
186
|
}
|
|
180
|
-
} else if (
|
|
187
|
+
} else if (
|
|
188
|
+
page.section === "auth" &&
|
|
189
|
+
modulePrefix === "auth" &&
|
|
190
|
+
isDebugMode
|
|
191
|
+
) {
|
|
181
192
|
console.log(
|
|
182
193
|
`🏠 Auth module in auth section, no prefix needed: ${page.path}`,
|
|
183
194
|
);
|
|
@@ -270,7 +281,9 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"
|
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
fs.writeFileSync(filePath, content);
|
|
273
|
-
|
|
284
|
+
if (isDebugMode) {
|
|
285
|
+
console.log(`⭐ Generated page: ${filePath}`);
|
|
286
|
+
}
|
|
274
287
|
const entry: MenuEntry = {
|
|
275
288
|
label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
|
|
276
289
|
path: effectivePath,
|
|
@@ -294,7 +307,9 @@ export const navigation = ${JSON.stringify(navigation, null, 2)};
|
|
|
294
307
|
export const userMenu = ${JSON.stringify(userMenu, null, 2)};
|
|
295
308
|
`;
|
|
296
309
|
fs.writeFileSync(navPath, content);
|
|
297
|
-
|
|
310
|
+
if (isDebugMode) {
|
|
311
|
+
console.log(`🧭 Generated navigation metadata: ${navPath}`);
|
|
312
|
+
}
|
|
298
313
|
}
|
|
299
314
|
|
|
300
315
|
/**
|
|
@@ -306,25 +321,45 @@ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
306
321
|
|
|
307
322
|
const menuPath = path.join(configDir, "menu.ts");
|
|
308
323
|
|
|
309
|
-
// Collecter les menus de tous les modules
|
|
310
|
-
const publicMenus: ModuleMenuItemConfig[] = [];
|
|
311
|
-
const authMenus: ModuleMenuItemConfig[] = [];
|
|
312
|
-
const adminMenus: ModuleMenuItemConfig[] = [];
|
|
313
|
-
const accountMenus: ModuleMenuItemConfig[] = [];
|
|
324
|
+
// Collecter les menus de tous les modules avec leurs modules sources
|
|
325
|
+
const publicMenus: (ModuleMenuItemConfig & { moduleName?: string })[] = [];
|
|
326
|
+
const authMenus: (ModuleMenuItemConfig & { moduleName?: string })[] = [];
|
|
327
|
+
const adminMenus: (ModuleMenuItemConfig & { moduleName?: string })[] = [];
|
|
328
|
+
const accountMenus: (ModuleMenuItemConfig & { moduleName?: string })[] = [];
|
|
314
329
|
|
|
315
330
|
moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
|
|
316
331
|
if (moduleConfig.menu) {
|
|
317
332
|
if (moduleConfig.menu.public) {
|
|
318
|
-
publicMenus.push(
|
|
333
|
+
publicMenus.push(
|
|
334
|
+
...moduleConfig.menu.public.map((item) => ({
|
|
335
|
+
...item,
|
|
336
|
+
moduleName: moduleConfig.moduleName,
|
|
337
|
+
})),
|
|
338
|
+
);
|
|
319
339
|
}
|
|
320
340
|
if (moduleConfig.menu.auth) {
|
|
321
|
-
authMenus.push(
|
|
341
|
+
authMenus.push(
|
|
342
|
+
...moduleConfig.menu.auth.map((item) => ({
|
|
343
|
+
...item,
|
|
344
|
+
moduleName: moduleConfig.moduleName,
|
|
345
|
+
})),
|
|
346
|
+
);
|
|
322
347
|
}
|
|
323
348
|
if (moduleConfig.menu.admin) {
|
|
324
|
-
adminMenus.push(
|
|
349
|
+
adminMenus.push(
|
|
350
|
+
...moduleConfig.menu.admin.map((item) => ({
|
|
351
|
+
...item,
|
|
352
|
+
moduleName: moduleConfig.moduleName,
|
|
353
|
+
})),
|
|
354
|
+
);
|
|
325
355
|
}
|
|
326
356
|
if (moduleConfig.menu.account) {
|
|
327
|
-
accountMenus.push(
|
|
357
|
+
accountMenus.push(
|
|
358
|
+
...moduleConfig.menu.account.map((item) => ({
|
|
359
|
+
...item,
|
|
360
|
+
moduleName: moduleConfig.moduleName,
|
|
361
|
+
})),
|
|
362
|
+
);
|
|
328
363
|
}
|
|
329
364
|
}
|
|
330
365
|
});
|
|
@@ -339,9 +374,51 @@ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
339
374
|
adminMenus.sort(sortByOrder);
|
|
340
375
|
accountMenus.sort(sortByOrder);
|
|
341
376
|
|
|
377
|
+
// Collecter les composants à importer dynamiquement
|
|
378
|
+
const componentImports: string[] = [];
|
|
379
|
+
const allMenus = [
|
|
380
|
+
...publicMenus,
|
|
381
|
+
...authMenus,
|
|
382
|
+
...adminMenus,
|
|
383
|
+
...accountMenus,
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
allMenus.forEach((menu, index) => {
|
|
387
|
+
if (menu.componentExport && menu.moduleName) {
|
|
388
|
+
const componentName = `MenuComponent${index}`;
|
|
389
|
+
componentImports.push(
|
|
390
|
+
`const ${componentName} = dynamic(() => import("${menu.moduleName}").then(mod => ({ default: mod.${menu.componentExport} })), { ssr: false });`,
|
|
391
|
+
);
|
|
392
|
+
// Ajouter une référence au composant
|
|
393
|
+
(menu as any).__componentRef = componentName;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Fonction pour préparer les menus avec les composants
|
|
398
|
+
const prepareMenusForExport = (menus: any[]) => {
|
|
399
|
+
return menus.map((menu) => {
|
|
400
|
+
const { moduleName, __componentRef, ...cleanMenu } = menu;
|
|
401
|
+
if (__componentRef) {
|
|
402
|
+
// Retourner une référence au composant au lieu de la sérialiser
|
|
403
|
+
return `{ ...${JSON.stringify(cleanMenu)}, component: ${__componentRef} }`;
|
|
404
|
+
}
|
|
405
|
+
return JSON.stringify(cleanMenu);
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const publicMenusExport = prepareMenusForExport(publicMenus);
|
|
410
|
+
const authMenusExport = prepareMenusForExport(authMenus);
|
|
411
|
+
const adminMenusExport = prepareMenusForExport(adminMenus);
|
|
412
|
+
const accountMenusExport = prepareMenusForExport(accountMenus);
|
|
413
|
+
|
|
342
414
|
// Générer le contenu du fichier
|
|
343
415
|
const content = `// Auto-generated menu configuration
|
|
344
416
|
// Generated from module build configs
|
|
417
|
+
"use client";
|
|
418
|
+
|
|
419
|
+
import dynamic from "next/dynamic";
|
|
420
|
+
|
|
421
|
+
${componentImports.join("\n")}
|
|
345
422
|
|
|
346
423
|
export interface MenuItem {
|
|
347
424
|
title: string;
|
|
@@ -351,6 +428,11 @@ export interface MenuItem {
|
|
|
351
428
|
order?: number;
|
|
352
429
|
shortcut?: string;
|
|
353
430
|
shortcutDisplay?: string;
|
|
431
|
+
type?: 'text' | 'icon' | 'textIcon';
|
|
432
|
+
position?: 'center' | 'end';
|
|
433
|
+
component?: React.ComponentType<any>;
|
|
434
|
+
componentExport?: string;
|
|
435
|
+
entryPoint?: string;
|
|
354
436
|
}
|
|
355
437
|
|
|
356
438
|
export interface MenuConfig {
|
|
@@ -361,15 +443,17 @@ export interface MenuConfig {
|
|
|
361
443
|
}
|
|
362
444
|
|
|
363
445
|
export const menuConfig: MenuConfig = {
|
|
364
|
-
public: ${
|
|
365
|
-
auth: ${
|
|
366
|
-
admin: ${
|
|
367
|
-
account: ${
|
|
446
|
+
public: [${publicMenusExport.join(",\n ")}],
|
|
447
|
+
auth: [${authMenusExport.join(",\n ")}],
|
|
448
|
+
admin: [${adminMenusExport.join(",\n ")}],
|
|
449
|
+
account: [${accountMenusExport.join(",\n ")}],
|
|
368
450
|
};
|
|
369
451
|
`;
|
|
370
452
|
|
|
371
453
|
fs.writeFileSync(menuPath, content);
|
|
372
|
-
|
|
454
|
+
if (isDebugMode) {
|
|
455
|
+
console.log(`🍔 Generated menu configuration: ${menuPath}`);
|
|
456
|
+
}
|
|
373
457
|
}
|
|
374
458
|
|
|
375
459
|
function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
@@ -557,7 +641,9 @@ ${moduleConfigurations.join(",\n")}
|
|
|
557
641
|
`;
|
|
558
642
|
|
|
559
643
|
fs.writeFileSync(docsPagePath, docsContent);
|
|
560
|
-
|
|
644
|
+
if (isDebugMode) {
|
|
645
|
+
console.log(`📚 Generated docs page: ${docsPagePath}`);
|
|
646
|
+
}
|
|
561
647
|
}
|
|
562
648
|
|
|
563
649
|
function buildGroupedApi(
|
|
@@ -595,7 +681,9 @@ function buildGroupedApi(
|
|
|
595
681
|
${content}`;
|
|
596
682
|
|
|
597
683
|
fs.writeFileSync(filePath, contentWithMarker);
|
|
598
|
-
|
|
684
|
+
if (isDebugMode) {
|
|
685
|
+
console.log(`🔌 Generated API route: ${filePath}`);
|
|
686
|
+
}
|
|
599
687
|
}
|
|
600
688
|
|
|
601
689
|
function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
|
|
@@ -663,7 +751,9 @@ function cleanGeneratedFiles() {
|
|
|
663
751
|
try {
|
|
664
752
|
if (!isProtected(itemPath) && fs.readdirSync(itemPath).length === 0) {
|
|
665
753
|
fs.rmdirSync(itemPath);
|
|
666
|
-
|
|
754
|
+
if (isDebugMode) {
|
|
755
|
+
console.log(`📁 Removed empty directory: ${itemPath}`);
|
|
756
|
+
}
|
|
667
757
|
}
|
|
668
758
|
} catch {
|
|
669
759
|
// Ignorer les erreurs de suppression de fichiers
|
|
@@ -681,12 +771,14 @@ function cleanGeneratedFiles() {
|
|
|
681
771
|
content.includes('} from "@lastbrain/module-'))
|
|
682
772
|
) {
|
|
683
773
|
fs.unlinkSync(itemPath);
|
|
684
|
-
|
|
774
|
+
if (isDebugMode) {
|
|
775
|
+
console.log(`🗑️ Removed: ${itemPath}`);
|
|
776
|
+
}
|
|
685
777
|
}
|
|
686
778
|
} catch {
|
|
687
779
|
// Ignorer les erreurs de lecture/suppression
|
|
688
780
|
}
|
|
689
|
-
} else {
|
|
781
|
+
} else if (isDebugMode) {
|
|
690
782
|
console.log(`🔒 Protected file skipped: ${itemPath}`);
|
|
691
783
|
}
|
|
692
784
|
}
|
|
@@ -708,11 +800,15 @@ function cleanGeneratedFiles() {
|
|
|
708
800
|
const filePath = path.join(appDirectory, file);
|
|
709
801
|
if (fs.existsSync(filePath)) {
|
|
710
802
|
fs.unlinkSync(filePath);
|
|
711
|
-
|
|
803
|
+
if (isDebugMode) {
|
|
804
|
+
console.log(`🗑️ Removed: ${filePath}`);
|
|
805
|
+
}
|
|
712
806
|
}
|
|
713
807
|
});
|
|
714
808
|
|
|
715
|
-
|
|
809
|
+
if (isDebugMode) {
|
|
810
|
+
console.log("🧹 Cleanup completed");
|
|
811
|
+
}
|
|
716
812
|
}
|
|
717
813
|
|
|
718
814
|
function generateAppAside() {
|
|
@@ -720,7 +816,9 @@ function generateAppAside() {
|
|
|
720
816
|
|
|
721
817
|
// Ne pas écraser si le fichier existe déjà
|
|
722
818
|
if (fs.existsSync(targetPath)) {
|
|
723
|
-
|
|
819
|
+
if (isDebugMode) {
|
|
820
|
+
console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
|
|
821
|
+
}
|
|
724
822
|
return;
|
|
725
823
|
}
|
|
726
824
|
|
|
@@ -751,7 +849,9 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
|
|
|
751
849
|
try {
|
|
752
850
|
ensureDirectory(path.dirname(targetPath));
|
|
753
851
|
fs.writeFileSync(targetPath, templateContent, "utf-8");
|
|
754
|
-
|
|
852
|
+
if (isDebugMode) {
|
|
853
|
+
console.log(`✅ Generated AppAside component: ${targetPath}`);
|
|
854
|
+
}
|
|
755
855
|
} catch (error) {
|
|
756
856
|
console.error(`❌ Error generating AppAside component: ${error}`);
|
|
757
857
|
}
|
|
@@ -779,11 +879,13 @@ export default function SectionLayout({
|
|
|
779
879
|
try {
|
|
780
880
|
ensureDirectory(path.dirname(authLayoutPath));
|
|
781
881
|
fs.writeFileSync(authLayoutPath, authLayoutContent, "utf-8");
|
|
782
|
-
|
|
882
|
+
if (isDebugMode) {
|
|
883
|
+
console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
|
|
884
|
+
}
|
|
783
885
|
} catch (error) {
|
|
784
886
|
console.error(`❌ Error generating auth layout: ${error}`);
|
|
785
887
|
}
|
|
786
|
-
} else {
|
|
888
|
+
} else if (isDebugMode) {
|
|
787
889
|
console.log(`⏭️ Auth layout already exists, skipping: ${authLayoutPath}`);
|
|
788
890
|
}
|
|
789
891
|
|
|
@@ -808,11 +910,15 @@ export default function AdminLayout({
|
|
|
808
910
|
try {
|
|
809
911
|
ensureDirectory(path.dirname(adminLayoutPath));
|
|
810
912
|
fs.writeFileSync(adminLayoutPath, adminLayoutContent, "utf-8");
|
|
811
|
-
|
|
913
|
+
if (isDebugMode) {
|
|
914
|
+
console.log(
|
|
915
|
+
`✅ Generated admin layout with sidebar: ${adminLayoutPath}`,
|
|
916
|
+
);
|
|
917
|
+
}
|
|
812
918
|
} catch (error) {
|
|
813
919
|
console.error(`❌ Error generating admin layout: ${error}`);
|
|
814
920
|
}
|
|
815
|
-
} else {
|
|
921
|
+
} else if (isDebugMode) {
|
|
816
922
|
console.log(`⏭️ Admin layout already exists, skipping: ${adminLayoutPath}`);
|
|
817
923
|
}
|
|
818
924
|
}
|
|
@@ -860,16 +966,20 @@ export default realtimeConfig;
|
|
|
860
966
|
// Écrire le fichier TypeScript
|
|
861
967
|
fs.writeFileSync(outputPath, content);
|
|
862
968
|
|
|
863
|
-
|
|
864
|
-
|
|
969
|
+
if (isDebugMode) {
|
|
970
|
+
console.log(`✅ Generated realtime configuration: ${outputPath}`);
|
|
971
|
+
console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
|
|
865
972
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
973
|
+
// Afficher un résumé
|
|
974
|
+
realtimeConfigs.forEach((module) => {
|
|
975
|
+
console.log(
|
|
976
|
+
` - ${module.moduleId}: ${module.tables.length} table(s)`,
|
|
977
|
+
);
|
|
978
|
+
module.tables.forEach((table) => {
|
|
979
|
+
console.log(` • ${table.schema}.${table.table} (${table.event})`);
|
|
980
|
+
});
|
|
871
981
|
});
|
|
872
|
-
}
|
|
982
|
+
}
|
|
873
983
|
} catch (error) {
|
|
874
984
|
console.error("❌ Error generating realtime configuration:", error);
|
|
875
985
|
}
|
|
@@ -928,13 +1038,15 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
928
1038
|
// Écrire le fichier TypeScript
|
|
929
1039
|
fs.writeFileSync(outputPath, appContent);
|
|
930
1040
|
|
|
931
|
-
|
|
932
|
-
|
|
1041
|
+
if (isDebugMode) {
|
|
1042
|
+
console.log(`✅ Generated user tabs configuration: ${outputPath}`);
|
|
1043
|
+
console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
|
|
933
1044
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1045
|
+
// Afficher un résumé
|
|
1046
|
+
userTabsConfigs.forEach((tab) => {
|
|
1047
|
+
console.log(` - ${tab.title} (${tab.moduleName})`);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
938
1050
|
|
|
939
1051
|
// Plus de copie vers app/config ni stub core
|
|
940
1052
|
} catch (error) {
|
|
@@ -942,25 +1054,333 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
942
1054
|
}
|
|
943
1055
|
}
|
|
944
1056
|
|
|
1057
|
+
async function generateBucketsConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
1058
|
+
try {
|
|
1059
|
+
// Extraire les configurations storage des modules
|
|
1060
|
+
const allBuckets = moduleConfigs
|
|
1061
|
+
.filter(
|
|
1062
|
+
(config) =>
|
|
1063
|
+
config.storage?.buckets && config.storage.buckets.length > 0,
|
|
1064
|
+
)
|
|
1065
|
+
.flatMap((config) => config.storage!.buckets);
|
|
1066
|
+
|
|
1067
|
+
if (allBuckets.length === 0) {
|
|
1068
|
+
console.log("⏭️ No storage buckets configuration found in modules");
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Générer le contenu du fichier
|
|
1073
|
+
const timestamp = new Date().toISOString();
|
|
1074
|
+
const bucketsEntries = allBuckets
|
|
1075
|
+
.map((bucket) => {
|
|
1076
|
+
// Sérialiser customAccessControl si elle existe
|
|
1077
|
+
const customAccessControl = bucket.customAccessControl
|
|
1078
|
+
? `\n customAccessControl: ${bucket.customAccessControl.toString()},`
|
|
1079
|
+
: "";
|
|
1080
|
+
|
|
1081
|
+
return ` ${bucket.name}: {
|
|
1082
|
+
name: "${bucket.name}",
|
|
1083
|
+
isPublic: ${bucket.public},
|
|
1084
|
+
description: "${bucket.description || `Storage bucket for ${bucket.name}`}",${bucket.allowedMimeTypes ? `\n allowedFileTypes: ${JSON.stringify(bucket.allowedMimeTypes)},` : ""}${bucket.maxFileSize ? `\n maxFileSize: ${bucket.maxFileSize}, // ${bucket.fileSizeLimit || `${Math.round(bucket.maxFileSize / 1024 / 1024)}MB`}` : ""}${customAccessControl}
|
|
1085
|
+
}`;
|
|
1086
|
+
})
|
|
1087
|
+
.join(",\n");
|
|
1088
|
+
|
|
1089
|
+
const content = `/**
|
|
1090
|
+
* Storage configuration for buckets and access control
|
|
1091
|
+
*
|
|
1092
|
+
* GENERATED FILE - DO NOT EDIT MANUALLY
|
|
1093
|
+
* Generated at: ${timestamp}
|
|
1094
|
+
* Generated from module build configs
|
|
1095
|
+
*/
|
|
1096
|
+
|
|
1097
|
+
export interface BucketConfig {
|
|
1098
|
+
name: string;
|
|
1099
|
+
isPublic: boolean;
|
|
1100
|
+
description: string;
|
|
1101
|
+
allowedFileTypes?: string[];
|
|
1102
|
+
maxFileSize?: number; // in bytes
|
|
1103
|
+
customAccessControl?: (userId: string, filePath: string) => boolean;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
|
|
1107
|
+
${bucketsEntries}
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Get bucket configuration
|
|
1112
|
+
*/
|
|
1113
|
+
export function getBucketConfig(bucketName: string): BucketConfig | null {
|
|
1114
|
+
return BUCKET_CONFIGS[bucketName] || null;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Check if bucket is public
|
|
1119
|
+
*/
|
|
1120
|
+
export function isPublicBucket(bucketName: string): boolean {
|
|
1121
|
+
const config = getBucketConfig(bucketName);
|
|
1122
|
+
return config?.isPublic ?? false;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Check if user has access to a specific file
|
|
1127
|
+
*/
|
|
1128
|
+
export function hasFileAccess(bucketName: string, userId: string, filePath: string): boolean {
|
|
1129
|
+
const config = getBucketConfig(bucketName);
|
|
1130
|
+
if (!config) return false;
|
|
1131
|
+
|
|
1132
|
+
// Public buckets are accessible to everyone
|
|
1133
|
+
if (config.isPublic) return true;
|
|
1134
|
+
|
|
1135
|
+
// Private buckets require authentication
|
|
1136
|
+
if (!userId) return false;
|
|
1137
|
+
|
|
1138
|
+
// Apply custom access control if defined
|
|
1139
|
+
if (config.customAccessControl) {
|
|
1140
|
+
return config.customAccessControl(userId, filePath);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Validate file type for bucket
|
|
1148
|
+
*/
|
|
1149
|
+
export function isValidFileType(bucketName: string, contentType: string): boolean {
|
|
1150
|
+
const config = getBucketConfig(bucketName);
|
|
1151
|
+
if (!config || !config.allowedFileTypes) return true;
|
|
1152
|
+
|
|
1153
|
+
return config.allowedFileTypes.includes(contentType);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Check if file size is within bucket limits
|
|
1158
|
+
*/
|
|
1159
|
+
export function isValidFileSize(bucketName: string, fileSize: number): boolean {
|
|
1160
|
+
const config = getBucketConfig(bucketName);
|
|
1161
|
+
if (!config || !config.maxFileSize) return true;
|
|
1162
|
+
|
|
1163
|
+
return fileSize <= config.maxFileSize;
|
|
1164
|
+
}
|
|
1165
|
+
`;
|
|
1166
|
+
|
|
1167
|
+
// Créer le fichier dans lib/
|
|
1168
|
+
const outputPath = path.join(projectRoot, "lib", "bucket-config.ts");
|
|
1169
|
+
const libDir = path.dirname(outputPath);
|
|
1170
|
+
|
|
1171
|
+
// Créer le dossier lib s'il n'existe pas
|
|
1172
|
+
if (!fs.existsSync(libDir)) {
|
|
1173
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Écrire le fichier TypeScript
|
|
1177
|
+
fs.writeFileSync(outputPath, content);
|
|
1178
|
+
|
|
1179
|
+
if (isDebugMode) {
|
|
1180
|
+
console.log(`✅ Generated storage buckets configuration: ${outputPath}`);
|
|
1181
|
+
console.log(`📊 Storage buckets count: ${allBuckets.length}`);
|
|
1182
|
+
|
|
1183
|
+
// Afficher un résumé
|
|
1184
|
+
allBuckets.forEach((bucket) => {
|
|
1185
|
+
const access = bucket.public ? "public" : "private";
|
|
1186
|
+
console.log(` - ${bucket.name} (${access})`);
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
console.error("❌ Error generating buckets configuration:", error);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async function generateStorageProxyApi(moduleConfigs: ModuleBuildConfig[]) {
|
|
1195
|
+
try {
|
|
1196
|
+
// Extraire les configurations storage des modules
|
|
1197
|
+
const allBuckets = moduleConfigs
|
|
1198
|
+
.filter(
|
|
1199
|
+
(config) =>
|
|
1200
|
+
config.storage?.buckets && config.storage.buckets.length > 0,
|
|
1201
|
+
)
|
|
1202
|
+
.flatMap((config) => config.storage!.buckets);
|
|
1203
|
+
|
|
1204
|
+
if (allBuckets.length === 0) {
|
|
1205
|
+
console.log("⏭️ No storage buckets found, skipping proxy API generation");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Identifier les buckets publics et privés
|
|
1210
|
+
const publicBuckets = allBuckets.filter((b) => b.public);
|
|
1211
|
+
const privateBuckets = allBuckets.filter((b) => !b.public);
|
|
1212
|
+
|
|
1213
|
+
// Générer les conditions pour les buckets publics
|
|
1214
|
+
const publicBucketConditions = publicBuckets
|
|
1215
|
+
.map((bucket) => `bucket === "${bucket.name}"`)
|
|
1216
|
+
.filter(Boolean);
|
|
1217
|
+
|
|
1218
|
+
// Ajouter les conditions pour les chemins publics dans les buckets privés
|
|
1219
|
+
const publicPathConditions: string[] = [];
|
|
1220
|
+
if (publicBuckets.some((b) => b.name === "recipes")) {
|
|
1221
|
+
publicPathConditions.push('storagePath.startsWith("recipes/")');
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const allPublicConditions = [
|
|
1225
|
+
...publicBucketConditions,
|
|
1226
|
+
...publicPathConditions,
|
|
1227
|
+
];
|
|
1228
|
+
const publicCondition =
|
|
1229
|
+
allPublicConditions.length > 0
|
|
1230
|
+
? allPublicConditions.join(" || ")
|
|
1231
|
+
: "false";
|
|
1232
|
+
|
|
1233
|
+
const timestamp = new Date().toISOString();
|
|
1234
|
+
const content = `import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
1235
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* GET /api/storage/[bucket]/[...path]
|
|
1239
|
+
* Proxy pour servir les images avec authentication
|
|
1240
|
+
*
|
|
1241
|
+
* GENERATED FILE - DO NOT EDIT MANUALLY
|
|
1242
|
+
* Generated at: ${timestamp}
|
|
1243
|
+
* Generated from module storage configurations
|
|
1244
|
+
*
|
|
1245
|
+
* Buckets configurés:
|
|
1246
|
+
${allBuckets.map((b) => ` * - ${b.name} (${b.public ? "public" : "private"})`).join("\n")}
|
|
1247
|
+
*/
|
|
1248
|
+
export async function GET(
|
|
1249
|
+
request: NextRequest,
|
|
1250
|
+
context: { params: Promise<{ bucket: string; path: string[] }> },
|
|
1251
|
+
) {
|
|
1252
|
+
try {
|
|
1253
|
+
const { bucket, path } = await context.params;
|
|
1254
|
+
const storagePath = path.join("/");
|
|
1255
|
+
|
|
1256
|
+
// Les images publiques sont accessibles sans auth
|
|
1257
|
+
if (${publicCondition}) {
|
|
1258
|
+
const supabase = await getSupabaseServerClient();
|
|
1259
|
+
const { data, error } = await supabase.storage
|
|
1260
|
+
.from(bucket)
|
|
1261
|
+
.createSignedUrl(storagePath, 3600); // 1 heure
|
|
1262
|
+
|
|
1263
|
+
if (error) {
|
|
1264
|
+
console.error(\`[storage] Error creating signed URL for public image:\`, error);
|
|
1265
|
+
return new NextResponse("Not found", { status: 404 });
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Rediriger vers l'URL signée
|
|
1269
|
+
return NextResponse.redirect(data.signedUrl);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Les images privées nécessitent une authentification
|
|
1273
|
+
const supabase = await getSupabaseServerClient();
|
|
1274
|
+
const {
|
|
1275
|
+
data: { user },
|
|
1276
|
+
error: authError,
|
|
1277
|
+
} = await supabase.auth.getUser();
|
|
1278
|
+
|
|
1279
|
+
if (authError || !user) {
|
|
1280
|
+
return new NextResponse("Unauthorized", { status: 401 });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Cas spécial: si le chemin commence par /product/ ou /recipe/,
|
|
1284
|
+
// c'est une image avec format court qui nécessite le préfixe userId
|
|
1285
|
+
let actualStoragePath = storagePath;
|
|
1286
|
+
|
|
1287
|
+
if (storagePath.startsWith("product/") || storagePath.startsWith("recipe/")) {
|
|
1288
|
+
actualStoragePath = \`\${user.id}/\${storagePath}\`;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Vérifier que l'utilisateur a accès à cette image
|
|
1292
|
+
// Format: {userId}/recipe/{recipeId}/{filename} ou {userId}/product/{productId}/{filename}
|
|
1293
|
+
const pathParts = actualStoragePath.split("/");
|
|
1294
|
+
const pathUserId = pathParts[0];
|
|
1295
|
+
|
|
1296
|
+
if (pathUserId !== user.id) {
|
|
1297
|
+
return new NextResponse("Forbidden", { status: 403 });
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Créer une URL signée pour l'image privée
|
|
1301
|
+
const { data, error } = await supabase.storage
|
|
1302
|
+
.from(bucket)
|
|
1303
|
+
.createSignedUrl(actualStoragePath, 3600); // 1 heure
|
|
1304
|
+
|
|
1305
|
+
if (error) {
|
|
1306
|
+
console.error(\`[storage] Error creating signed URL:\`, error);
|
|
1307
|
+
return new NextResponse("Not found", { status: 404 });
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Rediriger vers l'URL signée
|
|
1311
|
+
return NextResponse.redirect(data.signedUrl);
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
console.error("[storage] Error:", error);
|
|
1314
|
+
return new NextResponse("Internal server error", { status: 500 });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
`;
|
|
1318
|
+
|
|
1319
|
+
// Créer le fichier dans app/api/storage/[bucket]/[...path]/
|
|
1320
|
+
const outputPath = path.join(
|
|
1321
|
+
projectRoot,
|
|
1322
|
+
"app",
|
|
1323
|
+
"api",
|
|
1324
|
+
"storage",
|
|
1325
|
+
"[bucket]",
|
|
1326
|
+
"[...path]",
|
|
1327
|
+
"route.ts",
|
|
1328
|
+
);
|
|
1329
|
+
const outputDir = path.dirname(outputPath);
|
|
1330
|
+
|
|
1331
|
+
// Créer le dossier s'il n'existe pas
|
|
1332
|
+
if (!fs.existsSync(outputDir)) {
|
|
1333
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Écrire le fichier TypeScript
|
|
1337
|
+
fs.writeFileSync(outputPath, content);
|
|
1338
|
+
|
|
1339
|
+
if (isDebugMode) {
|
|
1340
|
+
console.log(`✅ Generated storage proxy API: ${outputPath}`);
|
|
1341
|
+
console.log(
|
|
1342
|
+
`📊 Public buckets: ${publicBuckets.map((b) => b.name).join(", ") || "none"}`,
|
|
1343
|
+
);
|
|
1344
|
+
console.log(
|
|
1345
|
+
`📊 Private buckets: ${privateBuckets.map((b) => b.name).join(", ") || "none"}`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
console.error("❌ Error generating storage proxy API:", error);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
945
1353
|
export async function runModuleBuild() {
|
|
946
1354
|
ensureDirectory(appDirectory);
|
|
947
1355
|
|
|
948
1356
|
// Nettoyer les fichiers générés précédemment
|
|
949
|
-
|
|
1357
|
+
if (isDebugMode) {
|
|
1358
|
+
console.log("🧹 Cleaning previously generated files...");
|
|
1359
|
+
}
|
|
950
1360
|
cleanGeneratedFiles();
|
|
951
1361
|
|
|
952
1362
|
const moduleConfigs = await loadModuleConfigs();
|
|
953
|
-
|
|
1363
|
+
if (isDebugMode) {
|
|
1364
|
+
console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
|
|
1365
|
+
}
|
|
954
1366
|
|
|
955
1367
|
// Générer les pages
|
|
1368
|
+
if (isDebugMode) {
|
|
1369
|
+
console.log("\n📝 Generating pages...");
|
|
1370
|
+
}
|
|
956
1371
|
moduleConfigs.forEach((moduleConfig) => {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1372
|
+
if (isDebugMode) {
|
|
1373
|
+
console.log(
|
|
1374
|
+
`📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`,
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
960
1377
|
moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
|
|
961
1378
|
});
|
|
962
1379
|
|
|
963
1380
|
// Grouper les APIs par chemin pour éviter les écrasements de fichier
|
|
1381
|
+
if (isDebugMode) {
|
|
1382
|
+
console.log("\n🔌 Generating API routes...");
|
|
1383
|
+
}
|
|
964
1384
|
const apisByPath = new Map<
|
|
965
1385
|
string,
|
|
966
1386
|
Array<{ moduleConfig: ModuleBuildConfig; api: ModuleApiConfig }>
|
|
@@ -988,10 +1408,35 @@ export async function runModuleBuild() {
|
|
|
988
1408
|
copyModuleMigrations(moduleConfigs);
|
|
989
1409
|
|
|
990
1410
|
// Générer la configuration realtime
|
|
991
|
-
|
|
1411
|
+
if (isDebugMode) {
|
|
1412
|
+
console.log("🔄 Generating realtime configuration...");
|
|
1413
|
+
}
|
|
992
1414
|
await generateRealtimeConfig(moduleConfigs);
|
|
993
1415
|
|
|
994
1416
|
// Générer la configuration des user tabs
|
|
995
|
-
|
|
1417
|
+
if (isDebugMode) {
|
|
1418
|
+
console.log("📑 Generating user tabs configuration...");
|
|
1419
|
+
}
|
|
996
1420
|
await generateUserTabsConfig(moduleConfigs);
|
|
1421
|
+
|
|
1422
|
+
// Générer la configuration des buckets storage
|
|
1423
|
+
if (isDebugMode) {
|
|
1424
|
+
console.log("🗄️ Generating storage buckets configuration...");
|
|
1425
|
+
}
|
|
1426
|
+
await generateBucketsConfig(moduleConfigs);
|
|
1427
|
+
|
|
1428
|
+
// Générer le proxy storage API
|
|
1429
|
+
if (isDebugMode) {
|
|
1430
|
+
console.log("🔌 Generating storage proxy API...");
|
|
1431
|
+
}
|
|
1432
|
+
await generateStorageProxyApi(moduleConfigs);
|
|
1433
|
+
|
|
1434
|
+
// Message de succès final
|
|
1435
|
+
if (!isDebugMode) {
|
|
1436
|
+
console.log("\n✅ Module build completed successfully!");
|
|
1437
|
+
console.log(`📦 ${moduleConfigs.length} module(s) processed`);
|
|
1438
|
+
console.log("💡 Use --debug flag for detailed logs\n");
|
|
1439
|
+
} else {
|
|
1440
|
+
console.log("\n✅ Module build completed (debug mode)\n");
|
|
1441
|
+
}
|
|
997
1442
|
}
|