@lastbrain/app 0.1.40 → 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.
@@ -321,25 +321,45 @@ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
321
321
 
322
322
  const menuPath = path.join(configDir, "menu.ts");
323
323
 
324
- // Collecter les menus de tous les modules
325
- const publicMenus: ModuleMenuItemConfig[] = [];
326
- const authMenus: ModuleMenuItemConfig[] = [];
327
- const adminMenus: ModuleMenuItemConfig[] = [];
328
- 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 })[] = [];
329
329
 
330
330
  moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
331
331
  if (moduleConfig.menu) {
332
332
  if (moduleConfig.menu.public) {
333
- publicMenus.push(...moduleConfig.menu.public);
333
+ publicMenus.push(
334
+ ...moduleConfig.menu.public.map((item) => ({
335
+ ...item,
336
+ moduleName: moduleConfig.moduleName,
337
+ })),
338
+ );
334
339
  }
335
340
  if (moduleConfig.menu.auth) {
336
- authMenus.push(...moduleConfig.menu.auth);
341
+ authMenus.push(
342
+ ...moduleConfig.menu.auth.map((item) => ({
343
+ ...item,
344
+ moduleName: moduleConfig.moduleName,
345
+ })),
346
+ );
337
347
  }
338
348
  if (moduleConfig.menu.admin) {
339
- adminMenus.push(...moduleConfig.menu.admin);
349
+ adminMenus.push(
350
+ ...moduleConfig.menu.admin.map((item) => ({
351
+ ...item,
352
+ moduleName: moduleConfig.moduleName,
353
+ })),
354
+ );
340
355
  }
341
356
  if (moduleConfig.menu.account) {
342
- accountMenus.push(...moduleConfig.menu.account);
357
+ accountMenus.push(
358
+ ...moduleConfig.menu.account.map((item) => ({
359
+ ...item,
360
+ moduleName: moduleConfig.moduleName,
361
+ })),
362
+ );
343
363
  }
344
364
  }
345
365
  });
@@ -354,9 +374,51 @@ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
354
374
  adminMenus.sort(sortByOrder);
355
375
  accountMenus.sort(sortByOrder);
356
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
+
357
414
  // Générer le contenu du fichier
358
415
  const content = `// Auto-generated menu configuration
359
416
  // Generated from module build configs
417
+ "use client";
418
+
419
+ import dynamic from "next/dynamic";
420
+
421
+ ${componentImports.join("\n")}
360
422
 
361
423
  export interface MenuItem {
362
424
  title: string;
@@ -366,6 +428,11 @@ export interface MenuItem {
366
428
  order?: number;
367
429
  shortcut?: string;
368
430
  shortcutDisplay?: string;
431
+ type?: 'text' | 'icon' | 'textIcon';
432
+ position?: 'center' | 'end';
433
+ component?: React.ComponentType<any>;
434
+ componentExport?: string;
435
+ entryPoint?: string;
369
436
  }
370
437
 
371
438
  export interface MenuConfig {
@@ -376,10 +443,10 @@ export interface MenuConfig {
376
443
  }
377
444
 
378
445
  export const menuConfig: MenuConfig = {
379
- public: ${JSON.stringify(publicMenus, null, 2)},
380
- auth: ${JSON.stringify(authMenus, null, 2)},
381
- admin: ${JSON.stringify(adminMenus, null, 2)},
382
- 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 ")}],
383
450
  };
384
451
  `;
385
452
 
@@ -987,6 +1054,302 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
987
1054
  }
988
1055
  }
989
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
+
990
1353
  export async function runModuleBuild() {
991
1354
  ensureDirectory(appDirectory);
992
1355
 
@@ -1056,6 +1419,18 @@ export async function runModuleBuild() {
1056
1419
  }
1057
1420
  await generateUserTabsConfig(moduleConfigs);
1058
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
+
1059
1434
  // Message de succès final
1060
1435
  if (!isDebugMode) {
1061
1436
  console.log("\n✅ Module build completed successfully!");