@blackcode_sa/metaestetics-api 1.6.16 → 1.6.18

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.
@@ -0,0 +1,959 @@
1
+ import {
2
+ Auth,
3
+ User as FirebaseUser,
4
+ signInWithEmailAndPassword,
5
+ createUserWithEmailAndPassword,
6
+ signInAnonymously as firebaseSignInAnonymously,
7
+ signOut as firebaseSignOut,
8
+ GoogleAuthProvider,
9
+ FacebookAuthProvider,
10
+ OAuthProvider,
11
+ signInWithPopup,
12
+ signInWithRedirect,
13
+ getRedirectResult,
14
+ linkWithCredential,
15
+ EmailAuthProvider,
16
+ onAuthStateChanged,
17
+ sendPasswordResetEmail,
18
+ verifyPasswordResetCode,
19
+ confirmPasswordReset,
20
+ linkWithPopup,
21
+ } from "firebase/auth";
22
+ import {
23
+ doc,
24
+ setDoc,
25
+ getDoc,
26
+ serverTimestamp,
27
+ collection,
28
+ query,
29
+ where,
30
+ getDocs,
31
+ Timestamp,
32
+ updateDoc,
33
+ Firestore,
34
+ } from "firebase/firestore";
35
+ import { FirebaseApp } from "firebase/app";
36
+ import { User, UserRole, USERS_COLLECTION } from "../types";
37
+ import { z } from "zod";
38
+ import {
39
+ emailSchema,
40
+ passwordSchema,
41
+ userRoleSchema,
42
+ } 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.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 { getFirebaseFunctions } from "../config/firebase";
76
+ import {
77
+ httpsCallable,
78
+ HttpsCallableResult,
79
+ Functions,
80
+ } from "firebase/functions";
81
+
82
+ // Define types for our cloud function responses
83
+ interface PatientProfileResponse {
84
+ user: User;
85
+ patientProfile: any;
86
+ }
87
+
88
+ interface ClinicAdminResponse {
89
+ user: User;
90
+ clinicGroup: ClinicGroup;
91
+ clinicAdmin: ClinicAdmin;
92
+ }
93
+
94
+ interface PractitionerResponse {
95
+ user: User;
96
+ practitioner: Practitioner;
97
+ }
98
+
99
+ export class AuthServiceV2 extends BaseService {
100
+ private googleProvider = new GoogleAuthProvider();
101
+ private facebookProvider = new FacebookAuthProvider();
102
+ private appleProvider = new OAuthProvider("apple.com");
103
+ private userService: UserService;
104
+ private functions!: Functions;
105
+
106
+ constructor(
107
+ db: Firestore,
108
+ auth: Auth,
109
+ app: FirebaseApp,
110
+ userService?: UserService
111
+ ) {
112
+ super(db, auth, app);
113
+
114
+ // Initialize UserService if not provided
115
+ if (!userService) {
116
+ userService = new UserService(db, auth, app);
117
+ }
118
+ this.userService = userService;
119
+
120
+ // Initialize functions
121
+ getFirebaseFunctions().then((functions) => {
122
+ this.functions = functions;
123
+ });
124
+ }
125
+
126
+ // Make sure to handle the case where this.functions is not yet initialized
127
+ private async getFunctions(): Promise<Functions> {
128
+ if (!this.functions) {
129
+ this.functions = await getFirebaseFunctions();
130
+ }
131
+ return this.functions;
132
+ }
133
+
134
+ /**
135
+ * Registruje novog korisnika sa email-om i lozinkom
136
+ */
137
+ async signUp(
138
+ email: string,
139
+ password: string,
140
+ initialRole: UserRole = UserRole.PATIENT
141
+ ): Promise<User> {
142
+ // Create Firebase Auth user
143
+ const { user: firebaseUser } = await createUserWithEmailAndPassword(
144
+ this.auth,
145
+ email,
146
+ password
147
+ );
148
+
149
+ if (initialRole === UserRole.PATIENT) {
150
+ // For patient role, we now use cloud function
151
+ const functions = await this.getFunctions();
152
+ const createAnonymousPatientProfile = httpsCallable<
153
+ {},
154
+ PatientProfileResponse
155
+ >(functions, "createAnonymousPatientProfile");
156
+
157
+ const result = await createAnonymousPatientProfile({});
158
+ return (result.data as PatientProfileResponse).user;
159
+ } else {
160
+ // For other roles, we still use the local service for now
161
+ // This will be updated in future PRs
162
+ return this.userService.createUser(firebaseUser, [initialRole]);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Registers a new clinic admin user with email and password
168
+ * Can either create a new clinic group or join an existing one with a token
169
+ *
170
+ * @param data - Clinic admin signup data
171
+ * @returns Object containing the created user, clinic group, and clinic admin
172
+ */
173
+ async signUpClinicAdmin(data: ClinicAdminSignupData): Promise<{
174
+ user: User;
175
+ clinicGroup: ClinicGroup;
176
+ clinicAdmin: ClinicAdmin;
177
+ }> {
178
+ try {
179
+ console.log("[AUTH] Starting clinic admin signup process", {
180
+ email: data.email,
181
+ });
182
+
183
+ // Create Firebase user
184
+ console.log("[AUTH] Creating Firebase user");
185
+ let firebaseUser;
186
+ try {
187
+ const result = await createUserWithEmailAndPassword(
188
+ this.auth,
189
+ data.email,
190
+ data.password
191
+ );
192
+ firebaseUser = result.user;
193
+ console.log("[AUTH] Firebase user created successfully", {
194
+ uid: firebaseUser.uid,
195
+ });
196
+ } catch (firebaseError) {
197
+ console.error("[AUTH] Firebase user creation failed:", firebaseError);
198
+ throw firebaseError;
199
+ }
200
+
201
+ // Prepare contact person info
202
+ const contactPerson: ContactPerson = {
203
+ firstName: data.firstName,
204
+ lastName: data.lastName,
205
+ title: data.title,
206
+ email: data.email,
207
+ phoneNumber: data.phoneNumber,
208
+ };
209
+
210
+ if (data.isCreatingNewGroup) {
211
+ // Creating new group - call cloud function
212
+ const functions = await this.getFunctions();
213
+ const createClinicGroupWithAdmin = httpsCallable<
214
+ {
215
+ groupData: any;
216
+ contactInfo: ContactPerson;
217
+ },
218
+ ClinicAdminResponse
219
+ >(functions, "createClinicGroupWithAdmin");
220
+
221
+ // Call cloud function
222
+ const result = await createClinicGroupWithAdmin({
223
+ groupData: data.clinicGroupData,
224
+ contactInfo: contactPerson,
225
+ });
226
+
227
+ return result.data as ClinicAdminResponse;
228
+ } else {
229
+ // Joining existing group with token
230
+ if (!data.inviteToken) {
231
+ throw new Error(
232
+ "Invite token is required when joining an existing group"
233
+ );
234
+ }
235
+
236
+ // Call cloud function
237
+ const functions = await this.getFunctions();
238
+ const joinClinicGroupWithToken = httpsCallable<
239
+ {
240
+ token: string;
241
+ contactInfo: ContactPerson;
242
+ },
243
+ ClinicAdminResponse
244
+ >(functions, "joinClinicGroupWithToken");
245
+
246
+ const result = await joinClinicGroupWithToken({
247
+ token: data.inviteToken,
248
+ contactInfo: contactPerson,
249
+ });
250
+
251
+ return result.data as ClinicAdminResponse;
252
+ }
253
+ } catch (error) {
254
+ if (error instanceof z.ZodError) {
255
+ console.error(
256
+ "[AUTH] Zod validation error in signUpClinicAdmin:",
257
+ JSON.stringify(error.errors, null, 2)
258
+ );
259
+ throw AUTH_ERRORS.VALIDATION_ERROR;
260
+ }
261
+
262
+ const firebaseError = error as FirebaseError;
263
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
264
+ console.error("[AUTH] Email already in use:", data.email);
265
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
266
+ }
267
+
268
+ console.error("[AUTH] Unhandled error in signUpClinicAdmin:", error);
269
+ throw error;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Prijavljuje korisnika sa email-om i lozinkom
275
+ */
276
+ async signIn(email: string, password: string): Promise<User> {
277
+ const { user: firebaseUser } = await signInWithEmailAndPassword(
278
+ this.auth,
279
+ email,
280
+ password
281
+ );
282
+
283
+ // Update login timestamp via UserService
284
+ return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
285
+ }
286
+
287
+ /**
288
+ * Prijavljuje korisnika sa email-om i lozinkom samo za clinic_admin role
289
+ * @param email - Email korisnika
290
+ * @param password - Lozinka korisnika
291
+ * @returns Objekat koji sadrži korisnika, admin profil i grupu klinika
292
+ * @throws {AUTH_ERRORS.INVALID_ROLE} Ako korisnik nema clinic_admin rolu
293
+ * @throws {AUTH_ERRORS.NOT_FOUND} Ako admin profil nije pronađen
294
+ */
295
+ async signInClinicAdmin(
296
+ email: string,
297
+ password: string
298
+ ): Promise<{
299
+ user: User;
300
+ clinicAdmin: ClinicAdmin;
301
+ clinicGroup: ClinicGroup;
302
+ }> {
303
+ try {
304
+ // Sign in with email/password
305
+ const { user: firebaseUser } = await signInWithEmailAndPassword(
306
+ this.auth,
307
+ email,
308
+ password
309
+ );
310
+
311
+ // Get user
312
+ const user = await this.userService.getUserById(firebaseUser.uid);
313
+
314
+ // Check if user has clinic_admin role
315
+ if (!user.roles?.includes(UserRole.CLINIC_ADMIN)) {
316
+ console.error("[AUTH] User is not a clinic admin:", user.uid);
317
+ throw AUTH_ERRORS.INVALID_ROLE;
318
+ }
319
+
320
+ // Check and get admin profile
321
+ if (!user.adminProfile) {
322
+ console.error("[AUTH] User has no admin profile:", user.uid);
323
+ throw AUTH_ERRORS.NOT_FOUND;
324
+ }
325
+
326
+ // This part would ideally use cloud functions to get the admin profile and clinic group
327
+ // But for now, we'll continue using the existing services
328
+
329
+ // Initialize services
330
+ const clinicAdminService = new ClinicAdminService(
331
+ this.db,
332
+ this.auth,
333
+ this.app
334
+ );
335
+ const clinicGroupService = new ClinicGroupService(
336
+ this.db,
337
+ this.auth,
338
+ this.app,
339
+ clinicAdminService
340
+ );
341
+ clinicAdminService.setServices(clinicGroupService, null as any);
342
+
343
+ // Get admin profile
344
+ const adminProfile = await clinicAdminService.getClinicAdmin(
345
+ user.adminProfile
346
+ );
347
+ if (!adminProfile) {
348
+ console.error("[AUTH] Admin profile not found:", user.adminProfile);
349
+ throw AUTH_ERRORS.NOT_FOUND;
350
+ }
351
+
352
+ // Get clinic group
353
+ const clinicGroup = await clinicGroupService.getClinicGroup(
354
+ adminProfile.clinicGroupId
355
+ );
356
+ if (!clinicGroup) {
357
+ console.error(
358
+ "[AUTH] Clinic group not found:",
359
+ adminProfile.clinicGroupId
360
+ );
361
+ throw AUTH_ERRORS.NOT_FOUND;
362
+ }
363
+
364
+ return {
365
+ user,
366
+ clinicAdmin: adminProfile,
367
+ clinicGroup,
368
+ };
369
+ } catch (error) {
370
+ console.error("[AUTH] Error in signInClinicAdmin:", error);
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Prijavljuje korisnika sa Facebook-om
377
+ */
378
+ async signInWithFacebook(): Promise<User> {
379
+ const provider = new FacebookAuthProvider();
380
+ provider.addScope("email");
381
+ const { user: firebaseUser } = await signInWithPopup(this.auth, provider);
382
+
383
+ // Check for existing user
384
+ try {
385
+ const existingUser = await this.userService.getUserById(firebaseUser.uid);
386
+ return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
387
+ } catch (error) {
388
+ // User doesn't exist, create anonymous patient
389
+ const functions = await this.getFunctions();
390
+ const createAnonymousPatientProfile = httpsCallable<
391
+ {},
392
+ PatientProfileResponse
393
+ >(functions, "createAnonymousPatientProfile");
394
+
395
+ const result = await createAnonymousPatientProfile({});
396
+ return (result.data as PatientProfileResponse).user;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Prijavljuje korisnika sa Google nalogom
402
+ */
403
+ async signInWithGoogle(
404
+ initialRole: UserRole = UserRole.PATIENT
405
+ ): Promise<User> {
406
+ this.googleProvider.addScope("email");
407
+ const { user: firebaseUser } = await signInWithPopup(
408
+ this.auth,
409
+ this.googleProvider
410
+ );
411
+
412
+ // Check for existing user
413
+ try {
414
+ const existingUser = await this.userService.getUserById(firebaseUser.uid);
415
+ return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
416
+ } catch (error) {
417
+ // User doesn't exist, create anonymous patient
418
+ const functions = await this.getFunctions();
419
+ const createAnonymousPatientProfile = httpsCallable<
420
+ {},
421
+ PatientProfileResponse
422
+ >(functions, "createAnonymousPatientProfile");
423
+
424
+ const result = await createAnonymousPatientProfile({});
425
+ return result.data.user;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Prijavljuje korisnika sa Apple-om
431
+ */
432
+ async signInWithApple(): Promise<User> {
433
+ const provider = new OAuthProvider("apple.com");
434
+ provider.addScope("email");
435
+ provider.addScope("name");
436
+ const { user: firebaseUser } = await signInWithPopup(this.auth, provider);
437
+
438
+ // Check for existing user
439
+ try {
440
+ const existingUser = await this.userService.getUserById(firebaseUser.uid);
441
+ return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
442
+ } catch (error) {
443
+ // User doesn't exist, create anonymous patient
444
+ const functions = await this.getFunctions();
445
+ const createAnonymousPatientProfile = httpsCallable(
446
+ functions,
447
+ "createAnonymousPatientProfile"
448
+ );
449
+
450
+ const result = await createAnonymousPatientProfile({});
451
+ return result.data.user;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Prijavljuje korisnika anonimno
457
+ */
458
+ async signInAnonymously(): Promise<User> {
459
+ const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
460
+
461
+ // Create anonymous patient profile using cloud function
462
+ const functions = await this.getFunctions();
463
+ const createAnonymousPatientProfile = httpsCallable(
464
+ functions,
465
+ "createAnonymousPatientProfile"
466
+ );
467
+
468
+ const result = await createAnonymousPatientProfile({});
469
+ return result.data.user;
470
+ }
471
+
472
+ /**
473
+ * Odjavljuje trenutnog korisnika
474
+ */
475
+ async signOut(): Promise<void> {
476
+ await firebaseSignOut(this.auth);
477
+ }
478
+
479
+ /**
480
+ * Vraća trenutno prijavljenog korisnika
481
+ */
482
+ async getCurrentUser(): Promise<User | null> {
483
+ const firebaseUser = this.auth.currentUser;
484
+ if (!firebaseUser) return null;
485
+
486
+ try {
487
+ return this.userService.getUserById(firebaseUser.uid);
488
+ } catch (error) {
489
+ console.error("Error getting current user:", error);
490
+ return null;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Registruje callback za promene stanja autentifikacije
496
+ */
497
+ onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
498
+ return onAuthStateChanged(this.auth, callback);
499
+ }
500
+
501
+ /**
502
+ * Upgrades an anonymous user to a regular user with email and password
503
+ */
504
+ async upgradeAnonymousUser(email: string, password: string): Promise<User> {
505
+ try {
506
+ await emailSchema.parseAsync(email);
507
+ await passwordSchema.parseAsync(password);
508
+
509
+ const currentUser = this.auth.currentUser;
510
+ if (!currentUser) {
511
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
512
+ }
513
+ if (!currentUser.isAnonymous) {
514
+ throw new AuthError(
515
+ "User is not anonymous",
516
+ "AUTH/NOT_ANONYMOUS_USER",
517
+ 400
518
+ );
519
+ }
520
+
521
+ // Create email credential
522
+ const credential = EmailAuthProvider.credential(email, password);
523
+
524
+ // Link credential to current user
525
+ await linkWithCredential(currentUser, credential);
526
+
527
+ // Call cloud function to update backend
528
+ const functions = await this.getFunctions();
529
+ const upgradeAnonymousPatient = httpsCallable<
530
+ {
531
+ email: string;
532
+ profileData: {
533
+ email: string;
534
+ };
535
+ },
536
+ PatientProfileResponse
537
+ >(functions, "upgradeAnonymousPatient");
538
+
539
+ const result = await upgradeAnonymousPatient({
540
+ email,
541
+ profileData: {
542
+ email,
543
+ },
544
+ });
545
+
546
+ return result.data.user;
547
+ } catch (error) {
548
+ if (error instanceof z.ZodError) {
549
+ throw AUTH_ERRORS.VALIDATION_ERROR;
550
+ }
551
+ const firebaseError = error as FirebaseError;
552
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
553
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
554
+ }
555
+ throw error;
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Upgrades an anonymous user to a regular user by signing in with a Google account.
561
+ */
562
+ async upgradeAnonymousUserWithGoogle(): Promise<User> {
563
+ try {
564
+ const currentUser = this.auth.currentUser;
565
+ if (!currentUser) {
566
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
567
+ }
568
+ if (!currentUser.isAnonymous) {
569
+ throw new AuthError(
570
+ "User is not anonymous",
571
+ "AUTH/NOT_ANONYMOUS_USER",
572
+ 400
573
+ );
574
+ }
575
+
576
+ this.googleProvider.addScope("email");
577
+ const userCredential = await linkWithPopup(
578
+ currentUser,
579
+ this.googleProvider
580
+ );
581
+
582
+ if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
583
+ if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
584
+
585
+ // Call cloud function to update backend
586
+ const functions = await this.getFunctions();
587
+ const upgradeAnonymousPatient = httpsCallable(
588
+ functions,
589
+ "upgradeAnonymousPatient"
590
+ );
591
+
592
+ const result = await upgradeAnonymousPatient({
593
+ email: userCredential.user.email,
594
+ profileData: {
595
+ email: userCredential.user.email,
596
+ },
597
+ });
598
+
599
+ return result.data.user;
600
+ } catch (error: unknown) {
601
+ const firebaseError = error as FirebaseError;
602
+ if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
603
+ throw AUTH_ERRORS.POPUP_CLOSED;
604
+ }
605
+ throw error;
606
+ }
607
+ }
608
+
609
+ async upgradeAnonymousUserWithFacebook(): Promise<User> {
610
+ try {
611
+ const currentUser = this.auth.currentUser;
612
+ if (!currentUser) {
613
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
614
+ }
615
+ if (!currentUser.isAnonymous) {
616
+ throw new AuthError(
617
+ "User is not anonymous",
618
+ "AUTH/NOT_ANONYMOUS_USER",
619
+ 400
620
+ );
621
+ }
622
+
623
+ this.facebookProvider.addScope("email");
624
+ const userCredential = await linkWithPopup(
625
+ currentUser,
626
+ this.facebookProvider
627
+ );
628
+
629
+ if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
630
+ if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
631
+
632
+ // Call cloud function to update backend
633
+ const functions = await this.getFunctions();
634
+ const upgradeAnonymousPatient = httpsCallable(
635
+ functions,
636
+ "upgradeAnonymousPatient"
637
+ );
638
+
639
+ const result = await upgradeAnonymousPatient({
640
+ email: userCredential.user.email,
641
+ profileData: {
642
+ email: userCredential.user.email,
643
+ },
644
+ });
645
+
646
+ return result.data.user;
647
+ } catch (error: unknown) {
648
+ const firebaseError = error as FirebaseError;
649
+ if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
650
+ throw AUTH_ERRORS.POPUP_CLOSED;
651
+ }
652
+ throw error;
653
+ }
654
+ }
655
+
656
+ async upgradeAnonymousUserWithApple(): Promise<User> {
657
+ try {
658
+ const currentUser = this.auth.currentUser;
659
+ if (!currentUser) {
660
+ throw AUTH_ERRORS.NOT_AUTHENTICATED;
661
+ }
662
+ if (!currentUser.isAnonymous) {
663
+ throw new AuthError(
664
+ "User is not anonymous",
665
+ "AUTH/NOT_ANONYMOUS_USER",
666
+ 400
667
+ );
668
+ }
669
+
670
+ this.appleProvider.addScope("email");
671
+ this.appleProvider.addScope("name");
672
+ const userCredential = await linkWithPopup(
673
+ currentUser,
674
+ this.appleProvider
675
+ );
676
+
677
+ if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
678
+ if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
679
+
680
+ // Call cloud function to update backend
681
+ const functions = await this.getFunctions();
682
+ const upgradeAnonymousPatient = httpsCallable(
683
+ functions,
684
+ "upgradeAnonymousPatient"
685
+ );
686
+
687
+ const result = await upgradeAnonymousPatient({
688
+ email: userCredential.user.email,
689
+ profileData: {
690
+ email: userCredential.user.email,
691
+ },
692
+ });
693
+
694
+ return result.data.user;
695
+ } catch (error: unknown) {
696
+ const firebaseError = error as FirebaseError;
697
+ if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
698
+ throw AUTH_ERRORS.POPUP_CLOSED;
699
+ }
700
+ throw error;
701
+ }
702
+ }
703
+
704
+ /**
705
+ * Šalje email za resetovanje lozinke korisniku
706
+ * @param email Email adresa korisnika
707
+ * @returns Promise koji se razrešava kada je email poslat
708
+ */
709
+ async sendPasswordResetEmail(email: string): Promise<void> {
710
+ try {
711
+ await emailSchema.parseAsync(email);
712
+ const functions = await this.getFunctions();
713
+ await sendPasswordResetEmail(this.auth, email);
714
+ } catch (error) {
715
+ if (error instanceof z.ZodError) {
716
+ throw AUTH_ERRORS.VALIDATION_ERROR;
717
+ }
718
+
719
+ const firebaseError = error as FirebaseError;
720
+ if (firebaseError.code === FirebaseErrorCode.USER_NOT_FOUND) {
721
+ throw AUTH_ERRORS.USER_NOT_FOUND;
722
+ }
723
+
724
+ throw error;
725
+ }
726
+ }
727
+
728
+ /**
729
+ * Verifikuje kod za resetovanje lozinke iz email linka
730
+ * @param oobCode Kod iz URL-a za resetovanje lozinke
731
+ * @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
732
+ */
733
+ async verifyPasswordResetCode(oobCode: string): Promise<string> {
734
+ try {
735
+ const functions = await this.getFunctions();
736
+ return await verifyPasswordResetCode(this.auth, oobCode);
737
+ } catch (error) {
738
+ const firebaseError = error as FirebaseError;
739
+ if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
740
+ throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
741
+ } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
742
+ throw AUTH_ERRORS.INVALID_ACTION_CODE;
743
+ }
744
+
745
+ throw error;
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Potvrđuje resetovanje lozinke i postavlja novu lozinku
751
+ * @param oobCode Kod iz URL-a za resetovanje lozinke
752
+ * @param newPassword Nova lozinka
753
+ * @returns Promise koji se razrešava kada je lozinka promenjena
754
+ */
755
+ async confirmPasswordReset(
756
+ oobCode: string,
757
+ newPassword: string
758
+ ): Promise<void> {
759
+ try {
760
+ await passwordSchema.parseAsync(newPassword);
761
+ const functions = await this.getFunctions();
762
+ await confirmPasswordReset(this.auth, oobCode, newPassword);
763
+ } catch (error) {
764
+ if (error instanceof z.ZodError) {
765
+ throw AUTH_ERRORS.VALIDATION_ERROR;
766
+ }
767
+
768
+ const firebaseError = error as FirebaseError;
769
+ if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
770
+ throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
771
+ } else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
772
+ throw AUTH_ERRORS.INVALID_ACTION_CODE;
773
+ } else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
774
+ throw AUTH_ERRORS.WEAK_PASSWORD;
775
+ }
776
+
777
+ throw error;
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Registers a new practitioner user with email and password
783
+ * Can either create a new practitioner profile or claim an existing draft profile with a token
784
+ */
785
+ async signUpPractitioner(data: {
786
+ email: string;
787
+ password: string;
788
+ firstName: string;
789
+ lastName: string;
790
+ token?: string;
791
+ profileData?: Partial<CreatePractitionerData>;
792
+ }): Promise<{
793
+ user: User;
794
+ practitioner: Practitioner;
795
+ }> {
796
+ try {
797
+ console.log("[AUTH] Starting practitioner signup process", {
798
+ email: data.email,
799
+ hasToken: !!data.token,
800
+ });
801
+
802
+ // Create Firebase user
803
+ console.log("[AUTH] Creating Firebase user");
804
+ let firebaseUser;
805
+ try {
806
+ const result = await createUserWithEmailAndPassword(
807
+ this.auth,
808
+ data.email,
809
+ data.password
810
+ );
811
+ firebaseUser = result.user;
812
+ console.log("[AUTH] Firebase user created successfully", {
813
+ uid: firebaseUser.uid,
814
+ });
815
+ } catch (firebaseError) {
816
+ console.error("[AUTH] Firebase user creation failed:", firebaseError);
817
+ throw firebaseError;
818
+ }
819
+
820
+ if (data.token) {
821
+ // Claiming existing profile with token
822
+ console.log("[AUTH] Claiming draft profile with token");
823
+
824
+ const functions = await this.getFunctions();
825
+ const validateTokenAndClaimProfile = httpsCallable<
826
+ {
827
+ token: string;
828
+ },
829
+ PractitionerResponse
830
+ >(functions, "validateTokenAndClaimProfile");
831
+
832
+ const result = await validateTokenAndClaimProfile({
833
+ token: data.token,
834
+ });
835
+
836
+ return result.data as PractitionerResponse;
837
+ } else {
838
+ // Creating new profile
839
+ console.log("[AUTH] Creating new practitioner profile");
840
+
841
+ // Prepare basic info based on form data
842
+ const profileData = data.profileData || {};
843
+ if (!profileData.basicInfo) {
844
+ profileData.basicInfo = {
845
+ firstName: data.firstName,
846
+ lastName: data.lastName,
847
+ email: data.email,
848
+ phoneNumber: "",
849
+ title: "Practitioner",
850
+ profileImageUrl: "",
851
+ dateOfBirth: new Date(),
852
+ gender: "other",
853
+ languages: ["English"],
854
+ bio: "",
855
+ };
856
+ }
857
+
858
+ const functions = await this.getFunctions();
859
+ const createPractitionerProfile = httpsCallable<
860
+ {
861
+ profileData: Partial<CreatePractitionerData>;
862
+ },
863
+ PractitionerResponse
864
+ >(functions, "createPractitionerProfile");
865
+
866
+ const result = await createPractitionerProfile({
867
+ profileData,
868
+ });
869
+
870
+ return result.data as PractitionerResponse;
871
+ }
872
+ } catch (error) {
873
+ if (error instanceof z.ZodError) {
874
+ console.error(
875
+ "[AUTH] Zod validation error in signUpPractitioner:",
876
+ JSON.stringify(error.errors, null, 2)
877
+ );
878
+ throw AUTH_ERRORS.VALIDATION_ERROR;
879
+ }
880
+
881
+ const firebaseError = error as FirebaseError;
882
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
883
+ console.error("[AUTH] Email already in use:", data.email);
884
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
885
+ }
886
+
887
+ console.error("[AUTH] Unhandled error in signUpPractitioner:", error);
888
+ throw error;
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Signs in a user with email and password specifically for practitioner role
894
+ */
895
+ async signInPractitioner(
896
+ email: string,
897
+ password: string
898
+ ): Promise<{
899
+ user: User;
900
+ practitioner: Practitioner;
901
+ }> {
902
+ try {
903
+ console.log("[AUTH] Starting practitioner signin process", {
904
+ email: email,
905
+ });
906
+
907
+ // Sign in with email/password
908
+ const { user: firebaseUser } = await signInWithEmailAndPassword(
909
+ this.auth,
910
+ email,
911
+ password
912
+ );
913
+
914
+ // Get user data
915
+ const user = await this.userService.getUserById(firebaseUser.uid);
916
+ console.log("[AUTH] User retrieved", { uid: user.uid });
917
+
918
+ // Check if user has practitioner role
919
+ if (!user.roles?.includes(UserRole.PRACTITIONER)) {
920
+ console.error("[AUTH] User is not a practitioner:", user.uid);
921
+ throw AUTH_ERRORS.INVALID_ROLE;
922
+ }
923
+
924
+ // Check and get practitioner profile
925
+ if (!user.practitionerProfile) {
926
+ console.error("[AUTH] User has no practitioner profile:", user.uid);
927
+ throw AUTH_ERRORS.NOT_FOUND;
928
+ }
929
+
930
+ // Get practitioner profile using local service
931
+ // This will be updated to use cloud functions in future PRs
932
+ const practitionerService = new PractitionerService(
933
+ this.db,
934
+ this.auth,
935
+ this.app
936
+ );
937
+
938
+ const practitioner = await practitionerService.getPractitioner(
939
+ user.practitionerProfile
940
+ );
941
+
942
+ if (!practitioner) {
943
+ console.error(
944
+ "[AUTH] Practitioner profile not found:",
945
+ user.practitionerProfile
946
+ );
947
+ throw AUTH_ERRORS.NOT_FOUND;
948
+ }
949
+
950
+ return {
951
+ user,
952
+ practitioner,
953
+ };
954
+ } catch (error) {
955
+ console.error("[AUTH] Error in signInPractitioner:", error);
956
+ throw error;
957
+ }
958
+ }
959
+ }