@blackcode_sa/metaestetics-api 1.15.16 → 1.15.17-staging.0

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 (326) hide show
  1. package/dist/admin/index.d.mts +377 -222
  2. package/dist/admin/index.d.ts +377 -222
  3. package/dist/admin/index.js +625 -206
  4. package/dist/admin/index.mjs +624 -206
  5. package/dist/backoffice/index.d.mts +24 -0
  6. package/dist/backoffice/index.d.ts +24 -0
  7. package/dist/index.d.mts +292 -4
  8. package/dist/index.d.ts +292 -4
  9. package/dist/index.js +1142 -630
  10. package/dist/index.mjs +1137 -617
  11. package/package.json +2 -1
  12. package/src/__mocks__/firstore.ts +10 -10
  13. package/src/admin/aggregation/README.md +79 -79
  14. package/src/admin/aggregation/appointment/README.md +151 -129
  15. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +2137 -2091
  16. package/src/admin/aggregation/appointment/index.ts +1 -1
  17. package/src/admin/aggregation/clinic/README.md +52 -52
  18. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -966
  19. package/src/admin/aggregation/clinic/index.ts +1 -1
  20. package/src/admin/aggregation/forms/README.md +13 -13
  21. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  22. package/src/admin/aggregation/forms/index.ts +1 -1
  23. package/src/admin/aggregation/index.ts +8 -8
  24. package/src/admin/aggregation/patient/README.md +27 -27
  25. package/src/admin/aggregation/patient/index.ts +1 -1
  26. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  27. package/src/admin/aggregation/practitioner/README.md +42 -42
  28. package/src/admin/aggregation/practitioner/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  30. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  31. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  32. package/src/admin/aggregation/procedure/README.md +43 -43
  33. package/src/admin/aggregation/procedure/index.ts +1 -1
  34. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  35. package/src/admin/aggregation/reviews/index.ts +1 -1
  36. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  37. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  38. package/src/admin/analytics/index.ts +2 -2
  39. package/src/admin/booking/README.md +184 -125
  40. package/src/admin/booking/booking.admin.ts +1330 -1073
  41. package/src/admin/booking/booking.calculator.ts +850 -712
  42. package/src/admin/booking/booking.types.ts +76 -59
  43. package/src/admin/booking/index.ts +3 -3
  44. package/src/admin/booking/timezones-problem.md +185 -185
  45. package/src/admin/calendar/README.md +62 -7
  46. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  47. package/src/admin/calendar/index.ts +2 -1
  48. package/src/admin/calendar/resource-calendar.admin.ts +198 -0
  49. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  50. package/src/admin/documentation-templates/index.ts +1 -1
  51. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  52. package/src/admin/free-consultation/index.ts +1 -1
  53. package/src/admin/index.ts +83 -83
  54. package/src/admin/logger/index.ts +78 -78
  55. package/src/admin/mailing/README.md +139 -139
  56. package/src/admin/mailing/appointment/appointment.mailing.service.ts +1253 -1253
  57. package/src/admin/mailing/appointment/index.ts +1 -1
  58. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  59. package/src/admin/mailing/base.mailing.service.ts +208 -208
  60. package/src/admin/mailing/clinicWelcome/clinicWelcome.mailing.ts +292 -292
  61. package/src/admin/mailing/clinicWelcome/index.ts +1 -1
  62. package/src/admin/mailing/clinicWelcome/templates/welcome.template.ts +225 -225
  63. package/src/admin/mailing/index.ts +5 -5
  64. package/src/admin/mailing/patientInvite/index.ts +2 -2
  65. package/src/admin/mailing/patientInvite/patientInvite.mailing.ts +415 -415
  66. package/src/admin/mailing/patientInvite/templates/invitation.template.ts +105 -105
  67. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  68. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  69. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  70. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  71. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  72. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  73. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  74. package/src/admin/notifications/index.ts +1 -1
  75. package/src/admin/notifications/notifications.admin.ts +818 -818
  76. package/src/admin/requirements/README.md +128 -128
  77. package/src/admin/requirements/index.ts +1 -1
  78. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  79. package/src/admin/users/index.ts +1 -1
  80. package/src/admin/users/user-profile.admin.ts +405 -405
  81. package/src/backoffice/constants/certification.constants.ts +13 -13
  82. package/src/backoffice/constants/index.ts +1 -1
  83. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  84. package/src/backoffice/errors/index.ts +1 -1
  85. package/src/backoffice/expo-safe/README.md +26 -26
  86. package/src/backoffice/expo-safe/index.ts +41 -41
  87. package/src/backoffice/index.ts +5 -5
  88. package/src/backoffice/services/FIXES_README.md +102 -102
  89. package/src/backoffice/services/README.md +57 -57
  90. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  91. package/src/backoffice/services/analytics.service.summary.md +143 -143
  92. package/src/backoffice/services/brand.service.ts +260 -260
  93. package/src/backoffice/services/category.service.ts +384 -384
  94. package/src/backoffice/services/constants.service.ts +385 -385
  95. package/src/backoffice/services/documentation-template.service.ts +202 -202
  96. package/src/backoffice/services/index.ts +10 -10
  97. package/src/backoffice/services/migrate-products.ts +116 -116
  98. package/src/backoffice/services/product.service.ts +557 -557
  99. package/src/backoffice/services/requirement.service.ts +235 -235
  100. package/src/backoffice/services/subcategory.service.ts +461 -461
  101. package/src/backoffice/services/technology.service.ts +1153 -1153
  102. package/src/backoffice/types/README.md +12 -12
  103. package/src/backoffice/types/admin-constants.types.ts +69 -69
  104. package/src/backoffice/types/brand.types.ts +29 -29
  105. package/src/backoffice/types/category.types.ts +67 -67
  106. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  107. package/src/backoffice/types/index.ts +10 -10
  108. package/src/backoffice/types/procedure-product.types.ts +38 -38
  109. package/src/backoffice/types/product.types.ts +239 -239
  110. package/src/backoffice/types/requirement.types.ts +63 -63
  111. package/src/backoffice/types/static/README.md +18 -18
  112. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  113. package/src/backoffice/types/static/certification.types.ts +37 -37
  114. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  115. package/src/backoffice/types/static/index.ts +6 -6
  116. package/src/backoffice/types/static/pricing.types.ts +16 -16
  117. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  118. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  119. package/src/backoffice/types/subcategory.types.ts +34 -34
  120. package/src/backoffice/types/technology.types.ts +168 -168
  121. package/src/backoffice/validations/index.ts +1 -1
  122. package/src/backoffice/validations/schemas.ts +164 -164
  123. package/src/config/__mocks__/firebase.ts +99 -99
  124. package/src/config/firebase.ts +78 -78
  125. package/src/config/index.ts +17 -17
  126. package/src/config/tiers.config.ts +255 -229
  127. package/src/errors/auth.error.ts +6 -6
  128. package/src/errors/auth.errors.ts +211 -211
  129. package/src/errors/clinic.errors.ts +32 -32
  130. package/src/errors/firebase.errors.ts +47 -47
  131. package/src/errors/user.errors.ts +99 -99
  132. package/src/index.backup.ts +407 -407
  133. package/src/index.ts +6 -6
  134. package/src/locales/en.ts +31 -31
  135. package/src/recommender/admin/index.ts +1 -1
  136. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  137. package/src/recommender/front/index.ts +1 -1
  138. package/src/recommender/front/services/onboarding.service.ts +5 -5
  139. package/src/recommender/front/services/recommender.service.ts +3 -3
  140. package/src/recommender/index.ts +1 -1
  141. package/src/services/PATIENTAUTH.MD +197 -197
  142. package/src/services/README.md +106 -106
  143. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  144. package/src/services/__tests__/auth/auth.setup.ts +298 -298
  145. package/src/services/__tests__/auth.service.test.ts +310 -310
  146. package/src/services/__tests__/base.service.test.ts +36 -36
  147. package/src/services/__tests__/user.service.test.ts +530 -530
  148. package/src/services/analytics/ARCHITECTURE.md +199 -199
  149. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  150. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  151. package/src/services/analytics/QUICK_START.md +393 -393
  152. package/src/services/analytics/README.md +304 -304
  153. package/src/services/analytics/SUMMARY.md +141 -141
  154. package/src/services/analytics/TRENDS.md +380 -380
  155. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  156. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  157. package/src/services/analytics/analytics.service.ts +2148 -2148
  158. package/src/services/analytics/index.ts +4 -4
  159. package/src/services/analytics/review-analytics.service.ts +941 -941
  160. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  161. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  162. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  163. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  164. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  165. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  166. package/src/services/appointment/README.md +17 -17
  167. package/src/services/appointment/appointment.service.ts +2943 -2941
  168. package/src/services/appointment/index.ts +1 -1
  169. package/src/services/appointment/utils/appointment.utils.ts +620 -620
  170. package/src/services/appointment/utils/extended-procedure.utils.ts +354 -354
  171. package/src/services/appointment/utils/form-initialization.utils.ts +516 -516
  172. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  173. package/src/services/appointment/utils/zone-management.utils.ts +468 -468
  174. package/src/services/appointment/utils/zone-photo.utils.ts +302 -302
  175. package/src/services/auth/auth.service.ts +1435 -1435
  176. package/src/services/auth/auth.v2.service.ts +961 -961
  177. package/src/services/auth/index.ts +7 -7
  178. package/src/services/auth/utils/error.utils.ts +90 -90
  179. package/src/services/auth/utils/firebase.utils.ts +49 -49
  180. package/src/services/auth/utils/index.ts +21 -21
  181. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  182. package/src/services/base.service.ts +41 -41
  183. package/src/services/calendar/calendar.service.ts +1077 -1077
  184. package/src/services/calendar/calendar.v2.service.ts +1693 -1693
  185. package/src/services/calendar/calendar.v3.service.ts +313 -313
  186. package/src/services/calendar/externalCalendar.service.ts +178 -178
  187. package/src/services/calendar/index.ts +5 -5
  188. package/src/services/calendar/synced-calendars.service.ts +743 -743
  189. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  190. package/src/services/calendar/utils/calendar-event.utils.ts +676 -676
  191. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  192. package/src/services/calendar/utils/docs.utils.ts +157 -157
  193. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  194. package/src/services/calendar/utils/index.ts +8 -8
  195. package/src/services/calendar/utils/patient.utils.ts +198 -198
  196. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  197. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  198. package/src/services/clinic/README.md +204 -204
  199. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +265 -265
  200. package/src/services/clinic/__tests__/clinic-group.service.test.ts +222 -222
  201. package/src/services/clinic/__tests__/clinic.service.test.ts +302 -302
  202. package/src/services/clinic/billing-transactions.service.ts +217 -217
  203. package/src/services/clinic/clinic-admin.service.ts +202 -202
  204. package/src/services/clinic/clinic-group.service.ts +310 -310
  205. package/src/services/clinic/clinic.service.ts +720 -720
  206. package/src/services/clinic/index.ts +5 -5
  207. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  208. package/src/services/clinic/utils/admin.utils.ts +551 -551
  209. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  210. package/src/services/clinic/utils/clinic.utils.ts +1023 -1023
  211. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  212. package/src/services/clinic/utils/filter.utils.ts +462 -462
  213. package/src/services/clinic/utils/index.ts +10 -10
  214. package/src/services/clinic/utils/photos.utils.ts +188 -188
  215. package/src/services/clinic/utils/search.utils.ts +83 -83
  216. package/src/services/clinic/utils/tag.utils.ts +124 -124
  217. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  218. package/src/services/documentation-templates/filled-document.service.ts +597 -597
  219. package/src/services/documentation-templates/index.ts +2 -2
  220. package/src/services/index.ts +16 -15
  221. package/src/services/media/index.ts +1 -1
  222. package/src/services/media/media.service.ts +418 -418
  223. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  224. package/src/services/notifications/index.ts +1 -1
  225. package/src/services/notifications/notification.service.ts +215 -215
  226. package/src/services/patient/README.md +48 -48
  227. package/src/services/patient/To-Do.md +43 -43
  228. package/src/services/patient/__tests__/patient.service.test.ts +286 -286
  229. package/src/services/patient/index.ts +2 -2
  230. package/src/services/patient/patient.service.ts +1021 -1021
  231. package/src/services/patient/patientRequirements.service.ts +309 -309
  232. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  233. package/src/services/patient/utils/body-assessment.utils.ts +159 -159
  234. package/src/services/patient/utils/clinic.utils.ts +159 -159
  235. package/src/services/patient/utils/docs.utils.ts +142 -142
  236. package/src/services/patient/utils/hair-scalp-assessment.utils.ts +158 -158
  237. package/src/services/patient/utils/index.ts +9 -9
  238. package/src/services/patient/utils/location.utils.ts +126 -126
  239. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  240. package/src/services/patient/utils/medical.utils.ts +458 -458
  241. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  242. package/src/services/patient/utils/pre-surgical-assessment.utils.ts +161 -161
  243. package/src/services/patient/utils/profile.utils.ts +510 -510
  244. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  245. package/src/services/patient/utils/skin-quality-assessment.utils.ts +160 -160
  246. package/src/services/patient/utils/token.utils.ts +211 -211
  247. package/src/services/practitioner/README.md +145 -145
  248. package/src/services/practitioner/index.ts +1 -1
  249. package/src/services/practitioner/practitioner.service.ts +2355 -2354
  250. package/src/services/procedure/README.md +163 -163
  251. package/src/services/procedure/index.ts +1 -1
  252. package/src/services/procedure/procedure.service.ts +2521 -2521
  253. package/src/services/resource/README.md +119 -0
  254. package/src/services/resource/index.ts +1 -0
  255. package/src/services/resource/resource.service.ts +555 -0
  256. package/src/services/reviews/index.ts +1 -1
  257. package/src/services/reviews/reviews.service.ts +745 -745
  258. package/src/services/tier-enforcement.ts +240 -240
  259. package/src/services/user/index.ts +1 -1
  260. package/src/services/user/user.service.ts +533 -533
  261. package/src/services/user/user.v2.service.ts +467 -467
  262. package/src/types/analytics/analytics.types.ts +597 -597
  263. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  264. package/src/types/analytics/index.ts +4 -4
  265. package/src/types/analytics/stored-analytics.types.ts +137 -137
  266. package/src/types/appointment/index.ts +524 -517
  267. package/src/types/calendar/index.ts +261 -260
  268. package/src/types/calendar/synced-calendar.types.ts +66 -66
  269. package/src/types/clinic/index.ts +530 -529
  270. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  271. package/src/types/clinic/preferences.types.ts +159 -159
  272. package/src/types/clinic/rbac.types.ts +64 -63
  273. package/src/types/clinic/to-do +3 -3
  274. package/src/types/documentation-templates/index.ts +308 -308
  275. package/src/types/index.ts +50 -47
  276. package/src/types/notifications/README.md +77 -77
  277. package/src/types/notifications/index.ts +300 -300
  278. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  279. package/src/types/patient/allergies.ts +58 -58
  280. package/src/types/patient/body-assessment.types.ts +93 -93
  281. package/src/types/patient/hair-scalp-assessment.types.ts +98 -98
  282. package/src/types/patient/index.ts +279 -279
  283. package/src/types/patient/medical-info.types.ts +152 -152
  284. package/src/types/patient/patient-requirements.ts +92 -92
  285. package/src/types/patient/pre-surgical-assessment.types.ts +95 -95
  286. package/src/types/patient/skin-quality-assessment.types.ts +105 -105
  287. package/src/types/patient/token.types.ts +61 -61
  288. package/src/types/practitioner/index.ts +208 -208
  289. package/src/types/procedure/index.ts +189 -183
  290. package/src/types/profile/index.ts +39 -39
  291. package/src/types/resource/README.md +153 -0
  292. package/src/types/resource/index.ts +199 -0
  293. package/src/types/reviews/index.ts +132 -132
  294. package/src/types/tz-lookup.d.ts +4 -4
  295. package/src/types/user/index.ts +60 -60
  296. package/src/utils/TIMESTAMPS.md +176 -176
  297. package/src/utils/TimestampUtils.ts +241 -241
  298. package/src/utils/index.ts +1 -1
  299. package/src/validations/README.md +94 -0
  300. package/src/validations/appointment.schema.ts +589 -589
  301. package/src/validations/calendar.schema.ts +225 -225
  302. package/src/validations/clinic.schema.ts +494 -494
  303. package/src/validations/common.schema.ts +25 -25
  304. package/src/validations/documentation-templates/index.ts +1 -1
  305. package/src/validations/documentation-templates/template.schema.ts +220 -220
  306. package/src/validations/documentation-templates.schema.ts +10 -10
  307. package/src/validations/index.ts +21 -20
  308. package/src/validations/media.schema.ts +10 -10
  309. package/src/validations/notification.schema.ts +90 -90
  310. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  311. package/src/validations/patient/body-assessment.schema.ts +82 -82
  312. package/src/validations/patient/hair-scalp-assessment.schema.ts +70 -70
  313. package/src/validations/patient/medical-info.schema.ts +177 -177
  314. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  315. package/src/validations/patient/pre-surgical-assessment.schema.ts +78 -78
  316. package/src/validations/patient/skin-quality-assessment.schema.ts +70 -70
  317. package/src/validations/patient/token.schema.ts +29 -29
  318. package/src/validations/patient.schema.ts +217 -217
  319. package/src/validations/practitioner.schema.ts +224 -224
  320. package/src/validations/procedure-product.schema.ts +41 -41
  321. package/src/validations/procedure.schema.ts +136 -124
  322. package/src/validations/profile-info.schema.ts +41 -41
  323. package/src/validations/resource.schema.ts +57 -0
  324. package/src/validations/reviews.schema.ts +195 -195
  325. package/src/validations/schemas.ts +109 -109
  326. package/src/validations/shared.schema.ts +78 -78
@@ -1,1435 +1,1435 @@
1
- import {
2
- Auth,
3
- User as FirebaseUser,
4
- signInWithEmailAndPassword,
5
- createUserWithEmailAndPassword,
6
- signInAnonymously as firebaseSignInAnonymously,
7
- signOut as firebaseSignOut,
8
- GoogleAuthProvider,
9
- signInWithPopup,
10
- signInWithRedirect,
11
- getRedirectResult,
12
- linkWithCredential,
13
- EmailAuthProvider,
14
- onAuthStateChanged,
15
- sendPasswordResetEmail,
16
- verifyPasswordResetCode,
17
- confirmPasswordReset,
18
- fetchSignInMethodsForEmail,
19
- signInWithCredential,
20
- OAuthProvider,
21
- } from 'firebase/auth';
22
- import {
23
- getFirestore,
24
- collection,
25
- doc,
26
- getDoc,
27
- setDoc,
28
- updateDoc,
29
- deleteDoc,
30
- query,
31
- where,
32
- getDocs,
33
- orderBy,
34
- limit,
35
- startAfter,
36
- Timestamp,
37
- runTransaction,
38
- Firestore,
39
- } from 'firebase/firestore';
40
- import { FirebaseApp } from 'firebase/app';
41
- import { User, UserRole, USERS_COLLECTION } from '../../types';
42
- import { z } from 'zod';
43
- import { emailSchema, passwordSchema, userRoleSchema } from '../../validations/schemas';
44
- import { AuthError, AUTH_ERRORS } from '../../errors/auth.errors';
45
- import { FirebaseErrorCode } from '../../errors/firebase.errors';
46
- import { FirebaseError } from '../../errors/firebase.errors';
47
- import { BaseService } from '../base.service';
48
- import { UserService } from '../user/user.service';
49
- import {
50
- ClinicGroup,
51
- AdminToken,
52
- AdminTokenStatus,
53
- CreateClinicGroupData,
54
- CreateClinicAdminData,
55
- ContactPerson,
56
- ClinicAdminSignupData,
57
- SubscriptionModel,
58
- CLINIC_GROUPS_COLLECTION,
59
- ClinicAdmin,
60
- } from '../../types/clinic';
61
- import { clinicAdminSignupSchema } from '../../validations/clinic.schema';
62
- import { ClinicGroupService } from '../clinic/clinic-group.service';
63
- import { ClinicAdminService } from '../clinic/clinic-admin.service';
64
- import { ClinicService } from '../clinic/clinic.service';
65
- import {
66
- Practitioner,
67
- CreatePractitionerData,
68
- PractitionerStatus,
69
- PractitionerBasicInfo,
70
- PractitionerCertification,
71
- } from '../../types/practitioner';
72
- import { PractitionerService } from '../practitioner/practitioner.service';
73
- import { practitionerSignupSchema } from '../../validations/practitioner.schema';
74
- import { CertificationLevel } from '../../backoffice/types/static/certification.types';
75
- import { MediaService } from '../media/media.service';
76
- // Import utility functions
77
- import {
78
- checkEmailExists,
79
- cleanupFirebaseUser,
80
- handleFirebaseError,
81
- handleSignupError,
82
- buildPractitionerData,
83
- validatePractitionerProfileData,
84
- } from './utils';
85
-
86
- export class AuthService extends BaseService {
87
- private googleProvider = new GoogleAuthProvider();
88
- private userService: UserService;
89
-
90
- constructor(db: Firestore, auth: Auth, app: FirebaseApp, userService: UserService) {
91
- super(db, auth, app);
92
- this.userService = userService || new UserService(db, auth, app);
93
- }
94
-
95
- /**
96
- * Waits for Firebase Auth state to settle after sign-in.
97
- * In React Native with AsyncStorage persistence, auth state may not be immediately available.
98
- */
99
- private async waitForAuthStateToSettle(expectedUid: string, timeoutMs: number = 5000): Promise<void> {
100
- if (this.auth.currentUser?.uid === expectedUid) {
101
- await new Promise(resolve => setTimeout(resolve, 200));
102
- if (this.auth.currentUser?.uid === expectedUid) {
103
- return;
104
- }
105
- }
106
-
107
- return new Promise((resolve, reject) => {
108
- const startTime = Date.now();
109
- let resolved = false;
110
-
111
- const unsubscribe = onAuthStateChanged(this.auth, (user) => {
112
- if (resolved) return;
113
-
114
- const currentUid = user?.uid || null;
115
-
116
- if (currentUid === expectedUid) {
117
- setTimeout(() => {
118
- if (resolved) return;
119
-
120
- if (this.auth.currentUser?.uid === expectedUid) {
121
- resolved = true;
122
- unsubscribe();
123
- clearTimeout(timeout);
124
- resolve();
125
- }
126
- }, 300);
127
- }
128
- });
129
-
130
- const timeout = setTimeout(() => {
131
- if (resolved) return;
132
- resolved = true;
133
- unsubscribe();
134
- reject(new Error(`Timeout waiting for auth state to settle. Expected: ${expectedUid}, Got: ${this.auth.currentUser?.uid || 'NULL'}`));
135
- }, timeoutMs);
136
- });
137
- }
138
-
139
- /**
140
- * Registruje novog korisnika sa email-om i lozinkom
141
- */
142
- async signUp(
143
- email: string,
144
- password: string,
145
- initialRole: UserRole = UserRole.PATIENT,
146
- options?: {
147
- patientInviteToken?: string;
148
- },
149
- ): Promise<User> {
150
- const { user: firebaseUser } = await createUserWithEmailAndPassword(this.auth, email, password);
151
-
152
- return this.userService.createUser(firebaseUser, [initialRole], options);
153
- }
154
-
155
- /**
156
- * Registers a new clinic admin user with email and password
157
- * Can either create a new clinic group or join an existing one with a token
158
- *
159
- * @param data - Clinic admin signup data
160
- * @returns Object containing the created user, clinic group, and clinic admin
161
- */
162
- async signUpClinicAdmin(data: ClinicAdminSignupData): Promise<{
163
- user: User;
164
- clinicGroup: ClinicGroup;
165
- clinicAdmin: ClinicAdmin;
166
- }> {
167
- try {
168
- console.log('[AUTH] Starting clinic admin signup process', {
169
- email: data.email,
170
- });
171
-
172
- // Validate data
173
- try {
174
- await clinicAdminSignupSchema.parseAsync(data);
175
- console.log('[AUTH] Clinic admin signup data validation passed');
176
- } catch (validationError) {
177
- console.error('[AUTH] Validation error in signUpClinicAdmin:', validationError);
178
- throw validationError;
179
- }
180
-
181
- // Create Firebase user
182
- console.log('[AUTH] Creating Firebase user');
183
- let firebaseUser;
184
- try {
185
- const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
186
- firebaseUser = result.user;
187
- console.log('[AUTH] Firebase user created successfully', {
188
- uid: firebaseUser.uid,
189
- });
190
- } catch (firebaseError) {
191
- console.error('[AUTH] Firebase user creation failed:', firebaseError);
192
- throw handleFirebaseError(firebaseError);
193
- }
194
-
195
- // Create user with CLINIC_ADMIN role
196
- console.log('[AUTH] Creating user with CLINIC_ADMIN role');
197
- let user;
198
- try {
199
- user = await this.userService.createUser(firebaseUser, [UserRole.CLINIC_ADMIN], {
200
- skipProfileCreation: true,
201
- });
202
- console.log('[AUTH] User with CLINIC_ADMIN role created successfully', {
203
- userId: user.uid,
204
- });
205
- } catch (userCreationError) {
206
- console.error('[AUTH] User creation failed:', userCreationError);
207
- throw userCreationError;
208
- }
209
-
210
- // Create contact person object
211
- const contactPerson: ContactPerson = {
212
- firstName: data.firstName,
213
- lastName: data.lastName,
214
- title: data.title,
215
- email: data.email,
216
- phoneNumber: data.phoneNumber,
217
- };
218
- console.log('[AUTH] Contact person object created');
219
-
220
- // Initialize services
221
- console.log('[AUTH] Initializing clinic services');
222
- const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
223
- const clinicGroupService = new ClinicGroupService(
224
- this.db,
225
- this.auth,
226
- this.app,
227
- clinicAdminService,
228
- );
229
- const mediaService = new MediaService(this.db, this.auth, this.app);
230
- const clinicService = new ClinicService(
231
- this.db,
232
- this.auth,
233
- this.app,
234
- clinicGroupService,
235
- clinicAdminService,
236
- mediaService,
237
- );
238
-
239
- // Set services to resolve circular dependencies
240
- clinicAdminService.setServices(clinicGroupService, clinicService);
241
- console.log('[AUTH] Services initialized and circular dependencies resolved');
242
-
243
- let clinicGroup: ClinicGroup | null = null;
244
- let adminProfile: ClinicAdmin | null = null;
245
-
246
- if (data.isCreatingNewGroup) {
247
- console.log('[AUTH] Creating new clinic group flow');
248
- // Create new clinic group
249
- if (!data.clinicGroupData) {
250
- console.error('[AUTH] Clinic group data is missing');
251
- throw new Error('Clinic group data is required when creating a new group');
252
- }
253
-
254
- // First create the clinic admin without a group
255
- console.log('[AUTH] Creating clinic admin first (without group)');
256
- const createClinicAdminData: CreateClinicAdminData = {
257
- userRef: firebaseUser.uid,
258
- isGroupOwner: true,
259
- clinicsManaged: [],
260
- contactInfo: contactPerson,
261
- roleTitle: data.title,
262
- isActive: true,
263
- // No clinicGroupId yet
264
- };
265
-
266
- try {
267
- adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
268
- console.log('[AUTH] Clinic admin created successfully', {
269
- adminId: adminProfile.id,
270
- });
271
- } catch (adminCreationError) {
272
- console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
273
- throw adminCreationError;
274
- }
275
-
276
- // Update user document with admin profile reference
277
- try {
278
- console.log('[AUTH] Updating user with admin profile reference');
279
- user = await this.userService.updateUser(firebaseUser.uid, {
280
- adminProfile: adminProfile.id,
281
- });
282
- console.log('[AUTH] User updated with admin profile reference successfully');
283
- } catch (userUpdateError) {
284
- console.error('[AUTH] Failed to update user with admin profile:', userUpdateError);
285
- throw userUpdateError;
286
- }
287
-
288
- // Then create clinic group
289
- const createClinicGroupData: CreateClinicGroupData = {
290
- name: data.clinicGroupData.name,
291
- hqLocation: data.clinicGroupData.hqLocation,
292
- contactInfo: data.clinicGroupData.contactInfo,
293
- contactPerson: contactPerson,
294
- ownerId: adminProfile.id, // Use admin profile ID, not user UID
295
- isActive: true,
296
- logo: data.clinicGroupData.logo || null,
297
- subscriptionModel:
298
- data.clinicGroupData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
299
- onboarding: {
300
- completed: false,
301
- step: 1,
302
- },
303
- };
304
- console.log('[AUTH] Clinic group data prepared', {
305
- groupName: createClinicGroupData.name,
306
- });
307
-
308
- // Create clinic group
309
- try {
310
- clinicGroup = await clinicGroupService.createClinicGroup(
311
- createClinicGroupData,
312
- adminProfile.id, // Use admin profile ID, not user UID
313
- false, // This is not a default group since we're providing complete data
314
- );
315
- console.log('[AUTH] Clinic group created successfully', {
316
- groupId: clinicGroup.id,
317
- });
318
-
319
- // Now update the admin with the group ID
320
- console.log('[AUTH] Updating admin with clinic group ID');
321
- await clinicAdminService.updateClinicAdmin(adminProfile.id, {
322
- // Use admin profile ID, not user UID
323
- clinicGroupId: clinicGroup.id,
324
- });
325
- console.log('[AUTH] Admin updated with clinic group ID successfully');
326
-
327
- // Get the updated admin profile
328
- adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
329
- } catch (groupCreationError) {
330
- console.error('[AUTH] Clinic group creation failed:', groupCreationError);
331
- throw groupCreationError;
332
- }
333
- } else {
334
- console.log('[AUTH] Joining existing clinic group flow');
335
- // Join existing clinic group with token
336
- if (!data.inviteToken) {
337
- console.error('[AUTH] Invite token is missing');
338
- throw new Error('Invite token is required when joining an existing group');
339
- }
340
- console.log('[AUTH] Invite token provided', {
341
- token: data.inviteToken,
342
- });
343
-
344
- // Find the token in the database
345
- console.log('[AUTH] Searching for token in clinic groups');
346
- const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
347
- const q = query(groupsRef);
348
- const querySnapshot = await getDocs(q);
349
-
350
- let foundGroup: ClinicGroup | null = null;
351
- let foundToken: AdminToken | null = null;
352
-
353
- console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
354
- for (const docSnapshot of querySnapshot.docs) {
355
- const group = docSnapshot.data() as ClinicGroup;
356
- console.log('[AUTH] Checking group', {
357
- groupId: group.id,
358
- groupName: group.name,
359
- });
360
-
361
- // Find the token in the group's tokens
362
- const token = group.adminTokens.find(t => {
363
- const isMatch =
364
- t.token === data.inviteToken &&
365
- t.status === AdminTokenStatus.ACTIVE &&
366
- new Date(t.expiresAt.toDate()) > new Date();
367
-
368
- console.log('[AUTH] Checking token', {
369
- tokenId: t.id,
370
- tokenMatch: t.token === data.inviteToken,
371
- tokenStatus: t.status,
372
- tokenActive: t.status === AdminTokenStatus.ACTIVE,
373
- tokenExpiry: new Date(t.expiresAt.toDate()),
374
- tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
375
- isMatch,
376
- });
377
-
378
- return isMatch;
379
- });
380
-
381
- if (token) {
382
- foundGroup = group;
383
- foundToken = token;
384
- console.log('[AUTH] Found matching token in group', {
385
- groupId: group.id,
386
- tokenId: token.id,
387
- });
388
- break;
389
- }
390
- }
391
-
392
- if (!foundGroup || !foundToken) {
393
- console.error('[AUTH] No valid token found in any clinic group');
394
- throw new Error('Invalid or expired invite token');
395
- }
396
-
397
- clinicGroup = foundGroup;
398
-
399
- // Create clinic admin
400
- console.log('[AUTH] Creating clinic admin');
401
- const createClinicAdminData: CreateClinicAdminData = {
402
- userRef: firebaseUser.uid,
403
- clinicGroupId: foundGroup.id,
404
- isGroupOwner: false,
405
- clinicsManaged: [],
406
- contactInfo: contactPerson,
407
- roleTitle: data.title,
408
- isActive: true,
409
- };
410
-
411
- try {
412
- adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
413
- console.log('[AUTH] Clinic admin created successfully', {
414
- adminId: adminProfile.id,
415
- });
416
- } catch (adminCreationError) {
417
- console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
418
- throw adminCreationError;
419
- }
420
-
421
- // Mark token as used
422
- try {
423
- await clinicGroupService.verifyAndUseAdminToken(
424
- foundGroup.id,
425
- data.inviteToken,
426
- firebaseUser.uid,
427
- );
428
- console.log('[AUTH] Token marked as used successfully');
429
- } catch (tokenUseError) {
430
- console.error('[AUTH] Failed to mark token as used:', tokenUseError);
431
- throw tokenUseError;
432
- }
433
- }
434
-
435
- console.log('[AUTH] Clinic admin signup completed successfully', {
436
- userId: user.uid,
437
- clinicGroupId: clinicGroup.id,
438
- clinicAdminId: adminProfile?.id || 'unknown',
439
- });
440
-
441
- // Ensure we have all required data before returning
442
- if (!clinicGroup || !adminProfile) {
443
- throw new Error('Failed to create or retrieve clinic group or admin profile');
444
- }
445
-
446
- return {
447
- user,
448
- clinicGroup,
449
- clinicAdmin: adminProfile,
450
- };
451
- } catch (error) {
452
- if (error instanceof z.ZodError) {
453
- console.error(
454
- '[AUTH] Zod validation error in signUpClinicAdmin:',
455
- JSON.stringify(error.errors, null, 2),
456
- );
457
- throw AUTH_ERRORS.VALIDATION_ERROR;
458
- }
459
-
460
- const firebaseError = error as FirebaseError;
461
- if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
462
- console.error('[AUTH] Email already in use:', data.email);
463
- throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
464
- }
465
-
466
- console.error('[AUTH] Unhandled error in signUpClinicAdmin:', error);
467
- throw error;
468
- }
469
- }
470
-
471
- /**
472
- * Prijavljuje korisnika sa email-om i lozinkom
473
- */
474
- async signIn(email: string, password: string): Promise<User> {
475
- const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
476
-
477
- return this.userService.getOrCreateUser(firebaseUser);
478
- }
479
-
480
- /**
481
- * Prijavljuje korisnika sa email-om i lozinkom samo za clinic_admin role
482
- * @param email - Email korisnika
483
- * @param password - Lozinka korisnika
484
- * @returns Objekat koji sadrži korisnika, admin profil i grupu klinika
485
- * @throws {AUTH_ERRORS.INVALID_ROLE} Ako korisnik nema clinic_admin rolu
486
- * @throws {AUTH_ERRORS.NOT_FOUND} Ako admin profil nije pronađen
487
- */
488
- async signInClinicAdmin(
489
- email: string,
490
- password: string,
491
- ): Promise<{
492
- user: User;
493
- clinicAdmin: ClinicAdmin;
494
- clinicGroup: ClinicGroup;
495
- }> {
496
- try {
497
- // Initialize required services
498
- const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
499
- const clinicGroupService = new ClinicGroupService(
500
- this.db,
501
- this.auth,
502
- this.app,
503
- clinicAdminService,
504
- );
505
- const mediaService = new MediaService(this.db, this.auth, this.app);
506
- const clinicService = new ClinicService(
507
- this.db,
508
- this.auth,
509
- this.app,
510
- clinicGroupService,
511
- clinicAdminService,
512
- mediaService,
513
- );
514
-
515
- // Set services to resolve circular dependencies
516
- clinicAdminService.setServices(clinicGroupService, clinicService);
517
-
518
- // Sign in with email/password
519
- const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
520
-
521
- // Get or create user
522
- const user = await this.userService.getOrCreateUser(firebaseUser);
523
-
524
- // Check if user has clinic_admin role
525
- if (!user.roles?.includes(UserRole.CLINIC_ADMIN)) {
526
- console.error('[AUTH] User is not a clinic admin:', user.uid);
527
- // Sign out the user immediately for security
528
- await this.auth.signOut();
529
- throw AUTH_ERRORS.UNAUTHORIZED_ROLE;
530
- }
531
-
532
- // Check and get admin profile
533
- if (!user.adminProfile) {
534
- console.error('[AUTH] User has no admin profile:', user.uid);
535
- throw AUTH_ERRORS.NOT_FOUND;
536
- }
537
-
538
- // Get clinic admin profile
539
- const adminProfile = await clinicAdminService.getClinicAdmin(user.adminProfile);
540
- if (!adminProfile) {
541
- console.error('[AUTH] Admin profile not found:', user.adminProfile);
542
- throw AUTH_ERRORS.NOT_FOUND;
543
- }
544
-
545
- // Get clinic group
546
- const clinicGroup = await clinicGroupService.getClinicGroup(adminProfile.clinicGroupId);
547
- if (!clinicGroup) {
548
- console.error('[AUTH] Clinic group not found:', adminProfile.clinicGroupId);
549
- throw AUTH_ERRORS.NOT_FOUND;
550
- }
551
-
552
- return {
553
- user,
554
- clinicAdmin: adminProfile,
555
- clinicGroup,
556
- };
557
- } catch (error) {
558
- console.error('[AUTH] Error in signInClinicAdmin:', error);
559
- throw error;
560
- }
561
- }
562
-
563
- /**
564
- * Prijavljuje korisnika anonimno
565
- */
566
- async signInAnonymously(options?: { skipProfileCreation?: boolean }): Promise<User> {
567
- const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
568
-
569
- if (options?.skipProfileCreation) {
570
- return this.userService.getOrCreateUser(firebaseUser, UserRole.PATIENT, { skipProfileCreation: true });
571
- }
572
-
573
- return this.userService.getOrCreateUser(firebaseUser);
574
- }
575
-
576
- /**
577
- * Odjavljuje trenutnog korisnika
578
- */
579
- async signOut(): Promise<void> {
580
- await firebaseSignOut(this.auth);
581
- }
582
-
583
- /**
584
- * Vraća trenutno prijavljenog korisnika
585
- */
586
- async getCurrentUser(): Promise<User | null> {
587
- const firebaseUser = this.auth.currentUser;
588
- if (!firebaseUser) return null;
589
-
590
- return this.userService.getUserById(firebaseUser.uid);
591
- }
592
-
593
- /**
594
- * Registruje callback za promene stanja autentifikacije
595
- */
596
- onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
597
- return onAuthStateChanged(this.auth, callback);
598
- }
599
-
600
- async upgradeAnonymousUser(email: string, password: string): Promise<User> {
601
- try {
602
- await emailSchema.parseAsync(email);
603
- await passwordSchema.parseAsync(password);
604
-
605
- const currentUser = this.auth.currentUser;
606
- if (!currentUser) {
607
- throw AUTH_ERRORS.NOT_AUTHENTICATED;
608
- }
609
- if (!currentUser.isAnonymous) {
610
- throw new AuthError('User is not anonymous', 'AUTH/NOT_ANONYMOUS_USER', 400);
611
- }
612
-
613
- const credential = EmailAuthProvider.credential(email, password);
614
- await linkWithCredential(currentUser, credential);
615
-
616
- return await this.userService.upgradeAnonymousUser(currentUser.uid, email);
617
- } catch (error) {
618
- if (error instanceof z.ZodError) {
619
- throw AUTH_ERRORS.VALIDATION_ERROR;
620
- }
621
- const firebaseError = error as FirebaseError;
622
- if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
623
- throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
624
- }
625
- throw error;
626
- }
627
- }
628
-
629
- /**
630
- * Šalje email za resetovanje lozinke korisniku
631
- * @param email Email adresa korisnika
632
- * @returns Promise koji se razrešava kada je email poslat
633
- */
634
- async sendPasswordResetEmail(email: string): Promise<void> {
635
- try {
636
- await emailSchema.parseAsync(email);
637
-
638
- // Šaljemo email za resetovanje lozinke
639
- await sendPasswordResetEmail(this.auth, email);
640
- } catch (error) {
641
- if (error instanceof z.ZodError) {
642
- throw AUTH_ERRORS.VALIDATION_ERROR;
643
- }
644
-
645
- const firebaseError = error as FirebaseError;
646
-
647
- // Handle specific Firebase errors
648
- switch (firebaseError.code) {
649
- case FirebaseErrorCode.USER_NOT_FOUND:
650
- throw AUTH_ERRORS.USER_NOT_FOUND;
651
- case FirebaseErrorCode.INVALID_EMAIL:
652
- throw AUTH_ERRORS.INVALID_EMAIL;
653
- case FirebaseErrorCode.TOO_MANY_REQUESTS:
654
- throw AUTH_ERRORS.TOO_MANY_REQUESTS;
655
- case FirebaseErrorCode.NETWORK_ERROR:
656
- throw AUTH_ERRORS.NETWORK_ERROR;
657
- case FirebaseErrorCode.OPERATION_NOT_ALLOWED:
658
- throw AUTH_ERRORS.OPERATION_NOT_ALLOWED;
659
- default:
660
- // Re-throw unknown errors as-is
661
- throw error;
662
- }
663
- }
664
- }
665
-
666
- /**
667
- * Verifikuje kod za resetovanje lozinke iz email linka
668
- * @param oobCode Kod iz URL-a za resetovanje lozinke
669
- * @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
670
- */
671
- async verifyPasswordResetCode(oobCode: string): Promise<string> {
672
- try {
673
- // Verifikujemo kod i vraćamo email korisnika
674
- return await verifyPasswordResetCode(this.auth, oobCode);
675
- } catch (error) {
676
- const firebaseError = error as FirebaseError;
677
- if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
678
- throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
679
- } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
680
- throw AUTH_ERRORS.INVALID_ACTION_CODE;
681
- }
682
-
683
- throw error;
684
- }
685
- }
686
-
687
- /**
688
- * Potvrđuje resetovanje lozinke i postavlja novu lozinku
689
- * @param oobCode Kod iz URL-a za resetovanje lozinke
690
- * @param newPassword Nova lozinka
691
- * @returns Promise koji se razrešava kada je lozinka promenjena
692
- */
693
- async confirmPasswordReset(oobCode: string, newPassword: string): Promise<void> {
694
- try {
695
- await passwordSchema.parseAsync(newPassword);
696
-
697
- // Potvrđujemo resetovanje lozinke i postavljamo novu lozinku
698
- await confirmPasswordReset(this.auth, oobCode, newPassword);
699
- } catch (error) {
700
- if (error instanceof z.ZodError) {
701
- throw AUTH_ERRORS.VALIDATION_ERROR;
702
- }
703
-
704
- const firebaseError = error as FirebaseError;
705
- if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
706
- throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
707
- } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
708
- throw AUTH_ERRORS.INVALID_ACTION_CODE;
709
- } else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
710
- throw AUTH_ERRORS.WEAK_PASSWORD;
711
- }
712
-
713
- throw error;
714
- }
715
- }
716
-
717
- /**
718
- * Registers a new practitioner user with email and password (ATOMIC VERSION)
719
- * Uses Firestore transactions to ensure atomicity and proper rollback on failures
720
- *
721
- * @param data - Practitioner signup data containing either new profile details or token for claiming draft profile
722
- * @returns Object containing the created user and practitioner profile
723
- */
724
- async signUpPractitioner(data: {
725
- email: string;
726
- password: string;
727
- firstName?: string;
728
- lastName?: string;
729
- token?: string;
730
- profileData?: Partial<CreatePractitionerData>;
731
- }): Promise<{
732
- user: User;
733
- practitioner: Practitioner;
734
- }> {
735
- let firebaseUser: any = null;
736
-
737
- try {
738
- console.log('[AUTH] Starting atomic practitioner signup process', {
739
- email: data.email,
740
- hasToken: !!data.token,
741
- });
742
-
743
- // Step 1: Pre-validate all data before any mutations
744
- await this.validateSignupData(data);
745
-
746
- // Step 2: Create Firebase user (outside transaction - can't be easily rolled back)
747
- console.log('[AUTH] Creating Firebase user');
748
- try {
749
- const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
750
- firebaseUser = result.user;
751
- console.log('[AUTH] Firebase user created successfully', {
752
- uid: firebaseUser.uid,
753
- });
754
- } catch (firebaseError) {
755
- console.error('[AUTH] Firebase user creation failed:', firebaseError);
756
- throw handleFirebaseError(firebaseError);
757
- }
758
-
759
- // Step 3: Execute all database operations in a single transaction
760
- console.log('[AUTH] Starting Firestore transaction');
761
- const transactionResult = await runTransaction(this.db, async transaction => {
762
- console.log('[AUTH] Transaction started - creating user and practitioner');
763
-
764
- // Initialize services
765
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
766
-
767
- // Create user document using existing method (not in transaction for now)
768
- console.log('[AUTH] Creating user document');
769
- const user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
770
- skipProfileCreation: true,
771
- });
772
-
773
- let practitioner: Practitioner;
774
-
775
- // Handle practitioner profile creation/claiming
776
- if (data.token) {
777
- console.log('[AUTH] Claiming existing practitioner profile with token');
778
- const claimedPractitioner = await practitionerService.validateTokenAndClaimProfile(
779
- data.token,
780
- firebaseUser.uid,
781
- );
782
- if (!claimedPractitioner) {
783
- throw new Error('Invalid or expired invitation token');
784
- }
785
- practitioner = claimedPractitioner;
786
- } else {
787
- // Check if a draft profile exists for this email
788
- console.log('[AUTH] Checking for existing draft practitioner profile', {
789
- email: data.email,
790
- });
791
- const draftPractitioner = await practitionerService.findDraftPractitionerByEmail(
792
- data.email
793
- );
794
-
795
- if (draftPractitioner) {
796
- console.log('[AUTH] Draft practitioner profile found', {
797
- practitionerId: draftPractitioner.id,
798
- email: data.email,
799
- clinics: draftPractitioner.clinics,
800
- });
801
-
802
- // Extract clinic names from clinicsInfo (should be populated when draft is created)
803
- let clinicNames: string[] = [];
804
- if (draftPractitioner.clinicsInfo && draftPractitioner.clinicsInfo.length > 0) {
805
- clinicNames = draftPractitioner.clinicsInfo
806
- .map((clinic) => clinic.name)
807
- .filter((name): name is string => !!name);
808
- } else if (draftPractitioner.clinics && draftPractitioner.clinics.length > 0) {
809
- // Fallback: fetch clinic names if clinicsInfo is missing
810
- console.log('[AUTH] clinicsInfo missing, fetching clinic names from clinic IDs');
811
- const clinicService = practitionerService.getClinicService();
812
- const clinicNamePromises = draftPractitioner.clinics.map(async (clinicId) => {
813
- try {
814
- const clinic = await clinicService.getClinic(clinicId);
815
- return clinic?.name || null;
816
- } catch (error) {
817
- console.error(`[AUTH] Error fetching clinic ${clinicId}:`, error);
818
- return null;
819
- }
820
- });
821
- const names = await Promise.all(clinicNamePromises);
822
- clinicNames = names.filter((name): name is string => !!name);
823
- }
824
-
825
- // Cleanup Firebase user since we're not creating a profile
826
- await cleanupFirebaseUser(firebaseUser);
827
-
828
- // Throw error with clinic information
829
- throw new AuthError(
830
- AUTH_ERRORS.DRAFT_PROFILE_EXISTS.message,
831
- AUTH_ERRORS.DRAFT_PROFILE_EXISTS.code,
832
- AUTH_ERRORS.DRAFT_PROFILE_EXISTS.status,
833
- {
834
- clinicNames,
835
- clinics: draftPractitioner.clinics,
836
- clinicsInfo: draftPractitioner.clinicsInfo,
837
- }
838
- );
839
- }
840
-
841
- console.log('[AUTH] No draft profile found, creating new practitioner profile');
842
- const practitionerData = buildPractitionerData(data, firebaseUser.uid);
843
- practitioner = await practitionerService.createPractitioner(practitionerData);
844
- }
845
-
846
- // Link practitioner to user
847
- console.log('[AUTH] Linking practitioner to user');
848
- await this.userService.updateUser(firebaseUser.uid, {
849
- practitionerProfile: practitioner.id,
850
- });
851
-
852
- console.log('[AUTH] Transaction operations completed successfully');
853
- return { user, practitioner };
854
- });
855
-
856
- console.log('[AUTH] Atomic practitioner signup completed successfully', {
857
- userId: transactionResult.user.uid,
858
- practitionerId: transactionResult.practitioner.id,
859
- });
860
-
861
- return transactionResult;
862
- } catch (error) {
863
- console.error('[AUTH] Atomic signup failed, initiating cleanup...', error);
864
-
865
- // Cleanup Firebase user if transaction failed
866
- if (firebaseUser) {
867
- await cleanupFirebaseUser(firebaseUser);
868
- }
869
-
870
- throw handleSignupError(error);
871
- }
872
- }
873
-
874
- /**
875
- * Claims draft practitioner profiles after Google Sign-In.
876
- * Uses the current authenticated user (from initial Google Sign-In).
877
- *
878
- * @param idToken - The Google ID token (used to re-authenticate if needed)
879
- * @param practitionerIds - Array of draft practitioner profile IDs to claim
880
- * @returns Object containing user and claimed practitioner
881
- */
882
- async claimDraftProfilesWithGoogle(
883
- idToken: string,
884
- practitionerIds: string[]
885
- ): Promise<{
886
- user: User;
887
- practitioner: Practitioner;
888
- }> {
889
- try {
890
- console.log('[AUTH] Starting claim draft profiles with Google', {
891
- practitionerIdsCount: practitionerIds.length,
892
- practitionerIds,
893
- });
894
-
895
- if (practitionerIds.length === 0) {
896
- throw new AuthError('No practitioner profiles selected to claim', 'AUTH/NO_PROFILES_SELECTED', 400);
897
- }
898
-
899
- const credential = GoogleAuthProvider.credential(idToken);
900
- const result = await signInWithCredential(this.auth, credential);
901
- const firebaseUser = result.user;
902
-
903
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
904
-
905
- let user: User;
906
- try {
907
- user = await this.userService.getUserById(firebaseUser.uid);
908
- } catch (userError) {
909
- throw new AuthError(
910
- 'User account not properly initialized. Please try signing in again.',
911
- 'AUTH/USER_NOT_INITIALIZED',
912
- 500,
913
- );
914
- }
915
-
916
- let practitioner: Practitioner;
917
- if (practitionerIds.length === 1) {
918
- practitioner = await practitionerService.claimDraftProfileWithGoogle(
919
- practitionerIds[0],
920
- firebaseUser.uid
921
- );
922
- } else {
923
- practitioner = await practitionerService.claimMultipleDraftProfilesWithGoogle(
924
- practitionerIds,
925
- firebaseUser.uid
926
- );
927
- }
928
-
929
- if (!user.practitionerProfile || user.practitionerProfile !== practitioner.id) {
930
- await this.userService.updateUser(firebaseUser.uid, {
931
- practitionerProfile: practitioner.id,
932
- });
933
- }
934
-
935
- const updatedUser = await this.userService.getUserById(firebaseUser.uid);
936
-
937
- return {
938
- user: updatedUser,
939
- practitioner,
940
- };
941
- } catch (error: any) {
942
- console.error('[AUTH] Error claiming draft profiles with Google:', error);
943
- throw handleSignupError(error);
944
- }
945
- }
946
-
947
- /**
948
- * Pre-validate all signup data before any mutations
949
- * Prevents partial creation by catching issues early
950
- */
951
- private async validateSignupData(data: {
952
- email: string;
953
- password: string;
954
- firstName?: string;
955
- lastName?: string;
956
- token?: string;
957
- profileData?: Partial<CreatePractitionerData>;
958
- }): Promise<void> {
959
- console.log('[AUTH] Pre-validating signup data');
960
-
961
- try {
962
- // 1. Schema validation
963
- await practitionerSignupSchema.parseAsync(data);
964
- console.log('[AUTH] Schema validation passed');
965
-
966
- // 2. Check if email already exists (before creating Firebase user)
967
- const emailExists = await checkEmailExists(this.auth, data.email);
968
- if (emailExists) {
969
- console.log('[AUTH] Email already exists:', data.email);
970
- throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
971
- }
972
- console.log('[AUTH] Email availability confirmed');
973
-
974
- // 3. Validate token if provided
975
- if (data.token) {
976
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
977
- const isValidToken = await practitionerService.validateToken(data.token);
978
- if (!isValidToken) {
979
- console.log('[AUTH] Invalid token provided:', data.token);
980
- throw new Error('Invalid or expired invitation token');
981
- }
982
- console.log('[AUTH] Token validation passed');
983
- }
984
-
985
- // 4. Validate profile data structure if provided
986
- if (data.profileData) {
987
- await validatePractitionerProfileData(data.profileData);
988
- console.log('[AUTH] Profile data validation passed');
989
- }
990
-
991
- console.log('[AUTH] All pre-validation checks passed');
992
- } catch (error) {
993
- console.error('[AUTH] Pre-validation failed:', error);
994
- throw error;
995
- }
996
- }
997
-
998
- /**
999
- * Signs in a user with email and password specifically for practitioner role
1000
- * @param email - User's email
1001
- * @param password - User's password
1002
- * @returns Object containing the user and practitioner profile
1003
- * @throws {AUTH_ERRORS.INVALID_ROLE} If user doesn't have practitioner role
1004
- * @throws {AUTH_ERRORS.NOT_FOUND} If practitioner profile is not found
1005
- */
1006
- async signInPractitioner(
1007
- email: string,
1008
- password: string,
1009
- ): Promise<{
1010
- user: User;
1011
- practitioner: Practitioner;
1012
- }> {
1013
- try {
1014
- console.log('[AUTH] Starting practitioner signin process', {
1015
- email: email,
1016
- });
1017
-
1018
- // Initialize required service
1019
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1020
-
1021
- // Sign in with email/password
1022
- const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
1023
-
1024
- // Get or create user
1025
- const user = await this.userService.getOrCreateUser(firebaseUser);
1026
- console.log('[AUTH] User retrieved', { uid: user.uid });
1027
-
1028
- // Check if user has practitioner role
1029
- if (!user.roles?.includes(UserRole.PRACTITIONER)) {
1030
- console.error('[AUTH] User is not a practitioner:', user.uid);
1031
- throw AUTH_ERRORS.INVALID_ROLE;
1032
- }
1033
-
1034
- // Check and get practitioner profile
1035
- if (!user.practitionerProfile) {
1036
- console.error('[AUTH] User has no practitioner profile:', user.uid);
1037
- throw AUTH_ERRORS.NOT_FOUND;
1038
- }
1039
-
1040
- // Get practitioner profile
1041
- const practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1042
- if (!practitioner) {
1043
- console.error('[AUTH] Practitioner profile not found:', user.practitionerProfile);
1044
- throw AUTH_ERRORS.NOT_FOUND;
1045
- }
1046
-
1047
- console.log('[AUTH] Practitioner signin completed successfully', {
1048
- userId: user.uid,
1049
- practitionerId: practitioner.id,
1050
- });
1051
-
1052
- return {
1053
- user,
1054
- practitioner,
1055
- };
1056
- } catch (error) {
1057
- console.error('[AUTH] Error in signInPractitioner:', error);
1058
- throw error;
1059
- }
1060
- }
1061
-
1062
- /**
1063
- * Signs in a user with a Google ID token from a mobile client.
1064
- * If the user does not exist, a new user is created.
1065
- * @param idToken - The Google ID token obtained from the mobile app.
1066
- * @param initialRole - The role to assign to the user if they are being created.
1067
- * @returns The signed-in or newly created user.
1068
- */
1069
- async signInWithGoogleIdToken(
1070
- idToken: string,
1071
- initialRole: UserRole = UserRole.PATIENT,
1072
- ): Promise<User> {
1073
- try {
1074
- console.log('[AUTH] Signing in with Google ID Token');
1075
-
1076
- // Sign in with Google credential — auto-creates Firebase Auth user if needed.
1077
- const credential = GoogleAuthProvider.credential(idToken);
1078
- const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1079
- console.log('[AUTH] Firebase user signed in:', firebaseUser.uid);
1080
-
1081
- // Load or create domain user document
1082
- return await this.userService.getOrCreateUser(firebaseUser, UserRole.PATIENT);
1083
- } catch (error) {
1084
- console.error('[AUTH] Error in signInWithGoogleIdToken:', error);
1085
- throw handleFirebaseError(error);
1086
- }
1087
- }
1088
-
1089
- /**
1090
- * Signs up or signs in a practitioner with Google authentication.
1091
- * Checks for existing practitioner account or draft profiles.
1092
- *
1093
- * @param idToken - The Google ID token obtained from the mobile app
1094
- * @returns Object containing user, practitioner (if exists), and draft profiles (if any)
1095
- */
1096
- async signUpPractitionerWithGoogle(
1097
- idToken: string
1098
- ): Promise<{
1099
- user: User | null;
1100
- practitioner: Practitioner | null;
1101
- draftProfiles: Practitioner[];
1102
- }> {
1103
- try {
1104
- console.log('[AUTH] Starting practitioner Google Sign-In/Sign-Up');
1105
-
1106
- // Extract email from Google ID token
1107
- let email: string | undefined;
1108
- try {
1109
- const payloadBase64 = idToken.split('.')[1];
1110
- const payloadJson = globalThis.atob
1111
- ? globalThis.atob(payloadBase64)
1112
- : Buffer.from(payloadBase64, 'base64').toString('utf8');
1113
- const payload = JSON.parse(payloadJson);
1114
- email = payload.email as string | undefined;
1115
- } catch (decodeError) {
1116
- console.error('[AUTH] Failed to decode email from Google ID token:', decodeError);
1117
- throw new AuthError(
1118
- 'Unable to read email from Google token. Please try again.',
1119
- 'AUTH/INVALID_GOOGLE_TOKEN',
1120
- 400,
1121
- );
1122
- }
1123
-
1124
- if (!email) {
1125
- throw new AuthError(
1126
- 'Unable to read email from Google token. Please try again.',
1127
- 'AUTH/INVALID_GOOGLE_TOKEN',
1128
- 400,
1129
- );
1130
- }
1131
-
1132
- const normalizedEmail = email.toLowerCase().trim();
1133
-
1134
- const methods = await fetchSignInMethodsForEmail(this.auth, normalizedEmail);
1135
- const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
1136
- const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
1137
-
1138
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1139
-
1140
- if (hasGoogleMethod) {
1141
- const credential = GoogleAuthProvider.credential(idToken);
1142
- const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1143
-
1144
- await this.waitForAuthStateToSettle(firebaseUser.uid);
1145
-
1146
- let existingUser: User | null = null;
1147
- try {
1148
- existingUser = await this.userService.getUserById(firebaseUser.uid);
1149
- } catch (userError: any) {
1150
- if (!this.auth.currentUser || this.auth.currentUser.uid !== firebaseUser.uid) {
1151
- const credential = GoogleAuthProvider.credential(idToken);
1152
- await signInWithCredential(this.auth, credential);
1153
- await this.waitForAuthStateToSettle(firebaseUser.uid, 2000);
1154
- }
1155
-
1156
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1157
- const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1158
-
1159
- if (draftProfiles.length === 0) {
1160
- try {
1161
- await firebaseSignOut(this.auth);
1162
- } catch (signOutError) {
1163
- console.warn('[AUTH] Error signing out:', signOutError);
1164
- }
1165
- throw new AuthError(
1166
- 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1167
- 'AUTH/NO_DRAFT_PROFILES',
1168
- 404,
1169
- );
1170
- }
1171
-
1172
- try {
1173
- const newUser = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1174
- skipProfileCreation: true,
1175
- });
1176
-
1177
- return {
1178
- user: newUser,
1179
- practitioner: null,
1180
- draftProfiles: draftProfiles,
1181
- };
1182
- } catch (createUserError: any) {
1183
- try {
1184
- await firebaseSignOut(this.auth);
1185
- } catch (signOutError) {
1186
- console.warn('[AUTH] Error signing out:', signOutError);
1187
- }
1188
- throw createUserError;
1189
- }
1190
- }
1191
-
1192
- // User document exists - check for practitioner profile and draft profiles
1193
- if (!existingUser) {
1194
- await firebaseSignOut(this.auth);
1195
- throw new AuthError(
1196
- 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1197
- 'AUTH/NO_DRAFT_PROFILES',
1198
- 404,
1199
- );
1200
- }
1201
-
1202
- // Check if user has practitioner profile
1203
- let practitioner: Practitioner | null = null;
1204
- if (existingUser.practitionerProfile) {
1205
- practitioner = await practitionerService.getPractitioner(existingUser.practitionerProfile);
1206
- }
1207
-
1208
- // Check for any new draft profiles
1209
- const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1210
-
1211
- return {
1212
- user: existingUser,
1213
- practitioner,
1214
- draftProfiles,
1215
- };
1216
- }
1217
-
1218
- if (hasEmailMethod && !hasGoogleMethod) {
1219
- throw new AuthError(
1220
- 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1221
- 'AUTH/EMAIL_ALREADY_EXISTS',
1222
- 409,
1223
- );
1224
- }
1225
-
1226
- const credential = GoogleAuthProvider.credential(idToken);
1227
-
1228
- let firebaseUser: FirebaseUser;
1229
- try {
1230
- const result = await signInWithCredential(this.auth, credential);
1231
- firebaseUser = result.user;
1232
- } catch (error: any) {
1233
- // If sign-in fails because email already exists with different provider
1234
- if (error.code === 'auth/account-exists-with-different-credential') {
1235
- throw new AuthError(
1236
- 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1237
- 'AUTH/EMAIL_ALREADY_EXISTS',
1238
- 409,
1239
- );
1240
- }
1241
- throw error;
1242
- }
1243
-
1244
- await this.waitForAuthStateToSettle(firebaseUser.uid);
1245
-
1246
- let existingUser: User | null = null;
1247
- try {
1248
- const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
1249
- if (existingUserDoc) {
1250
- existingUser = existingUserDoc;
1251
- }
1252
- } catch (error) {
1253
- // Continue with new user creation
1254
- }
1255
-
1256
- let draftProfiles: Practitioner[] = [];
1257
- try {
1258
- draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1259
- } catch (draftCheckError: any) {
1260
- try {
1261
- await firebaseSignOut(this.auth);
1262
- } catch (signOutError) {
1263
- console.warn('[AUTH] Error signing out:', signOutError);
1264
- }
1265
- throw new AuthError(
1266
- 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1267
- 'AUTH/NO_DRAFT_PROFILES',
1268
- 404,
1269
- );
1270
- }
1271
-
1272
- let user: User;
1273
- if (existingUser) {
1274
- user = existingUser;
1275
- } else {
1276
- if (draftProfiles.length === 0) {
1277
- try {
1278
- await firebaseSignOut(this.auth);
1279
- } catch (signOutError) {
1280
- console.warn('[AUTH] Error signing out:', signOutError);
1281
- }
1282
- throw new AuthError(
1283
- 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1284
- 'AUTH/NO_DRAFT_PROFILES',
1285
- 404,
1286
- );
1287
- }
1288
-
1289
- user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1290
- skipProfileCreation: true,
1291
- });
1292
- }
1293
-
1294
- let practitioner: Practitioner | null = null;
1295
- if (user.practitionerProfile) {
1296
- practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1297
- }
1298
-
1299
- return {
1300
- user,
1301
- practitioner,
1302
- draftProfiles,
1303
- };
1304
- } catch (error: any) {
1305
- if (error instanceof AuthError) {
1306
- throw error;
1307
- }
1308
-
1309
- const errorMessage = error?.message || error?.toString() || '';
1310
- if (errorMessage.includes('NO_DRAFT_PROFILES') || errorMessage.includes('clinic invitation')) {
1311
- throw new AuthError(
1312
- 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1313
- 'AUTH/NO_DRAFT_PROFILES',
1314
- 404,
1315
- );
1316
- }
1317
-
1318
- const wrappedError = handleFirebaseError(error);
1319
-
1320
- if (wrappedError.message.includes('permissions') || wrappedError.message.includes('Account creation failed')) {
1321
- throw new AuthError(
1322
- 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1323
- 'AUTH/NO_DRAFT_PROFILES',
1324
- 404,
1325
- );
1326
- }
1327
-
1328
- throw wrappedError;
1329
- }
1330
- }
1331
-
1332
- /**
1333
- * Links a Google account to the currently signed-in user using an ID token.
1334
- * This is used to upgrade an anonymous user or to allow an existing user
1335
- * to sign in with Google in the future.
1336
- * @param idToken - The Google ID token obtained from the mobile app.
1337
- * @returns The updated user profile.
1338
- */
1339
- async linkGoogleAccount(idToken: string): Promise<User> {
1340
- try {
1341
- console.log('[AUTH] Linking Google account with ID Token');
1342
- const currentUser = this.auth.currentUser;
1343
- if (!currentUser) {
1344
- throw AUTH_ERRORS.NOT_AUTHENTICATED;
1345
- }
1346
-
1347
- const wasAnonymous = currentUser.isAnonymous;
1348
- console.log(`[AUTH] Current user is ${wasAnonymous ? 'anonymous' : 'not anonymous'}`);
1349
-
1350
- const credential = GoogleAuthProvider.credential(idToken);
1351
- const userCredential = await linkWithCredential(currentUser, credential);
1352
- const linkedFirebaseUser = userCredential.user;
1353
- console.log('[AUTH] Google account linked successfully to user:', linkedFirebaseUser.uid);
1354
-
1355
- if (wasAnonymous) {
1356
- console.log('[AUTH] Upgrading anonymous user profile');
1357
- return await this.userService.upgradeAnonymousUser(
1358
- linkedFirebaseUser.uid,
1359
- linkedFirebaseUser.email!,
1360
- );
1361
- }
1362
-
1363
- // If the user was not anonymous, just return their updated profile
1364
- return (await this.userService.getUserById(linkedFirebaseUser.uid))!;
1365
- } catch (error) {
1366
- console.error('[AUTH] Error in linkGoogleAccount:', error);
1367
- throw handleFirebaseError(error);
1368
- }
1369
- }
1370
-
1371
- /**
1372
- * Signs in or registers a user with an Apple ID token.
1373
- * If the user does not exist, a new user is created.
1374
- */
1375
- async signInWithAppleIdToken(
1376
- idToken: string,
1377
- rawNonce: string,
1378
- appleUserInfo?: { fullName?: { givenName?: string; familyName?: string }; email?: string },
1379
- ): Promise<User> {
1380
- try {
1381
- console.log('[AUTH] Signing in with Apple ID Token');
1382
-
1383
- // Build Apple OAuth credential
1384
- const provider = new OAuthProvider('apple.com');
1385
- const credential = provider.credential({ idToken, rawNonce });
1386
-
1387
- // Sign in to Firebase — auto-creates Firebase Auth user if needed
1388
- const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1389
- console.log('[AUTH] Firebase user signed in via Apple:', firebaseUser.uid);
1390
-
1391
- // Load or create domain user document
1392
- return await this.userService.getOrCreateUser(firebaseUser, UserRole.PATIENT);
1393
- } catch (error) {
1394
- console.error('[AUTH] Error in signInWithAppleIdToken:', error);
1395
- throw handleFirebaseError(error);
1396
- }
1397
- }
1398
-
1399
- /**
1400
- * Link an Apple account to the current user (anonymous → full account upgrade).
1401
- * Mirrors linkGoogleAccount.
1402
- */
1403
- async linkAppleAccount(idToken: string, rawNonce: string): Promise<User> {
1404
- try {
1405
- console.log('[AUTH] Linking Apple account with ID Token');
1406
- const currentUser = this.auth.currentUser;
1407
- if (!currentUser) {
1408
- throw AUTH_ERRORS.NOT_AUTHENTICATED;
1409
- }
1410
-
1411
- const wasAnonymous = currentUser.isAnonymous;
1412
- console.log(`[AUTH] Current user is ${wasAnonymous ? 'anonymous' : 'not anonymous'}`);
1413
-
1414
- const provider = new OAuthProvider('apple.com');
1415
- const credential = provider.credential({ idToken, rawNonce });
1416
- const userCredential = await linkWithCredential(currentUser, credential);
1417
- const linkedFirebaseUser = userCredential.user;
1418
- console.log('[AUTH] Apple account linked successfully to user:', linkedFirebaseUser.uid);
1419
-
1420
- if (wasAnonymous) {
1421
- console.log('[AUTH] Upgrading anonymous user profile');
1422
- const email = linkedFirebaseUser.email || '';
1423
- return await this.userService.upgradeAnonymousUser(
1424
- linkedFirebaseUser.uid,
1425
- email,
1426
- );
1427
- }
1428
-
1429
- return (await this.userService.getUserById(linkedFirebaseUser.uid))!;
1430
- } catch (error) {
1431
- console.error('[AUTH] Error in linkAppleAccount:', error);
1432
- throw handleFirebaseError(error);
1433
- }
1434
- }
1435
- }
1
+ import {
2
+ Auth,
3
+ User as FirebaseUser,
4
+ signInWithEmailAndPassword,
5
+ createUserWithEmailAndPassword,
6
+ signInAnonymously as firebaseSignInAnonymously,
7
+ signOut as firebaseSignOut,
8
+ GoogleAuthProvider,
9
+ signInWithPopup,
10
+ signInWithRedirect,
11
+ getRedirectResult,
12
+ linkWithCredential,
13
+ EmailAuthProvider,
14
+ onAuthStateChanged,
15
+ sendPasswordResetEmail,
16
+ verifyPasswordResetCode,
17
+ confirmPasswordReset,
18
+ fetchSignInMethodsForEmail,
19
+ signInWithCredential,
20
+ OAuthProvider,
21
+ } from 'firebase/auth';
22
+ import {
23
+ getFirestore,
24
+ collection,
25
+ doc,
26
+ getDoc,
27
+ setDoc,
28
+ updateDoc,
29
+ deleteDoc,
30
+ query,
31
+ where,
32
+ getDocs,
33
+ orderBy,
34
+ limit,
35
+ startAfter,
36
+ Timestamp,
37
+ runTransaction,
38
+ Firestore,
39
+ } from 'firebase/firestore';
40
+ import { FirebaseApp } from 'firebase/app';
41
+ import { User, UserRole, USERS_COLLECTION } from '../../types';
42
+ import { z } from 'zod';
43
+ import { emailSchema, passwordSchema, userRoleSchema } from '../../validations/schemas';
44
+ import { AuthError, AUTH_ERRORS } from '../../errors/auth.errors';
45
+ import { FirebaseErrorCode } from '../../errors/firebase.errors';
46
+ import { FirebaseError } from '../../errors/firebase.errors';
47
+ import { BaseService } from '../base.service';
48
+ import { UserService } from '../user/user.service';
49
+ import {
50
+ ClinicGroup,
51
+ AdminToken,
52
+ AdminTokenStatus,
53
+ CreateClinicGroupData,
54
+ CreateClinicAdminData,
55
+ ContactPerson,
56
+ ClinicAdminSignupData,
57
+ SubscriptionModel,
58
+ CLINIC_GROUPS_COLLECTION,
59
+ ClinicAdmin,
60
+ } from '../../types/clinic';
61
+ import { clinicAdminSignupSchema } from '../../validations/clinic.schema';
62
+ import { ClinicGroupService } from '../clinic/clinic-group.service';
63
+ import { ClinicAdminService } from '../clinic/clinic-admin.service';
64
+ import { ClinicService } from '../clinic/clinic.service';
65
+ import {
66
+ Practitioner,
67
+ CreatePractitionerData,
68
+ PractitionerStatus,
69
+ PractitionerBasicInfo,
70
+ PractitionerCertification,
71
+ } from '../../types/practitioner';
72
+ import { PractitionerService } from '../practitioner/practitioner.service';
73
+ import { practitionerSignupSchema } from '../../validations/practitioner.schema';
74
+ import { CertificationLevel } from '../../backoffice/types/static/certification.types';
75
+ import { MediaService } from '../media/media.service';
76
+ // Import utility functions
77
+ import {
78
+ checkEmailExists,
79
+ cleanupFirebaseUser,
80
+ handleFirebaseError,
81
+ handleSignupError,
82
+ buildPractitionerData,
83
+ validatePractitionerProfileData,
84
+ } from './utils';
85
+
86
+ export class AuthService extends BaseService {
87
+ private googleProvider = new GoogleAuthProvider();
88
+ private userService: UserService;
89
+
90
+ constructor(db: Firestore, auth: Auth, app: FirebaseApp, userService: UserService) {
91
+ super(db, auth, app);
92
+ this.userService = userService || new UserService(db, auth, app);
93
+ }
94
+
95
+ /**
96
+ * Waits for Firebase Auth state to settle after sign-in.
97
+ * In React Native with AsyncStorage persistence, auth state may not be immediately available.
98
+ */
99
+ private async waitForAuthStateToSettle(expectedUid: string, timeoutMs: number = 5000): Promise<void> {
100
+ if (this.auth.currentUser?.uid === expectedUid) {
101
+ await new Promise(resolve => setTimeout(resolve, 200));
102
+ if (this.auth.currentUser?.uid === expectedUid) {
103
+ return;
104
+ }
105
+ }
106
+
107
+ return new Promise((resolve, reject) => {
108
+ const startTime = Date.now();
109
+ let resolved = false;
110
+
111
+ const unsubscribe = onAuthStateChanged(this.auth, (user) => {
112
+ if (resolved) return;
113
+
114
+ const currentUid = user?.uid || null;
115
+
116
+ if (currentUid === expectedUid) {
117
+ setTimeout(() => {
118
+ if (resolved) return;
119
+
120
+ if (this.auth.currentUser?.uid === expectedUid) {
121
+ resolved = true;
122
+ unsubscribe();
123
+ clearTimeout(timeout);
124
+ resolve();
125
+ }
126
+ }, 300);
127
+ }
128
+ });
129
+
130
+ const timeout = setTimeout(() => {
131
+ if (resolved) return;
132
+ resolved = true;
133
+ unsubscribe();
134
+ reject(new Error(`Timeout waiting for auth state to settle. Expected: ${expectedUid}, Got: ${this.auth.currentUser?.uid || 'NULL'}`));
135
+ }, timeoutMs);
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Registruje novog korisnika sa email-om i lozinkom
141
+ */
142
+ async signUp(
143
+ email: string,
144
+ password: string,
145
+ initialRole: UserRole = UserRole.PATIENT,
146
+ options?: {
147
+ patientInviteToken?: string;
148
+ },
149
+ ): Promise<User> {
150
+ const { user: firebaseUser } = await createUserWithEmailAndPassword(this.auth, email, password);
151
+
152
+ return this.userService.createUser(firebaseUser, [initialRole], options);
153
+ }
154
+
155
+ /**
156
+ * Registers a new clinic admin user with email and password
157
+ * Can either create a new clinic group or join an existing one with a token
158
+ *
159
+ * @param data - Clinic admin signup data
160
+ * @returns Object containing the created user, clinic group, and clinic admin
161
+ */
162
+ async signUpClinicAdmin(data: ClinicAdminSignupData): Promise<{
163
+ user: User;
164
+ clinicGroup: ClinicGroup;
165
+ clinicAdmin: ClinicAdmin;
166
+ }> {
167
+ try {
168
+ console.log('[AUTH] Starting clinic admin signup process', {
169
+ email: data.email,
170
+ });
171
+
172
+ // Validate data
173
+ try {
174
+ await clinicAdminSignupSchema.parseAsync(data);
175
+ console.log('[AUTH] Clinic admin signup data validation passed');
176
+ } catch (validationError) {
177
+ console.error('[AUTH] Validation error in signUpClinicAdmin:', validationError);
178
+ throw validationError;
179
+ }
180
+
181
+ // Create Firebase user
182
+ console.log('[AUTH] Creating Firebase user');
183
+ let firebaseUser;
184
+ try {
185
+ const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
186
+ firebaseUser = result.user;
187
+ console.log('[AUTH] Firebase user created successfully', {
188
+ uid: firebaseUser.uid,
189
+ });
190
+ } catch (firebaseError) {
191
+ console.error('[AUTH] Firebase user creation failed:', firebaseError);
192
+ throw handleFirebaseError(firebaseError);
193
+ }
194
+
195
+ // Create user with CLINIC_ADMIN role
196
+ console.log('[AUTH] Creating user with CLINIC_ADMIN role');
197
+ let user;
198
+ try {
199
+ user = await this.userService.createUser(firebaseUser, [UserRole.CLINIC_ADMIN], {
200
+ skipProfileCreation: true,
201
+ });
202
+ console.log('[AUTH] User with CLINIC_ADMIN role created successfully', {
203
+ userId: user.uid,
204
+ });
205
+ } catch (userCreationError) {
206
+ console.error('[AUTH] User creation failed:', userCreationError);
207
+ throw userCreationError;
208
+ }
209
+
210
+ // Create contact person object
211
+ const contactPerson: ContactPerson = {
212
+ firstName: data.firstName,
213
+ lastName: data.lastName,
214
+ title: data.title,
215
+ email: data.email,
216
+ phoneNumber: data.phoneNumber,
217
+ };
218
+ console.log('[AUTH] Contact person object created');
219
+
220
+ // Initialize services
221
+ console.log('[AUTH] Initializing clinic services');
222
+ const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
223
+ const clinicGroupService = new ClinicGroupService(
224
+ this.db,
225
+ this.auth,
226
+ this.app,
227
+ clinicAdminService,
228
+ );
229
+ const mediaService = new MediaService(this.db, this.auth, this.app);
230
+ const clinicService = new ClinicService(
231
+ this.db,
232
+ this.auth,
233
+ this.app,
234
+ clinicGroupService,
235
+ clinicAdminService,
236
+ mediaService,
237
+ );
238
+
239
+ // Set services to resolve circular dependencies
240
+ clinicAdminService.setServices(clinicGroupService, clinicService);
241
+ console.log('[AUTH] Services initialized and circular dependencies resolved');
242
+
243
+ let clinicGroup: ClinicGroup | null = null;
244
+ let adminProfile: ClinicAdmin | null = null;
245
+
246
+ if (data.isCreatingNewGroup) {
247
+ console.log('[AUTH] Creating new clinic group flow');
248
+ // Create new clinic group
249
+ if (!data.clinicGroupData) {
250
+ console.error('[AUTH] Clinic group data is missing');
251
+ throw new Error('Clinic group data is required when creating a new group');
252
+ }
253
+
254
+ // First create the clinic admin without a group
255
+ console.log('[AUTH] Creating clinic admin first (without group)');
256
+ const createClinicAdminData: CreateClinicAdminData = {
257
+ userRef: firebaseUser.uid,
258
+ isGroupOwner: true,
259
+ clinicsManaged: [],
260
+ contactInfo: contactPerson,
261
+ roleTitle: data.title,
262
+ isActive: true,
263
+ // No clinicGroupId yet
264
+ };
265
+
266
+ try {
267
+ adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
268
+ console.log('[AUTH] Clinic admin created successfully', {
269
+ adminId: adminProfile.id,
270
+ });
271
+ } catch (adminCreationError) {
272
+ console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
273
+ throw adminCreationError;
274
+ }
275
+
276
+ // Update user document with admin profile reference
277
+ try {
278
+ console.log('[AUTH] Updating user with admin profile reference');
279
+ user = await this.userService.updateUser(firebaseUser.uid, {
280
+ adminProfile: adminProfile.id,
281
+ });
282
+ console.log('[AUTH] User updated with admin profile reference successfully');
283
+ } catch (userUpdateError) {
284
+ console.error('[AUTH] Failed to update user with admin profile:', userUpdateError);
285
+ throw userUpdateError;
286
+ }
287
+
288
+ // Then create clinic group
289
+ const createClinicGroupData: CreateClinicGroupData = {
290
+ name: data.clinicGroupData.name,
291
+ hqLocation: data.clinicGroupData.hqLocation,
292
+ contactInfo: data.clinicGroupData.contactInfo,
293
+ contactPerson: contactPerson,
294
+ ownerId: adminProfile.id, // Use admin profile ID, not user UID
295
+ isActive: true,
296
+ logo: data.clinicGroupData.logo || null,
297
+ subscriptionModel:
298
+ data.clinicGroupData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
299
+ onboarding: {
300
+ completed: false,
301
+ step: 1,
302
+ },
303
+ };
304
+ console.log('[AUTH] Clinic group data prepared', {
305
+ groupName: createClinicGroupData.name,
306
+ });
307
+
308
+ // Create clinic group
309
+ try {
310
+ clinicGroup = await clinicGroupService.createClinicGroup(
311
+ createClinicGroupData,
312
+ adminProfile.id, // Use admin profile ID, not user UID
313
+ false, // This is not a default group since we're providing complete data
314
+ );
315
+ console.log('[AUTH] Clinic group created successfully', {
316
+ groupId: clinicGroup.id,
317
+ });
318
+
319
+ // Now update the admin with the group ID
320
+ console.log('[AUTH] Updating admin with clinic group ID');
321
+ await clinicAdminService.updateClinicAdmin(adminProfile.id, {
322
+ // Use admin profile ID, not user UID
323
+ clinicGroupId: clinicGroup.id,
324
+ });
325
+ console.log('[AUTH] Admin updated with clinic group ID successfully');
326
+
327
+ // Get the updated admin profile
328
+ adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
329
+ } catch (groupCreationError) {
330
+ console.error('[AUTH] Clinic group creation failed:', groupCreationError);
331
+ throw groupCreationError;
332
+ }
333
+ } else {
334
+ console.log('[AUTH] Joining existing clinic group flow');
335
+ // Join existing clinic group with token
336
+ if (!data.inviteToken) {
337
+ console.error('[AUTH] Invite token is missing');
338
+ throw new Error('Invite token is required when joining an existing group');
339
+ }
340
+ console.log('[AUTH] Invite token provided', {
341
+ token: data.inviteToken,
342
+ });
343
+
344
+ // Find the token in the database
345
+ console.log('[AUTH] Searching for token in clinic groups');
346
+ const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
347
+ const q = query(groupsRef);
348
+ const querySnapshot = await getDocs(q);
349
+
350
+ let foundGroup: ClinicGroup | null = null;
351
+ let foundToken: AdminToken | null = null;
352
+
353
+ console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
354
+ for (const docSnapshot of querySnapshot.docs) {
355
+ const group = docSnapshot.data() as ClinicGroup;
356
+ console.log('[AUTH] Checking group', {
357
+ groupId: group.id,
358
+ groupName: group.name,
359
+ });
360
+
361
+ // Find the token in the group's tokens
362
+ const token = group.adminTokens.find(t => {
363
+ const isMatch =
364
+ t.token === data.inviteToken &&
365
+ t.status === AdminTokenStatus.ACTIVE &&
366
+ new Date(t.expiresAt.toDate()) > new Date();
367
+
368
+ console.log('[AUTH] Checking token', {
369
+ tokenId: t.id,
370
+ tokenMatch: t.token === data.inviteToken,
371
+ tokenStatus: t.status,
372
+ tokenActive: t.status === AdminTokenStatus.ACTIVE,
373
+ tokenExpiry: new Date(t.expiresAt.toDate()),
374
+ tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
375
+ isMatch,
376
+ });
377
+
378
+ return isMatch;
379
+ });
380
+
381
+ if (token) {
382
+ foundGroup = group;
383
+ foundToken = token;
384
+ console.log('[AUTH] Found matching token in group', {
385
+ groupId: group.id,
386
+ tokenId: token.id,
387
+ });
388
+ break;
389
+ }
390
+ }
391
+
392
+ if (!foundGroup || !foundToken) {
393
+ console.error('[AUTH] No valid token found in any clinic group');
394
+ throw new Error('Invalid or expired invite token');
395
+ }
396
+
397
+ clinicGroup = foundGroup;
398
+
399
+ // Create clinic admin
400
+ console.log('[AUTH] Creating clinic admin');
401
+ const createClinicAdminData: CreateClinicAdminData = {
402
+ userRef: firebaseUser.uid,
403
+ clinicGroupId: foundGroup.id,
404
+ isGroupOwner: false,
405
+ clinicsManaged: [],
406
+ contactInfo: contactPerson,
407
+ roleTitle: data.title,
408
+ isActive: true,
409
+ };
410
+
411
+ try {
412
+ adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
413
+ console.log('[AUTH] Clinic admin created successfully', {
414
+ adminId: adminProfile.id,
415
+ });
416
+ } catch (adminCreationError) {
417
+ console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
418
+ throw adminCreationError;
419
+ }
420
+
421
+ // Mark token as used
422
+ try {
423
+ await clinicGroupService.verifyAndUseAdminToken(
424
+ foundGroup.id,
425
+ data.inviteToken,
426
+ firebaseUser.uid,
427
+ );
428
+ console.log('[AUTH] Token marked as used successfully');
429
+ } catch (tokenUseError) {
430
+ console.error('[AUTH] Failed to mark token as used:', tokenUseError);
431
+ throw tokenUseError;
432
+ }
433
+ }
434
+
435
+ console.log('[AUTH] Clinic admin signup completed successfully', {
436
+ userId: user.uid,
437
+ clinicGroupId: clinicGroup.id,
438
+ clinicAdminId: adminProfile?.id || 'unknown',
439
+ });
440
+
441
+ // Ensure we have all required data before returning
442
+ if (!clinicGroup || !adminProfile) {
443
+ throw new Error('Failed to create or retrieve clinic group or admin profile');
444
+ }
445
+
446
+ return {
447
+ user,
448
+ clinicGroup,
449
+ clinicAdmin: adminProfile,
450
+ };
451
+ } catch (error) {
452
+ if (error instanceof z.ZodError) {
453
+ console.error(
454
+ '[AUTH] Zod validation error in signUpClinicAdmin:',
455
+ JSON.stringify(error.errors, null, 2),
456
+ );
457
+ throw AUTH_ERRORS.VALIDATION_ERROR;
458
+ }
459
+
460
+ const firebaseError = error as FirebaseError;
461
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
462
+ console.error('[AUTH] Email already in use:', data.email);
463
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
464
+ }
465
+
466
+ console.error('[AUTH] Unhandled error in signUpClinicAdmin:', error);
467
+ throw error;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Prijavljuje korisnika sa email-om i lozinkom
473
+ */
474
+ async signIn(email: string, password: string): Promise<User> {
475
+ const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
476
+
477
+ return this.userService.getOrCreateUser(firebaseUser);
478
+ }
479
+
480
+ /**
481
+ * Prijavljuje korisnika sa email-om i lozinkom samo za clinic_admin role
482
+ * @param email - Email korisnika
483
+ * @param password - Lozinka korisnika
484
+ * @returns Objekat koji sadrži korisnika, admin profil i grupu klinika
485
+ * @throws {AUTH_ERRORS.INVALID_ROLE} Ako korisnik nema clinic_admin rolu
486
+ * @throws {AUTH_ERRORS.NOT_FOUND} Ako admin profil nije pronađen
487
+ */
488
+ async signInClinicAdmin(
489
+ email: string,
490
+ password: string,
491
+ ): Promise<{
492
+ user: User;
493
+ clinicAdmin: ClinicAdmin;
494
+ clinicGroup: ClinicGroup;
495
+ }> {
496
+ try {
497
+ // Initialize required services
498
+ const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
499
+ const clinicGroupService = new ClinicGroupService(
500
+ this.db,
501
+ this.auth,
502
+ this.app,
503
+ clinicAdminService,
504
+ );
505
+ const mediaService = new MediaService(this.db, this.auth, this.app);
506
+ const clinicService = new ClinicService(
507
+ this.db,
508
+ this.auth,
509
+ this.app,
510
+ clinicGroupService,
511
+ clinicAdminService,
512
+ mediaService,
513
+ );
514
+
515
+ // Set services to resolve circular dependencies
516
+ clinicAdminService.setServices(clinicGroupService, clinicService);
517
+
518
+ // Sign in with email/password
519
+ const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
520
+
521
+ // Get or create user
522
+ const user = await this.userService.getOrCreateUser(firebaseUser);
523
+
524
+ // Check if user has clinic_admin role
525
+ if (!user.roles?.includes(UserRole.CLINIC_ADMIN)) {
526
+ console.error('[AUTH] User is not a clinic admin:', user.uid);
527
+ // Sign out the user immediately for security
528
+ await this.auth.signOut();
529
+ throw AUTH_ERRORS.UNAUTHORIZED_ROLE;
530
+ }
531
+
532
+ // Check and get admin profile
533
+ if (!user.adminProfile) {
534
+ console.error('[AUTH] User has no admin profile:', user.uid);
535
+ throw AUTH_ERRORS.NOT_FOUND;
536
+ }
537
+
538
+ // Get clinic admin profile
539
+ const adminProfile = await clinicAdminService.getClinicAdmin(user.adminProfile);
540
+ if (!adminProfile) {
541
+ console.error('[AUTH] Admin profile not found:', user.adminProfile);
542
+ throw AUTH_ERRORS.NOT_FOUND;
543
+ }
544
+
545
+ // Get clinic group
546
+ const clinicGroup = await clinicGroupService.getClinicGroup(adminProfile.clinicGroupId);
547
+ if (!clinicGroup) {
548
+ console.error('[AUTH] Clinic group not found:', adminProfile.clinicGroupId);
549
+ throw AUTH_ERRORS.NOT_FOUND;
550
+ }
551
+
552
+ return {
553
+ user,
554
+ clinicAdmin: adminProfile,
555
+ clinicGroup,
556
+ };
557
+ } catch (error) {
558
+ console.error('[AUTH] Error in signInClinicAdmin:', error);
559
+ throw error;
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Prijavljuje korisnika anonimno
565
+ */
566
+ async signInAnonymously(options?: { skipProfileCreation?: boolean }): Promise<User> {
567
+ const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
568
+
569
+ if (options?.skipProfileCreation) {
570
+ return this.userService.getOrCreateUser(firebaseUser, UserRole.PATIENT, { skipProfileCreation: true });
571
+ }
572
+
573
+ return this.userService.getOrCreateUser(firebaseUser);
574
+ }
575
+
576
+ /**
577
+ * Odjavljuje trenutnog korisnika
578
+ */
579
+ async signOut(): Promise<void> {
580
+ await firebaseSignOut(this.auth);
581
+ }
582
+
583
+ /**
584
+ * Vraća trenutno prijavljenog korisnika
585
+ */
586
+ async getCurrentUser(): Promise<User | null> {
587
+ const firebaseUser = this.auth.currentUser;
588
+ if (!firebaseUser) return null;
589
+
590
+ return this.userService.getUserById(firebaseUser.uid);
591
+ }
592
+
593
+ /**
594
+ * Registruje callback za promene stanja autentifikacije
595
+ */
596
+ onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
597
+ return onAuthStateChanged(this.auth, callback);
598
+ }
599
+
600
+ async upgradeAnonymousUser(email: string, password: string): Promise<User> {
601
+ try {
602
+ await emailSchema.parseAsync(email);
603
+ await passwordSchema.parseAsync(password);
604
+
605
+ const currentUser = this.auth.currentUser;
606
+ if (!currentUser) {
607
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
608
+ }
609
+ if (!currentUser.isAnonymous) {
610
+ throw new AuthError('User is not anonymous', 'AUTH/NOT_ANONYMOUS_USER', 400);
611
+ }
612
+
613
+ const credential = EmailAuthProvider.credential(email, password);
614
+ await linkWithCredential(currentUser, credential);
615
+
616
+ return await this.userService.upgradeAnonymousUser(currentUser.uid, email);
617
+ } catch (error) {
618
+ if (error instanceof z.ZodError) {
619
+ throw AUTH_ERRORS.VALIDATION_ERROR;
620
+ }
621
+ const firebaseError = error as FirebaseError;
622
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
623
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
624
+ }
625
+ throw error;
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Šalje email za resetovanje lozinke korisniku
631
+ * @param email Email adresa korisnika
632
+ * @returns Promise koji se razrešava kada je email poslat
633
+ */
634
+ async sendPasswordResetEmail(email: string): Promise<void> {
635
+ try {
636
+ await emailSchema.parseAsync(email);
637
+
638
+ // Šaljemo email za resetovanje lozinke
639
+ await sendPasswordResetEmail(this.auth, email);
640
+ } catch (error) {
641
+ if (error instanceof z.ZodError) {
642
+ throw AUTH_ERRORS.VALIDATION_ERROR;
643
+ }
644
+
645
+ const firebaseError = error as FirebaseError;
646
+
647
+ // Handle specific Firebase errors
648
+ switch (firebaseError.code) {
649
+ case FirebaseErrorCode.USER_NOT_FOUND:
650
+ throw AUTH_ERRORS.USER_NOT_FOUND;
651
+ case FirebaseErrorCode.INVALID_EMAIL:
652
+ throw AUTH_ERRORS.INVALID_EMAIL;
653
+ case FirebaseErrorCode.TOO_MANY_REQUESTS:
654
+ throw AUTH_ERRORS.TOO_MANY_REQUESTS;
655
+ case FirebaseErrorCode.NETWORK_ERROR:
656
+ throw AUTH_ERRORS.NETWORK_ERROR;
657
+ case FirebaseErrorCode.OPERATION_NOT_ALLOWED:
658
+ throw AUTH_ERRORS.OPERATION_NOT_ALLOWED;
659
+ default:
660
+ // Re-throw unknown errors as-is
661
+ throw error;
662
+ }
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Verifikuje kod za resetovanje lozinke iz email linka
668
+ * @param oobCode Kod iz URL-a za resetovanje lozinke
669
+ * @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
670
+ */
671
+ async verifyPasswordResetCode(oobCode: string): Promise<string> {
672
+ try {
673
+ // Verifikujemo kod i vraćamo email korisnika
674
+ return await verifyPasswordResetCode(this.auth, oobCode);
675
+ } catch (error) {
676
+ const firebaseError = error as FirebaseError;
677
+ if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
678
+ throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
679
+ } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
680
+ throw AUTH_ERRORS.INVALID_ACTION_CODE;
681
+ }
682
+
683
+ throw error;
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Potvrđuje resetovanje lozinke i postavlja novu lozinku
689
+ * @param oobCode Kod iz URL-a za resetovanje lozinke
690
+ * @param newPassword Nova lozinka
691
+ * @returns Promise koji se razrešava kada je lozinka promenjena
692
+ */
693
+ async confirmPasswordReset(oobCode: string, newPassword: string): Promise<void> {
694
+ try {
695
+ await passwordSchema.parseAsync(newPassword);
696
+
697
+ // Potvrđujemo resetovanje lozinke i postavljamo novu lozinku
698
+ await confirmPasswordReset(this.auth, oobCode, newPassword);
699
+ } catch (error) {
700
+ if (error instanceof z.ZodError) {
701
+ throw AUTH_ERRORS.VALIDATION_ERROR;
702
+ }
703
+
704
+ const firebaseError = error as FirebaseError;
705
+ if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
706
+ throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
707
+ } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
708
+ throw AUTH_ERRORS.INVALID_ACTION_CODE;
709
+ } else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
710
+ throw AUTH_ERRORS.WEAK_PASSWORD;
711
+ }
712
+
713
+ throw error;
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Registers a new practitioner user with email and password (ATOMIC VERSION)
719
+ * Uses Firestore transactions to ensure atomicity and proper rollback on failures
720
+ *
721
+ * @param data - Practitioner signup data containing either new profile details or token for claiming draft profile
722
+ * @returns Object containing the created user and practitioner profile
723
+ */
724
+ async signUpPractitioner(data: {
725
+ email: string;
726
+ password: string;
727
+ firstName?: string;
728
+ lastName?: string;
729
+ token?: string;
730
+ profileData?: Partial<CreatePractitionerData>;
731
+ }): Promise<{
732
+ user: User;
733
+ practitioner: Practitioner;
734
+ }> {
735
+ let firebaseUser: any = null;
736
+
737
+ try {
738
+ console.log('[AUTH] Starting atomic practitioner signup process', {
739
+ email: data.email,
740
+ hasToken: !!data.token,
741
+ });
742
+
743
+ // Step 1: Pre-validate all data before any mutations
744
+ await this.validateSignupData(data);
745
+
746
+ // Step 2: Create Firebase user (outside transaction - can't be easily rolled back)
747
+ console.log('[AUTH] Creating Firebase user');
748
+ try {
749
+ const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
750
+ firebaseUser = result.user;
751
+ console.log('[AUTH] Firebase user created successfully', {
752
+ uid: firebaseUser.uid,
753
+ });
754
+ } catch (firebaseError) {
755
+ console.error('[AUTH] Firebase user creation failed:', firebaseError);
756
+ throw handleFirebaseError(firebaseError);
757
+ }
758
+
759
+ // Step 3: Execute all database operations in a single transaction
760
+ console.log('[AUTH] Starting Firestore transaction');
761
+ const transactionResult = await runTransaction(this.db, async transaction => {
762
+ console.log('[AUTH] Transaction started - creating user and practitioner');
763
+
764
+ // Initialize services
765
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
766
+
767
+ // Create user document using existing method (not in transaction for now)
768
+ console.log('[AUTH] Creating user document');
769
+ const user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
770
+ skipProfileCreation: true,
771
+ });
772
+
773
+ let practitioner: Practitioner;
774
+
775
+ // Handle practitioner profile creation/claiming
776
+ if (data.token) {
777
+ console.log('[AUTH] Claiming existing practitioner profile with token');
778
+ const claimedPractitioner = await practitionerService.validateTokenAndClaimProfile(
779
+ data.token,
780
+ firebaseUser.uid,
781
+ );
782
+ if (!claimedPractitioner) {
783
+ throw new Error('Invalid or expired invitation token');
784
+ }
785
+ practitioner = claimedPractitioner;
786
+ } else {
787
+ // Check if a draft profile exists for this email
788
+ console.log('[AUTH] Checking for existing draft practitioner profile', {
789
+ email: data.email,
790
+ });
791
+ const draftPractitioner = await practitionerService.findDraftPractitionerByEmail(
792
+ data.email
793
+ );
794
+
795
+ if (draftPractitioner) {
796
+ console.log('[AUTH] Draft practitioner profile found', {
797
+ practitionerId: draftPractitioner.id,
798
+ email: data.email,
799
+ clinics: draftPractitioner.clinics,
800
+ });
801
+
802
+ // Extract clinic names from clinicsInfo (should be populated when draft is created)
803
+ let clinicNames: string[] = [];
804
+ if (draftPractitioner.clinicsInfo && draftPractitioner.clinicsInfo.length > 0) {
805
+ clinicNames = draftPractitioner.clinicsInfo
806
+ .map((clinic) => clinic.name)
807
+ .filter((name): name is string => !!name);
808
+ } else if (draftPractitioner.clinics && draftPractitioner.clinics.length > 0) {
809
+ // Fallback: fetch clinic names if clinicsInfo is missing
810
+ console.log('[AUTH] clinicsInfo missing, fetching clinic names from clinic IDs');
811
+ const clinicService = practitionerService.getClinicService();
812
+ const clinicNamePromises = draftPractitioner.clinics.map(async (clinicId) => {
813
+ try {
814
+ const clinic = await clinicService.getClinic(clinicId);
815
+ return clinic?.name || null;
816
+ } catch (error) {
817
+ console.error(`[AUTH] Error fetching clinic ${clinicId}:`, error);
818
+ return null;
819
+ }
820
+ });
821
+ const names = await Promise.all(clinicNamePromises);
822
+ clinicNames = names.filter((name): name is string => !!name);
823
+ }
824
+
825
+ // Cleanup Firebase user since we're not creating a profile
826
+ await cleanupFirebaseUser(firebaseUser);
827
+
828
+ // Throw error with clinic information
829
+ throw new AuthError(
830
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.message,
831
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.code,
832
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.status,
833
+ {
834
+ clinicNames,
835
+ clinics: draftPractitioner.clinics,
836
+ clinicsInfo: draftPractitioner.clinicsInfo,
837
+ }
838
+ );
839
+ }
840
+
841
+ console.log('[AUTH] No draft profile found, creating new practitioner profile');
842
+ const practitionerData = buildPractitionerData(data, firebaseUser.uid);
843
+ practitioner = await practitionerService.createPractitioner(practitionerData);
844
+ }
845
+
846
+ // Link practitioner to user
847
+ console.log('[AUTH] Linking practitioner to user');
848
+ await this.userService.updateUser(firebaseUser.uid, {
849
+ practitionerProfile: practitioner.id,
850
+ });
851
+
852
+ console.log('[AUTH] Transaction operations completed successfully');
853
+ return { user, practitioner };
854
+ });
855
+
856
+ console.log('[AUTH] Atomic practitioner signup completed successfully', {
857
+ userId: transactionResult.user.uid,
858
+ practitionerId: transactionResult.practitioner.id,
859
+ });
860
+
861
+ return transactionResult;
862
+ } catch (error) {
863
+ console.error('[AUTH] Atomic signup failed, initiating cleanup...', error);
864
+
865
+ // Cleanup Firebase user if transaction failed
866
+ if (firebaseUser) {
867
+ await cleanupFirebaseUser(firebaseUser);
868
+ }
869
+
870
+ throw handleSignupError(error);
871
+ }
872
+ }
873
+
874
+ /**
875
+ * Claims draft practitioner profiles after Google Sign-In.
876
+ * Uses the current authenticated user (from initial Google Sign-In).
877
+ *
878
+ * @param idToken - The Google ID token (used to re-authenticate if needed)
879
+ * @param practitionerIds - Array of draft practitioner profile IDs to claim
880
+ * @returns Object containing user and claimed practitioner
881
+ */
882
+ async claimDraftProfilesWithGoogle(
883
+ idToken: string,
884
+ practitionerIds: string[]
885
+ ): Promise<{
886
+ user: User;
887
+ practitioner: Practitioner;
888
+ }> {
889
+ try {
890
+ console.log('[AUTH] Starting claim draft profiles with Google', {
891
+ practitionerIdsCount: practitionerIds.length,
892
+ practitionerIds,
893
+ });
894
+
895
+ if (practitionerIds.length === 0) {
896
+ throw new AuthError('No practitioner profiles selected to claim', 'AUTH/NO_PROFILES_SELECTED', 400);
897
+ }
898
+
899
+ const credential = GoogleAuthProvider.credential(idToken);
900
+ const result = await signInWithCredential(this.auth, credential);
901
+ const firebaseUser = result.user;
902
+
903
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
904
+
905
+ let user: User;
906
+ try {
907
+ user = await this.userService.getUserById(firebaseUser.uid);
908
+ } catch (userError) {
909
+ throw new AuthError(
910
+ 'User account not properly initialized. Please try signing in again.',
911
+ 'AUTH/USER_NOT_INITIALIZED',
912
+ 500,
913
+ );
914
+ }
915
+
916
+ let practitioner: Practitioner;
917
+ if (practitionerIds.length === 1) {
918
+ practitioner = await practitionerService.claimDraftProfileWithGoogle(
919
+ practitionerIds[0],
920
+ firebaseUser.uid
921
+ );
922
+ } else {
923
+ practitioner = await practitionerService.claimMultipleDraftProfilesWithGoogle(
924
+ practitionerIds,
925
+ firebaseUser.uid
926
+ );
927
+ }
928
+
929
+ if (!user.practitionerProfile || user.practitionerProfile !== practitioner.id) {
930
+ await this.userService.updateUser(firebaseUser.uid, {
931
+ practitionerProfile: practitioner.id,
932
+ });
933
+ }
934
+
935
+ const updatedUser = await this.userService.getUserById(firebaseUser.uid);
936
+
937
+ return {
938
+ user: updatedUser,
939
+ practitioner,
940
+ };
941
+ } catch (error: any) {
942
+ console.error('[AUTH] Error claiming draft profiles with Google:', error);
943
+ throw handleSignupError(error);
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Pre-validate all signup data before any mutations
949
+ * Prevents partial creation by catching issues early
950
+ */
951
+ private async validateSignupData(data: {
952
+ email: string;
953
+ password: string;
954
+ firstName?: string;
955
+ lastName?: string;
956
+ token?: string;
957
+ profileData?: Partial<CreatePractitionerData>;
958
+ }): Promise<void> {
959
+ console.log('[AUTH] Pre-validating signup data');
960
+
961
+ try {
962
+ // 1. Schema validation
963
+ await practitionerSignupSchema.parseAsync(data);
964
+ console.log('[AUTH] Schema validation passed');
965
+
966
+ // 2. Check if email already exists (before creating Firebase user)
967
+ const emailExists = await checkEmailExists(this.auth, data.email);
968
+ if (emailExists) {
969
+ console.log('[AUTH] Email already exists:', data.email);
970
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
971
+ }
972
+ console.log('[AUTH] Email availability confirmed');
973
+
974
+ // 3. Validate token if provided
975
+ if (data.token) {
976
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
977
+ const isValidToken = await practitionerService.validateToken(data.token);
978
+ if (!isValidToken) {
979
+ console.log('[AUTH] Invalid token provided:', data.token);
980
+ throw new Error('Invalid or expired invitation token');
981
+ }
982
+ console.log('[AUTH] Token validation passed');
983
+ }
984
+
985
+ // 4. Validate profile data structure if provided
986
+ if (data.profileData) {
987
+ await validatePractitionerProfileData(data.profileData);
988
+ console.log('[AUTH] Profile data validation passed');
989
+ }
990
+
991
+ console.log('[AUTH] All pre-validation checks passed');
992
+ } catch (error) {
993
+ console.error('[AUTH] Pre-validation failed:', error);
994
+ throw error;
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Signs in a user with email and password specifically for practitioner role
1000
+ * @param email - User's email
1001
+ * @param password - User's password
1002
+ * @returns Object containing the user and practitioner profile
1003
+ * @throws {AUTH_ERRORS.INVALID_ROLE} If user doesn't have practitioner role
1004
+ * @throws {AUTH_ERRORS.NOT_FOUND} If practitioner profile is not found
1005
+ */
1006
+ async signInPractitioner(
1007
+ email: string,
1008
+ password: string,
1009
+ ): Promise<{
1010
+ user: User;
1011
+ practitioner: Practitioner;
1012
+ }> {
1013
+ try {
1014
+ console.log('[AUTH] Starting practitioner signin process', {
1015
+ email: email,
1016
+ });
1017
+
1018
+ // Initialize required service
1019
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1020
+
1021
+ // Sign in with email/password
1022
+ const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
1023
+
1024
+ // Get or create user
1025
+ const user = await this.userService.getOrCreateUser(firebaseUser);
1026
+ console.log('[AUTH] User retrieved', { uid: user.uid });
1027
+
1028
+ // Check if user has practitioner role
1029
+ if (!user.roles?.includes(UserRole.PRACTITIONER)) {
1030
+ console.error('[AUTH] User is not a practitioner:', user.uid);
1031
+ throw AUTH_ERRORS.INVALID_ROLE;
1032
+ }
1033
+
1034
+ // Check and get practitioner profile
1035
+ if (!user.practitionerProfile) {
1036
+ console.error('[AUTH] User has no practitioner profile:', user.uid);
1037
+ throw AUTH_ERRORS.NOT_FOUND;
1038
+ }
1039
+
1040
+ // Get practitioner profile
1041
+ const practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1042
+ if (!practitioner) {
1043
+ console.error('[AUTH] Practitioner profile not found:', user.practitionerProfile);
1044
+ throw AUTH_ERRORS.NOT_FOUND;
1045
+ }
1046
+
1047
+ console.log('[AUTH] Practitioner signin completed successfully', {
1048
+ userId: user.uid,
1049
+ practitionerId: practitioner.id,
1050
+ });
1051
+
1052
+ return {
1053
+ user,
1054
+ practitioner,
1055
+ };
1056
+ } catch (error) {
1057
+ console.error('[AUTH] Error in signInPractitioner:', error);
1058
+ throw error;
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * Signs in a user with a Google ID token from a mobile client.
1064
+ * If the user does not exist, a new user is created.
1065
+ * @param idToken - The Google ID token obtained from the mobile app.
1066
+ * @param initialRole - The role to assign to the user if they are being created.
1067
+ * @returns The signed-in or newly created user.
1068
+ */
1069
+ async signInWithGoogleIdToken(
1070
+ idToken: string,
1071
+ initialRole: UserRole = UserRole.PATIENT,
1072
+ ): Promise<User> {
1073
+ try {
1074
+ console.log('[AUTH] Signing in with Google ID Token');
1075
+
1076
+ // Sign in with Google credential — auto-creates Firebase Auth user if needed.
1077
+ const credential = GoogleAuthProvider.credential(idToken);
1078
+ const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1079
+ console.log('[AUTH] Firebase user signed in:', firebaseUser.uid);
1080
+
1081
+ // Load or create domain user document
1082
+ return await this.userService.getOrCreateUser(firebaseUser, UserRole.PATIENT);
1083
+ } catch (error) {
1084
+ console.error('[AUTH] Error in signInWithGoogleIdToken:', error);
1085
+ throw handleFirebaseError(error);
1086
+ }
1087
+ }
1088
+
1089
+ /**
1090
+ * Signs up or signs in a practitioner with Google authentication.
1091
+ * Checks for existing practitioner account or draft profiles.
1092
+ *
1093
+ * @param idToken - The Google ID token obtained from the mobile app
1094
+ * @returns Object containing user, practitioner (if exists), and draft profiles (if any)
1095
+ */
1096
+ async signUpPractitionerWithGoogle(
1097
+ idToken: string
1098
+ ): Promise<{
1099
+ user: User | null;
1100
+ practitioner: Practitioner | null;
1101
+ draftProfiles: Practitioner[];
1102
+ }> {
1103
+ try {
1104
+ console.log('[AUTH] Starting practitioner Google Sign-In/Sign-Up');
1105
+
1106
+ // Extract email from Google ID token
1107
+ let email: string | undefined;
1108
+ try {
1109
+ const payloadBase64 = idToken.split('.')[1];
1110
+ const payloadJson = globalThis.atob
1111
+ ? globalThis.atob(payloadBase64)
1112
+ : Buffer.from(payloadBase64, 'base64').toString('utf8');
1113
+ const payload = JSON.parse(payloadJson);
1114
+ email = payload.email as string | undefined;
1115
+ } catch (decodeError) {
1116
+ console.error('[AUTH] Failed to decode email from Google ID token:', decodeError);
1117
+ throw new AuthError(
1118
+ 'Unable to read email from Google token. Please try again.',
1119
+ 'AUTH/INVALID_GOOGLE_TOKEN',
1120
+ 400,
1121
+ );
1122
+ }
1123
+
1124
+ if (!email) {
1125
+ throw new AuthError(
1126
+ 'Unable to read email from Google token. Please try again.',
1127
+ 'AUTH/INVALID_GOOGLE_TOKEN',
1128
+ 400,
1129
+ );
1130
+ }
1131
+
1132
+ const normalizedEmail = email.toLowerCase().trim();
1133
+
1134
+ const methods = await fetchSignInMethodsForEmail(this.auth, normalizedEmail);
1135
+ const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
1136
+ const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
1137
+
1138
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1139
+
1140
+ if (hasGoogleMethod) {
1141
+ const credential = GoogleAuthProvider.credential(idToken);
1142
+ const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1143
+
1144
+ await this.waitForAuthStateToSettle(firebaseUser.uid);
1145
+
1146
+ let existingUser: User | null = null;
1147
+ try {
1148
+ existingUser = await this.userService.getUserById(firebaseUser.uid);
1149
+ } catch (userError: any) {
1150
+ if (!this.auth.currentUser || this.auth.currentUser.uid !== firebaseUser.uid) {
1151
+ const credential = GoogleAuthProvider.credential(idToken);
1152
+ await signInWithCredential(this.auth, credential);
1153
+ await this.waitForAuthStateToSettle(firebaseUser.uid, 2000);
1154
+ }
1155
+
1156
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1157
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1158
+
1159
+ if (draftProfiles.length === 0) {
1160
+ try {
1161
+ await firebaseSignOut(this.auth);
1162
+ } catch (signOutError) {
1163
+ console.warn('[AUTH] Error signing out:', signOutError);
1164
+ }
1165
+ throw new AuthError(
1166
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1167
+ 'AUTH/NO_DRAFT_PROFILES',
1168
+ 404,
1169
+ );
1170
+ }
1171
+
1172
+ try {
1173
+ const newUser = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1174
+ skipProfileCreation: true,
1175
+ });
1176
+
1177
+ return {
1178
+ user: newUser,
1179
+ practitioner: null,
1180
+ draftProfiles: draftProfiles,
1181
+ };
1182
+ } catch (createUserError: any) {
1183
+ try {
1184
+ await firebaseSignOut(this.auth);
1185
+ } catch (signOutError) {
1186
+ console.warn('[AUTH] Error signing out:', signOutError);
1187
+ }
1188
+ throw createUserError;
1189
+ }
1190
+ }
1191
+
1192
+ // User document exists - check for practitioner profile and draft profiles
1193
+ if (!existingUser) {
1194
+ await firebaseSignOut(this.auth);
1195
+ throw new AuthError(
1196
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1197
+ 'AUTH/NO_DRAFT_PROFILES',
1198
+ 404,
1199
+ );
1200
+ }
1201
+
1202
+ // Check if user has practitioner profile
1203
+ let practitioner: Practitioner | null = null;
1204
+ if (existingUser.practitionerProfile) {
1205
+ practitioner = await practitionerService.getPractitioner(existingUser.practitionerProfile);
1206
+ }
1207
+
1208
+ // Check for any new draft profiles
1209
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1210
+
1211
+ return {
1212
+ user: existingUser,
1213
+ practitioner,
1214
+ draftProfiles,
1215
+ };
1216
+ }
1217
+
1218
+ if (hasEmailMethod && !hasGoogleMethod) {
1219
+ throw new AuthError(
1220
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1221
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1222
+ 409,
1223
+ );
1224
+ }
1225
+
1226
+ const credential = GoogleAuthProvider.credential(idToken);
1227
+
1228
+ let firebaseUser: FirebaseUser;
1229
+ try {
1230
+ const result = await signInWithCredential(this.auth, credential);
1231
+ firebaseUser = result.user;
1232
+ } catch (error: any) {
1233
+ // If sign-in fails because email already exists with different provider
1234
+ if (error.code === 'auth/account-exists-with-different-credential') {
1235
+ throw new AuthError(
1236
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1237
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1238
+ 409,
1239
+ );
1240
+ }
1241
+ throw error;
1242
+ }
1243
+
1244
+ await this.waitForAuthStateToSettle(firebaseUser.uid);
1245
+
1246
+ let existingUser: User | null = null;
1247
+ try {
1248
+ const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
1249
+ if (existingUserDoc) {
1250
+ existingUser = existingUserDoc;
1251
+ }
1252
+ } catch (error) {
1253
+ // Continue with new user creation
1254
+ }
1255
+
1256
+ let draftProfiles: Practitioner[] = [];
1257
+ try {
1258
+ draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1259
+ } catch (draftCheckError: any) {
1260
+ try {
1261
+ await firebaseSignOut(this.auth);
1262
+ } catch (signOutError) {
1263
+ console.warn('[AUTH] Error signing out:', signOutError);
1264
+ }
1265
+ throw new AuthError(
1266
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1267
+ 'AUTH/NO_DRAFT_PROFILES',
1268
+ 404,
1269
+ );
1270
+ }
1271
+
1272
+ let user: User;
1273
+ if (existingUser) {
1274
+ user = existingUser;
1275
+ } else {
1276
+ if (draftProfiles.length === 0) {
1277
+ try {
1278
+ await firebaseSignOut(this.auth);
1279
+ } catch (signOutError) {
1280
+ console.warn('[AUTH] Error signing out:', signOutError);
1281
+ }
1282
+ throw new AuthError(
1283
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1284
+ 'AUTH/NO_DRAFT_PROFILES',
1285
+ 404,
1286
+ );
1287
+ }
1288
+
1289
+ user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1290
+ skipProfileCreation: true,
1291
+ });
1292
+ }
1293
+
1294
+ let practitioner: Practitioner | null = null;
1295
+ if (user.practitionerProfile) {
1296
+ practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1297
+ }
1298
+
1299
+ return {
1300
+ user,
1301
+ practitioner,
1302
+ draftProfiles,
1303
+ };
1304
+ } catch (error: any) {
1305
+ if (error instanceof AuthError) {
1306
+ throw error;
1307
+ }
1308
+
1309
+ const errorMessage = error?.message || error?.toString() || '';
1310
+ if (errorMessage.includes('NO_DRAFT_PROFILES') || errorMessage.includes('clinic invitation')) {
1311
+ throw new AuthError(
1312
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1313
+ 'AUTH/NO_DRAFT_PROFILES',
1314
+ 404,
1315
+ );
1316
+ }
1317
+
1318
+ const wrappedError = handleFirebaseError(error);
1319
+
1320
+ if (wrappedError.message.includes('permissions') || wrappedError.message.includes('Account creation failed')) {
1321
+ throw new AuthError(
1322
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1323
+ 'AUTH/NO_DRAFT_PROFILES',
1324
+ 404,
1325
+ );
1326
+ }
1327
+
1328
+ throw wrappedError;
1329
+ }
1330
+ }
1331
+
1332
+ /**
1333
+ * Links a Google account to the currently signed-in user using an ID token.
1334
+ * This is used to upgrade an anonymous user or to allow an existing user
1335
+ * to sign in with Google in the future.
1336
+ * @param idToken - The Google ID token obtained from the mobile app.
1337
+ * @returns The updated user profile.
1338
+ */
1339
+ async linkGoogleAccount(idToken: string): Promise<User> {
1340
+ try {
1341
+ console.log('[AUTH] Linking Google account with ID Token');
1342
+ const currentUser = this.auth.currentUser;
1343
+ if (!currentUser) {
1344
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
1345
+ }
1346
+
1347
+ const wasAnonymous = currentUser.isAnonymous;
1348
+ console.log(`[AUTH] Current user is ${wasAnonymous ? 'anonymous' : 'not anonymous'}`);
1349
+
1350
+ const credential = GoogleAuthProvider.credential(idToken);
1351
+ const userCredential = await linkWithCredential(currentUser, credential);
1352
+ const linkedFirebaseUser = userCredential.user;
1353
+ console.log('[AUTH] Google account linked successfully to user:', linkedFirebaseUser.uid);
1354
+
1355
+ if (wasAnonymous) {
1356
+ console.log('[AUTH] Upgrading anonymous user profile');
1357
+ return await this.userService.upgradeAnonymousUser(
1358
+ linkedFirebaseUser.uid,
1359
+ linkedFirebaseUser.email!,
1360
+ );
1361
+ }
1362
+
1363
+ // If the user was not anonymous, just return their updated profile
1364
+ return (await this.userService.getUserById(linkedFirebaseUser.uid))!;
1365
+ } catch (error) {
1366
+ console.error('[AUTH] Error in linkGoogleAccount:', error);
1367
+ throw handleFirebaseError(error);
1368
+ }
1369
+ }
1370
+
1371
+ /**
1372
+ * Signs in or registers a user with an Apple ID token.
1373
+ * If the user does not exist, a new user is created.
1374
+ */
1375
+ async signInWithAppleIdToken(
1376
+ idToken: string,
1377
+ rawNonce: string,
1378
+ appleUserInfo?: { fullName?: { givenName?: string; familyName?: string }; email?: string },
1379
+ ): Promise<User> {
1380
+ try {
1381
+ console.log('[AUTH] Signing in with Apple ID Token');
1382
+
1383
+ // Build Apple OAuth credential
1384
+ const provider = new OAuthProvider('apple.com');
1385
+ const credential = provider.credential({ idToken, rawNonce });
1386
+
1387
+ // Sign in to Firebase — auto-creates Firebase Auth user if needed
1388
+ const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1389
+ console.log('[AUTH] Firebase user signed in via Apple:', firebaseUser.uid);
1390
+
1391
+ // Load or create domain user document
1392
+ return await this.userService.getOrCreateUser(firebaseUser, UserRole.PATIENT);
1393
+ } catch (error) {
1394
+ console.error('[AUTH] Error in signInWithAppleIdToken:', error);
1395
+ throw handleFirebaseError(error);
1396
+ }
1397
+ }
1398
+
1399
+ /**
1400
+ * Link an Apple account to the current user (anonymous → full account upgrade).
1401
+ * Mirrors linkGoogleAccount.
1402
+ */
1403
+ async linkAppleAccount(idToken: string, rawNonce: string): Promise<User> {
1404
+ try {
1405
+ console.log('[AUTH] Linking Apple account with ID Token');
1406
+ const currentUser = this.auth.currentUser;
1407
+ if (!currentUser) {
1408
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
1409
+ }
1410
+
1411
+ const wasAnonymous = currentUser.isAnonymous;
1412
+ console.log(`[AUTH] Current user is ${wasAnonymous ? 'anonymous' : 'not anonymous'}`);
1413
+
1414
+ const provider = new OAuthProvider('apple.com');
1415
+ const credential = provider.credential({ idToken, rawNonce });
1416
+ const userCredential = await linkWithCredential(currentUser, credential);
1417
+ const linkedFirebaseUser = userCredential.user;
1418
+ console.log('[AUTH] Apple account linked successfully to user:', linkedFirebaseUser.uid);
1419
+
1420
+ if (wasAnonymous) {
1421
+ console.log('[AUTH] Upgrading anonymous user profile');
1422
+ const email = linkedFirebaseUser.email || '';
1423
+ return await this.userService.upgradeAnonymousUser(
1424
+ linkedFirebaseUser.uid,
1425
+ email,
1426
+ );
1427
+ }
1428
+
1429
+ return (await this.userService.getUserById(linkedFirebaseUser.uid))!;
1430
+ } catch (error) {
1431
+ console.error('[AUTH] Error in linkAppleAccount:', error);
1432
+ throw handleFirebaseError(error);
1433
+ }
1434
+ }
1435
+ }