@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8

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 (295) hide show
  1. package/dist/admin/index.d.mts +20 -1
  2. package/dist/admin/index.d.ts +20 -1
  3. package/dist/admin/index.js +217 -1
  4. package/dist/admin/index.mjs +217 -1
  5. package/dist/index.d.mts +26 -3
  6. package/dist/index.d.ts +26 -3
  7. package/dist/index.js +168 -6
  8. package/dist/index.mjs +168 -6
  9. package/package.json +121 -121
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  35. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  36. package/src/admin/analytics/index.ts +2 -2
  37. package/src/admin/booking/README.md +125 -125
  38. package/src/admin/booking/booking.admin.ts +1037 -1037
  39. package/src/admin/booking/booking.calculator.ts +712 -712
  40. package/src/admin/booking/booking.types.ts +59 -59
  41. package/src/admin/booking/index.ts +3 -3
  42. package/src/admin/booking/timezones-problem.md +185 -185
  43. package/src/admin/calendar/README.md +7 -7
  44. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  45. package/src/admin/calendar/index.ts +1 -1
  46. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  47. package/src/admin/documentation-templates/index.ts +1 -1
  48. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  49. package/src/admin/free-consultation/index.ts +1 -1
  50. package/src/admin/index.ts +81 -81
  51. package/src/admin/logger/index.ts +78 -78
  52. package/src/admin/mailing/README.md +95 -95
  53. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  54. package/src/admin/mailing/appointment/index.ts +1 -1
  55. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  56. package/src/admin/mailing/base.mailing.service.ts +208 -208
  57. package/src/admin/mailing/index.ts +3 -3
  58. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  59. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  60. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  61. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  62. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  63. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  64. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  65. package/src/admin/notifications/index.ts +1 -1
  66. package/src/admin/notifications/notifications.admin.ts +710 -710
  67. package/src/admin/requirements/README.md +128 -128
  68. package/src/admin/requirements/index.ts +1 -1
  69. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  70. package/src/admin/users/index.ts +1 -1
  71. package/src/admin/users/user-profile.admin.ts +405 -405
  72. package/src/backoffice/constants/certification.constants.ts +13 -13
  73. package/src/backoffice/constants/index.ts +1 -1
  74. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  75. package/src/backoffice/errors/index.ts +1 -1
  76. package/src/backoffice/expo-safe/README.md +26 -26
  77. package/src/backoffice/expo-safe/index.ts +41 -41
  78. package/src/backoffice/index.ts +5 -5
  79. package/src/backoffice/services/FIXES_README.md +102 -102
  80. package/src/backoffice/services/README.md +57 -57
  81. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  82. package/src/backoffice/services/analytics.service.summary.md +143 -143
  83. package/src/backoffice/services/brand.service.ts +256 -256
  84. package/src/backoffice/services/category.service.ts +384 -384
  85. package/src/backoffice/services/constants.service.ts +385 -385
  86. package/src/backoffice/services/documentation-template.service.ts +202 -202
  87. package/src/backoffice/services/index.ts +10 -10
  88. package/src/backoffice/services/migrate-products.ts +116 -116
  89. package/src/backoffice/services/product.service.ts +553 -553
  90. package/src/backoffice/services/requirement.service.ts +235 -235
  91. package/src/backoffice/services/subcategory.service.ts +461 -461
  92. package/src/backoffice/services/technology.service.ts +1151 -1151
  93. package/src/backoffice/types/README.md +12 -12
  94. package/src/backoffice/types/admin-constants.types.ts +69 -69
  95. package/src/backoffice/types/brand.types.ts +29 -29
  96. package/src/backoffice/types/category.types.ts +67 -67
  97. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  98. package/src/backoffice/types/index.ts +10 -10
  99. package/src/backoffice/types/procedure-product.types.ts +38 -38
  100. package/src/backoffice/types/product.types.ts +240 -240
  101. package/src/backoffice/types/requirement.types.ts +63 -63
  102. package/src/backoffice/types/static/README.md +18 -18
  103. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  104. package/src/backoffice/types/static/certification.types.ts +37 -37
  105. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  106. package/src/backoffice/types/static/index.ts +6 -6
  107. package/src/backoffice/types/static/pricing.types.ts +16 -16
  108. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  109. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  110. package/src/backoffice/types/subcategory.types.ts +34 -34
  111. package/src/backoffice/types/technology.types.ts +168 -168
  112. package/src/backoffice/validations/index.ts +1 -1
  113. package/src/backoffice/validations/schemas.ts +164 -164
  114. package/src/config/__mocks__/firebase.ts +99 -99
  115. package/src/config/firebase.ts +78 -78
  116. package/src/config/index.ts +9 -9
  117. package/src/errors/auth.error.ts +6 -6
  118. package/src/errors/auth.errors.ts +211 -200
  119. package/src/errors/clinic.errors.ts +32 -32
  120. package/src/errors/firebase.errors.ts +47 -47
  121. package/src/errors/user.errors.ts +99 -99
  122. package/src/index.backup.ts +407 -407
  123. package/src/index.ts +6 -6
  124. package/src/locales/en.ts +31 -31
  125. package/src/recommender/admin/index.ts +1 -1
  126. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  127. package/src/recommender/front/index.ts +1 -1
  128. package/src/recommender/front/services/onboarding.service.ts +5 -5
  129. package/src/recommender/front/services/recommender.service.ts +3 -3
  130. package/src/recommender/index.ts +1 -1
  131. package/src/services/PATIENTAUTH.MD +197 -197
  132. package/src/services/README.md +106 -106
  133. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  134. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  135. package/src/services/__tests__/auth.service.test.ts +346 -346
  136. package/src/services/__tests__/base.service.test.ts +77 -77
  137. package/src/services/__tests__/user.service.test.ts +528 -528
  138. package/src/services/analytics/ARCHITECTURE.md +199 -199
  139. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  140. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  141. package/src/services/analytics/QUICK_START.md +393 -393
  142. package/src/services/analytics/README.md +304 -304
  143. package/src/services/analytics/SUMMARY.md +141 -141
  144. package/src/services/analytics/TRENDS.md +380 -380
  145. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  146. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  147. package/src/services/analytics/analytics.service.ts +2142 -2142
  148. package/src/services/analytics/index.ts +4 -4
  149. package/src/services/analytics/review-analytics.service.ts +941 -941
  150. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  151. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  152. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  153. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  154. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  155. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  156. package/src/services/appointment/README.md +17 -17
  157. package/src/services/appointment/appointment.service.ts +2558 -2558
  158. package/src/services/appointment/index.ts +1 -1
  159. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  160. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  161. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  162. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  163. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  164. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  165. package/src/services/auth/auth.service.ts +1043 -989
  166. package/src/services/auth/auth.v2.service.ts +961 -961
  167. package/src/services/auth/index.ts +7 -7
  168. package/src/services/auth/utils/error.utils.ts +90 -90
  169. package/src/services/auth/utils/firebase.utils.ts +49 -49
  170. package/src/services/auth/utils/index.ts +21 -21
  171. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  172. package/src/services/base.service.ts +41 -41
  173. package/src/services/calendar/calendar.service.ts +1077 -1077
  174. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  175. package/src/services/calendar/calendar.v3.service.ts +313 -313
  176. package/src/services/calendar/externalCalendar.service.ts +178 -178
  177. package/src/services/calendar/index.ts +5 -5
  178. package/src/services/calendar/synced-calendars.service.ts +743 -743
  179. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  180. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  181. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  182. package/src/services/calendar/utils/docs.utils.ts +157 -157
  183. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  184. package/src/services/calendar/utils/index.ts +8 -8
  185. package/src/services/calendar/utils/patient.utils.ts +198 -198
  186. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  187. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  188. package/src/services/clinic/README.md +204 -204
  189. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  190. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  191. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  192. package/src/services/clinic/billing-transactions.service.ts +217 -217
  193. package/src/services/clinic/clinic-admin.service.ts +202 -202
  194. package/src/services/clinic/clinic-group.service.ts +310 -310
  195. package/src/services/clinic/clinic.service.ts +708 -708
  196. package/src/services/clinic/index.ts +5 -5
  197. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  198. package/src/services/clinic/utils/admin.utils.ts +551 -551
  199. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  200. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  201. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  202. package/src/services/clinic/utils/filter.utils.ts +446 -446
  203. package/src/services/clinic/utils/index.ts +11 -11
  204. package/src/services/clinic/utils/photos.utils.ts +188 -188
  205. package/src/services/clinic/utils/search.utils.ts +84 -84
  206. package/src/services/clinic/utils/tag.utils.ts +124 -124
  207. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  208. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  209. package/src/services/documentation-templates/index.ts +2 -2
  210. package/src/services/index.ts +14 -14
  211. package/src/services/media/index.ts +1 -1
  212. package/src/services/media/media.service.ts +418 -418
  213. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  214. package/src/services/notifications/index.ts +1 -1
  215. package/src/services/notifications/notification.service.ts +215 -215
  216. package/src/services/patient/README.md +48 -48
  217. package/src/services/patient/To-Do.md +43 -43
  218. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  219. package/src/services/patient/index.ts +2 -2
  220. package/src/services/patient/patient.service.ts +883 -883
  221. package/src/services/patient/patientRequirements.service.ts +285 -285
  222. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  223. package/src/services/patient/utils/clinic.utils.ts +80 -80
  224. package/src/services/patient/utils/docs.utils.ts +142 -142
  225. package/src/services/patient/utils/index.ts +9 -9
  226. package/src/services/patient/utils/location.utils.ts +126 -126
  227. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  228. package/src/services/patient/utils/medical.utils.ts +458 -458
  229. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  230. package/src/services/patient/utils/profile.utils.ts +510 -510
  231. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  232. package/src/services/patient/utils/token.utils.ts +211 -211
  233. package/src/services/practitioner/README.md +145 -145
  234. package/src/services/practitioner/index.ts +1 -1
  235. package/src/services/practitioner/practitioner.service.ts +1799 -1742
  236. package/src/services/procedure/README.md +163 -163
  237. package/src/services/procedure/index.ts +1 -1
  238. package/src/services/procedure/procedure.service.ts +2307 -2200
  239. package/src/services/reviews/index.ts +1 -1
  240. package/src/services/reviews/reviews.service.ts +734 -734
  241. package/src/services/user/index.ts +1 -1
  242. package/src/services/user/user.service.ts +489 -489
  243. package/src/services/user/user.v2.service.ts +466 -466
  244. package/src/types/analytics/analytics.types.ts +597 -597
  245. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  246. package/src/types/analytics/index.ts +4 -4
  247. package/src/types/analytics/stored-analytics.types.ts +137 -137
  248. package/src/types/appointment/index.ts +480 -480
  249. package/src/types/calendar/index.ts +258 -258
  250. package/src/types/calendar/synced-calendar.types.ts +66 -66
  251. package/src/types/clinic/index.ts +498 -498
  252. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  253. package/src/types/clinic/preferences.types.ts +159 -159
  254. package/src/types/clinic/to-do +3 -3
  255. package/src/types/documentation-templates/index.ts +308 -308
  256. package/src/types/index.ts +47 -47
  257. package/src/types/notifications/README.md +77 -77
  258. package/src/types/notifications/index.ts +286 -286
  259. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  260. package/src/types/patient/allergies.ts +58 -58
  261. package/src/types/patient/index.ts +275 -275
  262. package/src/types/patient/medical-info.types.ts +152 -152
  263. package/src/types/patient/patient-requirements.ts +92 -92
  264. package/src/types/patient/token.types.ts +61 -61
  265. package/src/types/practitioner/index.ts +206 -206
  266. package/src/types/procedure/index.ts +181 -181
  267. package/src/types/profile/index.ts +39 -39
  268. package/src/types/reviews/index.ts +132 -132
  269. package/src/types/tz-lookup.d.ts +4 -4
  270. package/src/types/user/index.ts +38 -38
  271. package/src/utils/TIMESTAMPS.md +176 -176
  272. package/src/utils/TimestampUtils.ts +241 -241
  273. package/src/utils/index.ts +1 -1
  274. package/src/validations/appointment.schema.ts +574 -574
  275. package/src/validations/calendar.schema.ts +225 -225
  276. package/src/validations/clinic.schema.ts +494 -494
  277. package/src/validations/common.schema.ts +25 -25
  278. package/src/validations/documentation-templates/index.ts +1 -1
  279. package/src/validations/documentation-templates/template.schema.ts +220 -220
  280. package/src/validations/documentation-templates.schema.ts +10 -10
  281. package/src/validations/index.ts +20 -20
  282. package/src/validations/media.schema.ts +10 -10
  283. package/src/validations/notification.schema.ts +90 -90
  284. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  285. package/src/validations/patient/medical-info.schema.ts +125 -125
  286. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  287. package/src/validations/patient/token.schema.ts +29 -29
  288. package/src/validations/patient.schema.ts +217 -217
  289. package/src/validations/practitioner.schema.ts +222 -222
  290. package/src/validations/procedure-product.schema.ts +41 -41
  291. package/src/validations/procedure.schema.ts +124 -124
  292. package/src/validations/profile-info.schema.ts +41 -41
  293. package/src/validations/reviews.schema.ts +195 -195
  294. package/src/validations/schemas.ts +104 -104
  295. package/src/validations/shared.schema.ts +78 -78
@@ -1,989 +1,1043 @@
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
- } from 'firebase/auth';
21
- import {
22
- getFirestore,
23
- collection,
24
- doc,
25
- getDoc,
26
- setDoc,
27
- updateDoc,
28
- deleteDoc,
29
- query,
30
- where,
31
- getDocs,
32
- orderBy,
33
- limit,
34
- startAfter,
35
- Timestamp,
36
- runTransaction,
37
- Firestore,
38
- } from 'firebase/firestore';
39
- import { FirebaseApp } from 'firebase/app';
40
- import { User, UserRole, USERS_COLLECTION } from '../../types';
41
- import { z } from 'zod';
42
- import { emailSchema, passwordSchema, userRoleSchema } from '../../validations/schemas';
43
- import { AuthError, AUTH_ERRORS } from '../../errors/auth.errors';
44
- import { FirebaseErrorCode } from '../../errors/firebase.errors';
45
- import { FirebaseError } from '../../errors/firebase.errors';
46
- import { BaseService } from '../base.service';
47
- import { UserService } from '../user/user.service';
48
- import { throws } from 'assert';
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
- * Registruje novog korisnika sa email-om i lozinkom
97
- */
98
- async signUp(
99
- email: string,
100
- password: string,
101
- initialRole: UserRole = UserRole.PATIENT,
102
- options?: {
103
- patientInviteToken?: string;
104
- },
105
- ): Promise<User> {
106
- const { user: firebaseUser } = await createUserWithEmailAndPassword(this.auth, email, password);
107
-
108
- return this.userService.createUser(firebaseUser, [initialRole], options);
109
- }
110
-
111
- /**
112
- * Registers a new clinic admin user with email and password
113
- * Can either create a new clinic group or join an existing one with a token
114
- *
115
- * @param data - Clinic admin signup data
116
- * @returns Object containing the created user, clinic group, and clinic admin
117
- */
118
- async signUpClinicAdmin(data: ClinicAdminSignupData): Promise<{
119
- user: User;
120
- clinicGroup: ClinicGroup;
121
- clinicAdmin: ClinicAdmin;
122
- }> {
123
- try {
124
- console.log('[AUTH] Starting clinic admin signup process', {
125
- email: data.email,
126
- });
127
-
128
- // Validate data
129
- try {
130
- await clinicAdminSignupSchema.parseAsync(data);
131
- console.log('[AUTH] Clinic admin signup data validation passed');
132
- } catch (validationError) {
133
- console.error('[AUTH] Validation error in signUpClinicAdmin:', validationError);
134
- throw validationError;
135
- }
136
-
137
- // Create Firebase user
138
- console.log('[AUTH] Creating Firebase user');
139
- let firebaseUser;
140
- try {
141
- const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
142
- firebaseUser = result.user;
143
- console.log('[AUTH] Firebase user created successfully', {
144
- uid: firebaseUser.uid,
145
- });
146
- } catch (firebaseError) {
147
- console.error('[AUTH] Firebase user creation failed:', firebaseError);
148
- throw handleFirebaseError(firebaseError);
149
- }
150
-
151
- // Create user with CLINIC_ADMIN role
152
- console.log('[AUTH] Creating user with CLINIC_ADMIN role');
153
- let user;
154
- try {
155
- user = await this.userService.createUser(firebaseUser, [UserRole.CLINIC_ADMIN], {
156
- skipProfileCreation: true,
157
- });
158
- console.log('[AUTH] User with CLINIC_ADMIN role created successfully', {
159
- userId: user.uid,
160
- });
161
- } catch (userCreationError) {
162
- console.error('[AUTH] User creation failed:', userCreationError);
163
- throw userCreationError;
164
- }
165
-
166
- // Create contact person object
167
- const contactPerson: ContactPerson = {
168
- firstName: data.firstName,
169
- lastName: data.lastName,
170
- title: data.title,
171
- email: data.email,
172
- phoneNumber: data.phoneNumber,
173
- };
174
- console.log('[AUTH] Contact person object created');
175
-
176
- // Initialize services
177
- console.log('[AUTH] Initializing clinic services');
178
- const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
179
- const clinicGroupService = new ClinicGroupService(
180
- this.db,
181
- this.auth,
182
- this.app,
183
- clinicAdminService,
184
- );
185
- const mediaService = new MediaService(this.db, this.auth, this.app);
186
- const clinicService = new ClinicService(
187
- this.db,
188
- this.auth,
189
- this.app,
190
- clinicGroupService,
191
- clinicAdminService,
192
- mediaService,
193
- );
194
-
195
- // Set services to resolve circular dependencies
196
- clinicAdminService.setServices(clinicGroupService, clinicService);
197
- console.log('[AUTH] Services initialized and circular dependencies resolved');
198
-
199
- let clinicGroup: ClinicGroup | null = null;
200
- let adminProfile: ClinicAdmin | null = null;
201
-
202
- if (data.isCreatingNewGroup) {
203
- console.log('[AUTH] Creating new clinic group flow');
204
- // Create new clinic group
205
- if (!data.clinicGroupData) {
206
- console.error('[AUTH] Clinic group data is missing');
207
- throw new Error('Clinic group data is required when creating a new group');
208
- }
209
-
210
- // First create the clinic admin without a group
211
- console.log('[AUTH] Creating clinic admin first (without group)');
212
- const createClinicAdminData: CreateClinicAdminData = {
213
- userRef: firebaseUser.uid,
214
- isGroupOwner: true,
215
- clinicsManaged: [],
216
- contactInfo: contactPerson,
217
- roleTitle: data.title,
218
- isActive: true,
219
- // No clinicGroupId yet
220
- };
221
-
222
- try {
223
- adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
224
- console.log('[AUTH] Clinic admin created successfully', {
225
- adminId: adminProfile.id,
226
- });
227
- } catch (adminCreationError) {
228
- console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
229
- throw adminCreationError;
230
- }
231
-
232
- // Update user document with admin profile reference
233
- try {
234
- console.log('[AUTH] Updating user with admin profile reference');
235
- user = await this.userService.updateUser(firebaseUser.uid, {
236
- adminProfile: adminProfile.id,
237
- });
238
- console.log('[AUTH] User updated with admin profile reference successfully');
239
- } catch (userUpdateError) {
240
- console.error('[AUTH] Failed to update user with admin profile:', userUpdateError);
241
- throw userUpdateError;
242
- }
243
-
244
- // Then create clinic group
245
- const createClinicGroupData: CreateClinicGroupData = {
246
- name: data.clinicGroupData.name,
247
- hqLocation: data.clinicGroupData.hqLocation,
248
- contactInfo: data.clinicGroupData.contactInfo,
249
- contactPerson: contactPerson,
250
- ownerId: adminProfile.id, // Use admin profile ID, not user UID
251
- isActive: true,
252
- logo: data.clinicGroupData.logo || null,
253
- subscriptionModel:
254
- data.clinicGroupData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
255
- onboarding: {
256
- completed: false,
257
- step: 1,
258
- },
259
- };
260
- console.log('[AUTH] Clinic group data prepared', {
261
- groupName: createClinicGroupData.name,
262
- });
263
-
264
- // Create clinic group
265
- try {
266
- clinicGroup = await clinicGroupService.createClinicGroup(
267
- createClinicGroupData,
268
- adminProfile.id, // Use admin profile ID, not user UID
269
- false, // This is not a default group since we're providing complete data
270
- );
271
- console.log('[AUTH] Clinic group created successfully', {
272
- groupId: clinicGroup.id,
273
- });
274
-
275
- // Now update the admin with the group ID
276
- console.log('[AUTH] Updating admin with clinic group ID');
277
- await clinicAdminService.updateClinicAdmin(adminProfile.id, {
278
- // Use admin profile ID, not user UID
279
- clinicGroupId: clinicGroup.id,
280
- });
281
- console.log('[AUTH] Admin updated with clinic group ID successfully');
282
-
283
- // Get the updated admin profile
284
- adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
285
- } catch (groupCreationError) {
286
- console.error('[AUTH] Clinic group creation failed:', groupCreationError);
287
- throw groupCreationError;
288
- }
289
- } else {
290
- console.log('[AUTH] Joining existing clinic group flow');
291
- // Join existing clinic group with token
292
- if (!data.inviteToken) {
293
- console.error('[AUTH] Invite token is missing');
294
- throw new Error('Invite token is required when joining an existing group');
295
- }
296
- console.log('[AUTH] Invite token provided', {
297
- token: data.inviteToken,
298
- });
299
-
300
- // Find the token in the database
301
- console.log('[AUTH] Searching for token in clinic groups');
302
- const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
303
- const q = query(groupsRef);
304
- const querySnapshot = await getDocs(q);
305
-
306
- let foundGroup: ClinicGroup | null = null;
307
- let foundToken: AdminToken | null = null;
308
-
309
- console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
310
- for (const docSnapshot of querySnapshot.docs) {
311
- const group = docSnapshot.data() as ClinicGroup;
312
- console.log('[AUTH] Checking group', {
313
- groupId: group.id,
314
- groupName: group.name,
315
- });
316
-
317
- // Find the token in the group's tokens
318
- const token = group.adminTokens.find(t => {
319
- const isMatch =
320
- t.token === data.inviteToken &&
321
- t.status === AdminTokenStatus.ACTIVE &&
322
- new Date(t.expiresAt.toDate()) > new Date();
323
-
324
- console.log('[AUTH] Checking token', {
325
- tokenId: t.id,
326
- tokenMatch: t.token === data.inviteToken,
327
- tokenStatus: t.status,
328
- tokenActive: t.status === AdminTokenStatus.ACTIVE,
329
- tokenExpiry: new Date(t.expiresAt.toDate()),
330
- tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
331
- isMatch,
332
- });
333
-
334
- return isMatch;
335
- });
336
-
337
- if (token) {
338
- foundGroup = group;
339
- foundToken = token;
340
- console.log('[AUTH] Found matching token in group', {
341
- groupId: group.id,
342
- tokenId: token.id,
343
- });
344
- break;
345
- }
346
- }
347
-
348
- if (!foundGroup || !foundToken) {
349
- console.error('[AUTH] No valid token found in any clinic group');
350
- throw new Error('Invalid or expired invite token');
351
- }
352
-
353
- clinicGroup = foundGroup;
354
-
355
- // Create clinic admin
356
- console.log('[AUTH] Creating clinic admin');
357
- const createClinicAdminData: CreateClinicAdminData = {
358
- userRef: firebaseUser.uid,
359
- clinicGroupId: foundGroup.id,
360
- isGroupOwner: false,
361
- clinicsManaged: [],
362
- contactInfo: contactPerson,
363
- roleTitle: data.title,
364
- isActive: true,
365
- };
366
-
367
- try {
368
- adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
369
- console.log('[AUTH] Clinic admin created successfully', {
370
- adminId: adminProfile.id,
371
- });
372
- } catch (adminCreationError) {
373
- console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
374
- throw adminCreationError;
375
- }
376
-
377
- // Mark token as used
378
- try {
379
- await clinicGroupService.verifyAndUseAdminToken(
380
- foundGroup.id,
381
- data.inviteToken,
382
- firebaseUser.uid,
383
- );
384
- console.log('[AUTH] Token marked as used successfully');
385
- } catch (tokenUseError) {
386
- console.error('[AUTH] Failed to mark token as used:', tokenUseError);
387
- throw tokenUseError;
388
- }
389
- }
390
-
391
- console.log('[AUTH] Clinic admin signup completed successfully', {
392
- userId: user.uid,
393
- clinicGroupId: clinicGroup.id,
394
- clinicAdminId: adminProfile?.id || 'unknown',
395
- });
396
-
397
- // Ensure we have all required data before returning
398
- if (!clinicGroup || !adminProfile) {
399
- throw new Error('Failed to create or retrieve clinic group or admin profile');
400
- }
401
-
402
- return {
403
- user,
404
- clinicGroup,
405
- clinicAdmin: adminProfile,
406
- };
407
- } catch (error) {
408
- if (error instanceof z.ZodError) {
409
- console.error(
410
- '[AUTH] Zod validation error in signUpClinicAdmin:',
411
- JSON.stringify(error.errors, null, 2),
412
- );
413
- throw AUTH_ERRORS.VALIDATION_ERROR;
414
- }
415
-
416
- const firebaseError = error as FirebaseError;
417
- if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
418
- console.error('[AUTH] Email already in use:', data.email);
419
- throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
420
- }
421
-
422
- console.error('[AUTH] Unhandled error in signUpClinicAdmin:', error);
423
- throw error;
424
- }
425
- }
426
-
427
- /**
428
- * Prijavljuje korisnika sa email-om i lozinkom
429
- */
430
- async signIn(email: string, password: string): Promise<User> {
431
- const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
432
-
433
- return this.userService.getOrCreateUser(firebaseUser);
434
- }
435
-
436
- /**
437
- * Prijavljuje korisnika sa email-om i lozinkom samo za clinic_admin role
438
- * @param email - Email korisnika
439
- * @param password - Lozinka korisnika
440
- * @returns Objekat koji sadrži korisnika, admin profil i grupu klinika
441
- * @throws {AUTH_ERRORS.INVALID_ROLE} Ako korisnik nema clinic_admin rolu
442
- * @throws {AUTH_ERRORS.NOT_FOUND} Ako admin profil nije pronađen
443
- */
444
- async signInClinicAdmin(
445
- email: string,
446
- password: string,
447
- ): Promise<{
448
- user: User;
449
- clinicAdmin: ClinicAdmin;
450
- clinicGroup: ClinicGroup;
451
- }> {
452
- try {
453
- // Initialize required services
454
- const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
455
- const clinicGroupService = new ClinicGroupService(
456
- this.db,
457
- this.auth,
458
- this.app,
459
- clinicAdminService,
460
- );
461
- const mediaService = new MediaService(this.db, this.auth, this.app);
462
- const clinicService = new ClinicService(
463
- this.db,
464
- this.auth,
465
- this.app,
466
- clinicGroupService,
467
- clinicAdminService,
468
- mediaService,
469
- );
470
-
471
- // Set services to resolve circular dependencies
472
- clinicAdminService.setServices(clinicGroupService, clinicService);
473
-
474
- // Sign in with email/password
475
- const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
476
-
477
- // Get or create user
478
- const user = await this.userService.getOrCreateUser(firebaseUser);
479
-
480
- // Check if user has clinic_admin role
481
- if (!user.roles?.includes(UserRole.CLINIC_ADMIN)) {
482
- console.error('[AUTH] User is not a clinic admin:', user.uid);
483
- // Sign out the user immediately for security
484
- await this.auth.signOut();
485
- throw AUTH_ERRORS.UNAUTHORIZED_ROLE;
486
- }
487
-
488
- // Check and get admin profile
489
- if (!user.adminProfile) {
490
- console.error('[AUTH] User has no admin profile:', user.uid);
491
- throw AUTH_ERRORS.NOT_FOUND;
492
- }
493
-
494
- // Get clinic admin profile
495
- const adminProfile = await clinicAdminService.getClinicAdmin(user.adminProfile);
496
- if (!adminProfile) {
497
- console.error('[AUTH] Admin profile not found:', user.adminProfile);
498
- throw AUTH_ERRORS.NOT_FOUND;
499
- }
500
-
501
- // Get clinic group
502
- const clinicGroup = await clinicGroupService.getClinicGroup(adminProfile.clinicGroupId);
503
- if (!clinicGroup) {
504
- console.error('[AUTH] Clinic group not found:', adminProfile.clinicGroupId);
505
- throw AUTH_ERRORS.NOT_FOUND;
506
- }
507
-
508
- return {
509
- user,
510
- clinicAdmin: adminProfile,
511
- clinicGroup,
512
- };
513
- } catch (error) {
514
- console.error('[AUTH] Error in signInClinicAdmin:', error);
515
- throw error;
516
- }
517
- }
518
-
519
- /**
520
- * Prijavljuje korisnika anonimno
521
- */
522
- async signInAnonymously(): Promise<User> {
523
- const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
524
-
525
- return this.userService.getOrCreateUser(firebaseUser);
526
- }
527
-
528
- /**
529
- * Odjavljuje trenutnog korisnika
530
- */
531
- async signOut(): Promise<void> {
532
- await firebaseSignOut(this.auth);
533
- }
534
-
535
- /**
536
- * Vraća trenutno prijavljenog korisnika
537
- */
538
- async getCurrentUser(): Promise<User | null> {
539
- const firebaseUser = this.auth.currentUser;
540
- if (!firebaseUser) return null;
541
-
542
- return this.userService.getUserById(firebaseUser.uid);
543
- }
544
-
545
- /**
546
- * Registruje callback za promene stanja autentifikacije
547
- */
548
- onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
549
- return onAuthStateChanged(this.auth, callback);
550
- }
551
-
552
- async upgradeAnonymousUser(email: string, password: string): Promise<User> {
553
- try {
554
- await emailSchema.parseAsync(email);
555
- await passwordSchema.parseAsync(password);
556
-
557
- const currentUser = this.auth.currentUser;
558
- if (!currentUser) {
559
- throw AUTH_ERRORS.NOT_AUTHENTICATED;
560
- }
561
- if (!currentUser.isAnonymous) {
562
- throw new AuthError('User is not anonymous', 'AUTH/NOT_ANONYMOUS_USER', 400);
563
- }
564
-
565
- const credential = EmailAuthProvider.credential(email, password);
566
- await linkWithCredential(currentUser, credential);
567
-
568
- return await this.userService.upgradeAnonymousUser(currentUser.uid, email);
569
- } catch (error) {
570
- if (error instanceof z.ZodError) {
571
- throw AUTH_ERRORS.VALIDATION_ERROR;
572
- }
573
- const firebaseError = error as FirebaseError;
574
- if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
575
- throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
576
- }
577
- throw error;
578
- }
579
- }
580
-
581
- /**
582
- * Šalje email za resetovanje lozinke korisniku
583
- * @param email Email adresa korisnika
584
- * @returns Promise koji se razrešava kada je email poslat
585
- */
586
- async sendPasswordResetEmail(email: string): Promise<void> {
587
- try {
588
- await emailSchema.parseAsync(email);
589
-
590
- // Šaljemo email za resetovanje lozinke
591
- await sendPasswordResetEmail(this.auth, email);
592
- } catch (error) {
593
- if (error instanceof z.ZodError) {
594
- throw AUTH_ERRORS.VALIDATION_ERROR;
595
- }
596
-
597
- const firebaseError = error as FirebaseError;
598
- if (firebaseError.code === FirebaseErrorCode.USER_NOT_FOUND) {
599
- throw AUTH_ERRORS.USER_NOT_FOUND;
600
- }
601
-
602
- throw error;
603
- }
604
- }
605
-
606
- /**
607
- * Verifikuje kod za resetovanje lozinke iz email linka
608
- * @param oobCode Kod iz URL-a za resetovanje lozinke
609
- * @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
610
- */
611
- async verifyPasswordResetCode(oobCode: string): Promise<string> {
612
- try {
613
- // Verifikujemo kod i vraćamo email korisnika
614
- return await verifyPasswordResetCode(this.auth, oobCode);
615
- } catch (error) {
616
- const firebaseError = error as FirebaseError;
617
- if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
618
- throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
619
- } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
620
- throw AUTH_ERRORS.INVALID_ACTION_CODE;
621
- }
622
-
623
- throw error;
624
- }
625
- }
626
-
627
- /**
628
- * Potvrđuje resetovanje lozinke i postavlja novu lozinku
629
- * @param oobCode Kod iz URL-a za resetovanje lozinke
630
- * @param newPassword Nova lozinka
631
- * @returns Promise koji se razrešava kada je lozinka promenjena
632
- */
633
- async confirmPasswordReset(oobCode: string, newPassword: string): Promise<void> {
634
- try {
635
- await passwordSchema.parseAsync(newPassword);
636
-
637
- // Potvrđujemo resetovanje lozinke i postavljamo novu lozinku
638
- await confirmPasswordReset(this.auth, oobCode, newPassword);
639
- } catch (error) {
640
- if (error instanceof z.ZodError) {
641
- throw AUTH_ERRORS.VALIDATION_ERROR;
642
- }
643
-
644
- const firebaseError = error as FirebaseError;
645
- if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
646
- throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
647
- } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
648
- throw AUTH_ERRORS.INVALID_ACTION_CODE;
649
- } else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
650
- throw AUTH_ERRORS.WEAK_PASSWORD;
651
- }
652
-
653
- throw error;
654
- }
655
- }
656
-
657
- /**
658
- * Registers a new practitioner user with email and password (ATOMIC VERSION)
659
- * Uses Firestore transactions to ensure atomicity and proper rollback on failures
660
- *
661
- * @param data - Practitioner signup data containing either new profile details or token for claiming draft profile
662
- * @returns Object containing the created user and practitioner profile
663
- */
664
- async signUpPractitioner(data: {
665
- email: string;
666
- password: string;
667
- firstName?: string;
668
- lastName?: string;
669
- token?: string;
670
- profileData?: Partial<CreatePractitionerData>;
671
- }): Promise<{
672
- user: User;
673
- practitioner: Practitioner;
674
- }> {
675
- let firebaseUser: any = null;
676
-
677
- try {
678
- console.log('[AUTH] Starting atomic practitioner signup process', {
679
- email: data.email,
680
- hasToken: !!data.token,
681
- });
682
-
683
- // Step 1: Pre-validate all data before any mutations
684
- await this.validateSignupData(data);
685
-
686
- // Step 2: Create Firebase user (outside transaction - can't be easily rolled back)
687
- console.log('[AUTH] Creating Firebase user');
688
- try {
689
- const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
690
- firebaseUser = result.user;
691
- console.log('[AUTH] Firebase user created successfully', {
692
- uid: firebaseUser.uid,
693
- });
694
- } catch (firebaseError) {
695
- console.error('[AUTH] Firebase user creation failed:', firebaseError);
696
- throw handleFirebaseError(firebaseError);
697
- }
698
-
699
- // Step 3: Execute all database operations in a single transaction
700
- console.log('[AUTH] Starting Firestore transaction');
701
- const transactionResult = await runTransaction(this.db, async transaction => {
702
- console.log('[AUTH] Transaction started - creating user and practitioner');
703
-
704
- // Initialize services
705
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
706
-
707
- // Create user document using existing method (not in transaction for now)
708
- console.log('[AUTH] Creating user document');
709
- const user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
710
- skipProfileCreation: true,
711
- });
712
-
713
- let practitioner: Practitioner;
714
-
715
- // Handle practitioner profile creation/claiming
716
- if (data.token) {
717
- console.log('[AUTH] Claiming existing practitioner profile with token');
718
- const claimedPractitioner = await practitionerService.validateTokenAndClaimProfile(
719
- data.token,
720
- firebaseUser.uid,
721
- );
722
- if (!claimedPractitioner) {
723
- throw new Error('Invalid or expired invitation token');
724
- }
725
- practitioner = claimedPractitioner;
726
- } else {
727
- console.log('[AUTH] Creating new practitioner profile');
728
- const practitionerData = buildPractitionerData(data, firebaseUser.uid);
729
- practitioner = await practitionerService.createPractitioner(practitionerData);
730
- }
731
-
732
- // Link practitioner to user
733
- console.log('[AUTH] Linking practitioner to user');
734
- await this.userService.updateUser(firebaseUser.uid, {
735
- practitionerProfile: practitioner.id,
736
- });
737
-
738
- console.log('[AUTH] Transaction operations completed successfully');
739
- return { user, practitioner };
740
- });
741
-
742
- console.log('[AUTH] Atomic practitioner signup completed successfully', {
743
- userId: transactionResult.user.uid,
744
- practitionerId: transactionResult.practitioner.id,
745
- });
746
-
747
- return transactionResult;
748
- } catch (error) {
749
- console.error('[AUTH] Atomic signup failed, initiating cleanup...', error);
750
-
751
- // Cleanup Firebase user if transaction failed
752
- if (firebaseUser) {
753
- await cleanupFirebaseUser(firebaseUser);
754
- }
755
-
756
- throw handleSignupError(error);
757
- }
758
- }
759
-
760
- /**
761
- * Pre-validate all signup data before any mutations
762
- * Prevents partial creation by catching issues early
763
- */
764
- private async validateSignupData(data: {
765
- email: string;
766
- password: string;
767
- firstName?: string;
768
- lastName?: string;
769
- token?: string;
770
- profileData?: Partial<CreatePractitionerData>;
771
- }): Promise<void> {
772
- console.log('[AUTH] Pre-validating signup data');
773
-
774
- try {
775
- // 1. Schema validation
776
- await practitionerSignupSchema.parseAsync(data);
777
- console.log('[AUTH] Schema validation passed');
778
-
779
- // 2. Check if email already exists (before creating Firebase user)
780
- const emailExists = await checkEmailExists(this.auth, data.email);
781
- if (emailExists) {
782
- console.log('[AUTH] Email already exists:', data.email);
783
- throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
784
- }
785
- console.log('[AUTH] Email availability confirmed');
786
-
787
- // 3. Validate token if provided
788
- if (data.token) {
789
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
790
- const isValidToken = await practitionerService.validateToken(data.token);
791
- if (!isValidToken) {
792
- console.log('[AUTH] Invalid token provided:', data.token);
793
- throw new Error('Invalid or expired invitation token');
794
- }
795
- console.log('[AUTH] Token validation passed');
796
- }
797
-
798
- // 4. Validate profile data structure if provided
799
- if (data.profileData) {
800
- await validatePractitionerProfileData(data.profileData);
801
- console.log('[AUTH] Profile data validation passed');
802
- }
803
-
804
- console.log('[AUTH] All pre-validation checks passed');
805
- } catch (error) {
806
- console.error('[AUTH] Pre-validation failed:', error);
807
- throw error;
808
- }
809
- }
810
-
811
- /**
812
- * Signs in a user with email and password specifically for practitioner role
813
- * @param email - User's email
814
- * @param password - User's password
815
- * @returns Object containing the user and practitioner profile
816
- * @throws {AUTH_ERRORS.INVALID_ROLE} If user doesn't have practitioner role
817
- * @throws {AUTH_ERRORS.NOT_FOUND} If practitioner profile is not found
818
- */
819
- async signInPractitioner(
820
- email: string,
821
- password: string,
822
- ): Promise<{
823
- user: User;
824
- practitioner: Practitioner;
825
- }> {
826
- try {
827
- console.log('[AUTH] Starting practitioner signin process', {
828
- email: email,
829
- });
830
-
831
- // Initialize required service
832
- const practitionerService = new PractitionerService(this.db, this.auth, this.app);
833
-
834
- // Sign in with email/password
835
- const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
836
-
837
- // Get or create user
838
- const user = await this.userService.getOrCreateUser(firebaseUser);
839
- console.log('[AUTH] User retrieved', { uid: user.uid });
840
-
841
- // Check if user has practitioner role
842
- if (!user.roles?.includes(UserRole.PRACTITIONER)) {
843
- console.error('[AUTH] User is not a practitioner:', user.uid);
844
- throw AUTH_ERRORS.INVALID_ROLE;
845
- }
846
-
847
- // Check and get practitioner profile
848
- if (!user.practitionerProfile) {
849
- console.error('[AUTH] User has no practitioner profile:', user.uid);
850
- throw AUTH_ERRORS.NOT_FOUND;
851
- }
852
-
853
- // Get practitioner profile
854
- const practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
855
- if (!practitioner) {
856
- console.error('[AUTH] Practitioner profile not found:', user.practitionerProfile);
857
- throw AUTH_ERRORS.NOT_FOUND;
858
- }
859
-
860
- console.log('[AUTH] Practitioner signin completed successfully', {
861
- userId: user.uid,
862
- practitionerId: practitioner.id,
863
- });
864
-
865
- return {
866
- user,
867
- practitioner,
868
- };
869
- } catch (error) {
870
- console.error('[AUTH] Error in signInPractitioner:', error);
871
- throw error;
872
- }
873
- }
874
-
875
- /**
876
- * Signs in a user with a Google ID token from a mobile client.
877
- * If the user does not exist, a new user is created.
878
- * @param idToken - The Google ID token obtained from the mobile app.
879
- * @param initialRole - The role to assign to the user if they are being created.
880
- * @returns The signed-in or newly created user.
881
- */
882
- async signInWithGoogleIdToken(
883
- idToken: string,
884
- initialRole: UserRole = UserRole.PATIENT,
885
- ): Promise<User> {
886
- try {
887
- console.log('[AUTH] Signing in with Google ID Token');
888
-
889
- // 1) Extract the email claim from the raw JWT so we can check Auth records *before* sign-in
890
- let email: string | undefined;
891
- try {
892
- const payloadBase64 = idToken.split('.')[1];
893
- const payloadJson = globalThis.atob
894
- ? globalThis.atob(payloadBase64)
895
- : Buffer.from(payloadBase64, 'base64').toString('utf8');
896
- const payload = JSON.parse(payloadJson);
897
- email = payload.email as string | undefined;
898
- } catch (decodeError) {
899
- console.warn('[AUTH] Failed to decode email from Google ID token:', decodeError);
900
- }
901
-
902
- if (!email) {
903
- throw new AuthError(
904
- 'Unable to read email from Google token. Please try again or use another sign-in method.',
905
- 'AUTH/INVALID_GOOGLE_TOKEN',
906
- 400,
907
- );
908
- }
909
-
910
- // 2) Check if this email already has a Google credential in Firebase Auth.
911
- // If not, abort early so we *never* create an unwanted Auth user.
912
- const methods = await fetchSignInMethodsForEmail(this.auth, email);
913
- console.log('[AUTH] Fetch sign in methods for email:', email, methods);
914
- const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
915
- console.log('[AUTH] Has Google method:', hasGoogleMethod);
916
- if (!hasGoogleMethod) {
917
- console.log('[AUTH] No existing Google credential for email, aborting login:', email);
918
- throw new AuthError(
919
- 'No account found for this Google user. Please complete registration first.',
920
- 'AUTH/USER_NOT_FOUND',
921
- 404,
922
- );
923
- }
924
-
925
- // 3) Safe to sign-in – we know the credential belongs to an existing Auth account.
926
- const credential = GoogleAuthProvider.credential(idToken);
927
- const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
928
- console.log('[AUTH] Firebase user signed in:', firebaseUser.uid);
929
-
930
- // 4) Load our domain user document.
931
- const existingUser = await this.userService.getUserById(firebaseUser.uid);
932
- if (existingUser) {
933
- console.log('[AUTH] Existing user found, returning profile:', existingUser.uid);
934
- return existingUser;
935
- }
936
-
937
- // 5) If no profile exists we sign out immediately and error – but crucially no phantom user was created.
938
- console.log('[AUTH] No existing MetaEstetics user for Google account – signing out.');
939
- await firebaseSignOut(this.auth);
940
- throw new AuthError(
941
- 'No account found. Please complete registration by starting with "Get Started".',
942
- 'AUTH/USER_NOT_FOUND',
943
- 404,
944
- );
945
- } catch (error) {
946
- console.error('[AUTH] Error in signInWithGoogleIdToken:', error);
947
- throw handleFirebaseError(error);
948
- }
949
- }
950
-
951
- /**
952
- * Links a Google account to the currently signed-in user using an ID token.
953
- * This is used to upgrade an anonymous user or to allow an existing user
954
- * to sign in with Google in the future.
955
- * @param idToken - The Google ID token obtained from the mobile app.
956
- * @returns The updated user profile.
957
- */
958
- async linkGoogleAccount(idToken: string): Promise<User> {
959
- try {
960
- console.log('[AUTH] Linking Google account with ID Token');
961
- const currentUser = this.auth.currentUser;
962
- if (!currentUser) {
963
- throw AUTH_ERRORS.NOT_AUTHENTICATED;
964
- }
965
-
966
- const wasAnonymous = currentUser.isAnonymous;
967
- console.log(`[AUTH] Current user is ${wasAnonymous ? 'anonymous' : 'not anonymous'}`);
968
-
969
- const credential = GoogleAuthProvider.credential(idToken);
970
- const userCredential = await linkWithCredential(currentUser, credential);
971
- const linkedFirebaseUser = userCredential.user;
972
- console.log('[AUTH] Google account linked successfully to user:', linkedFirebaseUser.uid);
973
-
974
- if (wasAnonymous) {
975
- console.log('[AUTH] Upgrading anonymous user profile');
976
- return await this.userService.upgradeAnonymousUser(
977
- linkedFirebaseUser.uid,
978
- linkedFirebaseUser.email!,
979
- );
980
- }
981
-
982
- // If the user was not anonymous, just return their updated profile
983
- return (await this.userService.getUserById(linkedFirebaseUser.uid))!;
984
- } catch (error) {
985
- console.error('[AUTH] Error in linkGoogleAccount:', error);
986
- throw handleFirebaseError(error);
987
- }
988
- }
989
- }
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
+ } from 'firebase/auth';
21
+ import {
22
+ getFirestore,
23
+ collection,
24
+ doc,
25
+ getDoc,
26
+ setDoc,
27
+ updateDoc,
28
+ deleteDoc,
29
+ query,
30
+ where,
31
+ getDocs,
32
+ orderBy,
33
+ limit,
34
+ startAfter,
35
+ Timestamp,
36
+ runTransaction,
37
+ Firestore,
38
+ } from 'firebase/firestore';
39
+ import { FirebaseApp } from 'firebase/app';
40
+ import { User, UserRole, USERS_COLLECTION } from '../../types';
41
+ import { z } from 'zod';
42
+ import { emailSchema, passwordSchema, userRoleSchema } from '../../validations/schemas';
43
+ import { AuthError, AUTH_ERRORS } from '../../errors/auth.errors';
44
+ import { FirebaseErrorCode } from '../../errors/firebase.errors';
45
+ import { FirebaseError } from '../../errors/firebase.errors';
46
+ import { BaseService } from '../base.service';
47
+ import { UserService } from '../user/user.service';
48
+ import { throws } from 'assert';
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
+ * Registruje novog korisnika sa email-om i lozinkom
97
+ */
98
+ async signUp(
99
+ email: string,
100
+ password: string,
101
+ initialRole: UserRole = UserRole.PATIENT,
102
+ options?: {
103
+ patientInviteToken?: string;
104
+ },
105
+ ): Promise<User> {
106
+ const { user: firebaseUser } = await createUserWithEmailAndPassword(this.auth, email, password);
107
+
108
+ return this.userService.createUser(firebaseUser, [initialRole], options);
109
+ }
110
+
111
+ /**
112
+ * Registers a new clinic admin user with email and password
113
+ * Can either create a new clinic group or join an existing one with a token
114
+ *
115
+ * @param data - Clinic admin signup data
116
+ * @returns Object containing the created user, clinic group, and clinic admin
117
+ */
118
+ async signUpClinicAdmin(data: ClinicAdminSignupData): Promise<{
119
+ user: User;
120
+ clinicGroup: ClinicGroup;
121
+ clinicAdmin: ClinicAdmin;
122
+ }> {
123
+ try {
124
+ console.log('[AUTH] Starting clinic admin signup process', {
125
+ email: data.email,
126
+ });
127
+
128
+ // Validate data
129
+ try {
130
+ await clinicAdminSignupSchema.parseAsync(data);
131
+ console.log('[AUTH] Clinic admin signup data validation passed');
132
+ } catch (validationError) {
133
+ console.error('[AUTH] Validation error in signUpClinicAdmin:', validationError);
134
+ throw validationError;
135
+ }
136
+
137
+ // Create Firebase user
138
+ console.log('[AUTH] Creating Firebase user');
139
+ let firebaseUser;
140
+ try {
141
+ const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
142
+ firebaseUser = result.user;
143
+ console.log('[AUTH] Firebase user created successfully', {
144
+ uid: firebaseUser.uid,
145
+ });
146
+ } catch (firebaseError) {
147
+ console.error('[AUTH] Firebase user creation failed:', firebaseError);
148
+ throw handleFirebaseError(firebaseError);
149
+ }
150
+
151
+ // Create user with CLINIC_ADMIN role
152
+ console.log('[AUTH] Creating user with CLINIC_ADMIN role');
153
+ let user;
154
+ try {
155
+ user = await this.userService.createUser(firebaseUser, [UserRole.CLINIC_ADMIN], {
156
+ skipProfileCreation: true,
157
+ });
158
+ console.log('[AUTH] User with CLINIC_ADMIN role created successfully', {
159
+ userId: user.uid,
160
+ });
161
+ } catch (userCreationError) {
162
+ console.error('[AUTH] User creation failed:', userCreationError);
163
+ throw userCreationError;
164
+ }
165
+
166
+ // Create contact person object
167
+ const contactPerson: ContactPerson = {
168
+ firstName: data.firstName,
169
+ lastName: data.lastName,
170
+ title: data.title,
171
+ email: data.email,
172
+ phoneNumber: data.phoneNumber,
173
+ };
174
+ console.log('[AUTH] Contact person object created');
175
+
176
+ // Initialize services
177
+ console.log('[AUTH] Initializing clinic services');
178
+ const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
179
+ const clinicGroupService = new ClinicGroupService(
180
+ this.db,
181
+ this.auth,
182
+ this.app,
183
+ clinicAdminService,
184
+ );
185
+ const mediaService = new MediaService(this.db, this.auth, this.app);
186
+ const clinicService = new ClinicService(
187
+ this.db,
188
+ this.auth,
189
+ this.app,
190
+ clinicGroupService,
191
+ clinicAdminService,
192
+ mediaService,
193
+ );
194
+
195
+ // Set services to resolve circular dependencies
196
+ clinicAdminService.setServices(clinicGroupService, clinicService);
197
+ console.log('[AUTH] Services initialized and circular dependencies resolved');
198
+
199
+ let clinicGroup: ClinicGroup | null = null;
200
+ let adminProfile: ClinicAdmin | null = null;
201
+
202
+ if (data.isCreatingNewGroup) {
203
+ console.log('[AUTH] Creating new clinic group flow');
204
+ // Create new clinic group
205
+ if (!data.clinicGroupData) {
206
+ console.error('[AUTH] Clinic group data is missing');
207
+ throw new Error('Clinic group data is required when creating a new group');
208
+ }
209
+
210
+ // First create the clinic admin without a group
211
+ console.log('[AUTH] Creating clinic admin first (without group)');
212
+ const createClinicAdminData: CreateClinicAdminData = {
213
+ userRef: firebaseUser.uid,
214
+ isGroupOwner: true,
215
+ clinicsManaged: [],
216
+ contactInfo: contactPerson,
217
+ roleTitle: data.title,
218
+ isActive: true,
219
+ // No clinicGroupId yet
220
+ };
221
+
222
+ try {
223
+ adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
224
+ console.log('[AUTH] Clinic admin created successfully', {
225
+ adminId: adminProfile.id,
226
+ });
227
+ } catch (adminCreationError) {
228
+ console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
229
+ throw adminCreationError;
230
+ }
231
+
232
+ // Update user document with admin profile reference
233
+ try {
234
+ console.log('[AUTH] Updating user with admin profile reference');
235
+ user = await this.userService.updateUser(firebaseUser.uid, {
236
+ adminProfile: adminProfile.id,
237
+ });
238
+ console.log('[AUTH] User updated with admin profile reference successfully');
239
+ } catch (userUpdateError) {
240
+ console.error('[AUTH] Failed to update user with admin profile:', userUpdateError);
241
+ throw userUpdateError;
242
+ }
243
+
244
+ // Then create clinic group
245
+ const createClinicGroupData: CreateClinicGroupData = {
246
+ name: data.clinicGroupData.name,
247
+ hqLocation: data.clinicGroupData.hqLocation,
248
+ contactInfo: data.clinicGroupData.contactInfo,
249
+ contactPerson: contactPerson,
250
+ ownerId: adminProfile.id, // Use admin profile ID, not user UID
251
+ isActive: true,
252
+ logo: data.clinicGroupData.logo || null,
253
+ subscriptionModel:
254
+ data.clinicGroupData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
255
+ onboarding: {
256
+ completed: false,
257
+ step: 1,
258
+ },
259
+ };
260
+ console.log('[AUTH] Clinic group data prepared', {
261
+ groupName: createClinicGroupData.name,
262
+ });
263
+
264
+ // Create clinic group
265
+ try {
266
+ clinicGroup = await clinicGroupService.createClinicGroup(
267
+ createClinicGroupData,
268
+ adminProfile.id, // Use admin profile ID, not user UID
269
+ false, // This is not a default group since we're providing complete data
270
+ );
271
+ console.log('[AUTH] Clinic group created successfully', {
272
+ groupId: clinicGroup.id,
273
+ });
274
+
275
+ // Now update the admin with the group ID
276
+ console.log('[AUTH] Updating admin with clinic group ID');
277
+ await clinicAdminService.updateClinicAdmin(adminProfile.id, {
278
+ // Use admin profile ID, not user UID
279
+ clinicGroupId: clinicGroup.id,
280
+ });
281
+ console.log('[AUTH] Admin updated with clinic group ID successfully');
282
+
283
+ // Get the updated admin profile
284
+ adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
285
+ } catch (groupCreationError) {
286
+ console.error('[AUTH] Clinic group creation failed:', groupCreationError);
287
+ throw groupCreationError;
288
+ }
289
+ } else {
290
+ console.log('[AUTH] Joining existing clinic group flow');
291
+ // Join existing clinic group with token
292
+ if (!data.inviteToken) {
293
+ console.error('[AUTH] Invite token is missing');
294
+ throw new Error('Invite token is required when joining an existing group');
295
+ }
296
+ console.log('[AUTH] Invite token provided', {
297
+ token: data.inviteToken,
298
+ });
299
+
300
+ // Find the token in the database
301
+ console.log('[AUTH] Searching for token in clinic groups');
302
+ const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
303
+ const q = query(groupsRef);
304
+ const querySnapshot = await getDocs(q);
305
+
306
+ let foundGroup: ClinicGroup | null = null;
307
+ let foundToken: AdminToken | null = null;
308
+
309
+ console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
310
+ for (const docSnapshot of querySnapshot.docs) {
311
+ const group = docSnapshot.data() as ClinicGroup;
312
+ console.log('[AUTH] Checking group', {
313
+ groupId: group.id,
314
+ groupName: group.name,
315
+ });
316
+
317
+ // Find the token in the group's tokens
318
+ const token = group.adminTokens.find(t => {
319
+ const isMatch =
320
+ t.token === data.inviteToken &&
321
+ t.status === AdminTokenStatus.ACTIVE &&
322
+ new Date(t.expiresAt.toDate()) > new Date();
323
+
324
+ console.log('[AUTH] Checking token', {
325
+ tokenId: t.id,
326
+ tokenMatch: t.token === data.inviteToken,
327
+ tokenStatus: t.status,
328
+ tokenActive: t.status === AdminTokenStatus.ACTIVE,
329
+ tokenExpiry: new Date(t.expiresAt.toDate()),
330
+ tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
331
+ isMatch,
332
+ });
333
+
334
+ return isMatch;
335
+ });
336
+
337
+ if (token) {
338
+ foundGroup = group;
339
+ foundToken = token;
340
+ console.log('[AUTH] Found matching token in group', {
341
+ groupId: group.id,
342
+ tokenId: token.id,
343
+ });
344
+ break;
345
+ }
346
+ }
347
+
348
+ if (!foundGroup || !foundToken) {
349
+ console.error('[AUTH] No valid token found in any clinic group');
350
+ throw new Error('Invalid or expired invite token');
351
+ }
352
+
353
+ clinicGroup = foundGroup;
354
+
355
+ // Create clinic admin
356
+ console.log('[AUTH] Creating clinic admin');
357
+ const createClinicAdminData: CreateClinicAdminData = {
358
+ userRef: firebaseUser.uid,
359
+ clinicGroupId: foundGroup.id,
360
+ isGroupOwner: false,
361
+ clinicsManaged: [],
362
+ contactInfo: contactPerson,
363
+ roleTitle: data.title,
364
+ isActive: true,
365
+ };
366
+
367
+ try {
368
+ adminProfile = await clinicAdminService.createClinicAdmin(createClinicAdminData);
369
+ console.log('[AUTH] Clinic admin created successfully', {
370
+ adminId: adminProfile.id,
371
+ });
372
+ } catch (adminCreationError) {
373
+ console.error('[AUTH] Clinic admin creation failed:', adminCreationError);
374
+ throw adminCreationError;
375
+ }
376
+
377
+ // Mark token as used
378
+ try {
379
+ await clinicGroupService.verifyAndUseAdminToken(
380
+ foundGroup.id,
381
+ data.inviteToken,
382
+ firebaseUser.uid,
383
+ );
384
+ console.log('[AUTH] Token marked as used successfully');
385
+ } catch (tokenUseError) {
386
+ console.error('[AUTH] Failed to mark token as used:', tokenUseError);
387
+ throw tokenUseError;
388
+ }
389
+ }
390
+
391
+ console.log('[AUTH] Clinic admin signup completed successfully', {
392
+ userId: user.uid,
393
+ clinicGroupId: clinicGroup.id,
394
+ clinicAdminId: adminProfile?.id || 'unknown',
395
+ });
396
+
397
+ // Ensure we have all required data before returning
398
+ if (!clinicGroup || !adminProfile) {
399
+ throw new Error('Failed to create or retrieve clinic group or admin profile');
400
+ }
401
+
402
+ return {
403
+ user,
404
+ clinicGroup,
405
+ clinicAdmin: adminProfile,
406
+ };
407
+ } catch (error) {
408
+ if (error instanceof z.ZodError) {
409
+ console.error(
410
+ '[AUTH] Zod validation error in signUpClinicAdmin:',
411
+ JSON.stringify(error.errors, null, 2),
412
+ );
413
+ throw AUTH_ERRORS.VALIDATION_ERROR;
414
+ }
415
+
416
+ const firebaseError = error as FirebaseError;
417
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
418
+ console.error('[AUTH] Email already in use:', data.email);
419
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
420
+ }
421
+
422
+ console.error('[AUTH] Unhandled error in signUpClinicAdmin:', error);
423
+ throw error;
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Prijavljuje korisnika sa email-om i lozinkom
429
+ */
430
+ async signIn(email: string, password: string): Promise<User> {
431
+ const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
432
+
433
+ return this.userService.getOrCreateUser(firebaseUser);
434
+ }
435
+
436
+ /**
437
+ * Prijavljuje korisnika sa email-om i lozinkom samo za clinic_admin role
438
+ * @param email - Email korisnika
439
+ * @param password - Lozinka korisnika
440
+ * @returns Objekat koji sadrži korisnika, admin profil i grupu klinika
441
+ * @throws {AUTH_ERRORS.INVALID_ROLE} Ako korisnik nema clinic_admin rolu
442
+ * @throws {AUTH_ERRORS.NOT_FOUND} Ako admin profil nije pronađen
443
+ */
444
+ async signInClinicAdmin(
445
+ email: string,
446
+ password: string,
447
+ ): Promise<{
448
+ user: User;
449
+ clinicAdmin: ClinicAdmin;
450
+ clinicGroup: ClinicGroup;
451
+ }> {
452
+ try {
453
+ // Initialize required services
454
+ const clinicAdminService = new ClinicAdminService(this.db, this.auth, this.app);
455
+ const clinicGroupService = new ClinicGroupService(
456
+ this.db,
457
+ this.auth,
458
+ this.app,
459
+ clinicAdminService,
460
+ );
461
+ const mediaService = new MediaService(this.db, this.auth, this.app);
462
+ const clinicService = new ClinicService(
463
+ this.db,
464
+ this.auth,
465
+ this.app,
466
+ clinicGroupService,
467
+ clinicAdminService,
468
+ mediaService,
469
+ );
470
+
471
+ // Set services to resolve circular dependencies
472
+ clinicAdminService.setServices(clinicGroupService, clinicService);
473
+
474
+ // Sign in with email/password
475
+ const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
476
+
477
+ // Get or create user
478
+ const user = await this.userService.getOrCreateUser(firebaseUser);
479
+
480
+ // Check if user has clinic_admin role
481
+ if (!user.roles?.includes(UserRole.CLINIC_ADMIN)) {
482
+ console.error('[AUTH] User is not a clinic admin:', user.uid);
483
+ // Sign out the user immediately for security
484
+ await this.auth.signOut();
485
+ throw AUTH_ERRORS.UNAUTHORIZED_ROLE;
486
+ }
487
+
488
+ // Check and get admin profile
489
+ if (!user.adminProfile) {
490
+ console.error('[AUTH] User has no admin profile:', user.uid);
491
+ throw AUTH_ERRORS.NOT_FOUND;
492
+ }
493
+
494
+ // Get clinic admin profile
495
+ const adminProfile = await clinicAdminService.getClinicAdmin(user.adminProfile);
496
+ if (!adminProfile) {
497
+ console.error('[AUTH] Admin profile not found:', user.adminProfile);
498
+ throw AUTH_ERRORS.NOT_FOUND;
499
+ }
500
+
501
+ // Get clinic group
502
+ const clinicGroup = await clinicGroupService.getClinicGroup(adminProfile.clinicGroupId);
503
+ if (!clinicGroup) {
504
+ console.error('[AUTH] Clinic group not found:', adminProfile.clinicGroupId);
505
+ throw AUTH_ERRORS.NOT_FOUND;
506
+ }
507
+
508
+ return {
509
+ user,
510
+ clinicAdmin: adminProfile,
511
+ clinicGroup,
512
+ };
513
+ } catch (error) {
514
+ console.error('[AUTH] Error in signInClinicAdmin:', error);
515
+ throw error;
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Prijavljuje korisnika anonimno
521
+ */
522
+ async signInAnonymously(): Promise<User> {
523
+ const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
524
+
525
+ return this.userService.getOrCreateUser(firebaseUser);
526
+ }
527
+
528
+ /**
529
+ * Odjavljuje trenutnog korisnika
530
+ */
531
+ async signOut(): Promise<void> {
532
+ await firebaseSignOut(this.auth);
533
+ }
534
+
535
+ /**
536
+ * Vraća trenutno prijavljenog korisnika
537
+ */
538
+ async getCurrentUser(): Promise<User | null> {
539
+ const firebaseUser = this.auth.currentUser;
540
+ if (!firebaseUser) return null;
541
+
542
+ return this.userService.getUserById(firebaseUser.uid);
543
+ }
544
+
545
+ /**
546
+ * Registruje callback za promene stanja autentifikacije
547
+ */
548
+ onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
549
+ return onAuthStateChanged(this.auth, callback);
550
+ }
551
+
552
+ async upgradeAnonymousUser(email: string, password: string): Promise<User> {
553
+ try {
554
+ await emailSchema.parseAsync(email);
555
+ await passwordSchema.parseAsync(password);
556
+
557
+ const currentUser = this.auth.currentUser;
558
+ if (!currentUser) {
559
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
560
+ }
561
+ if (!currentUser.isAnonymous) {
562
+ throw new AuthError('User is not anonymous', 'AUTH/NOT_ANONYMOUS_USER', 400);
563
+ }
564
+
565
+ const credential = EmailAuthProvider.credential(email, password);
566
+ await linkWithCredential(currentUser, credential);
567
+
568
+ return await this.userService.upgradeAnonymousUser(currentUser.uid, email);
569
+ } catch (error) {
570
+ if (error instanceof z.ZodError) {
571
+ throw AUTH_ERRORS.VALIDATION_ERROR;
572
+ }
573
+ const firebaseError = error as FirebaseError;
574
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
575
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
576
+ }
577
+ throw error;
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Šalje email za resetovanje lozinke korisniku
583
+ * @param email Email adresa korisnika
584
+ * @returns Promise koji se razrešava kada je email poslat
585
+ */
586
+ async sendPasswordResetEmail(email: string): Promise<void> {
587
+ try {
588
+ await emailSchema.parseAsync(email);
589
+
590
+ // Šaljemo email za resetovanje lozinke
591
+ await sendPasswordResetEmail(this.auth, email);
592
+ } catch (error) {
593
+ if (error instanceof z.ZodError) {
594
+ throw AUTH_ERRORS.VALIDATION_ERROR;
595
+ }
596
+
597
+ const firebaseError = error as FirebaseError;
598
+ if (firebaseError.code === FirebaseErrorCode.USER_NOT_FOUND) {
599
+ throw AUTH_ERRORS.USER_NOT_FOUND;
600
+ }
601
+
602
+ throw error;
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Verifikuje kod za resetovanje lozinke iz email linka
608
+ * @param oobCode Kod iz URL-a za resetovanje lozinke
609
+ * @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
610
+ */
611
+ async verifyPasswordResetCode(oobCode: string): Promise<string> {
612
+ try {
613
+ // Verifikujemo kod i vraćamo email korisnika
614
+ return await verifyPasswordResetCode(this.auth, oobCode);
615
+ } catch (error) {
616
+ const firebaseError = error as FirebaseError;
617
+ if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
618
+ throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
619
+ } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
620
+ throw AUTH_ERRORS.INVALID_ACTION_CODE;
621
+ }
622
+
623
+ throw error;
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Potvrđuje resetovanje lozinke i postavlja novu lozinku
629
+ * @param oobCode Kod iz URL-a za resetovanje lozinke
630
+ * @param newPassword Nova lozinka
631
+ * @returns Promise koji se razrešava kada je lozinka promenjena
632
+ */
633
+ async confirmPasswordReset(oobCode: string, newPassword: string): Promise<void> {
634
+ try {
635
+ await passwordSchema.parseAsync(newPassword);
636
+
637
+ // Potvrđujemo resetovanje lozinke i postavljamo novu lozinku
638
+ await confirmPasswordReset(this.auth, oobCode, newPassword);
639
+ } catch (error) {
640
+ if (error instanceof z.ZodError) {
641
+ throw AUTH_ERRORS.VALIDATION_ERROR;
642
+ }
643
+
644
+ const firebaseError = error as FirebaseError;
645
+ if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
646
+ throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
647
+ } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
648
+ throw AUTH_ERRORS.INVALID_ACTION_CODE;
649
+ } else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
650
+ throw AUTH_ERRORS.WEAK_PASSWORD;
651
+ }
652
+
653
+ throw error;
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Registers a new practitioner user with email and password (ATOMIC VERSION)
659
+ * Uses Firestore transactions to ensure atomicity and proper rollback on failures
660
+ *
661
+ * @param data - Practitioner signup data containing either new profile details or token for claiming draft profile
662
+ * @returns Object containing the created user and practitioner profile
663
+ */
664
+ async signUpPractitioner(data: {
665
+ email: string;
666
+ password: string;
667
+ firstName?: string;
668
+ lastName?: string;
669
+ token?: string;
670
+ profileData?: Partial<CreatePractitionerData>;
671
+ }): Promise<{
672
+ user: User;
673
+ practitioner: Practitioner;
674
+ }> {
675
+ let firebaseUser: any = null;
676
+
677
+ try {
678
+ console.log('[AUTH] Starting atomic practitioner signup process', {
679
+ email: data.email,
680
+ hasToken: !!data.token,
681
+ });
682
+
683
+ // Step 1: Pre-validate all data before any mutations
684
+ await this.validateSignupData(data);
685
+
686
+ // Step 2: Create Firebase user (outside transaction - can't be easily rolled back)
687
+ console.log('[AUTH] Creating Firebase user');
688
+ try {
689
+ const result = await createUserWithEmailAndPassword(this.auth, data.email, data.password);
690
+ firebaseUser = result.user;
691
+ console.log('[AUTH] Firebase user created successfully', {
692
+ uid: firebaseUser.uid,
693
+ });
694
+ } catch (firebaseError) {
695
+ console.error('[AUTH] Firebase user creation failed:', firebaseError);
696
+ throw handleFirebaseError(firebaseError);
697
+ }
698
+
699
+ // Step 3: Execute all database operations in a single transaction
700
+ console.log('[AUTH] Starting Firestore transaction');
701
+ const transactionResult = await runTransaction(this.db, async transaction => {
702
+ console.log('[AUTH] Transaction started - creating user and practitioner');
703
+
704
+ // Initialize services
705
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
706
+
707
+ // Create user document using existing method (not in transaction for now)
708
+ console.log('[AUTH] Creating user document');
709
+ const user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
710
+ skipProfileCreation: true,
711
+ });
712
+
713
+ let practitioner: Practitioner;
714
+
715
+ // Handle practitioner profile creation/claiming
716
+ if (data.token) {
717
+ console.log('[AUTH] Claiming existing practitioner profile with token');
718
+ const claimedPractitioner = await practitionerService.validateTokenAndClaimProfile(
719
+ data.token,
720
+ firebaseUser.uid,
721
+ );
722
+ if (!claimedPractitioner) {
723
+ throw new Error('Invalid or expired invitation token');
724
+ }
725
+ practitioner = claimedPractitioner;
726
+ } else {
727
+ // Check if a draft profile exists for this email
728
+ console.log('[AUTH] Checking for existing draft practitioner profile', {
729
+ email: data.email,
730
+ });
731
+ const draftPractitioner = await practitionerService.findDraftPractitionerByEmail(
732
+ data.email
733
+ );
734
+
735
+ if (draftPractitioner) {
736
+ console.log('[AUTH] Draft practitioner profile found', {
737
+ practitionerId: draftPractitioner.id,
738
+ email: data.email,
739
+ clinics: draftPractitioner.clinics,
740
+ });
741
+
742
+ // Extract clinic names from clinicsInfo (should be populated when draft is created)
743
+ let clinicNames: string[] = [];
744
+ if (draftPractitioner.clinicsInfo && draftPractitioner.clinicsInfo.length > 0) {
745
+ clinicNames = draftPractitioner.clinicsInfo
746
+ .map((clinic) => clinic.name)
747
+ .filter((name): name is string => !!name);
748
+ } else if (draftPractitioner.clinics && draftPractitioner.clinics.length > 0) {
749
+ // Fallback: fetch clinic names if clinicsInfo is missing
750
+ console.log('[AUTH] clinicsInfo missing, fetching clinic names from clinic IDs');
751
+ const clinicService = practitionerService.getClinicService();
752
+ const clinicNamePromises = draftPractitioner.clinics.map(async (clinicId) => {
753
+ try {
754
+ const clinic = await clinicService.getClinic(clinicId);
755
+ return clinic?.name || null;
756
+ } catch (error) {
757
+ console.error(`[AUTH] Error fetching clinic ${clinicId}:`, error);
758
+ return null;
759
+ }
760
+ });
761
+ const names = await Promise.all(clinicNamePromises);
762
+ clinicNames = names.filter((name): name is string => !!name);
763
+ }
764
+
765
+ // Cleanup Firebase user since we're not creating a profile
766
+ await cleanupFirebaseUser(firebaseUser);
767
+
768
+ // Throw error with clinic information
769
+ throw new AuthError(
770
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.message,
771
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.code,
772
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.status,
773
+ {
774
+ clinicNames,
775
+ clinics: draftPractitioner.clinics,
776
+ clinicsInfo: draftPractitioner.clinicsInfo,
777
+ }
778
+ );
779
+ }
780
+
781
+ console.log('[AUTH] No draft profile found, creating new practitioner profile');
782
+ const practitionerData = buildPractitionerData(data, firebaseUser.uid);
783
+ practitioner = await practitionerService.createPractitioner(practitionerData);
784
+ }
785
+
786
+ // Link practitioner to user
787
+ console.log('[AUTH] Linking practitioner to user');
788
+ await this.userService.updateUser(firebaseUser.uid, {
789
+ practitionerProfile: practitioner.id,
790
+ });
791
+
792
+ console.log('[AUTH] Transaction operations completed successfully');
793
+ return { user, practitioner };
794
+ });
795
+
796
+ console.log('[AUTH] Atomic practitioner signup completed successfully', {
797
+ userId: transactionResult.user.uid,
798
+ practitionerId: transactionResult.practitioner.id,
799
+ });
800
+
801
+ return transactionResult;
802
+ } catch (error) {
803
+ console.error('[AUTH] Atomic signup failed, initiating cleanup...', error);
804
+
805
+ // Cleanup Firebase user if transaction failed
806
+ if (firebaseUser) {
807
+ await cleanupFirebaseUser(firebaseUser);
808
+ }
809
+
810
+ throw handleSignupError(error);
811
+ }
812
+ }
813
+
814
+ /**
815
+ * Pre-validate all signup data before any mutations
816
+ * Prevents partial creation by catching issues early
817
+ */
818
+ private async validateSignupData(data: {
819
+ email: string;
820
+ password: string;
821
+ firstName?: string;
822
+ lastName?: string;
823
+ token?: string;
824
+ profileData?: Partial<CreatePractitionerData>;
825
+ }): Promise<void> {
826
+ console.log('[AUTH] Pre-validating signup data');
827
+
828
+ try {
829
+ // 1. Schema validation
830
+ await practitionerSignupSchema.parseAsync(data);
831
+ console.log('[AUTH] Schema validation passed');
832
+
833
+ // 2. Check if email already exists (before creating Firebase user)
834
+ const emailExists = await checkEmailExists(this.auth, data.email);
835
+ if (emailExists) {
836
+ console.log('[AUTH] Email already exists:', data.email);
837
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
838
+ }
839
+ console.log('[AUTH] Email availability confirmed');
840
+
841
+ // 3. Validate token if provided
842
+ if (data.token) {
843
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
844
+ const isValidToken = await practitionerService.validateToken(data.token);
845
+ if (!isValidToken) {
846
+ console.log('[AUTH] Invalid token provided:', data.token);
847
+ throw new Error('Invalid or expired invitation token');
848
+ }
849
+ console.log('[AUTH] Token validation passed');
850
+ }
851
+
852
+ // 4. Validate profile data structure if provided
853
+ if (data.profileData) {
854
+ await validatePractitionerProfileData(data.profileData);
855
+ console.log('[AUTH] Profile data validation passed');
856
+ }
857
+
858
+ console.log('[AUTH] All pre-validation checks passed');
859
+ } catch (error) {
860
+ console.error('[AUTH] Pre-validation failed:', error);
861
+ throw error;
862
+ }
863
+ }
864
+
865
+ /**
866
+ * Signs in a user with email and password specifically for practitioner role
867
+ * @param email - User's email
868
+ * @param password - User's password
869
+ * @returns Object containing the user and practitioner profile
870
+ * @throws {AUTH_ERRORS.INVALID_ROLE} If user doesn't have practitioner role
871
+ * @throws {AUTH_ERRORS.NOT_FOUND} If practitioner profile is not found
872
+ */
873
+ async signInPractitioner(
874
+ email: string,
875
+ password: string,
876
+ ): Promise<{
877
+ user: User;
878
+ practitioner: Practitioner;
879
+ }> {
880
+ try {
881
+ console.log('[AUTH] Starting practitioner signin process', {
882
+ email: email,
883
+ });
884
+
885
+ // Initialize required service
886
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
887
+
888
+ // Sign in with email/password
889
+ const { user: firebaseUser } = await signInWithEmailAndPassword(this.auth, email, password);
890
+
891
+ // Get or create user
892
+ const user = await this.userService.getOrCreateUser(firebaseUser);
893
+ console.log('[AUTH] User retrieved', { uid: user.uid });
894
+
895
+ // Check if user has practitioner role
896
+ if (!user.roles?.includes(UserRole.PRACTITIONER)) {
897
+ console.error('[AUTH] User is not a practitioner:', user.uid);
898
+ throw AUTH_ERRORS.INVALID_ROLE;
899
+ }
900
+
901
+ // Check and get practitioner profile
902
+ if (!user.practitionerProfile) {
903
+ console.error('[AUTH] User has no practitioner profile:', user.uid);
904
+ throw AUTH_ERRORS.NOT_FOUND;
905
+ }
906
+
907
+ // Get practitioner profile
908
+ const practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
909
+ if (!practitioner) {
910
+ console.error('[AUTH] Practitioner profile not found:', user.practitionerProfile);
911
+ throw AUTH_ERRORS.NOT_FOUND;
912
+ }
913
+
914
+ console.log('[AUTH] Practitioner signin completed successfully', {
915
+ userId: user.uid,
916
+ practitionerId: practitioner.id,
917
+ });
918
+
919
+ return {
920
+ user,
921
+ practitioner,
922
+ };
923
+ } catch (error) {
924
+ console.error('[AUTH] Error in signInPractitioner:', error);
925
+ throw error;
926
+ }
927
+ }
928
+
929
+ /**
930
+ * Signs in a user with a Google ID token from a mobile client.
931
+ * If the user does not exist, a new user is created.
932
+ * @param idToken - The Google ID token obtained from the mobile app.
933
+ * @param initialRole - The role to assign to the user if they are being created.
934
+ * @returns The signed-in or newly created user.
935
+ */
936
+ async signInWithGoogleIdToken(
937
+ idToken: string,
938
+ initialRole: UserRole = UserRole.PATIENT,
939
+ ): Promise<User> {
940
+ try {
941
+ console.log('[AUTH] Signing in with Google ID Token');
942
+
943
+ // 1) Extract the email claim from the raw JWT so we can check Auth records *before* sign-in
944
+ let email: string | undefined;
945
+ try {
946
+ const payloadBase64 = idToken.split('.')[1];
947
+ const payloadJson = globalThis.atob
948
+ ? globalThis.atob(payloadBase64)
949
+ : Buffer.from(payloadBase64, 'base64').toString('utf8');
950
+ const payload = JSON.parse(payloadJson);
951
+ email = payload.email as string | undefined;
952
+ } catch (decodeError) {
953
+ console.warn('[AUTH] Failed to decode email from Google ID token:', decodeError);
954
+ }
955
+
956
+ if (!email) {
957
+ throw new AuthError(
958
+ 'Unable to read email from Google token. Please try again or use another sign-in method.',
959
+ 'AUTH/INVALID_GOOGLE_TOKEN',
960
+ 400,
961
+ );
962
+ }
963
+
964
+ // 2) Check if this email already has a Google credential in Firebase Auth.
965
+ // If not, abort early so we *never* create an unwanted Auth user.
966
+ const methods = await fetchSignInMethodsForEmail(this.auth, email);
967
+ console.log('[AUTH] Fetch sign in methods for email:', email, methods);
968
+ const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
969
+ console.log('[AUTH] Has Google method:', hasGoogleMethod);
970
+ if (!hasGoogleMethod) {
971
+ console.log('[AUTH] No existing Google credential for email, aborting login:', email);
972
+ throw new AuthError(
973
+ 'No account found for this Google user. Please complete registration first.',
974
+ 'AUTH/USER_NOT_FOUND',
975
+ 404,
976
+ );
977
+ }
978
+
979
+ // 3) Safe to sign-in – we know the credential belongs to an existing Auth account.
980
+ const credential = GoogleAuthProvider.credential(idToken);
981
+ const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
982
+ console.log('[AUTH] Firebase user signed in:', firebaseUser.uid);
983
+
984
+ // 4) Load our domain user document.
985
+ const existingUser = await this.userService.getUserById(firebaseUser.uid);
986
+ if (existingUser) {
987
+ console.log('[AUTH] Existing user found, returning profile:', existingUser.uid);
988
+ return existingUser;
989
+ }
990
+
991
+ // 5) If no profile exists we sign out immediately and error – but crucially no phantom user was created.
992
+ console.log('[AUTH] No existing MetaEstetics user for Google account – signing out.');
993
+ await firebaseSignOut(this.auth);
994
+ throw new AuthError(
995
+ 'No account found. Please complete registration by starting with "Get Started".',
996
+ 'AUTH/USER_NOT_FOUND',
997
+ 404,
998
+ );
999
+ } catch (error) {
1000
+ console.error('[AUTH] Error in signInWithGoogleIdToken:', error);
1001
+ throw handleFirebaseError(error);
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Links a Google account to the currently signed-in user using an ID token.
1007
+ * This is used to upgrade an anonymous user or to allow an existing user
1008
+ * to sign in with Google in the future.
1009
+ * @param idToken - The Google ID token obtained from the mobile app.
1010
+ * @returns The updated user profile.
1011
+ */
1012
+ async linkGoogleAccount(idToken: string): Promise<User> {
1013
+ try {
1014
+ console.log('[AUTH] Linking Google account with ID Token');
1015
+ const currentUser = this.auth.currentUser;
1016
+ if (!currentUser) {
1017
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
1018
+ }
1019
+
1020
+ const wasAnonymous = currentUser.isAnonymous;
1021
+ console.log(`[AUTH] Current user is ${wasAnonymous ? 'anonymous' : 'not anonymous'}`);
1022
+
1023
+ const credential = GoogleAuthProvider.credential(idToken);
1024
+ const userCredential = await linkWithCredential(currentUser, credential);
1025
+ const linkedFirebaseUser = userCredential.user;
1026
+ console.log('[AUTH] Google account linked successfully to user:', linkedFirebaseUser.uid);
1027
+
1028
+ if (wasAnonymous) {
1029
+ console.log('[AUTH] Upgrading anonymous user profile');
1030
+ return await this.userService.upgradeAnonymousUser(
1031
+ linkedFirebaseUser.uid,
1032
+ linkedFirebaseUser.email!,
1033
+ );
1034
+ }
1035
+
1036
+ // If the user was not anonymous, just return their updated profile
1037
+ return (await this.userService.getUserById(linkedFirebaseUser.uid))!;
1038
+ } catch (error) {
1039
+ console.error('[AUTH] Error in linkGoogleAccount:', error);
1040
+ throw handleFirebaseError(error);
1041
+ }
1042
+ }
1043
+ }