90dc-core 1.19.4 → 1.19.6

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.
Files changed (299) hide show
  1. package/dist/index.js +43 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/lib/Errors/AppError.js +104 -0
  4. package/dist/lib/Errors/AppError.js.map +1 -0
  5. package/dist/lib/Errors/Errors.js +42 -0
  6. package/dist/lib/Errors/Errors.js.map +1 -0
  7. package/dist/lib/classes/Database.js +154 -0
  8. package/dist/lib/classes/Database.js.map +1 -0
  9. package/dist/lib/classes/Redis.js +63 -0
  10. package/dist/lib/classes/Redis.js.map +1 -0
  11. package/dist/lib/clients/EmailClient.js +188 -0
  12. package/dist/lib/clients/EmailClient.js.map +1 -0
  13. package/dist/lib/clients/FirebasePushNotificationClient.js +688 -0
  14. package/dist/lib/clients/FirebasePushNotificationClient.js.map +1 -0
  15. package/dist/lib/clients/ImagesClient.d.ts +2 -1
  16. package/dist/lib/clients/ImagesClient.d.ts.map +1 -1
  17. package/dist/lib/clients/ImagesClient.js +436 -0
  18. package/dist/lib/clients/ImagesClient.js.map +1 -0
  19. package/dist/lib/clients/MailingClient.js +84 -0
  20. package/dist/lib/clients/MailingClient.js.map +1 -0
  21. package/dist/lib/clients/PushNotificationClient.js +478 -0
  22. package/dist/lib/clients/PushNotificationClient.js.map +1 -0
  23. package/dist/lib/clients/types/email.types.js +3 -0
  24. package/dist/lib/clients/types/email.types.js.map +1 -0
  25. package/dist/lib/clients/types/images.types.js +3 -0
  26. package/dist/lib/clients/types/images.types.js.map +1 -0
  27. package/dist/lib/clients/types/mailing.types.js +3 -0
  28. package/dist/lib/clients/types/mailing.types.js.map +1 -0
  29. package/dist/lib/config/ConfigValidator.js +162 -0
  30. package/dist/lib/config/ConfigValidator.js.map +1 -0
  31. package/dist/lib/controllers/BaseController.js +64 -0
  32. package/dist/lib/controllers/BaseController.js.map +1 -0
  33. package/dist/lib/db/migrations/20260330000000-create-user-progress-photos.js +60 -0
  34. package/dist/lib/db/migrations/20260330000000-create-user-progress-photos.js.map +1 -0
  35. package/dist/lib/dbmodels/coaching/Answer.js +51 -0
  36. package/dist/lib/dbmodels/coaching/Answer.js.map +1 -0
  37. package/dist/lib/dbmodels/coaching/ClientNote.js +75 -0
  38. package/dist/lib/dbmodels/coaching/ClientNote.js.map +1 -0
  39. package/dist/lib/dbmodels/coaching/ClientTag.js +45 -0
  40. package/dist/lib/dbmodels/coaching/ClientTag.js.map +1 -0
  41. package/dist/lib/dbmodels/coaching/Question.js +60 -0
  42. package/dist/lib/dbmodels/coaching/Question.js.map +1 -0
  43. package/dist/lib/dbmodels/coaching/Questionnaire.js +39 -0
  44. package/dist/lib/dbmodels/coaching/Questionnaire.js.map +1 -0
  45. package/dist/lib/dbmodels/coaching/QuestionnaireResponse.js +42 -0
  46. package/dist/lib/dbmodels/coaching/QuestionnaireResponse.js.map +1 -0
  47. package/dist/lib/dbmodels/coaching/WeeklyCheckIn.js +69 -0
  48. package/dist/lib/dbmodels/coaching/WeeklyCheckIn.js.map +1 -0
  49. package/dist/lib/dbmodels/coaching/WeightRecord.js +49 -0
  50. package/dist/lib/dbmodels/coaching/WeightRecord.js.map +1 -0
  51. package/dist/lib/dbmodels/diet/DietDay.js +50 -0
  52. package/dist/lib/dbmodels/diet/DietDay.js.map +1 -0
  53. package/dist/lib/dbmodels/diet/DietMeal.js +74 -0
  54. package/dist/lib/dbmodels/diet/DietMeal.js.map +1 -0
  55. package/dist/lib/dbmodels/diet/DietMealCompletion.js +69 -0
  56. package/dist/lib/dbmodels/diet/DietMealCompletion.js.map +1 -0
  57. package/dist/lib/dbmodels/diet/DietMealRecipe.js +67 -0
  58. package/dist/lib/dbmodels/diet/DietMealRecipe.js.map +1 -0
  59. package/dist/lib/dbmodels/diet/DietMealRecipeIngredient.js +46 -0
  60. package/dist/lib/dbmodels/diet/DietMealRecipeIngredient.js.map +1 -0
  61. package/dist/lib/dbmodels/diet/DietProgram.js +98 -0
  62. package/dist/lib/dbmodels/diet/DietProgram.js.map +1 -0
  63. package/dist/lib/dbmodels/diet/Ingredient.js +88 -0
  64. package/dist/lib/dbmodels/diet/Ingredient.js.map +1 -0
  65. package/dist/lib/dbmodels/diet/IngredientTag.js +64 -0
  66. package/dist/lib/dbmodels/diet/IngredientTag.js.map +1 -0
  67. package/dist/lib/dbmodels/diet/IngredientTags.js +29 -0
  68. package/dist/lib/dbmodels/diet/IngredientTags.js.map +1 -0
  69. package/dist/lib/dbmodels/diet/Recipe.js +173 -0
  70. package/dist/lib/dbmodels/diet/Recipe.js.map +1 -0
  71. package/dist/lib/dbmodels/diet/RecipeIngredient.js +78 -0
  72. package/dist/lib/dbmodels/diet/RecipeIngredient.js.map +1 -0
  73. package/dist/lib/dbmodels/diet/RecipeTag.js +44 -0
  74. package/dist/lib/dbmodels/diet/RecipeTag.js.map +1 -0
  75. package/dist/lib/dbmodels/diet/RecipeTags.js +24 -0
  76. package/dist/lib/dbmodels/diet/RecipeTags.js.map +1 -0
  77. package/dist/lib/dbmodels/diet/ShoppingList.js +69 -0
  78. package/dist/lib/dbmodels/diet/ShoppingList.js.map +1 -0
  79. package/dist/lib/dbmodels/diet/ShoppingListItem.js +66 -0
  80. package/dist/lib/dbmodels/diet/ShoppingListItem.js.map +1 -0
  81. package/dist/lib/dbmodels/diet/TranslatedRecipe.js +122 -0
  82. package/dist/lib/dbmodels/diet/TranslatedRecipe.js.map +1 -0
  83. package/dist/lib/dbmodels/diet/TranslatedRecipeTag.js +47 -0
  84. package/dist/lib/dbmodels/diet/TranslatedRecipeTag.js.map +1 -0
  85. package/dist/lib/dbmodels/diet/TranslatedRecipeTags.js +24 -0
  86. package/dist/lib/dbmodels/diet/TranslatedRecipeTags.js.map +1 -0
  87. package/dist/lib/dbmodels/diet/UserDietPreferences.js +85 -0
  88. package/dist/lib/dbmodels/diet/UserDietPreferences.js.map +1 -0
  89. package/dist/lib/dbmodels/gamification/Badge.js +62 -0
  90. package/dist/lib/dbmodels/gamification/Badge.js.map +1 -0
  91. package/dist/lib/dbmodels/gamification/StreaksLog.js +37 -0
  92. package/dist/lib/dbmodels/gamification/StreaksLog.js.map +1 -0
  93. package/dist/lib/dbmodels/gamification/TranslatedBadge.js +45 -0
  94. package/dist/lib/dbmodels/gamification/TranslatedBadge.js.map +1 -0
  95. package/dist/lib/dbmodels/gamification/xpAndLeaderboards/UserRankHistory.js +48 -0
  96. package/dist/lib/dbmodels/gamification/xpAndLeaderboards/UserRankHistory.js.map +1 -0
  97. package/dist/lib/dbmodels/gamification/xpAndLeaderboards/XpEvent.js +53 -0
  98. package/dist/lib/dbmodels/gamification/xpAndLeaderboards/XpEvent.js.map +1 -0
  99. package/dist/lib/dbmodels/gamification/xpAndLeaderboards/XpTransaction.js +68 -0
  100. package/dist/lib/dbmodels/gamification/xpAndLeaderboards/XpTransaction.js.map +1 -0
  101. package/dist/lib/dbmodels/index.js +288 -0
  102. package/dist/lib/dbmodels/index.js.map +1 -0
  103. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgram.d.ts +32 -0
  104. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgram.d.ts.map +1 -0
  105. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgram.js +155 -0
  106. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgram.js.map +1 -0
  107. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgramWebContent.d.ts +22 -0
  108. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgramWebContent.d.ts.map +1 -0
  109. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgramWebContent.js +108 -0
  110. package/dist/lib/dbmodels/nonconsprogram/NonConsumableProgramWebContent.js.map +1 -0
  111. package/dist/lib/dbmodels/nonconsprogram/TranslatedConsumableProgram.d.ts +20 -0
  112. package/dist/lib/dbmodels/nonconsprogram/TranslatedConsumableProgram.d.ts.map +1 -0
  113. package/dist/lib/dbmodels/nonconsprogram/TranslatedConsumableProgram.js +97 -0
  114. package/dist/lib/dbmodels/nonconsprogram/TranslatedConsumableProgram.js.map +1 -0
  115. package/dist/lib/dbmodels/nonconsprogram/TranslatedNonConsumableProgramWebContent.d.ts +22 -0
  116. package/dist/lib/dbmodels/nonconsprogram/TranslatedNonConsumableProgramWebContent.d.ts.map +1 -0
  117. package/dist/lib/dbmodels/nonconsprogram/TranslatedNonConsumableProgramWebContent.js +115 -0
  118. package/dist/lib/dbmodels/nonconsprogram/TranslatedNonConsumableProgramWebContent.js.map +1 -0
  119. package/dist/lib/dbmodels/nonconsprogram/UserNonConsumableProgram.d.ts +7 -0
  120. package/dist/lib/dbmodels/nonconsprogram/UserNonConsumableProgram.d.ts.map +1 -0
  121. package/dist/lib/dbmodels/nonconsprogram/UserNonConsumableProgram.js +41 -0
  122. package/dist/lib/dbmodels/nonconsprogram/UserNonConsumableProgram.js.map +1 -0
  123. package/dist/lib/dbmodels/notifications/NotificationModels.js +38 -0
  124. package/dist/lib/dbmodels/notifications/NotificationModels.js.map +1 -0
  125. package/dist/lib/dbmodels/notifications/TranslatedNotification.js +45 -0
  126. package/dist/lib/dbmodels/notifications/TranslatedNotification.js.map +1 -0
  127. package/dist/lib/dbmodels/program/Challenge.d.ts +13 -0
  128. package/dist/lib/dbmodels/program/Challenge.d.ts.map +1 -0
  129. package/dist/lib/dbmodels/program/Challenge.js +63 -0
  130. package/dist/lib/dbmodels/program/Challenge.js.map +1 -0
  131. package/dist/lib/dbmodels/program/ChallengeBlueprint.js +75 -0
  132. package/dist/lib/dbmodels/program/ChallengeBlueprint.js.map +1 -0
  133. package/dist/lib/dbmodels/program/CircularProgramDraft.js +82 -0
  134. package/dist/lib/dbmodels/program/CircularProgramDraft.js.map +1 -0
  135. package/dist/lib/dbmodels/program/CoachExerciseNote.js +61 -0
  136. package/dist/lib/dbmodels/program/CoachExerciseNote.js.map +1 -0
  137. package/dist/lib/dbmodels/program/CustomProgramBlueprint.js +84 -0
  138. package/dist/lib/dbmodels/program/CustomProgramBlueprint.js.map +1 -0
  139. package/dist/lib/dbmodels/program/CustomStrengthTest.js +55 -0
  140. package/dist/lib/dbmodels/program/CustomStrengthTest.js.map +1 -0
  141. package/dist/lib/dbmodels/program/CustomStrengthTestExercises.js +69 -0
  142. package/dist/lib/dbmodels/program/CustomStrengthTestExercises.js.map +1 -0
  143. package/dist/lib/dbmodels/program/CustomWorkoutBlueprint.js +58 -0
  144. package/dist/lib/dbmodels/program/CustomWorkoutBlueprint.js.map +1 -0
  145. package/dist/lib/dbmodels/program/Exercise.d.ts +23 -0
  146. package/dist/lib/dbmodels/program/Exercise.d.ts.map +1 -0
  147. package/dist/lib/dbmodels/program/Exercise.js +101 -0
  148. package/dist/lib/dbmodels/program/Exercise.js.map +1 -0
  149. package/dist/lib/dbmodels/program/ExerciseModels.d.ts +28 -0
  150. package/dist/lib/dbmodels/program/ExerciseModels.d.ts.map +1 -0
  151. package/dist/lib/dbmodels/program/ExerciseModels.js +149 -0
  152. package/dist/lib/dbmodels/program/ExerciseModels.js.map +1 -0
  153. package/dist/lib/dbmodels/program/Program.d.ts +15 -0
  154. package/dist/lib/dbmodels/program/Program.d.ts.map +1 -0
  155. package/dist/lib/dbmodels/program/Program.js +76 -0
  156. package/dist/lib/dbmodels/program/Program.js.map +1 -0
  157. package/dist/lib/dbmodels/program/ProgressEntry.d.ts +26 -0
  158. package/dist/lib/dbmodels/program/ProgressEntry.d.ts.map +1 -0
  159. package/dist/lib/dbmodels/program/ProgressEntry.js +77 -0
  160. package/dist/lib/dbmodels/program/ProgressEntry.js.map +1 -0
  161. package/dist/lib/dbmodels/program/RestDay.js +33 -0
  162. package/dist/lib/dbmodels/program/RestDay.js.map +1 -0
  163. package/dist/lib/dbmodels/program/StrengthTest.d.ts +10 -0
  164. package/dist/lib/dbmodels/program/StrengthTest.d.ts.map +1 -0
  165. package/dist/lib/dbmodels/program/StrengthTest.js +39 -0
  166. package/dist/lib/dbmodels/program/StrengthTest.js.map +1 -0
  167. package/dist/lib/dbmodels/program/StrengthTestExercise.d.ts +15 -0
  168. package/dist/lib/dbmodels/program/StrengthTestExercise.d.ts.map +1 -0
  169. package/dist/lib/dbmodels/program/StrengthTestExercise.js +72 -0
  170. package/dist/lib/dbmodels/program/StrengthTestExercise.js.map +1 -0
  171. package/dist/lib/dbmodels/program/StrengthTestSession.js +73 -0
  172. package/dist/lib/dbmodels/program/StrengthTestSession.js.map +1 -0
  173. package/dist/lib/dbmodels/program/Superset.d.ts +13 -0
  174. package/dist/lib/dbmodels/program/Superset.d.ts.map +1 -0
  175. package/dist/lib/dbmodels/program/Superset.js +46 -0
  176. package/dist/lib/dbmodels/program/Superset.js.map +1 -0
  177. package/dist/lib/dbmodels/program/ThirtyDayChallenge.js +70 -0
  178. package/dist/lib/dbmodels/program/ThirtyDayChallenge.js.map +1 -0
  179. package/dist/lib/dbmodels/program/ThirtyDayChallengeStrengthTest.js +55 -0
  180. package/dist/lib/dbmodels/program/ThirtyDayChallengeStrengthTest.js.map +1 -0
  181. package/dist/lib/dbmodels/program/TranslatedChallenge.d.ts +15 -0
  182. package/dist/lib/dbmodels/program/TranslatedChallenge.d.ts.map +1 -0
  183. package/dist/lib/dbmodels/program/TranslatedChallenge.js +69 -0
  184. package/dist/lib/dbmodels/program/TranslatedChallenge.js.map +1 -0
  185. package/dist/lib/dbmodels/program/TranslatedExerciseModel.d.ts +25 -0
  186. package/dist/lib/dbmodels/program/TranslatedExerciseModel.d.ts.map +1 -0
  187. package/dist/lib/dbmodels/program/TranslatedExerciseModel.js +123 -0
  188. package/dist/lib/dbmodels/program/TranslatedExerciseModel.js.map +1 -0
  189. package/dist/lib/dbmodels/program/TranslatedStrengthTest.d.ts +12 -0
  190. package/dist/lib/dbmodels/program/TranslatedStrengthTest.d.ts.map +1 -0
  191. package/dist/lib/dbmodels/program/TranslatedStrengthTest.js +45 -0
  192. package/dist/lib/dbmodels/program/TranslatedStrengthTest.js.map +1 -0
  193. package/dist/lib/dbmodels/program/TranslatedStrengthTestExercise.d.ts +16 -0
  194. package/dist/lib/dbmodels/program/TranslatedStrengthTestExercise.d.ts.map +1 -0
  195. package/dist/lib/dbmodels/program/TranslatedStrengthTestExercise.js +78 -0
  196. package/dist/lib/dbmodels/program/TranslatedStrengthTestExercise.js.map +1 -0
  197. package/dist/lib/dbmodels/program/UserChallenge.d.ts +10 -0
  198. package/dist/lib/dbmodels/program/UserChallenge.d.ts.map +1 -0
  199. package/dist/lib/dbmodels/program/UserChallenge.js +47 -0
  200. package/dist/lib/dbmodels/program/UserChallenge.js.map +1 -0
  201. package/dist/lib/dbmodels/program/UserExerciseNote.js +61 -0
  202. package/dist/lib/dbmodels/program/UserExerciseNote.js.map +1 -0
  203. package/dist/lib/dbmodels/program/UserStrengthTests.d.ts +11 -0
  204. package/dist/lib/dbmodels/program/UserStrengthTests.d.ts.map +1 -0
  205. package/dist/lib/dbmodels/program/UserStrengthTests.js +54 -0
  206. package/dist/lib/dbmodels/program/UserStrengthTests.js.map +1 -0
  207. package/dist/lib/dbmodels/program/Workout.d.ts +14 -0
  208. package/dist/lib/dbmodels/program/Workout.d.ts.map +1 -0
  209. package/dist/lib/dbmodels/program/Workout.js +66 -0
  210. package/dist/lib/dbmodels/program/Workout.js.map +1 -0
  211. package/dist/lib/dbmodels/program/WorkoutCompletion.js +65 -0
  212. package/dist/lib/dbmodels/program/WorkoutCompletion.js.map +1 -0
  213. package/dist/lib/dbmodels/program/WorkoutSession.js +93 -0
  214. package/dist/lib/dbmodels/program/WorkoutSession.js.map +1 -0
  215. package/dist/lib/dbmodels/subscription/Subscription.d.ts +19 -0
  216. package/dist/lib/dbmodels/subscription/Subscription.d.ts.map +1 -0
  217. package/dist/lib/dbmodels/subscription/Subscription.js +91 -0
  218. package/dist/lib/dbmodels/subscription/Subscription.js.map +1 -0
  219. package/dist/lib/dbmodels/subscription/SubscriptionEvent.js +79 -0
  220. package/dist/lib/dbmodels/subscription/SubscriptionEvent.js.map +1 -0
  221. package/dist/lib/dbmodels/subscription/SubscriptionLog.d.ts +9 -0
  222. package/dist/lib/dbmodels/subscription/SubscriptionLog.d.ts.map +1 -0
  223. package/dist/lib/dbmodels/subscription/SubscriptionLog.js +42 -0
  224. package/dist/lib/dbmodels/subscription/SubscriptionLog.js.map +1 -0
  225. package/dist/lib/dbmodels/subscription/SubscriptionRefund.js +92 -0
  226. package/dist/lib/dbmodels/subscription/SubscriptionRefund.js.map +1 -0
  227. package/dist/lib/dbmodels/user/DeviceTokens.js +34 -0
  228. package/dist/lib/dbmodels/user/DeviceTokens.js.map +1 -0
  229. package/dist/lib/dbmodels/user/PersistedUser.d.ts +43 -0
  230. package/dist/lib/dbmodels/user/PersistedUser.d.ts.map +1 -0
  231. package/dist/lib/dbmodels/user/PersistedUser.js +237 -0
  232. package/dist/lib/dbmodels/user/PersistedUser.js.map +1 -0
  233. package/dist/lib/dbmodels/user/UserAddons.js +26 -0
  234. package/dist/lib/dbmodels/user/UserAddons.js.map +1 -0
  235. package/dist/lib/dbmodels/user/UserBadges.js +63 -0
  236. package/dist/lib/dbmodels/user/UserBadges.js.map +1 -0
  237. package/dist/lib/dbmodels/user/UserCoach.js +40 -0
  238. package/dist/lib/dbmodels/user/UserCoach.js.map +1 -0
  239. package/dist/lib/dbmodels/user/UserDiscount.js +24 -0
  240. package/dist/lib/dbmodels/user/UserDiscount.js.map +1 -0
  241. package/dist/lib/dbmodels/user/UserOptions.js +34 -0
  242. package/dist/lib/dbmodels/user/UserOptions.js.map +1 -0
  243. package/dist/lib/dbmodels/user/UserProgressPhoto.js +59 -0
  244. package/dist/lib/dbmodels/user/UserProgressPhoto.js.map +1 -0
  245. package/dist/lib/dbmodels/user/UserStreaks.js +64 -0
  246. package/dist/lib/dbmodels/user/UserStreaks.js.map +1 -0
  247. package/dist/lib/dbmodels/user/UsersFriends.js +46 -0
  248. package/dist/lib/dbmodels/user/UsersFriends.js.map +1 -0
  249. package/dist/lib/enums/ProgramEnums.d.ts +18 -0
  250. package/dist/lib/enums/ProgramEnums.d.ts.map +1 -0
  251. package/dist/lib/enums/ProgramEnums.js +22 -0
  252. package/dist/lib/enums/ProgramEnums.js.map +1 -0
  253. package/dist/lib/enums/ProgramTemplates.js +738 -0
  254. package/dist/lib/enums/ProgramTemplates.js.map +1 -0
  255. package/dist/lib/middlewares/ErrorMiddleware.js +141 -0
  256. package/dist/lib/middlewares/ErrorMiddleware.js.map +1 -0
  257. package/dist/lib/middlewares/ValidationMiddleware.js +24 -0
  258. package/dist/lib/middlewares/ValidationMiddleware.js.map +1 -0
  259. package/dist/lib/models/BlueprintInterfaces.js +3 -0
  260. package/dist/lib/models/BlueprintInterfaces.js.map +1 -0
  261. package/dist/lib/models/ExerciseInterfaces.d.ts +68 -0
  262. package/dist/lib/models/ExerciseInterfaces.d.ts.map +1 -0
  263. package/dist/lib/models/ExerciseInterfaces.js +3 -0
  264. package/dist/lib/models/ExerciseInterfaces.js.map +1 -0
  265. package/dist/lib/models/NotificationInterfaces.js +11 -0
  266. package/dist/lib/models/NotificationInterfaces.js.map +1 -0
  267. package/dist/lib/models/ProgramInterfaces.js +3 -0
  268. package/dist/lib/models/ProgramInterfaces.js.map +1 -0
  269. package/dist/lib/models/UserInterfaces.js +3 -0
  270. package/dist/lib/models/UserInterfaces.js.map +1 -0
  271. package/dist/lib/models/WorkoutInterfaces.js +3 -0
  272. package/dist/lib/models/WorkoutInterfaces.js.map +1 -0
  273. package/dist/lib/scripts/cli.js +82 -0
  274. package/dist/lib/scripts/cli.js.map +1 -0
  275. package/dist/lib/scripts/populate-exercise-thumbnails.js +64 -0
  276. package/dist/lib/scripts/populate-exercise-thumbnails.js.map +1 -0
  277. package/dist/lib/scripts/setup-database.js +43 -0
  278. package/dist/lib/scripts/setup-database.js.map +1 -0
  279. package/dist/lib/scripts/verify-indexes.js +94 -0
  280. package/dist/lib/scripts/verify-indexes.js.map +1 -0
  281. package/dist/lib/testing/testFixtures.js +520 -0
  282. package/dist/lib/testing/testFixtures.js.map +1 -0
  283. package/dist/lib/testing/testHelpers.js +149 -0
  284. package/dist/lib/testing/testHelpers.js.map +1 -0
  285. package/dist/lib/utils/AuthenticationUtil.js +481 -0
  286. package/dist/lib/utils/AuthenticationUtil.js.map +1 -0
  287. package/dist/lib/utils/Logger.js +242 -0
  288. package/dist/lib/utils/Logger.js.map +1 -0
  289. package/dist/lib/utils/NotificationClient.js +371 -0
  290. package/dist/lib/utils/NotificationClient.js.map +1 -0
  291. package/dist/lib/utils/NotificationsUtil.js +95 -0
  292. package/dist/lib/utils/NotificationsUtil.js.map +1 -0
  293. package/dist/lib/utils/SecretManager.js +107 -0
  294. package/dist/lib/utils/SecretManager.js.map +1 -0
  295. package/dist/lib/utils/SentryUtil.js +140 -0
  296. package/dist/lib/utils/SentryUtil.js.map +1 -0
  297. package/dist/lib/utils/imageValidation.js +31 -0
  298. package/dist/lib/utils/imageValidation.js.map +1 -0
  299. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/clients/FirebasePushNotificationClient.ts"],"sourcesContent":["/**\n * Firebase Push Notification Client\n *\n * Industry-standard implementation using Firebase Admin SDK for:\n * - iOS (APNs via FCM)\n * - Android (FCM)\n *\n * Features:\n * - Multicast messaging (up to 500 tokens per request)\n * - Topic-based messaging for efficient broadcasting\n * - Automatic token validation and cleanup\n * - Built-in retry logic and error handling\n * - Message delivery tracking\n * - Type-safe API\n */\n\nimport admin from \"firebase-admin\";\nimport { z } from \"zod\";\nimport { CommonSchemas } from \"../config/ConfigValidator.js\";\nimport { ExternalAPIError } from \"../Errors/AppError.js\";\nimport { Log } from \"../utils/Logger.js\";\nimport { SecretManager } from \"../utils/SecretManager.js\";\nimport { DeviceTokens } from \"../dbmodels/user/DeviceTokens.js\";\nimport { PersistedUser } from \"../dbmodels/user/PersistedUser.js\";\nimport { NotificationModels } from \"../dbmodels/notifications/NotificationModels.js\";\nimport { TranslatedNotification } from \"../dbmodels/notifications/TranslatedNotification.js\";\n\ntype FirebaseConfig = z.infer<typeof CommonSchemas.pushNotification>;\n\ninterface ServiceAccount {\n type: string;\n project_id: string;\n private_key_id: string;\n private_key: string;\n client_email: string;\n client_id: string;\n auth_uri: string;\n token_uri: string;\n auth_provider_x509_cert_url: string;\n client_x509_cert_url: string;\n}\n\nexport interface NotificationPayload {\n title: string;\n body: string;\n imageUrl?: string;\n sound?: string;\n badge?: number;\n data?: Record<string, string>;\n redirectPath?: string;\n}\n\ninterface SendResult {\n successCount: number;\n failureCount: number;\n invalidTokens: string[];\n}\n\nexport class FirebasePushNotificationClient {\n private static instance: FirebasePushNotificationClient;\n private static initPromise: Promise<FirebasePushNotificationClient> | null = null;\n private logger = Log.getInstance().extend(\"firebase-push\");\n private app: admin.app.App | null = null;\n private serviceAccount: ServiceAccount | null = null;\n\n private constructor(_config: FirebaseConfig, serviceAccount: ServiceAccount) {\n // config parameter kept for backward compatibility but not used (FCM config comes from Secret Manager)\n this.serviceAccount = serviceAccount;\n this.initializeApp();\n }\n\n public static async getInstance(): Promise<FirebasePushNotificationClient> {\n if (FirebasePushNotificationClient.instance) {\n return FirebasePushNotificationClient.instance;\n }\n\n // If already initializing, wait for that to complete\n if (FirebasePushNotificationClient.initPromise) {\n return FirebasePushNotificationClient.initPromise;\n }\n\n // Start initialization\n FirebasePushNotificationClient.initPromise = (async () => {\n // Parse config (mostly for optional APNs fields, FCM comes from Secret Manager)\n const parsedConfig = CommonSchemas.pushNotification.parse(process.env);\n const serviceAccount = await FirebasePushNotificationClient.loadServiceAccount();\n FirebasePushNotificationClient.instance = new FirebasePushNotificationClient(parsedConfig, serviceAccount);\n FirebasePushNotificationClient.initPromise = null;\n return FirebasePushNotificationClient.instance;\n })();\n\n return FirebasePushNotificationClient.initPromise;\n }\n\n private static async loadServiceAccount(): Promise<ServiceAccount> {\n const logger = Log.getInstance().extend(\"firebase-push\");\n const PROJECT_ID = \"1033066542238\";\n const SECRET_NAME = \"firebase_new_key\";\n\n try {\n // Check if FIREBASE_SERVICE_ACCOUNT_KEY env variable is set (for local development)\n const localKeyPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;\n if (localKeyPath) {\n const fs = await import(\"fs/promises\");\n const serviceAccountJson = await fs.readFile(localKeyPath, \"utf-8\");\n const serviceAccount = JSON.parse(serviceAccountJson) as ServiceAccount;\n\n logger.info(\"Firebase service account loaded from local file\");\n return serviceAccount;\n }\n\n // Production: Load from Secret Manager using shared utility\n // This provides better error handling, caching, and reuses the client instance\n logger.info(\"Loading Firebase service account from Secret Manager\", {\n projectId: PROJECT_ID,\n secretName: SECRET_NAME,\n });\n\n const serviceAccount = await SecretManager.loadSecretJSON<ServiceAccount>(\n SECRET_NAME,\n PROJECT_ID\n );\n\n logger.info(\"Firebase service account loaded successfully from Secret Manager\", {\n projectId: serviceAccount.project_id,\n clientEmail: serviceAccount.client_email,\n });\n return serviceAccount;\n } catch (error) {\n logger.error(\"Failed to load Firebase service account\", {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n throw new ExternalAPIError(\n \"Failed to load Firebase credentials\",\n `Error: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n public static resetInstance(): void {\n FirebasePushNotificationClient.instance?.shutdown();\n FirebasePushNotificationClient.instance = undefined as any;\n FirebasePushNotificationClient.initPromise = null;\n }\n\n private initializeApp(): void {\n try {\n // Check if app already exists\n if (admin.apps?.length && admin.apps.length > 0) {\n this.app = admin.app();\n this.logger.info(\"Using existing Firebase app\");\n return;\n }\n\n if (!this.serviceAccount) {\n throw new Error(\"Service account not loaded\");\n }\n\n // Validate service account has all required fields\n if (!this.serviceAccount.project_id || !this.serviceAccount.client_email || !this.serviceAccount.private_key) {\n throw new Error(\"Invalid service account: missing required fields\");\n }\n\n this.logger.info(\"Initializing Firebase Admin SDK\", {\n projectId: this.serviceAccount.project_id,\n clientEmail: this.serviceAccount.client_email,\n });\n\n this.app = admin.initializeApp({\n credential: admin.credential.cert(this.serviceAccount as admin.ServiceAccount),\n });\n\n this.logger.info(\"Firebase Admin SDK initialized successfully\");\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 shutdown(): void {\n if (this.app) {\n this.app.delete().catch((error) => {\n this.logger.error(\"Error shutting down Firebase app\", { error });\n });\n this.app = null;\n }\n }\n\n /**\n * Detect if a token is a legacy APN token (hex format, 64 characters)\n */\n private isLegacyAPNToken(token: string): boolean {\n // Legacy APN tokens are 64 hex characters (sometimes with spaces or angle brackets)\n const cleanToken = token.replace(/[<>\\s]/g, '');\n return /^[0-9a-fA-F]{64}$/.test(cleanToken);\n }\n\n /**\n * Send notifications using multicast (up to 500 tokens at once)\n * Automatically handles token validation and cleanup\n */\n private async sendMulticast(\n tokens: string[],\n notification: NotificationPayload,\n platform?: \"ios\" | \"android\"\n ): Promise<SendResult> {\n if (tokens.length === 0) {\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n const messaging = admin.messaging();\n const invalidTokens: string[] = [];\n let successCount = 0;\n let failureCount = 0;\n\n // Detect if any tokens are legacy APN tokens\n const hasLegacyAPNTokens = tokens.some(token => this.isLegacyAPNToken(token));\n const isIOSPlatform = platform === \"ios\" || hasLegacyAPNTokens;\n\n if (hasLegacyAPNTokens) {\n this.logger.info(\"Detected legacy APN tokens in batch\", {\n totalTokens: tokens.length,\n legacyCount: tokens.filter(t => this.isLegacyAPNToken(t)).length,\n });\n }\n\n // Firebase allows max 500 tokens per multicast\n const BATCH_SIZE = 500;\n\n for (let i = 0; i < tokens.length; i += BATCH_SIZE) {\n const batch = tokens.slice(i, i + BATCH_SIZE);\n\n const message: admin.messaging.MulticastMessage = {\n tokens: batch,\n notification: {\n title: notification.title,\n body: notification.body,\n ...(notification.imageUrl && { imageUrl: notification.imageUrl }),\n },\n data: notification.data || {},\n ...(isIOSPlatform && {\n apns: {\n payload: {\n aps: {\n sound: notification.sound || \"default\",\n ...(notification.badge !== undefined && { badge: notification.badge }),\n contentAvailable: true,\n },\n },\n fcmOptions: {\n ...(notification.imageUrl && { imageUrl: notification.imageUrl }),\n },\n },\n }),\n ...(platform === \"android\" && {\n android: {\n priority: \"high\",\n notification: {\n sound: notification.sound || \"default\",\n channelId: \"default\",\n ...(notification.imageUrl && { imageUrl: notification.imageUrl }),\n },\n },\n }),\n };\n\n try {\n const response = await messaging.sendEachForMulticast(message);\n\n successCount += response.successCount;\n failureCount += response.failureCount;\n\n // Identify and collect invalid tokens\n response.responses.forEach((resp, idx) => {\n if (!resp.success && resp.error) {\n const errorCode = resp.error.code;\n\n // Token is invalid or unregistered\n if (\n errorCode === \"messaging/invalid-registration-token\" ||\n errorCode === \"messaging/registration-token-not-registered\"\n ) {\n invalidTokens.push(batch[idx]);\n }\n\n this.logger.warn(\"Message send failed\", {\n token: batch[idx],\n error: resp.error.message,\n code: errorCode,\n });\n }\n });\n\n this.logger.info(\"Multicast batch sent\", {\n batchSize: batch.length,\n successCount: response.successCount,\n failureCount: response.failureCount,\n });\n } catch (error) {\n this.logger.error(\"Multicast send error\", { error, batchSize: batch.length });\n failureCount += batch.length;\n }\n }\n\n // Clean up invalid tokens from database\n if (invalidTokens.length > 0) {\n await this.removeInvalidTokens(invalidTokens);\n }\n\n return { successCount, failureCount, invalidTokens };\n }\n\n /**\n * Remove invalid device tokens from database\n */\n private async removeInvalidTokens(tokens: string[]): Promise<void> {\n try {\n const deleted = await DeviceTokens.destroy({\n where: { deviceToken: tokens },\n });\n\n this.logger.info(\"Removed invalid tokens from database\", {\n count: deleted,\n tokens: tokens.length,\n });\n } catch (error) {\n this.logger.error(\"Failed to remove invalid tokens\", { error });\n }\n }\n\n /**\n * Send notification to specific device tokens\n */\n public async sendToTokens(\n tokens: string[],\n platform: \"ios\" | \"android\",\n notification: NotificationPayload\n ): Promise<SendResult> {\n if (tokens.length === 0) {\n this.logger.warn(\"No tokens provided, skipping notification\");\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n this.logger.info(\"Sending notification to tokens\", {\n tokenCount: tokens.length,\n platform,\n title: notification.title,\n });\n\n return await this.sendMulticast(tokens, notification, platform);\n }\n\n /**\n * Send notification to a single user (all their devices)\n */\n public async sendToUser(params: {\n userUuid: string;\n notification: NotificationPayload;\n }): Promise<SendResult> {\n this.logger.info(\"Sending notification to user\", { userUuid: params.userUuid });\n\n const deviceTokens = await DeviceTokens.findAll({\n where: { userUuid: params.userUuid },\n attributes: [\"deviceToken\", \"platform\"],\n });\n\n if (deviceTokens.length === 0) {\n this.logger.warn(\"No device tokens found for user\", {\n userUuid: params.userUuid,\n });\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n // Separate tokens by platform, also detecting legacy APN tokens\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\" || this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\" && !this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n\n const legacyAPNCount = deviceTokens.filter(dt => this.isLegacyAPNToken(dt.deviceToken)).length;\n if (legacyAPNCount > 0) {\n this.logger.info(\"Found legacy APN tokens for user\", {\n userUuid: params.userUuid,\n legacyTokenCount: legacyAPNCount,\n });\n }\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n return {\n successCount: iosResult.successCount + androidResult.successCount,\n failureCount: iosResult.failureCount + androidResult.failureCount,\n invalidTokens: [...iosResult.invalidTokens, ...androidResult.invalidTokens],\n };\n }\n\n /**\n * Send notification to multiple users\n */\n public async sendToUsers(params: {\n userUuids: string[];\n notification: NotificationPayload;\n }): Promise<SendResult> {\n this.logger.info(\"Sending notification to multiple users\", {\n count: params.userUuids.length,\n });\n\n const deviceTokens = await DeviceTokens.findAll({\n where: { userUuid: params.userUuids },\n attributes: [\"deviceToken\", \"platform\"],\n });\n\n if (deviceTokens.length === 0) {\n this.logger.warn(\"No device tokens found for users\");\n return { successCount: 0, failureCount: 0, invalidTokens: [] };\n }\n\n // Separate tokens by platform, also detecting legacy APN tokens\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\" || this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\" && !this.isLegacyAPNToken(dt.deviceToken))\n .map((dt) => dt.deviceToken);\n\n const legacyAPNCount = deviceTokens.filter(dt => this.isLegacyAPNToken(dt.deviceToken)).length;\n if (legacyAPNCount > 0) {\n this.logger.info(\"Found legacy APN tokens for users\", {\n legacyTokenCount: legacyAPNCount,\n });\n }\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n return {\n successCount: iosResult.successCount + androidResult.successCount,\n failureCount: iosResult.failureCount + androidResult.failureCount,\n invalidTokens: [...iosResult.invalidTokens, ...androidResult.invalidTokens],\n };\n }\n\n /**\n * Subscribe users to a topic for efficient group messaging\n */\n public async subscribeToTopic(\n tokens: string[],\n topic: string\n ): Promise<{ successCount: number; failureCount: number }> {\n if (tokens.length === 0) {\n return { successCount: 0, failureCount: 0 };\n }\n\n this.logger.info(\"Subscribing tokens to topic\", {\n tokenCount: tokens.length,\n topic,\n });\n\n try {\n const response = await admin.messaging().subscribeToTopic(tokens, topic);\n\n this.logger.info(\"Topic subscription complete\", {\n successCount: response.successCount,\n failureCount: response.failureCount,\n topic,\n });\n\n return {\n successCount: response.successCount,\n failureCount: response.failureCount,\n };\n } catch (error) {\n this.logger.error(\"Topic subscription error\", { error, topic });\n throw new ExternalAPIError(\n \"Failed to subscribe to topic\",\n `Topic: ${topic}, Error: ${String(error)}`\n );\n }\n }\n\n /**\n * Send notification to a topic (efficient for large groups)\n */\n public async sendToTopic(params: {\n topic: string;\n notification: NotificationPayload;\n }): Promise<{ messageId: string }> {\n this.logger.info(\"Sending notification to topic\", { topic: params.topic });\n\n const message: admin.messaging.Message = {\n topic: params.topic,\n notification: {\n title: params.notification.title,\n body: params.notification.body,\n ...(params.notification.imageUrl && {\n imageUrl: params.notification.imageUrl,\n }),\n },\n data: params.notification.data || {},\n apns: {\n payload: {\n aps: {\n sound: params.notification.sound || \"default\",\n ...(params.notification.badge !== undefined && { badge: params.notification.badge }),\n contentAvailable: true,\n },\n },\n },\n android: {\n priority: \"high\",\n notification: {\n sound: params.notification.sound || \"default\",\n channelId: \"default\",\n },\n },\n };\n\n try {\n const messageId = await admin.messaging().send(message);\n\n this.logger.info(\"Topic notification sent\", {\n topic: params.topic,\n messageId,\n });\n\n return { messageId };\n } catch (error) {\n this.logger.error(\"Topic send error\", { error, topic: params.topic });\n throw new ExternalAPIError(\n \"Failed to send topic notification\",\n `Topic: ${params.topic}, Error: ${String(error)}`\n );\n }\n }\n\n /**\n * Send notification using a template\n */\n public async sendTemplateToUser(params: {\n userUuid: string;\n notificationType: string;\n language?: string;\n data?: Record<string, string>;\n }): Promise<SendResult> {\n this.logger.info(\"Sending template notification to user\", {\n userUuid: params.userUuid,\n notificationType: params.notificationType,\n language: params.language,\n });\n\n const includeOptions: any = {\n model: TranslatedNotification,\n as: \"translations\",\n required: false,\n };\n\n if (params.language) {\n includeOptions.where = { language: params.language };\n }\n\n const template = await NotificationModels.findOne({\n where: { type: params.notificationType as any },\n include: [includeOptions],\n });\n\n if (!template) {\n throw new ExternalAPIError(\n \"Notification template not found\",\n `Type: ${params.notificationType}`\n );\n }\n\n const translation = template.translations?.find(\n (t) => t.language === params.language\n );\n let text = translation?.text || template.text;\n\n if (params.data) {\n Object.entries(params.data).forEach(([key, value]) => {\n text = text.replace(new RegExp(`{{${key}}}`, \"g\"), value);\n });\n }\n\n return await this.sendToUser({\n userUuid: params.userUuid,\n notification: {\n title: params.notificationType,\n body: text,\n ...(params.data && { data: params.data }),\n },\n });\n }\n\n /**\n * Send templated notification to multiple users\n */\n public async sendTemplateToUsers(params: {\n userUuids: string[];\n notificationType: string;\n language?: string;\n data?: Record<string, string>;\n }): Promise<SendResult> {\n this.logger.info(\"Sending template notification to multiple users\", {\n count: params.userUuids.length,\n notificationType: params.notificationType,\n });\n\n const includeOptions: any = {\n model: TranslatedNotification,\n as: \"translations\",\n required: false,\n };\n\n if (params.language) {\n includeOptions.where = { language: params.language };\n }\n\n const template = await NotificationModels.findOne({\n where: { type: params.notificationType as any },\n include: [includeOptions],\n });\n\n if (!template) {\n throw new ExternalAPIError(\n \"Notification template not found\",\n `Type: ${params.notificationType}`\n );\n }\n\n const translation = template.translations?.find(\n (t) => t.language === params.language\n );\n let text = translation?.text || template.text;\n\n if (params.data) {\n Object.entries(params.data).forEach(([key, value]) => {\n text = text.replace(new RegExp(`{{${key}}}`, \"g\"), value);\n });\n }\n\n return await this.sendToUsers({\n userUuids: params.userUuids,\n notification: {\n title: params.notificationType,\n body: text,\n ...(params.data && { data: params.data }),\n },\n });\n }\n\n /**\n * Send notification to all users (uses topic for efficiency)\n * Consider subscribing users to an \"all_users\" topic for better performance\n */\n public async sendToAllUsers(params: {\n notification: NotificationPayload;\n batchSize?: number;\n }): Promise<{ totalSent: number }> {\n const batchSize = params.batchSize || 1000;\n let offset = 0;\n let totalSent = 0;\n\n this.logger.info(\"Starting broadcast notification\", { batchSize });\n\n while (true) {\n const deviceTokens = await DeviceTokens.findAll({\n limit: batchSize,\n offset,\n attributes: [\"deviceToken\", \"platform\"],\n });\n\n if (deviceTokens.length === 0) {\n break;\n }\n\n const iosTokens = deviceTokens\n .filter((dt) => dt.platform === \"ios\")\n .map((dt) => dt.deviceToken);\n const androidTokens = deviceTokens\n .filter((dt) => dt.platform === \"android\")\n .map((dt) => dt.deviceToken);\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n totalSent += iosResult.successCount + androidResult.successCount;\n offset += batchSize;\n\n this.logger.info(\"Broadcast batch sent\", {\n batchSent: iosResult.successCount + androidResult.successCount,\n totalSent,\n });\n }\n\n this.logger.info(\"Broadcast notification complete\", { totalSent });\n return { totalSent };\n }\n\n /**\n * Send notification to a user group\n * Recommended: Use topics instead for better performance\n */\n public async sendToGroup(params: {\n notification: NotificationPayload;\n group: \"premium\" | \"free\" | \"trial\";\n batchSize?: number;\n }): Promise<{ totalSent: number }> {\n const batchSize = params.batchSize || 1000;\n let offset = 0;\n let totalSent = 0;\n\n this.logger.info(\"Starting group notification\", {\n group: params.group,\n batchSize,\n });\n\n while (true) {\n const users = await PersistedUser.findAll({\n where: { subscriptionType: params.group } as any,\n include: [\n {\n model: DeviceTokens,\n as: \"deviceTokens\",\n attributes: [\"deviceToken\", \"platform\"],\n },\n ],\n limit: batchSize,\n offset,\n });\n\n if (users.length === 0) {\n break;\n }\n\n const allTokens = users.flatMap((user) => (user as any).deviceTokens || []);\n const iosTokens = allTokens\n .filter((dt: any) => dt.platform === \"ios\")\n .map((dt: any) => dt.deviceToken);\n const androidTokens = allTokens\n .filter((dt: any) => dt.platform === \"android\")\n .map((dt: any) => dt.deviceToken);\n\n const [iosResult, androidResult] = await Promise.all([\n iosTokens.length > 0\n ? this.sendToTokens(iosTokens, \"ios\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n androidTokens.length > 0\n ? this.sendToTokens(androidTokens, \"android\", params.notification)\n : Promise.resolve({ successCount: 0, failureCount: 0, invalidTokens: [] }),\n ]);\n\n totalSent += iosResult.successCount + androidResult.successCount;\n offset += batchSize;\n\n this.logger.info(\"Group batch sent\", {\n batchSent: iosResult.successCount + androidResult.successCount,\n totalSent,\n });\n }\n\n this.logger.info(\"Group notification complete\", {\n totalSent,\n group: params.group,\n });\n return { totalSent };\n }\n}\n"],"names":["admin","CommonSchemas","ExternalAPIError","Log","SecretManager","DeviceTokens","PersistedUser","NotificationModels","TranslatedNotification","FirebasePushNotificationClient","instance","initPromise","logger","getInstance","extend","app","serviceAccount","_config","initializeApp","parsedConfig","pushNotification","parse","process","env","loadServiceAccount","PROJECT_ID","SECRET_NAME","localKeyPath","FIREBASE_SERVICE_ACCOUNT_KEY","fs","serviceAccountJson","readFile","JSON","info","projectId","secretName","loadSecretJSON","project_id","clientEmail","client_email","error","Error","message","String","stack","undefined","resetInstance","shutdown","apps","length","private_key","credential","cert","delete","catch","isLegacyAPNToken","token","cleanToken","replace","test","sendMulticast","tokens","notification","platform","successCount","failureCount","invalidTokens","messaging","hasLegacyAPNTokens","some","isIOSPlatform","totalTokens","legacyCount","filter","t","BATCH_SIZE","i","batch","slice","title","body","imageUrl","data","apns","payload","aps","sound","badge","contentAvailable","fcmOptions","android","priority","channelId","response","sendEachForMulticast","responses","forEach","resp","idx","success","errorCode","code","push","warn","batchSize","removeInvalidTokens","deleted","destroy","where","deviceToken","count","sendToTokens","tokenCount","sendToUser","params","userUuid","deviceTokens","findAll","attributes","iosTokens","dt","map","androidTokens","legacyAPNCount","legacyTokenCount","iosResult","androidResult","Promise","all","resolve","sendToUsers","userUuids","subscribeToTopic","topic","sendToTopic","messageId","send","sendTemplateToUser","notificationType","language","includeOptions","model","as","required","template","findOne","type","include","translation","translations","find","text","Object","entries","key","value","RegExp","sendTemplateToUsers","sendToAllUsers","offset","totalSent","limit","batchSent","sendToGroup","group","users","subscriptionType","allTokens","flatMap","user"],"mappings":"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"}
@@ -15,9 +15,10 @@ export declare class ImagesClient {
15
15
  getRecipePhotos(recipeUuid: string): Promise<t.RecipePhotos | undefined>;
16
16
  saveUserProgressPhoto(input: t.UserProgressPhotoInput): Promise<t.UserProgressPhotoRecord>;
17
17
  getUserProgressPhotos(userUuid: string): Promise<t.UserProgressPhotoRecord[]>;
18
- getProgressPhotosByProgram(programId: string): Promise<t.UserProgressPhotoRecord[]>;
18
+ getProgressPhotosByProgram(programId: string, userUuid?: string): Promise<t.UserProgressPhotoRecord[]>;
19
19
  updateProgressPhotoDate(input: t.UpdateProgressPhotoDateInput): Promise<t.UserProgressPhotoRecord | null>;
20
20
  deleteProgressPhoto(uuid: string, userUuid: string): Promise<boolean>;
21
+ private migrateLegacyPhotosForUser;
21
22
  private base64ToBuffer;
22
23
  private handleError;
23
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ImagesClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/ImagesClient.ts"],"names":[],"mappings":"AAOA,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;IA6CR,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;IAUjC,0BAA0B,CAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC;IAUjC,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;IA4BnB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;CAOpB"}
1
+ {"version":3,"file":"ImagesClient.d.ts","sourceRoot":"","sources":["../../../src/lib/clients/ImagesClient.ts"],"names":[],"mappings":"AAOA,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;IA6CR,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;IAwFxC,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;CAOpB"}
@@ -0,0 +1,436 @@
1
+ import admin from "firebase-admin";
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import { ExternalAPIError } from "../Errors/AppError.js";
4
+ import { Log } from "../utils/Logger.js";
5
+ import { validateBase64Image } from "../utils/imageValidation.js";
6
+ import { UserProgressPhoto } from "../dbmodels/user/UserProgressPhoto.js";
7
+ const PROGRESS_DAYS = [
8
+ 1,
9
+ 30,
10
+ 60,
11
+ 90
12
+ ];
13
+ const RECIPE_PHOTO_TYPES = [
14
+ "ingredients",
15
+ "cover",
16
+ "method"
17
+ ];
18
+ const imageName = {
19
+ avatar: (userUuid)=>`${userUuid}--avatar.png`,
20
+ progress: (userUuid, day, programId)=>`${userUuid}--${day}day--${programId}.png`,
21
+ recipe: (recipeUuid, type)=>`${recipeUuid}--${type}.png`
22
+ };
23
+ export class ImagesClient {
24
+ bucket;
25
+ logger = Log.getInstance().extend("images-client");
26
+ app;
27
+ constructor(config){
28
+ if (config.firebaseApp) {
29
+ // Use provided Firebase app
30
+ this.app = config.firebaseApp;
31
+ } else {
32
+ // Initialize Firebase if needed (following FirebasePushNotificationClient pattern)
33
+ this.initializeApp(config.storageBucket);
34
+ this.app = admin.app();
35
+ }
36
+ this.bucket = this.app.storage().bucket(config.storageBucket);
37
+ }
38
+ initializeApp(storageBucket) {
39
+ try {
40
+ // Check if app already exists
41
+ if (admin.apps?.length && admin.apps.length > 0) {
42
+ this.logger.info("Using existing Firebase app");
43
+ return;
44
+ }
45
+ // Try to load service account from environment (for local development)
46
+ const serviceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
47
+ if (serviceAccountPath) {
48
+ // Local development with service account file
49
+ const fs = require("fs");
50
+ const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountPath, "utf-8"));
51
+ admin.initializeApp({
52
+ credential: admin.credential.cert(serviceAccount),
53
+ storageBucket
54
+ });
55
+ this.logger.info("Firebase initialized with service account file", {
56
+ projectId: serviceAccount.project_id,
57
+ storageBucket
58
+ });
59
+ } else {
60
+ // Production: use application default credentials
61
+ admin.initializeApp({
62
+ credential: admin.credential.applicationDefault(),
63
+ storageBucket
64
+ });
65
+ this.logger.info("Firebase initialized with application default credentials", {
66
+ storageBucket
67
+ });
68
+ }
69
+ } catch (error) {
70
+ this.logger.error("Failed to initialize Firebase Admin SDK", {
71
+ error
72
+ });
73
+ throw new ExternalAPIError("Failed to initialize Firebase", `Error: ${String(error)}`);
74
+ }
75
+ }
76
+ async saveImage(base64, name) {
77
+ try {
78
+ this.logger.info("Saving image", {
79
+ name
80
+ });
81
+ const buffer = this.base64ToBuffer(base64);
82
+ const file = this.bucket.file(name);
83
+ await file.save(buffer, {
84
+ contentType: "image/png"
85
+ });
86
+ this.logger.info("Image saved successfully", {
87
+ name
88
+ });
89
+ } catch (error) {
90
+ this.handleError(error, "saveImage");
91
+ }
92
+ }
93
+ async getImage(name) {
94
+ try {
95
+ this.logger.info("Getting image", {
96
+ name
97
+ });
98
+ const file = this.bucket.file(name);
99
+ const [exists] = await file.exists();
100
+ if (!exists) {
101
+ return undefined;
102
+ }
103
+ const [contents] = await file.download();
104
+ return "data:image/jpeg;base64," + contents.toString("base64");
105
+ } catch (error) {
106
+ this.logger.error(`Failed to get image: ${name}`, {
107
+ error
108
+ });
109
+ return undefined;
110
+ }
111
+ }
112
+ async saveAvatar(userUuid, base64) {
113
+ await this.saveImage(base64, imageName.avatar(userUuid));
114
+ }
115
+ async getAvatar(userUuid) {
116
+ return this.getImage(imageName.avatar(userUuid));
117
+ }
118
+ async saveProgressPhoto(userUuid, day, programId, base64) {
119
+ await this.saveImage(base64, imageName.progress(userUuid, day, programId));
120
+ }
121
+ async getProgressPhotos(userUuid, programId) {
122
+ const photos = [];
123
+ for (const day of PROGRESS_DAYS){
124
+ const base64 = await this.getImage(imageName.progress(userUuid, day, programId));
125
+ if (base64) {
126
+ photos.push({
127
+ day,
128
+ base64
129
+ });
130
+ }
131
+ }
132
+ return photos;
133
+ }
134
+ async saveRecipePhotos(input) {
135
+ await Promise.all([
136
+ this.saveImage(input.ingredientBase64, imageName.recipe(input.recipeUuid, "ingredients")),
137
+ this.saveImage(input.coverBase64, imageName.recipe(input.recipeUuid, "cover")),
138
+ this.saveImage(input.methodBase64, imageName.recipe(input.recipeUuid, "method"))
139
+ ]);
140
+ }
141
+ async getRecipePhotos(recipeUuid) {
142
+ const results = await Promise.all(RECIPE_PHOTO_TYPES.map((type)=>this.getImage(imageName.recipe(recipeUuid, type))));
143
+ if (results.every((r)=>!r)) {
144
+ return undefined;
145
+ }
146
+ // BUG: cover/method swapped — preserved from original ImageController.ts to keep backward compat with frontend
147
+ return {
148
+ ingredientBase64: results[0] ?? "",
149
+ methodBase64: results[1] ?? "",
150
+ coverBase64: results[2] ?? ""
151
+ };
152
+ }
153
+ async saveUserProgressPhoto(input) {
154
+ // Validate image
155
+ validateBase64Image(input.base64);
156
+ // Generate UUID for the photo
157
+ const uuid = uuidv4();
158
+ // Prepare Firebase path
159
+ const firebasePath = `progress-pictures/${input.userUuid}/${uuid}.png`;
160
+ // Upload to Firebase Storage
161
+ const base64Data = input.base64.replace(/^data:image\/\w+;base64,/, "");
162
+ const buffer = Buffer.from(base64Data, "base64");
163
+ const file = this.bucket.file(firebasePath);
164
+ await file.save(buffer, {
165
+ metadata: {
166
+ contentType: "image/png"
167
+ }
168
+ });
169
+ // Save metadata to database
170
+ const photoDate = input.photoDate || new Date();
171
+ const record = await UserProgressPhoto.create({
172
+ uuid,
173
+ userUuid: input.userUuid,
174
+ firebasePath,
175
+ photoDate,
176
+ programId: input.programId || null
177
+ });
178
+ return record.toJSON();
179
+ }
180
+ async getUserProgressPhotos(userUuid) {
181
+ const records = await UserProgressPhoto.findAll({
182
+ where: {
183
+ userUuid
184
+ },
185
+ order: [
186
+ [
187
+ "photoDate",
188
+ "DESC"
189
+ ]
190
+ ],
191
+ raw: true,
192
+ attributes: [
193
+ 'uuid',
194
+ 'userUuid',
195
+ 'firebasePath',
196
+ 'photoDate',
197
+ 'programId',
198
+ 'createdAt',
199
+ 'updatedAt'
200
+ ]
201
+ });
202
+ // If no photos in new system, attempt to migrate from legacy Firebase structure
203
+ if (records.length === 0) {
204
+ this.logger.info("No photos in new system, checking for legacy photos to migrate", {
205
+ userUuid
206
+ });
207
+ await this.migrateLegacyPhotosForUser(userUuid);
208
+ // Re-fetch after migration
209
+ const migratedRecords = await UserProgressPhoto.findAll({
210
+ where: {
211
+ userUuid
212
+ },
213
+ order: [
214
+ [
215
+ "photoDate",
216
+ "DESC"
217
+ ]
218
+ ],
219
+ raw: true,
220
+ attributes: [
221
+ 'uuid',
222
+ 'userUuid',
223
+ 'firebasePath',
224
+ 'photoDate',
225
+ 'programId',
226
+ 'createdAt',
227
+ 'updatedAt'
228
+ ]
229
+ });
230
+ return migratedRecords;
231
+ }
232
+ return records;
233
+ }
234
+ async getProgressPhotosByProgram(programId, userUuid) {
235
+ const records = await UserProgressPhoto.findAll({
236
+ where: {
237
+ programId
238
+ },
239
+ order: [
240
+ [
241
+ "photoDate",
242
+ "DESC"
243
+ ]
244
+ ],
245
+ raw: true,
246
+ attributes: [
247
+ 'uuid',
248
+ 'userUuid',
249
+ 'firebasePath',
250
+ 'photoDate',
251
+ 'programId',
252
+ 'createdAt',
253
+ 'updatedAt'
254
+ ]
255
+ });
256
+ // If no photos found and userUuid provided, attempt migration
257
+ if (records.length === 0 && userUuid) {
258
+ this.logger.info("No photos found for program, attempting migration", {
259
+ programId,
260
+ userUuid
261
+ });
262
+ await this.migrateLegacyPhotosForUser(userUuid);
263
+ // Re-fetch after migration
264
+ const migratedRecords = await UserProgressPhoto.findAll({
265
+ where: {
266
+ programId
267
+ },
268
+ order: [
269
+ [
270
+ "photoDate",
271
+ "DESC"
272
+ ]
273
+ ],
274
+ raw: true,
275
+ attributes: [
276
+ 'uuid',
277
+ 'userUuid',
278
+ 'firebasePath',
279
+ 'photoDate',
280
+ 'programId',
281
+ 'createdAt',
282
+ 'updatedAt'
283
+ ]
284
+ });
285
+ return migratedRecords;
286
+ }
287
+ return records;
288
+ }
289
+ async updateProgressPhotoDate(input) {
290
+ const [affectedRows] = await UserProgressPhoto.update({
291
+ photoDate: input.photoDate
292
+ }, {
293
+ where: {
294
+ uuid: input.uuid,
295
+ userUuid: input.userUuid
296
+ }
297
+ });
298
+ if (affectedRows === 0) {
299
+ return null; // Photo not found or user doesn't own it
300
+ }
301
+ const updated = await UserProgressPhoto.findOne({
302
+ where: {
303
+ uuid: input.uuid
304
+ },
305
+ raw: true
306
+ });
307
+ return updated;
308
+ }
309
+ async deleteProgressPhoto(uuid, userUuid) {
310
+ // Find the photo to get the firebasePath
311
+ const photo = await UserProgressPhoto.findOne({
312
+ where: {
313
+ uuid,
314
+ userUuid
315
+ },
316
+ raw: true
317
+ });
318
+ if (!photo) {
319
+ return false; // Photo not found or user doesn't own it
320
+ }
321
+ // Delete from Firebase Storage
322
+ const file = this.bucket.file(photo.firebasePath);
323
+ try {
324
+ await file.delete();
325
+ } catch (error) {
326
+ // If file doesn't exist in storage, continue with database deletion
327
+ console.warn(`Failed to delete file from storage: ${photo.firebasePath}`, error);
328
+ }
329
+ // Delete from database
330
+ await UserProgressPhoto.destroy({
331
+ where: {
332
+ uuid,
333
+ userUuid
334
+ }
335
+ });
336
+ return true;
337
+ }
338
+ async migrateLegacyPhotosForUser(userUuid) {
339
+ try {
340
+ this.logger.info("Starting legacy photo migration", {
341
+ userUuid
342
+ });
343
+ // List all files in the legacy path pattern: {userUuid}--{day}day--{programId}.png
344
+ const [files] = await this.bucket.getFiles({
345
+ prefix: `${userUuid}--`
346
+ });
347
+ if (files.length === 0) {
348
+ this.logger.info("No legacy photos found for user", {
349
+ userUuid
350
+ });
351
+ return;
352
+ }
353
+ const migratedCount = 0;
354
+ const skippedCount = 0;
355
+ for (const file of files){
356
+ const fileName = file.name;
357
+ // Parse legacy filename: {userUuid}--{day}day--{programId}.png
358
+ const match = fileName.match(/^(.+)--(\d+)day--(.+)\.png$/);
359
+ if (!match) {
360
+ this.logger.warn("Skipping file with unexpected name format", {
361
+ fileName
362
+ });
363
+ continue;
364
+ }
365
+ const [, , dayStr, programId] = match;
366
+ const day = parseInt(dayStr, 10);
367
+ // Check if this photo was already migrated (by programId + userUuid + approximate day)
368
+ const existing = await UserProgressPhoto.findOne({
369
+ where: {
370
+ userUuid,
371
+ programId
372
+ },
373
+ raw: true
374
+ });
375
+ if (existing) {
376
+ this.logger.debug("Photo already migrated, skipping", {
377
+ fileName
378
+ });
379
+ continue;
380
+ }
381
+ // Download the legacy photo
382
+ const [contents] = await file.download();
383
+ // Calculate photoDate based on day offset (we don't have program start date, so use upload time)
384
+ // In reality, this would need Program model to calculate exact date
385
+ const photoDate = new Date(); // Fallback: use current time
386
+ photoDate.setDate(photoDate.getDate() - (90 - day)); // Rough estimate based on day
387
+ // Generate new UUID and path
388
+ const newUuid = uuidv4();
389
+ const newFirebasePath = `progress-pictures/${userUuid}/${newUuid}.png`;
390
+ // Copy to new Firebase location
391
+ await this.bucket.file(newFirebasePath).save(contents, {
392
+ metadata: {
393
+ contentType: "image/png"
394
+ }
395
+ });
396
+ // Create database record
397
+ await UserProgressPhoto.create({
398
+ uuid: newUuid,
399
+ userUuid,
400
+ firebasePath: newFirebasePath,
401
+ photoDate,
402
+ programId
403
+ });
404
+ this.logger.info("Migrated legacy photo", {
405
+ oldPath: fileName,
406
+ newPath: newFirebasePath,
407
+ day,
408
+ programId
409
+ });
410
+ }
411
+ this.logger.info("Legacy photo migration completed", {
412
+ userUuid,
413
+ migrated: migratedCount,
414
+ skipped: skippedCount
415
+ });
416
+ } catch (error) {
417
+ this.logger.error("Failed to migrate legacy photos", {
418
+ userUuid,
419
+ error
420
+ });
421
+ // Don't throw - migration failure shouldn't break the request
422
+ }
423
+ }
424
+ base64ToBuffer(base64) {
425
+ const data = base64.includes(",") ? base64.split(",")[1] : base64;
426
+ return Buffer.from(data, "base64");
427
+ }
428
+ handleError(error, method) {
429
+ this.logger.error(`Firebase Storage error in ${method}`, {
430
+ error
431
+ });
432
+ throw new ExternalAPIError(`Firebase Storage error: ${error instanceof Error ? error.message : String(error)}`, `Service: Firebase Storage, Method: ${method}`);
433
+ }
434
+ }
435
+
436
+ //# sourceMappingURL=ImagesClient.js.map
@@ -0,0 +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 { 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 fs = require(\"fs\");\n const serviceAccount = JSON.parse(fs.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 const [files] = await this.bucket.getFiles({\n prefix: `${userUuid}--`,\n });\n\n if (files.length === 0) {\n this.logger.info(\"No legacy photos found for user\", { userUuid });\n return;\n }\n\n const migratedCount = 0;\n const 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","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","fs","require","serviceAccount","JSON","parse","readFileSync","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","migratedCount","skippedCount","fileName","match","dayStr","parseInt","existing","debug","setDate","getDate","newUuid","newFirebasePath","oldPath","newPath","migrated","skipped","data","includes","split","method","Error","message"],"mappings":"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"}