90dc-core 1.19.7 → 1.19.9

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,8 @@ export declare class ImagesClient {
19
19
  updateProgressPhotoDate(input: t.UpdateProgressPhotoDateInput): Promise<t.UserProgressPhotoRecord | null>;
20
20
  deleteProgressPhoto(uuid: string, userUuid: string): Promise<boolean>;
21
21
  private migrateLegacyPhotosForUser;
22
+ private getSignedUrl;
23
+ private decryptLegacyImage;
22
24
  private base64ToBuffer;
23
25
  private handleError;
24
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ImagesClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/ImagesClient.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,KAAK,CAAC,MAAM,yBAAyB,CAAC;AAalD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,GAAG,CAAgB;gBAEf,MAAM,EAAE,CAAC,CAAC,MAAM;IAa5B,OAAO,CAAC,aAAa;IA4CR,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IActD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAkBnD,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAIxD,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAalF,gBAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/D,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,YAAY,GAAG,SAAS,CAAC;IAiBxE,qBAAqB,CAChC,KAAK,EAAE,CAAC,CAAC,sBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;IAkC/B,qBAAqB,CACzB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC;IA2BjC,0BAA0B,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC;IA2BjC,uBAAuB,CAC3B,KAAK,EAAE,CAAC,CAAC,4BAA4B,GACpC,OAAO,CAAC,CAAC,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAuBtC,mBAAmB,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC;YA4BL,0BAA0B;IAiGxC,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;CAOpB"}
1
+ {"version":3,"file":"ImagesClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/ImagesClient.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,KAAK,CAAC,MAAM,yBAAyB,CAAC;AAalD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,GAAG,CAAgB;gBAEf,MAAM,EAAE,CAAC,CAAC,MAAM;IAa5B,OAAO,CAAC,aAAa;IA4CR,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IActD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAkBnD,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAIxD,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAalF,gBAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/D,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,YAAY,GAAG,SAAS,CAAC;IAiBxE,qBAAqB,CAChC,KAAK,EAAE,CAAC,CAAC,sBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;IAkC/B,qBAAqB,CACzB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC;IA2CjC,0BAA0B,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC;IA2CjC,uBAAuB,CAC3B,KAAK,EAAE,CAAC,CAAC,4BAA4B,GACpC,OAAO,CAAC,CAAC,CAAC,uBAAuB,GAAG,IAAI,CAAC;IAuBtC,mBAAmB,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC;YA4BL,0BAA0B;YAgI1B,YAAY;IAgC1B,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;CAOpB"}
@@ -1,6 +1,7 @@
1
1
  import admin from "firebase-admin";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { readFileSync } from "fs";
4
+ import { createDecipheriv } from "crypto";
4
5
  import { ExternalAPIError } from "../Errors/AppError.js";
5
6
  import { Log } from "../utils/Logger.js";
6
7
  import { validateBase64Image } from "../utils/imageValidation.js";
@@ -18,7 +19,7 @@ const RECIPE_PHOTO_TYPES = [
18
19
  ];
19
20
  const imageName = {
20
21
  avatar: (userUuid)=>`${userUuid}--avatar.png`,
21
- progress: (userUuid, day, programId)=>`${userUuid}--${day}day--${programId}.png`,
22
+ progress: (userUuid, day, programId)=>`progress/${userUuid}--${day}day--${programId}.png`,
22
23
  recipe: (recipeUuid, type)=>`${recipeUuid}--${type}.png`
23
24
  };
24
25
  export class ImagesClient {
@@ -227,9 +228,19 @@ export class ImagesClient {
227
228
  'updatedAt'
228
229
  ]
229
230
  });
230
- return migratedRecords;
231
+ // Generate signed URLs for migrated records
232
+ const withUrls = await Promise.all(migratedRecords.map(async (record)=>({
233
+ ...record,
234
+ url: await this.getSignedUrl(record.firebasePath)
235
+ })));
236
+ return withUrls;
231
237
  }
232
- return records;
238
+ // Generate signed URLs for all records
239
+ const withUrls = await Promise.all(records.map(async (record)=>({
240
+ ...record,
241
+ url: await this.getSignedUrl(record.firebasePath)
242
+ })));
243
+ return withUrls;
233
244
  }
234
245
  async getProgressPhotosByProgram(programId, userUuid) {
235
246
  const records = await UserProgressPhoto.findAll({
@@ -282,9 +293,19 @@ export class ImagesClient {
282
293
  'updatedAt'
283
294
  ]
284
295
  });
285
- return migratedRecords;
296
+ // Generate signed URLs
297
+ const withUrls = await Promise.all(migratedRecords.map(async (record)=>({
298
+ ...record,
299
+ url: await this.getSignedUrl(record.firebasePath)
300
+ })));
301
+ return withUrls;
286
302
  }
287
- return records;
303
+ // Generate signed URLs for all records
304
+ const withUrls = await Promise.all(records.map(async (record)=>({
305
+ ...record,
306
+ url: await this.getSignedUrl(record.firebasePath)
307
+ })));
308
+ return withUrls;
288
309
  }
289
310
  async updateProgressPhotoDate(input) {
290
311
  const [affectedRows] = await UserProgressPhoto.update({
@@ -340,11 +361,11 @@ export class ImagesClient {
340
361
  this.logger.info("Starting legacy photo migration", {
341
362
  userUuid
342
363
  });
343
- // List all files in the legacy path pattern: {userUuid}--{day}day--{programId}.png
364
+ // List all files in the legacy /progress folder with pattern: progress/{userUuid}--{day}day--{programId}.png
344
365
  let files;
345
366
  try {
346
367
  [files] = await this.bucket.getFiles({
347
- prefix: `${userUuid}--`
368
+ prefix: `progress/${userUuid}--`
348
369
  });
349
370
  } catch (authError) {
350
371
  this.logger.error("Firebase authentication failed during migration, skipping", {
@@ -363,16 +384,25 @@ export class ImagesClient {
363
384
  let skippedCount = 0;
364
385
  for (const file of files){
365
386
  const fileName = file.name;
366
- // Parse legacy filename: {userUuid}--{day}day--{programId}.png
367
- const match = fileName.match(/^(.+)--(\d+)day--(.+)\.png$/);
387
+ // Parse legacy filename: progress/{userUuid}--{day}day--{programId}.png
388
+ const match = fileName.match(/^progress\/(.+)--(\d+)day--(.+)\.png$/);
368
389
  if (!match) {
369
390
  this.logger.warn("Skipping file with unexpected name format", {
370
391
  fileName
371
392
  });
372
393
  continue;
373
394
  }
374
- const [, , dayStr, programId] = match;
395
+ const [, fileUserUuid, dayStr, programId] = match;
375
396
  const day = parseInt(dayStr, 10);
397
+ // Verify this file belongs to the user
398
+ if (fileUserUuid !== userUuid) {
399
+ this.logger.warn("File doesn't belong to this user, skipping", {
400
+ fileName,
401
+ userUuid,
402
+ fileUserUuid
403
+ });
404
+ continue;
405
+ }
376
406
  // Check if this photo was already migrated (by programId + userUuid + approximate day)
377
407
  const existing = await UserProgressPhoto.findOne({
378
408
  where: {
@@ -387,35 +417,69 @@ export class ImagesClient {
387
417
  });
388
418
  continue;
389
419
  }
390
- // Download the legacy photo
391
- const [contents] = await file.download();
392
- // Calculate photoDate based on day offset (we don't have program start date, so use upload time)
393
- // In reality, this would need Program model to calculate exact date
394
- const photoDate = new Date(); // Fallback: use current time
395
- photoDate.setDate(photoDate.getDate() - (90 - day)); // Rough estimate based on day
396
- // Generate new UUID and path
397
- const newUuid = uuidv4();
398
- const newFirebasePath = `progress-pictures/${userUuid}/${newUuid}.png`;
399
- // Copy to new Firebase location
400
- await this.bucket.file(newFirebasePath).save(contents, {
401
- metadata: {
402
- contentType: "image/png"
420
+ try {
421
+ // Download the legacy photo (encrypted in storage)
422
+ this.logger.debug("Downloading legacy photo", {
423
+ fileName
424
+ });
425
+ const [encryptedContents] = await file.download();
426
+ // Decrypt the legacy photo
427
+ this.logger.debug("Decrypting legacy photo", {
428
+ fileName,
429
+ size: encryptedContents.length
430
+ });
431
+ const decryptedBuffer = this.decryptLegacyImage(encryptedContents);
432
+ this.logger.debug("Decryption successful", {
433
+ fileName,
434
+ decryptedSize: decryptedBuffer.length
435
+ });
436
+ // Calculate photoDate based on day offset
437
+ const photoDate = new Date();
438
+ photoDate.setDate(photoDate.getDate() - (90 - day));
439
+ // Generate new UUID and path
440
+ const newUuid = uuidv4();
441
+ const newFirebasePath = `progress-pictures/${userUuid}/${newUuid}.png`;
442
+ // Save decrypted (plain) image to new Firebase location
443
+ this.logger.debug("Uploading to new location", {
444
+ newFirebasePath
445
+ });
446
+ await this.bucket.file(newFirebasePath).save(decryptedBuffer, {
447
+ metadata: {
448
+ contentType: "image/png"
449
+ }
450
+ });
451
+ this.logger.debug("Upload successful", {
452
+ newFirebasePath
453
+ });
454
+ // Verify file exists before creating database record
455
+ const [exists] = await this.bucket.file(newFirebasePath).exists();
456
+ if (!exists) {
457
+ throw new Error("File was not saved to Firebase Storage");
403
458
  }
404
- });
405
- // Create database record
406
- await UserProgressPhoto.create({
407
- uuid: newUuid,
408
- userUuid,
409
- firebasePath: newFirebasePath,
410
- photoDate,
411
- programId
412
- });
413
- this.logger.info("Migrated legacy photo", {
414
- oldPath: fileName,
415
- newPath: newFirebasePath,
416
- day,
417
- programId
418
- });
459
+ // Create database record only after successful upload
460
+ await UserProgressPhoto.create({
461
+ uuid: newUuid,
462
+ userUuid,
463
+ firebasePath: newFirebasePath,
464
+ photoDate,
465
+ programId
466
+ });
467
+ migratedCount++;
468
+ this.logger.info("Successfully migrated legacy photo", {
469
+ oldPath: fileName,
470
+ newPath: newFirebasePath,
471
+ day,
472
+ programId
473
+ });
474
+ } catch (photoError) {
475
+ this.logger.error("Failed to migrate photo, skipping", {
476
+ fileName,
477
+ error: photoError instanceof Error ? photoError.message : String(photoError),
478
+ stack: photoError instanceof Error ? photoError.stack : undefined
479
+ });
480
+ skippedCount++;
481
+ continue;
482
+ }
419
483
  }
420
484
  this.logger.info("Legacy photo migration completed", {
421
485
  userUuid,
@@ -430,6 +494,65 @@ export class ImagesClient {
430
494
  // Don't throw - migration failure shouldn't break the request
431
495
  }
432
496
  }
497
+ async getSignedUrl(firebasePath) {
498
+ try {
499
+ const file = this.bucket.file(firebasePath);
500
+ // Check if file exists first
501
+ const [exists] = await file.exists();
502
+ if (!exists) {
503
+ this.logger.warn("File not found for signed URL generation", {
504
+ firebasePath
505
+ });
506
+ return undefined;
507
+ }
508
+ // Generate signed URL with proper expiration date
509
+ const expirationDate = new Date();
510
+ expirationDate.setDate(expirationDate.getDate() + 7); // 7 days from now
511
+ const [url] = await file.getSignedUrl({
512
+ action: "read",
513
+ expires: expirationDate
514
+ });
515
+ this.logger.debug("Generated signed URL", {
516
+ firebasePath
517
+ });
518
+ return url;
519
+ } catch (error) {
520
+ this.logger.error("Failed to generate signed URL", {
521
+ firebasePath,
522
+ error: error instanceof Error ? error.message : String(error),
523
+ errorDetails: error
524
+ });
525
+ return undefined;
526
+ }
527
+ }
528
+ decryptLegacyImage(encryptedBuffer) {
529
+ try {
530
+ const encryptionKey = process.env.ENCRYPTION_KEY;
531
+ if (!encryptionKey) {
532
+ throw new Error("ENCRYPTION_KEY not found in environment");
533
+ }
534
+ // Extract IV from first 16 bytes
535
+ const iv = encryptedBuffer.slice(0, 16);
536
+ const encryptedData = encryptedBuffer.slice(16);
537
+ // Decrypt using AES-256-CBC
538
+ const decipher = createDecipheriv("aes-256-cbc", Buffer.from(encryptionKey), iv);
539
+ const decryptedData = Buffer.concat([
540
+ decipher.update(encryptedData),
541
+ decipher.final()
542
+ ]);
543
+ // Original implementation: "data:image/jpeg;base64," + decryptedImageData.toString('base64').slice(20, ...)
544
+ // It converts to base64, slices 20 chars (removing "dataimage/jpegbase64" garbage), then returns
545
+ // We need to replicate this: base64 encode, slice 20 chars, base64 decode
546
+ const base64String = decryptedData.toString('base64');
547
+ const slicedBase64 = base64String.slice(20);
548
+ return Buffer.from(slicedBase64, 'base64');
549
+ } catch (error) {
550
+ this.logger.error("Failed to decrypt legacy image", {
551
+ error
552
+ });
553
+ throw new Error("Image decryption failed");
554
+ }
555
+ }
433
556
  base64ToBuffer(base64) {
434
557
  const data = base64.includes(",") ? base64.split(",")[1] : base64;
435
558
  return Buffer.from(data, "base64");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/clients/ImagesClient.ts"],"sourcesContent":["import admin from \"firebase-admin\";\nimport { Bucket } from \"@google-cloud/storage\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { readFileSync } from \"fs\";\nimport { ExternalAPIError } from \"../Errors/AppError.js\";\nimport { Log } from \"../utils/Logger.js\";\nimport { validateBase64Image } from \"../utils/imageValidation.js\";\nimport { UserProgressPhoto } from \"../dbmodels/user/UserProgressPhoto.js\";\nimport type * as t from \"./types/images.types.js\";\n\nconst PROGRESS_DAYS = [1, 30, 60, 90];\n\ntype RecipePhotoType = \"ingredients\" | \"cover\" | \"method\";\nconst RECIPE_PHOTO_TYPES: RecipePhotoType[] = [\"ingredients\", \"cover\", \"method\"];\n\nconst imageName = {\n avatar: (userUuid: string) => `${userUuid}--avatar.png`,\n progress: (userUuid: string, day: number, programId: string) => `${userUuid}--${day}day--${programId}.png`,\n recipe: (recipeUuid: string, type: RecipePhotoType) => `${recipeUuid}--${type}.png`,\n};\n\nexport class ImagesClient {\n private bucket: Bucket;\n private logger = Log.getInstance().extend(\"images-client\");\n private app: admin.app.App;\n\n constructor(config: t.Config) {\n if (config.firebaseApp) {\n // Use provided Firebase app\n this.app = config.firebaseApp;\n } else {\n // Initialize Firebase if needed (following FirebasePushNotificationClient pattern)\n this.initializeApp(config.storageBucket);\n this.app = admin.app();\n }\n\n this.bucket = this.app.storage().bucket(config.storageBucket);\n }\n\n private initializeApp(storageBucket: string): void {\n try {\n // Check if app already exists\n if (admin.apps?.length && admin.apps.length > 0) {\n this.logger.info(\"Using existing Firebase app\");\n return;\n }\n\n // Try to load service account from environment (for local development)\n const serviceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;\n\n if (serviceAccountPath) {\n // Local development with service account file\n const serviceAccount = JSON.parse(readFileSync(serviceAccountPath, \"utf-8\"));\n\n admin.initializeApp({\n credential: admin.credential.cert(serviceAccount),\n storageBucket,\n });\n\n this.logger.info(\"Firebase initialized with service account file\", {\n projectId: serviceAccount.project_id,\n storageBucket,\n });\n } else {\n // Production: use application default credentials\n admin.initializeApp({\n credential: admin.credential.applicationDefault(),\n storageBucket,\n });\n\n this.logger.info(\"Firebase initialized with application default credentials\", {\n storageBucket,\n });\n }\n } catch (error) {\n this.logger.error(\"Failed to initialize Firebase Admin SDK\", { error });\n throw new ExternalAPIError(\n \"Failed to initialize Firebase\",\n `Error: ${String(error)}`\n );\n }\n }\n\n public async saveImage(base64: string, name: string): Promise<void> {\n try {\n this.logger.info(\"Saving image\", { name });\n\n const buffer = this.base64ToBuffer(base64);\n const file = this.bucket.file(name);\n await file.save(buffer, { contentType: \"image/png\" });\n\n this.logger.info(\"Image saved successfully\", { name });\n } catch (error) {\n this.handleError(error, \"saveImage\");\n }\n }\n\n public async getImage(name: string): Promise<string | undefined> {\n try {\n this.logger.info(\"Getting image\", { name });\n\n const file = this.bucket.file(name);\n const [exists] = await file.exists();\n if (!exists) {\n return undefined;\n }\n\n const [contents] = await file.download();\n return \"data:image/jpeg;base64,\" + contents.toString(\"base64\");\n } catch (error) {\n this.logger.error(`Failed to get image: ${name}`, { error });\n return undefined;\n }\n }\n\n public async saveAvatar(userUuid: string, base64: string): Promise<void> {\n await this.saveImage(base64, imageName.avatar(userUuid));\n }\n\n public async getAvatar(userUuid: string): Promise<string | undefined> {\n return this.getImage(imageName.avatar(userUuid));\n }\n\n public async saveProgressPhoto(userUuid: string, day: number, programId: string, base64: string): Promise<void> {\n await this.saveImage(base64, imageName.progress(userUuid, day, programId));\n }\n\n public async getProgressPhotos(userUuid: string, programId: string): Promise<t.ProgressPhoto[]> {\n const photos: t.ProgressPhoto[] = [];\n\n for (const day of PROGRESS_DAYS) {\n const base64 = await this.getImage(imageName.progress(userUuid, day, programId));\n if (base64) {\n photos.push({ day, base64 });\n }\n }\n\n return photos;\n }\n\n public async saveRecipePhotos(input: t.SaveRecipePhotosInput): Promise<void> {\n await Promise.all([\n this.saveImage(input.ingredientBase64, imageName.recipe(input.recipeUuid, \"ingredients\")),\n this.saveImage(input.coverBase64, imageName.recipe(input.recipeUuid, \"cover\")),\n this.saveImage(input.methodBase64, imageName.recipe(input.recipeUuid, \"method\")),\n ]);\n }\n\n public async getRecipePhotos(recipeUuid: string): Promise<t.RecipePhotos | undefined> {\n const results = await Promise.all(\n RECIPE_PHOTO_TYPES.map((type) => this.getImage(imageName.recipe(recipeUuid, type)))\n );\n\n if (results.every((r) => !r)) {\n return undefined;\n }\n\n // BUG: cover/method swapped — preserved from original ImageController.ts to keep backward compat with frontend\n return {\n ingredientBase64: results[0] ?? \"\",\n methodBase64: results[1] ?? \"\",\n coverBase64: results[2] ?? \"\",\n };\n }\n\n public async saveUserProgressPhoto(\n input: t.UserProgressPhotoInput\n ): Promise<t.UserProgressPhotoRecord> {\n // Validate image\n validateBase64Image(input.base64);\n\n // Generate UUID for the photo\n const uuid = uuidv4();\n\n // Prepare Firebase path\n const firebasePath = `progress-pictures/${input.userUuid}/${uuid}.png`;\n\n // Upload to Firebase Storage\n const base64Data = input.base64.replace(/^data:image\\/\\w+;base64,/, \"\");\n const buffer = Buffer.from(base64Data, \"base64\");\n\n const file = this.bucket.file(firebasePath);\n await file.save(buffer, {\n metadata: {\n contentType: \"image/png\",\n },\n });\n\n // Save metadata to database\n const photoDate = input.photoDate || new Date();\n const record = await UserProgressPhoto.create({\n uuid,\n userUuid: input.userUuid,\n firebasePath,\n photoDate,\n programId: input.programId || null,\n });\n\n return record.toJSON() as t.UserProgressPhotoRecord;\n }\n\n async getUserProgressPhotos(\n userUuid: string\n ): Promise<t.UserProgressPhotoRecord[]> {\n const records = await UserProgressPhoto.findAll({\n where: { userUuid },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n // If no photos in new system, attempt to migrate from legacy Firebase structure\n if (records.length === 0) {\n this.logger.info(\"No photos in new system, checking for legacy photos to migrate\", { userUuid });\n await this.migrateLegacyPhotosForUser(userUuid);\n\n // Re-fetch after migration\n const migratedRecords = await UserProgressPhoto.findAll({\n where: { userUuid },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n return migratedRecords as t.UserProgressPhotoRecord[];\n }\n\n return records as t.UserProgressPhotoRecord[];\n }\n\n async getProgressPhotosByProgram(\n programId: string,\n userUuid?: string\n ): Promise<t.UserProgressPhotoRecord[]> {\n const records = await UserProgressPhoto.findAll({\n where: { programId },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n // If no photos found and userUuid provided, attempt migration\n if (records.length === 0 && userUuid) {\n this.logger.info(\"No photos found for program, attempting migration\", { programId, userUuid });\n await this.migrateLegacyPhotosForUser(userUuid);\n\n // Re-fetch after migration\n const migratedRecords = await UserProgressPhoto.findAll({\n where: { programId },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n return migratedRecords as t.UserProgressPhotoRecord[];\n }\n\n return records as t.UserProgressPhotoRecord[];\n }\n\n async updateProgressPhotoDate(\n input: t.UpdateProgressPhotoDateInput\n ): Promise<t.UserProgressPhotoRecord | null> {\n const [affectedRows] = await UserProgressPhoto.update(\n { photoDate: input.photoDate },\n {\n where: {\n uuid: input.uuid,\n userUuid: input.userUuid, // Ensure user owns the photo\n },\n }\n );\n\n if (affectedRows === 0) {\n return null; // Photo not found or user doesn't own it\n }\n\n const updated = await UserProgressPhoto.findOne({\n where: { uuid: input.uuid },\n raw: true,\n });\n\n return updated as t.UserProgressPhotoRecord;\n }\n\n async deleteProgressPhoto(\n uuid: string,\n userUuid: string\n ): Promise<boolean> {\n // Find the photo to get the firebasePath\n const photo = await UserProgressPhoto.findOne({\n where: { uuid, userUuid },\n raw: true,\n });\n\n if (!photo) {\n return false; // Photo not found or user doesn't own it\n }\n\n // Delete from Firebase Storage\n const file = this.bucket.file(photo.firebasePath);\n try {\n await file.delete();\n } catch (error) {\n // If file doesn't exist in storage, continue with database deletion\n console.warn(`Failed to delete file from storage: ${photo.firebasePath}`, error);\n }\n\n // Delete from database\n await UserProgressPhoto.destroy({\n where: { uuid, userUuid },\n });\n\n return true;\n }\n\n private async migrateLegacyPhotosForUser(userUuid: string): Promise<void> {\n try {\n this.logger.info(\"Starting legacy photo migration\", { userUuid });\n\n // List all files in the legacy path pattern: {userUuid}--{day}day--{programId}.png\n let files;\n try {\n [files] = await this.bucket.getFiles({\n prefix: `${userUuid}--`,\n });\n } catch (authError) {\n this.logger.error(\"Firebase authentication failed during migration, skipping\", {\n userUuid,\n error: authError instanceof Error ? authError.message : String(authError),\n });\n return; // Skip migration if auth fails\n }\n\n if (files.length === 0) {\n this.logger.info(\"No legacy photos found for user\", { userUuid });\n return;\n }\n\n let migratedCount = 0;\n let skippedCount = 0;\n\n for (const file of files) {\n const fileName = file.name;\n\n // Parse legacy filename: {userUuid}--{day}day--{programId}.png\n const match = fileName.match(/^(.+)--(\\d+)day--(.+)\\.png$/);\n if (!match) {\n this.logger.warn(\"Skipping file with unexpected name format\", { fileName });\n continue;\n }\n\n const [, , dayStr, programId] = match;\n const day = parseInt(dayStr, 10);\n\n // Check if this photo was already migrated (by programId + userUuid + approximate day)\n const existing = await UserProgressPhoto.findOne({\n where: { userUuid, programId },\n raw: true,\n });\n\n if (existing) {\n this.logger.debug(\"Photo already migrated, skipping\", { fileName });\n continue;\n }\n\n // Download the legacy photo\n const [contents] = await file.download();\n\n // Calculate photoDate based on day offset (we don't have program start date, so use upload time)\n // In reality, this would need Program model to calculate exact date\n const photoDate = new Date(); // Fallback: use current time\n photoDate.setDate(photoDate.getDate() - (90 - day)); // Rough estimate based on day\n\n // Generate new UUID and path\n const newUuid = uuidv4();\n const newFirebasePath = `progress-pictures/${userUuid}/${newUuid}.png`;\n\n // Copy to new Firebase location\n await this.bucket.file(newFirebasePath).save(contents, {\n metadata: {\n contentType: \"image/png\",\n },\n });\n\n // Create database record\n await UserProgressPhoto.create({\n uuid: newUuid,\n userUuid,\n firebasePath: newFirebasePath,\n photoDate,\n programId,\n });\n\n this.logger.info(\"Migrated legacy photo\", {\n oldPath: fileName,\n newPath: newFirebasePath,\n day,\n programId,\n });\n }\n\n this.logger.info(\"Legacy photo migration completed\", {\n userUuid,\n migrated: migratedCount,\n skipped: skippedCount,\n });\n } catch (error) {\n this.logger.error(\"Failed to migrate legacy photos\", { userUuid, error });\n // Don't throw - migration failure shouldn't break the request\n }\n }\n\n private base64ToBuffer(base64: string): Buffer {\n const data = base64.includes(\",\") ? base64.split(\",\")[1]! : base64;\n return Buffer.from(data, \"base64\");\n }\n\n private handleError(error: unknown, method: string): never {\n this.logger.error(`Firebase Storage error in ${method}`, { error });\n throw new ExternalAPIError(\n `Firebase Storage error: ${error instanceof Error ? error.message : String(error)}`,\n `Service: Firebase Storage, Method: ${method}`\n );\n }\n}\n"],"names":["admin","v4","uuidv4","readFileSync","ExternalAPIError","Log","validateBase64Image","UserProgressPhoto","PROGRESS_DAYS","RECIPE_PHOTO_TYPES","imageName","avatar","userUuid","progress","day","programId","recipe","recipeUuid","type","ImagesClient","bucket","logger","getInstance","extend","app","config","firebaseApp","initializeApp","storageBucket","storage","apps","length","info","serviceAccountPath","process","env","FIREBASE_SERVICE_ACCOUNT_KEY","serviceAccount","JSON","parse","credential","cert","projectId","project_id","applicationDefault","error","String","saveImage","base64","name","buffer","base64ToBuffer","file","save","contentType","handleError","getImage","exists","undefined","contents","download","toString","saveAvatar","getAvatar","saveProgressPhoto","getProgressPhotos","photos","push","saveRecipePhotos","input","Promise","all","ingredientBase64","coverBase64","methodBase64","getRecipePhotos","results","map","every","r","saveUserProgressPhoto","uuid","firebasePath","base64Data","replace","Buffer","from","metadata","photoDate","Date","record","create","toJSON","getUserProgressPhotos","records","findAll","where","order","raw","attributes","migrateLegacyPhotosForUser","migratedRecords","getProgressPhotosByProgram","updateProgressPhotoDate","affectedRows","update","updated","findOne","deleteProgressPhoto","photo","delete","console","warn","destroy","files","getFiles","prefix","authError","Error","message","migratedCount","skippedCount","fileName","match","dayStr","parseInt","existing","debug","setDate","getDate","newUuid","newFirebasePath","oldPath","newPath","migrated","skipped","data","includes","split","method"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/clients/ImagesClient.ts"],"sourcesContent":["import admin from \"firebase-admin\";\nimport { Bucket } from \"@google-cloud/storage\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { readFileSync } from \"fs\";\nimport { createDecipheriv } from \"crypto\";\nimport { ExternalAPIError } from \"../Errors/AppError.js\";\nimport { Log } from \"../utils/Logger.js\";\nimport { validateBase64Image } from \"../utils/imageValidation.js\";\nimport { UserProgressPhoto } from \"../dbmodels/user/UserProgressPhoto.js\";\nimport type * as t from \"./types/images.types.js\";\n\nconst PROGRESS_DAYS = [1, 30, 60, 90];\n\ntype RecipePhotoType = \"ingredients\" | \"cover\" | \"method\";\nconst RECIPE_PHOTO_TYPES: RecipePhotoType[] = [\"ingredients\", \"cover\", \"method\"];\n\nconst imageName = {\n avatar: (userUuid: string) => `${userUuid}--avatar.png`,\n progress: (userUuid: string, day: number, programId: string) => `progress/${userUuid}--${day}day--${programId}.png`,\n recipe: (recipeUuid: string, type: RecipePhotoType) => `${recipeUuid}--${type}.png`,\n};\n\nexport class ImagesClient {\n private bucket: Bucket;\n private logger = Log.getInstance().extend(\"images-client\");\n private app: admin.app.App;\n\n constructor(config: t.Config) {\n if (config.firebaseApp) {\n // Use provided Firebase app\n this.app = config.firebaseApp;\n } else {\n // Initialize Firebase if needed (following FirebasePushNotificationClient pattern)\n this.initializeApp(config.storageBucket);\n this.app = admin.app();\n }\n\n this.bucket = this.app.storage().bucket(config.storageBucket);\n }\n\n private initializeApp(storageBucket: string): void {\n try {\n // Check if app already exists\n if (admin.apps?.length && admin.apps.length > 0) {\n this.logger.info(\"Using existing Firebase app\");\n return;\n }\n\n // Try to load service account from environment (for local development)\n const serviceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;\n\n if (serviceAccountPath) {\n // Local development with service account file\n const serviceAccount = JSON.parse(readFileSync(serviceAccountPath, \"utf-8\"));\n\n admin.initializeApp({\n credential: admin.credential.cert(serviceAccount),\n storageBucket,\n });\n\n this.logger.info(\"Firebase initialized with service account file\", {\n projectId: serviceAccount.project_id,\n storageBucket,\n });\n } else {\n // Production: use application default credentials\n admin.initializeApp({\n credential: admin.credential.applicationDefault(),\n storageBucket,\n });\n\n this.logger.info(\"Firebase initialized with application default credentials\", {\n storageBucket,\n });\n }\n } catch (error) {\n this.logger.error(\"Failed to initialize Firebase Admin SDK\", { error });\n throw new ExternalAPIError(\n \"Failed to initialize Firebase\",\n `Error: ${String(error)}`\n );\n }\n }\n\n public async saveImage(base64: string, name: string): Promise<void> {\n try {\n this.logger.info(\"Saving image\", { name });\n\n const buffer = this.base64ToBuffer(base64);\n const file = this.bucket.file(name);\n await file.save(buffer, { contentType: \"image/png\" });\n\n this.logger.info(\"Image saved successfully\", { name });\n } catch (error) {\n this.handleError(error, \"saveImage\");\n }\n }\n\n public async getImage(name: string): Promise<string | undefined> {\n try {\n this.logger.info(\"Getting image\", { name });\n\n const file = this.bucket.file(name);\n const [exists] = await file.exists();\n if (!exists) {\n return undefined;\n }\n\n const [contents] = await file.download();\n return \"data:image/jpeg;base64,\" + contents.toString(\"base64\");\n } catch (error) {\n this.logger.error(`Failed to get image: ${name}`, { error });\n return undefined;\n }\n }\n\n public async saveAvatar(userUuid: string, base64: string): Promise<void> {\n await this.saveImage(base64, imageName.avatar(userUuid));\n }\n\n public async getAvatar(userUuid: string): Promise<string | undefined> {\n return this.getImage(imageName.avatar(userUuid));\n }\n\n public async saveProgressPhoto(userUuid: string, day: number, programId: string, base64: string): Promise<void> {\n await this.saveImage(base64, imageName.progress(userUuid, day, programId));\n }\n\n public async getProgressPhotos(userUuid: string, programId: string): Promise<t.ProgressPhoto[]> {\n const photos: t.ProgressPhoto[] = [];\n\n for (const day of PROGRESS_DAYS) {\n const base64 = await this.getImage(imageName.progress(userUuid, day, programId));\n if (base64) {\n photos.push({ day, base64 });\n }\n }\n\n return photos;\n }\n\n public async saveRecipePhotos(input: t.SaveRecipePhotosInput): Promise<void> {\n await Promise.all([\n this.saveImage(input.ingredientBase64, imageName.recipe(input.recipeUuid, \"ingredients\")),\n this.saveImage(input.coverBase64, imageName.recipe(input.recipeUuid, \"cover\")),\n this.saveImage(input.methodBase64, imageName.recipe(input.recipeUuid, \"method\")),\n ]);\n }\n\n public async getRecipePhotos(recipeUuid: string): Promise<t.RecipePhotos | undefined> {\n const results = await Promise.all(\n RECIPE_PHOTO_TYPES.map((type) => this.getImage(imageName.recipe(recipeUuid, type)))\n );\n\n if (results.every((r) => !r)) {\n return undefined;\n }\n\n // BUG: cover/method swapped — preserved from original ImageController.ts to keep backward compat with frontend\n return {\n ingredientBase64: results[0] ?? \"\",\n methodBase64: results[1] ?? \"\",\n coverBase64: results[2] ?? \"\",\n };\n }\n\n public async saveUserProgressPhoto(\n input: t.UserProgressPhotoInput\n ): Promise<t.UserProgressPhotoRecord> {\n // Validate image\n validateBase64Image(input.base64);\n\n // Generate UUID for the photo\n const uuid = uuidv4();\n\n // Prepare Firebase path\n const firebasePath = `progress-pictures/${input.userUuid}/${uuid}.png`;\n\n // Upload to Firebase Storage\n const base64Data = input.base64.replace(/^data:image\\/\\w+;base64,/, \"\");\n const buffer = Buffer.from(base64Data, \"base64\");\n\n const file = this.bucket.file(firebasePath);\n await file.save(buffer, {\n metadata: {\n contentType: \"image/png\",\n },\n });\n\n // Save metadata to database\n const photoDate = input.photoDate || new Date();\n const record = await UserProgressPhoto.create({\n uuid,\n userUuid: input.userUuid,\n firebasePath,\n photoDate,\n programId: input.programId || null,\n });\n\n return record.toJSON() as t.UserProgressPhotoRecord;\n }\n\n async getUserProgressPhotos(\n userUuid: string\n ): Promise<t.UserProgressPhotoRecord[]> {\n const records = await UserProgressPhoto.findAll({\n where: { userUuid },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n // If no photos in new system, attempt to migrate from legacy Firebase structure\n if (records.length === 0) {\n this.logger.info(\"No photos in new system, checking for legacy photos to migrate\", { userUuid });\n await this.migrateLegacyPhotosForUser(userUuid);\n\n // Re-fetch after migration\n const migratedRecords = await UserProgressPhoto.findAll({\n where: { userUuid },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n // Generate signed URLs for migrated records\n const withUrls = await Promise.all(\n (migratedRecords as t.UserProgressPhotoRecord[]).map(async (record) => ({\n ...record,\n url: await this.getSignedUrl(record.firebasePath),\n }))\n );\n\n return withUrls;\n }\n\n // Generate signed URLs for all records\n const withUrls = await Promise.all(\n (records as t.UserProgressPhotoRecord[]).map(async (record) => ({\n ...record,\n url: await this.getSignedUrl(record.firebasePath),\n }))\n );\n\n return withUrls;\n }\n\n async getProgressPhotosByProgram(\n programId: string,\n userUuid?: string\n ): Promise<t.UserProgressPhotoRecord[]> {\n const records = await UserProgressPhoto.findAll({\n where: { programId },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n // If no photos found and userUuid provided, attempt migration\n if (records.length === 0 && userUuid) {\n this.logger.info(\"No photos found for program, attempting migration\", { programId, userUuid });\n await this.migrateLegacyPhotosForUser(userUuid);\n\n // Re-fetch after migration\n const migratedRecords = await UserProgressPhoto.findAll({\n where: { programId },\n order: [[\"photoDate\", \"DESC\"]],\n raw: true,\n attributes: ['uuid', 'userUuid', 'firebasePath', 'photoDate', 'programId', 'createdAt', 'updatedAt'],\n });\n\n // Generate signed URLs\n const withUrls = await Promise.all(\n (migratedRecords as t.UserProgressPhotoRecord[]).map(async (record) => ({\n ...record,\n url: await this.getSignedUrl(record.firebasePath),\n }))\n );\n\n return withUrls;\n }\n\n // Generate signed URLs for all records\n const withUrls = await Promise.all(\n (records as t.UserProgressPhotoRecord[]).map(async (record) => ({\n ...record,\n url: await this.getSignedUrl(record.firebasePath),\n }))\n );\n\n return withUrls;\n }\n\n async updateProgressPhotoDate(\n input: t.UpdateProgressPhotoDateInput\n ): Promise<t.UserProgressPhotoRecord | null> {\n const [affectedRows] = await UserProgressPhoto.update(\n { photoDate: input.photoDate },\n {\n where: {\n uuid: input.uuid,\n userUuid: input.userUuid, // Ensure user owns the photo\n },\n }\n );\n\n if (affectedRows === 0) {\n return null; // Photo not found or user doesn't own it\n }\n\n const updated = await UserProgressPhoto.findOne({\n where: { uuid: input.uuid },\n raw: true,\n });\n\n return updated as t.UserProgressPhotoRecord;\n }\n\n async deleteProgressPhoto(\n uuid: string,\n userUuid: string\n ): Promise<boolean> {\n // Find the photo to get the firebasePath\n const photo = await UserProgressPhoto.findOne({\n where: { uuid, userUuid },\n raw: true,\n });\n\n if (!photo) {\n return false; // Photo not found or user doesn't own it\n }\n\n // Delete from Firebase Storage\n const file = this.bucket.file(photo.firebasePath);\n try {\n await file.delete();\n } catch (error) {\n // If file doesn't exist in storage, continue with database deletion\n console.warn(`Failed to delete file from storage: ${photo.firebasePath}`, error);\n }\n\n // Delete from database\n await UserProgressPhoto.destroy({\n where: { uuid, userUuid },\n });\n\n return true;\n }\n\n private async migrateLegacyPhotosForUser(userUuid: string): Promise<void> {\n try {\n this.logger.info(\"Starting legacy photo migration\", { userUuid });\n\n // List all files in the legacy /progress folder with pattern: progress/{userUuid}--{day}day--{programId}.png\n let files;\n try {\n [files] = await this.bucket.getFiles({\n prefix: `progress/${userUuid}--`,\n });\n } catch (authError) {\n this.logger.error(\"Firebase authentication failed during migration, skipping\", {\n userUuid,\n error: authError instanceof Error ? authError.message : String(authError),\n });\n return; // Skip migration if auth fails\n }\n\n if (files.length === 0) {\n this.logger.info(\"No legacy photos found for user\", { userUuid });\n return;\n }\n\n let migratedCount = 0;\n let skippedCount = 0;\n\n for (const file of files) {\n const fileName = file.name;\n\n // Parse legacy filename: progress/{userUuid}--{day}day--{programId}.png\n const match = fileName.match(/^progress\\/(.+)--(\\d+)day--(.+)\\.png$/);\n if (!match) {\n this.logger.warn(\"Skipping file with unexpected name format\", { fileName });\n continue;\n }\n\n const [, fileUserUuid, dayStr, programId] = match;\n const day = parseInt(dayStr, 10);\n\n // Verify this file belongs to the user\n if (fileUserUuid !== userUuid) {\n this.logger.warn(\"File doesn't belong to this user, skipping\", { fileName, userUuid, fileUserUuid });\n continue;\n }\n\n // Check if this photo was already migrated (by programId + userUuid + approximate day)\n const existing = await UserProgressPhoto.findOne({\n where: { userUuid, programId },\n raw: true,\n });\n\n if (existing) {\n this.logger.debug(\"Photo already migrated, skipping\", { fileName });\n continue;\n }\n\n try {\n // Download the legacy photo (encrypted in storage)\n this.logger.debug(\"Downloading legacy photo\", { fileName });\n const [encryptedContents] = await file.download();\n\n // Decrypt the legacy photo\n this.logger.debug(\"Decrypting legacy photo\", { fileName, size: encryptedContents.length });\n const decryptedBuffer = this.decryptLegacyImage(encryptedContents);\n this.logger.debug(\"Decryption successful\", { fileName, decryptedSize: decryptedBuffer.length });\n\n // Calculate photoDate based on day offset\n const photoDate = new Date();\n photoDate.setDate(photoDate.getDate() - (90 - day));\n\n // Generate new UUID and path\n const newUuid = uuidv4();\n const newFirebasePath = `progress-pictures/${userUuid}/${newUuid}.png`;\n\n // Save decrypted (plain) image to new Firebase location\n this.logger.debug(\"Uploading to new location\", { newFirebasePath });\n await this.bucket.file(newFirebasePath).save(decryptedBuffer, {\n metadata: {\n contentType: \"image/png\",\n },\n });\n this.logger.debug(\"Upload successful\", { newFirebasePath });\n\n // Verify file exists before creating database record\n const [exists] = await this.bucket.file(newFirebasePath).exists();\n if (!exists) {\n throw new Error(\"File was not saved to Firebase Storage\");\n }\n\n // Create database record only after successful upload\n await UserProgressPhoto.create({\n uuid: newUuid,\n userUuid,\n firebasePath: newFirebasePath,\n photoDate,\n programId,\n });\n\n migratedCount++;\n\n this.logger.info(\"Successfully migrated legacy photo\", {\n oldPath: fileName,\n newPath: newFirebasePath,\n day,\n programId,\n });\n } catch (photoError) {\n this.logger.error(\"Failed to migrate photo, skipping\", {\n fileName,\n error: photoError instanceof Error ? photoError.message : String(photoError),\n stack: photoError instanceof Error ? photoError.stack : undefined,\n });\n skippedCount++;\n continue;\n }\n }\n\n this.logger.info(\"Legacy photo migration completed\", {\n userUuid,\n migrated: migratedCount,\n skipped: skippedCount,\n });\n } catch (error) {\n this.logger.error(\"Failed to migrate legacy photos\", { userUuid, error });\n // Don't throw - migration failure shouldn't break the request\n }\n }\n\n private async getSignedUrl(firebasePath: string): Promise<string | undefined> {\n try {\n const file = this.bucket.file(firebasePath);\n\n // Check if file exists first\n const [exists] = await file.exists();\n if (!exists) {\n this.logger.warn(\"File not found for signed URL generation\", { firebasePath });\n return undefined;\n }\n\n // Generate signed URL with proper expiration date\n const expirationDate = new Date();\n expirationDate.setDate(expirationDate.getDate() + 7); // 7 days from now\n\n const [url] = await file.getSignedUrl({\n action: \"read\",\n expires: expirationDate,\n });\n\n this.logger.debug(\"Generated signed URL\", { firebasePath });\n return url;\n } catch (error) {\n this.logger.error(\"Failed to generate signed URL\", {\n firebasePath,\n error: error instanceof Error ? error.message : String(error),\n errorDetails: error\n });\n return undefined;\n }\n }\n\n private decryptLegacyImage(encryptedBuffer: Buffer): Buffer {\n try {\n const encryptionKey = process.env.ENCRYPTION_KEY;\n if (!encryptionKey) {\n throw new Error(\"ENCRYPTION_KEY not found in environment\");\n }\n\n // Extract IV from first 16 bytes\n const iv = encryptedBuffer.slice(0, 16);\n const encryptedData = encryptedBuffer.slice(16);\n\n // Decrypt using AES-256-CBC\n const decipher = createDecipheriv(\"aes-256-cbc\", Buffer.from(encryptionKey), iv);\n const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]);\n\n // Original implementation: \"data:image/jpeg;base64,\" + decryptedImageData.toString('base64').slice(20, ...)\n // It converts to base64, slices 20 chars (removing \"dataimage/jpegbase64\" garbage), then returns\n // We need to replicate this: base64 encode, slice 20 chars, base64 decode\n const base64String = decryptedData.toString('base64');\n const slicedBase64 = base64String.slice(20);\n return Buffer.from(slicedBase64, 'base64');\n } catch (error) {\n this.logger.error(\"Failed to decrypt legacy image\", { error });\n throw new Error(\"Image decryption failed\");\n }\n }\n\n private base64ToBuffer(base64: string): Buffer {\n const data = base64.includes(\",\") ? base64.split(\",\")[1]! : base64;\n return Buffer.from(data, \"base64\");\n }\n\n private handleError(error: unknown, method: string): never {\n this.logger.error(`Firebase Storage error in ${method}`, { error });\n throw new ExternalAPIError(\n `Firebase Storage error: ${error instanceof Error ? error.message : String(error)}`,\n `Service: Firebase Storage, Method: ${method}`\n );\n }\n}\n"],"names":["admin","v4","uuidv4","readFileSync","createDecipheriv","ExternalAPIError","Log","validateBase64Image","UserProgressPhoto","PROGRESS_DAYS","RECIPE_PHOTO_TYPES","imageName","avatar","userUuid","progress","day","programId","recipe","recipeUuid","type","ImagesClient","bucket","logger","getInstance","extend","app","config","firebaseApp","initializeApp","storageBucket","storage","apps","length","info","serviceAccountPath","process","env","FIREBASE_SERVICE_ACCOUNT_KEY","serviceAccount","JSON","parse","credential","cert","projectId","project_id","applicationDefault","error","String","saveImage","base64","name","buffer","base64ToBuffer","file","save","contentType","handleError","getImage","exists","undefined","contents","download","toString","saveAvatar","getAvatar","saveProgressPhoto","getProgressPhotos","photos","push","saveRecipePhotos","input","Promise","all","ingredientBase64","coverBase64","methodBase64","getRecipePhotos","results","map","every","r","saveUserProgressPhoto","uuid","firebasePath","base64Data","replace","Buffer","from","metadata","photoDate","Date","record","create","toJSON","getUserProgressPhotos","records","findAll","where","order","raw","attributes","migrateLegacyPhotosForUser","migratedRecords","withUrls","url","getSignedUrl","getProgressPhotosByProgram","updateProgressPhotoDate","affectedRows","update","updated","findOne","deleteProgressPhoto","photo","delete","console","warn","destroy","files","getFiles","prefix","authError","Error","message","migratedCount","skippedCount","fileName","match","fileUserUuid","dayStr","parseInt","existing","debug","encryptedContents","size","decryptedBuffer","decryptLegacyImage","decryptedSize","setDate","getDate","newUuid","newFirebasePath","oldPath","newPath","photoError","stack","migrated","skipped","expirationDate","action","expires","errorDetails","encryptedBuffer","encryptionKey","ENCRYPTION_KEY","iv","slice","encryptedData","decipher","decryptedData","concat","final","base64String","slicedBase64","data","includes","split","method"],"mappings":"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"}
@@ -32,6 +32,7 @@ export interface UserProgressPhotoRecord {
32
32
  programId: string | null;
33
33
  createdAt: Date;
34
34
  updatedAt: Date;
35
+ url?: string | undefined;
35
36
  }
36
37
  export interface UpdateProgressPhotoDateInput {
37
38
  uuid: string;
@@ -1 +1 @@
1
- {"version":3,"file":"images.types.d.ts","sourceRoot":"","sources":["../../../../src/lib/clients/types/images.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAExC,MAAM,WAAW,MAAM;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;CACjB"}
1
+ {"version":3,"file":"images.types.d.ts","sourceRoot":"","sources":["../../../../src/lib/clients/types/images.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAExC,MAAM,WAAW,MAAM;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;CACjB"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/lib/clients/types/images.types.ts"],"sourcesContent":["import type admin from \"firebase-admin\";\n\nexport interface Config {\n storageBucket: string;\n firebaseApp?: admin.app.App;\n}\n\nexport interface ProgressPhoto {\n day: number;\n base64: string;\n}\n\nexport interface RecipePhotos {\n ingredientBase64: string;\n coverBase64: string;\n methodBase64: string;\n}\n\nexport interface SaveRecipePhotosInput {\n recipeUuid: string;\n ingredientBase64: string;\n coverBase64: string;\n methodBase64: string;\n}\n\nexport interface UserProgressPhotoInput {\n userUuid: string;\n base64: string;\n photoDate?: Date; // Optional, defaults to now\n programId?: string; // Optional, for legacy program association\n}\n\nexport interface UserProgressPhotoRecord {\n uuid: string;\n userUuid: string;\n firebasePath: string;\n photoDate: Date;\n programId: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpdateProgressPhotoDateInput {\n uuid: string;\n userUuid: string;\n photoDate: Date;\n}\n"],"names":[],"mappings":"AA0CA,WAIC"}
1
+ {"version":3,"sources":["../../../../src/lib/clients/types/images.types.ts"],"sourcesContent":["import type admin from \"firebase-admin\";\n\nexport interface Config {\n storageBucket: string;\n firebaseApp?: admin.app.App;\n}\n\nexport interface ProgressPhoto {\n day: number;\n base64: string;\n}\n\nexport interface RecipePhotos {\n ingredientBase64: string;\n coverBase64: string;\n methodBase64: string;\n}\n\nexport interface SaveRecipePhotosInput {\n recipeUuid: string;\n ingredientBase64: string;\n coverBase64: string;\n methodBase64: string;\n}\n\nexport interface UserProgressPhotoInput {\n userUuid: string;\n base64: string;\n photoDate?: Date; // Optional, defaults to now\n programId?: string; // Optional, for legacy program association\n}\n\nexport interface UserProgressPhotoRecord {\n uuid: string;\n userUuid: string;\n firebasePath: string;\n photoDate: Date;\n programId: string | null;\n createdAt: Date;\n updatedAt: Date;\n url?: string | undefined; // Optional signed URL for frontend access\n}\n\nexport interface UpdateProgressPhotoDateInput {\n uuid: string;\n userUuid: string;\n photoDate: Date;\n}\n"],"names":[],"mappings":"AA2CA,WAIC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "90dc-core",
3
- "version": "1.19.7",
3
+ "version": "1.19.9",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",