@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.
@@ -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
- console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
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
- console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
177
- } else {
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 (page.section === "auth" && modulePrefix === "auth") {
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
- console.log(`⭐ Generated page: ${filePath}`);
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
- console.log(`🧭 Generated navigation metadata: ${navPath}`);
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(...moduleConfig.menu.public);
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(...moduleConfig.menu.auth);
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(...moduleConfig.menu.admin);
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(...moduleConfig.menu.account);
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: ${JSON.stringify(publicMenus, null, 2)},
365
- auth: ${JSON.stringify(authMenus, null, 2)},
366
- admin: ${JSON.stringify(adminMenus, null, 2)},
367
- account: ${JSON.stringify(accountMenus, null, 2)},
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
- console.log(`🍔 Generated menu configuration: ${menuPath}`);
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
- console.log(`📚 Generated docs page: ${docsPagePath}`);
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
- console.log(`🔌 Generated API route: ${filePath}`);
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
- console.log(`🗑️ Removed empty directory: ${itemPath}`);
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
- console.log(`🗑️ Cleaned generated file: ${itemPath}`);
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
- console.log(`🗑️ Cleaned root file: ${filePath}`);
803
+ if (isDebugMode) {
804
+ console.log(`🗑️ Removed: ${filePath}`);
805
+ }
712
806
  }
713
807
  });
714
808
 
715
- console.log("🧹 Cleanup completed");
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
- console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
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
- console.log(`✅ Generated AppAside component: ${targetPath}`);
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
- console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
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
- console.log(`✅ Generated admin layout with sidebar: ${adminLayoutPath}`);
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
- console.log(`✅ Generated realtime configuration: ${outputPath}`);
864
- console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
969
+ if (isDebugMode) {
970
+ console.log(`✅ Generated realtime configuration: ${outputPath}`);
971
+ console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
865
972
 
866
- // Afficher un résumé
867
- realtimeConfigs.forEach((module) => {
868
- console.log(` - ${module.moduleId}: ${module.tables.length} table(s)`);
869
- module.tables.forEach((table) => {
870
- console.log(` • ${table.schema}.${table.table} (${table.event})`);
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
- console.log(`✅ Generated user tabs configuration: ${outputPath}`);
932
- console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
1041
+ if (isDebugMode) {
1042
+ console.log(`✅ Generated user tabs configuration: ${outputPath}`);
1043
+ console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
933
1044
 
934
- // Afficher un résumé
935
- userTabsConfigs.forEach((tab) => {
936
- console.log(` - ${tab.title} (${tab.moduleName})`);
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
- console.log("🧹 Cleaning previously generated files...");
1357
+ if (isDebugMode) {
1358
+ console.log("🧹 Cleaning previously generated files...");
1359
+ }
950
1360
  cleanGeneratedFiles();
951
1361
 
952
1362
  const moduleConfigs = await loadModuleConfigs();
953
- console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
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
- console.log(
958
- `📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`,
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
- console.log("🔄 Generating realtime configuration...");
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
- console.log("📑 Generating user tabs configuration...");
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
  }