@blackcode_sa/metaestetics-api 1.13.4 → 1.13.5
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.
- package/dist/admin/index.d.mts +15 -28
- package/dist/admin/index.d.ts +15 -28
- package/dist/index.d.mts +16 -29
- package/dist/index.d.ts +16 -29
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +121 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +81 -81
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +67 -67
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +14 -14
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2200 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +494 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,961 +1,961 @@
|
|
|
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/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
|
-
// Sign out the user immediately for security
|
|
318
|
-
await this.auth.signOut();
|
|
319
|
-
throw AUTH_ERRORS.UNAUTHORIZED_ROLE;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Check and get admin profile
|
|
323
|
-
if (!user.adminProfile) {
|
|
324
|
-
console.error("[AUTH] User has no admin profile:", user.uid);
|
|
325
|
-
throw AUTH_ERRORS.NOT_FOUND;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// This part would ideally use cloud functions to get the admin profile and clinic group
|
|
329
|
-
// But for now, we'll continue using the existing services
|
|
330
|
-
|
|
331
|
-
// Initialize services
|
|
332
|
-
const clinicAdminService = new ClinicAdminService(
|
|
333
|
-
this.db,
|
|
334
|
-
this.auth,
|
|
335
|
-
this.app
|
|
336
|
-
);
|
|
337
|
-
const clinicGroupService = new ClinicGroupService(
|
|
338
|
-
this.db,
|
|
339
|
-
this.auth,
|
|
340
|
-
this.app,
|
|
341
|
-
clinicAdminService
|
|
342
|
-
);
|
|
343
|
-
clinicAdminService.setServices(clinicGroupService, null as any);
|
|
344
|
-
|
|
345
|
-
// Get admin profile
|
|
346
|
-
const adminProfile = await clinicAdminService.getClinicAdmin(
|
|
347
|
-
user.adminProfile
|
|
348
|
-
);
|
|
349
|
-
if (!adminProfile) {
|
|
350
|
-
console.error("[AUTH] Admin profile not found:", user.adminProfile);
|
|
351
|
-
throw AUTH_ERRORS.NOT_FOUND;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Get clinic group
|
|
355
|
-
const clinicGroup = await clinicGroupService.getClinicGroup(
|
|
356
|
-
adminProfile.clinicGroupId
|
|
357
|
-
);
|
|
358
|
-
if (!clinicGroup) {
|
|
359
|
-
console.error(
|
|
360
|
-
"[AUTH] Clinic group not found:",
|
|
361
|
-
adminProfile.clinicGroupId
|
|
362
|
-
);
|
|
363
|
-
throw AUTH_ERRORS.NOT_FOUND;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
user,
|
|
368
|
-
clinicAdmin: adminProfile,
|
|
369
|
-
clinicGroup,
|
|
370
|
-
};
|
|
371
|
-
} catch (error) {
|
|
372
|
-
console.error("[AUTH] Error in signInClinicAdmin:", error);
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Prijavljuje korisnika sa Facebook-om
|
|
379
|
-
*/
|
|
380
|
-
async signInWithFacebook(): Promise<User> {
|
|
381
|
-
const provider = new FacebookAuthProvider();
|
|
382
|
-
provider.addScope("email");
|
|
383
|
-
const { user: firebaseUser } = await signInWithPopup(this.auth, provider);
|
|
384
|
-
|
|
385
|
-
// Check for existing user
|
|
386
|
-
try {
|
|
387
|
-
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
388
|
-
return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
|
|
389
|
-
} catch (error) {
|
|
390
|
-
// User doesn't exist, create anonymous patient
|
|
391
|
-
const functions = await this.getFunctions();
|
|
392
|
-
const createAnonymousPatientProfile = httpsCallable<
|
|
393
|
-
{},
|
|
394
|
-
PatientProfileResponse
|
|
395
|
-
>(functions, "createAnonymousPatientProfile");
|
|
396
|
-
|
|
397
|
-
const result = await createAnonymousPatientProfile({});
|
|
398
|
-
return (result.data as PatientProfileResponse).user;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Prijavljuje korisnika sa Google nalogom
|
|
404
|
-
*/
|
|
405
|
-
async signInWithGoogle(
|
|
406
|
-
initialRole: UserRole = UserRole.PATIENT
|
|
407
|
-
): Promise<User> {
|
|
408
|
-
this.googleProvider.addScope("email");
|
|
409
|
-
const { user: firebaseUser } = await signInWithPopup(
|
|
410
|
-
this.auth,
|
|
411
|
-
this.googleProvider
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
// Check for existing user
|
|
415
|
-
try {
|
|
416
|
-
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
417
|
-
return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
|
|
418
|
-
} catch (error) {
|
|
419
|
-
// User doesn't exist, create anonymous patient
|
|
420
|
-
const functions = await this.getFunctions();
|
|
421
|
-
const createAnonymousPatientProfile = httpsCallable<
|
|
422
|
-
{},
|
|
423
|
-
PatientProfileResponse
|
|
424
|
-
>(functions, "createAnonymousPatientProfile");
|
|
425
|
-
|
|
426
|
-
const result = await createAnonymousPatientProfile({});
|
|
427
|
-
return result.data.user;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Prijavljuje korisnika sa Apple-om
|
|
433
|
-
*/
|
|
434
|
-
async signInWithApple(): Promise<User> {
|
|
435
|
-
const provider = new OAuthProvider("apple.com");
|
|
436
|
-
provider.addScope("email");
|
|
437
|
-
provider.addScope("name");
|
|
438
|
-
const { user: firebaseUser } = await signInWithPopup(this.auth, provider);
|
|
439
|
-
|
|
440
|
-
// Check for existing user
|
|
441
|
-
try {
|
|
442
|
-
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
443
|
-
return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
|
|
444
|
-
} catch (error) {
|
|
445
|
-
// User doesn't exist, create anonymous patient
|
|
446
|
-
const functions = await this.getFunctions();
|
|
447
|
-
const createAnonymousPatientProfile = httpsCallable(
|
|
448
|
-
functions,
|
|
449
|
-
"createAnonymousPatientProfile"
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
const result = await createAnonymousPatientProfile({});
|
|
453
|
-
return result.data.user;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Prijavljuje korisnika anonimno
|
|
459
|
-
*/
|
|
460
|
-
async signInAnonymously(): Promise<User> {
|
|
461
|
-
const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
|
|
462
|
-
|
|
463
|
-
// Create anonymous patient profile using cloud function
|
|
464
|
-
const functions = await this.getFunctions();
|
|
465
|
-
const createAnonymousPatientProfile = httpsCallable(
|
|
466
|
-
functions,
|
|
467
|
-
"createAnonymousPatientProfile"
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
const result = await createAnonymousPatientProfile({});
|
|
471
|
-
return result.data.user;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Odjavljuje trenutnog korisnika
|
|
476
|
-
*/
|
|
477
|
-
async signOut(): Promise<void> {
|
|
478
|
-
await firebaseSignOut(this.auth);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Vraća trenutno prijavljenog korisnika
|
|
483
|
-
*/
|
|
484
|
-
async getCurrentUser(): Promise<User | null> {
|
|
485
|
-
const firebaseUser = this.auth.currentUser;
|
|
486
|
-
if (!firebaseUser) return null;
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
return this.userService.getUserById(firebaseUser.uid);
|
|
490
|
-
} catch (error) {
|
|
491
|
-
console.error("Error getting current user:", error);
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Registruje callback za promene stanja autentifikacije
|
|
498
|
-
*/
|
|
499
|
-
onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
|
|
500
|
-
return onAuthStateChanged(this.auth, callback);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Upgrades an anonymous user to a regular user with email and password
|
|
505
|
-
*/
|
|
506
|
-
async upgradeAnonymousUser(email: string, password: string): Promise<User> {
|
|
507
|
-
try {
|
|
508
|
-
await emailSchema.parseAsync(email);
|
|
509
|
-
await passwordSchema.parseAsync(password);
|
|
510
|
-
|
|
511
|
-
const currentUser = this.auth.currentUser;
|
|
512
|
-
if (!currentUser) {
|
|
513
|
-
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
514
|
-
}
|
|
515
|
-
if (!currentUser.isAnonymous) {
|
|
516
|
-
throw new AuthError(
|
|
517
|
-
"User is not anonymous",
|
|
518
|
-
"AUTH/NOT_ANONYMOUS_USER",
|
|
519
|
-
400
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Create email credential
|
|
524
|
-
const credential = EmailAuthProvider.credential(email, password);
|
|
525
|
-
|
|
526
|
-
// Link credential to current user
|
|
527
|
-
await linkWithCredential(currentUser, credential);
|
|
528
|
-
|
|
529
|
-
// Call cloud function to update backend
|
|
530
|
-
const functions = await this.getFunctions();
|
|
531
|
-
const upgradeAnonymousPatient = httpsCallable<
|
|
532
|
-
{
|
|
533
|
-
email: string;
|
|
534
|
-
profileData: {
|
|
535
|
-
email: string;
|
|
536
|
-
};
|
|
537
|
-
},
|
|
538
|
-
PatientProfileResponse
|
|
539
|
-
>(functions, "upgradeAnonymousPatient");
|
|
540
|
-
|
|
541
|
-
const result = await upgradeAnonymousPatient({
|
|
542
|
-
email,
|
|
543
|
-
profileData: {
|
|
544
|
-
email,
|
|
545
|
-
},
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
return result.data.user;
|
|
549
|
-
} catch (error) {
|
|
550
|
-
if (error instanceof z.ZodError) {
|
|
551
|
-
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
552
|
-
}
|
|
553
|
-
const firebaseError = error as FirebaseError;
|
|
554
|
-
if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
|
|
555
|
-
throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
|
|
556
|
-
}
|
|
557
|
-
throw error;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Upgrades an anonymous user to a regular user by signing in with a Google account.
|
|
563
|
-
*/
|
|
564
|
-
async upgradeAnonymousUserWithGoogle(): Promise<User> {
|
|
565
|
-
try {
|
|
566
|
-
const currentUser = this.auth.currentUser;
|
|
567
|
-
if (!currentUser) {
|
|
568
|
-
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
569
|
-
}
|
|
570
|
-
if (!currentUser.isAnonymous) {
|
|
571
|
-
throw new AuthError(
|
|
572
|
-
"User is not anonymous",
|
|
573
|
-
"AUTH/NOT_ANONYMOUS_USER",
|
|
574
|
-
400
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
this.googleProvider.addScope("email");
|
|
579
|
-
const userCredential = await linkWithPopup(
|
|
580
|
-
currentUser,
|
|
581
|
-
this.googleProvider
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
585
|
-
if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
586
|
-
|
|
587
|
-
// Call cloud function to update backend
|
|
588
|
-
const functions = await this.getFunctions();
|
|
589
|
-
const upgradeAnonymousPatient = httpsCallable(
|
|
590
|
-
functions,
|
|
591
|
-
"upgradeAnonymousPatient"
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
const result = await upgradeAnonymousPatient({
|
|
595
|
-
email: userCredential.user.email,
|
|
596
|
-
profileData: {
|
|
597
|
-
email: userCredential.user.email,
|
|
598
|
-
},
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
return result.data.user;
|
|
602
|
-
} catch (error: unknown) {
|
|
603
|
-
const firebaseError = error as FirebaseError;
|
|
604
|
-
if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
|
|
605
|
-
throw AUTH_ERRORS.POPUP_CLOSED;
|
|
606
|
-
}
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async upgradeAnonymousUserWithFacebook(): Promise<User> {
|
|
612
|
-
try {
|
|
613
|
-
const currentUser = this.auth.currentUser;
|
|
614
|
-
if (!currentUser) {
|
|
615
|
-
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
616
|
-
}
|
|
617
|
-
if (!currentUser.isAnonymous) {
|
|
618
|
-
throw new AuthError(
|
|
619
|
-
"User is not anonymous",
|
|
620
|
-
"AUTH/NOT_ANONYMOUS_USER",
|
|
621
|
-
400
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
this.facebookProvider.addScope("email");
|
|
626
|
-
const userCredential = await linkWithPopup(
|
|
627
|
-
currentUser,
|
|
628
|
-
this.facebookProvider
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
632
|
-
if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
633
|
-
|
|
634
|
-
// Call cloud function to update backend
|
|
635
|
-
const functions = await this.getFunctions();
|
|
636
|
-
const upgradeAnonymousPatient = httpsCallable(
|
|
637
|
-
functions,
|
|
638
|
-
"upgradeAnonymousPatient"
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
const result = await upgradeAnonymousPatient({
|
|
642
|
-
email: userCredential.user.email,
|
|
643
|
-
profileData: {
|
|
644
|
-
email: userCredential.user.email,
|
|
645
|
-
},
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
return result.data.user;
|
|
649
|
-
} catch (error: unknown) {
|
|
650
|
-
const firebaseError = error as FirebaseError;
|
|
651
|
-
if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
|
|
652
|
-
throw AUTH_ERRORS.POPUP_CLOSED;
|
|
653
|
-
}
|
|
654
|
-
throw error;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
async upgradeAnonymousUserWithApple(): Promise<User> {
|
|
659
|
-
try {
|
|
660
|
-
const currentUser = this.auth.currentUser;
|
|
661
|
-
if (!currentUser) {
|
|
662
|
-
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
663
|
-
}
|
|
664
|
-
if (!currentUser.isAnonymous) {
|
|
665
|
-
throw new AuthError(
|
|
666
|
-
"User is not anonymous",
|
|
667
|
-
"AUTH/NOT_ANONYMOUS_USER",
|
|
668
|
-
400
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
this.appleProvider.addScope("email");
|
|
673
|
-
this.appleProvider.addScope("name");
|
|
674
|
-
const userCredential = await linkWithPopup(
|
|
675
|
-
currentUser,
|
|
676
|
-
this.appleProvider
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
680
|
-
if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
681
|
-
|
|
682
|
-
// Call cloud function to update backend
|
|
683
|
-
const functions = await this.getFunctions();
|
|
684
|
-
const upgradeAnonymousPatient = httpsCallable(
|
|
685
|
-
functions,
|
|
686
|
-
"upgradeAnonymousPatient"
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
const result = await upgradeAnonymousPatient({
|
|
690
|
-
email: userCredential.user.email,
|
|
691
|
-
profileData: {
|
|
692
|
-
email: userCredential.user.email,
|
|
693
|
-
},
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
return result.data.user;
|
|
697
|
-
} catch (error: unknown) {
|
|
698
|
-
const firebaseError = error as FirebaseError;
|
|
699
|
-
if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
|
|
700
|
-
throw AUTH_ERRORS.POPUP_CLOSED;
|
|
701
|
-
}
|
|
702
|
-
throw error;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Šalje email za resetovanje lozinke korisniku
|
|
708
|
-
* @param email Email adresa korisnika
|
|
709
|
-
* @returns Promise koji se razrešava kada je email poslat
|
|
710
|
-
*/
|
|
711
|
-
async sendPasswordResetEmail(email: string): Promise<void> {
|
|
712
|
-
try {
|
|
713
|
-
await emailSchema.parseAsync(email);
|
|
714
|
-
const functions = await this.getFunctions();
|
|
715
|
-
await sendPasswordResetEmail(this.auth, email);
|
|
716
|
-
} catch (error) {
|
|
717
|
-
if (error instanceof z.ZodError) {
|
|
718
|
-
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const firebaseError = error as FirebaseError;
|
|
722
|
-
if (firebaseError.code === FirebaseErrorCode.USER_NOT_FOUND) {
|
|
723
|
-
throw AUTH_ERRORS.USER_NOT_FOUND;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
throw error;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Verifikuje kod za resetovanje lozinke iz email linka
|
|
732
|
-
* @param oobCode Kod iz URL-a za resetovanje lozinke
|
|
733
|
-
* @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
|
|
734
|
-
*/
|
|
735
|
-
async verifyPasswordResetCode(oobCode: string): Promise<string> {
|
|
736
|
-
try {
|
|
737
|
-
const functions = await this.getFunctions();
|
|
738
|
-
return await verifyPasswordResetCode(this.auth, oobCode);
|
|
739
|
-
} catch (error) {
|
|
740
|
-
const firebaseError = error as FirebaseError;
|
|
741
|
-
if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
|
|
742
|
-
throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
|
|
743
|
-
} else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
|
|
744
|
-
throw AUTH_ERRORS.INVALID_ACTION_CODE;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
throw error;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Potvrđuje resetovanje lozinke i postavlja novu lozinku
|
|
753
|
-
* @param oobCode Kod iz URL-a za resetovanje lozinke
|
|
754
|
-
* @param newPassword Nova lozinka
|
|
755
|
-
* @returns Promise koji se razrešava kada je lozinka promenjena
|
|
756
|
-
*/
|
|
757
|
-
async confirmPasswordReset(
|
|
758
|
-
oobCode: string,
|
|
759
|
-
newPassword: string
|
|
760
|
-
): Promise<void> {
|
|
761
|
-
try {
|
|
762
|
-
await passwordSchema.parseAsync(newPassword);
|
|
763
|
-
const functions = await this.getFunctions();
|
|
764
|
-
await confirmPasswordReset(this.auth, oobCode, newPassword);
|
|
765
|
-
} catch (error) {
|
|
766
|
-
if (error instanceof z.ZodError) {
|
|
767
|
-
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const firebaseError = error as FirebaseError;
|
|
771
|
-
if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
|
|
772
|
-
throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
|
|
773
|
-
} else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
|
|
774
|
-
throw AUTH_ERRORS.INVALID_ACTION_CODE;
|
|
775
|
-
} else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
|
|
776
|
-
throw AUTH_ERRORS.WEAK_PASSWORD;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
throw error;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Registers a new practitioner user with email and password
|
|
785
|
-
* Can either create a new practitioner profile or claim an existing draft profile with a token
|
|
786
|
-
*/
|
|
787
|
-
async signUpPractitioner(data: {
|
|
788
|
-
email: string;
|
|
789
|
-
password: string;
|
|
790
|
-
firstName: string;
|
|
791
|
-
lastName: string;
|
|
792
|
-
token?: string;
|
|
793
|
-
profileData?: Partial<CreatePractitionerData>;
|
|
794
|
-
}): Promise<{
|
|
795
|
-
user: User;
|
|
796
|
-
practitioner: Practitioner;
|
|
797
|
-
}> {
|
|
798
|
-
try {
|
|
799
|
-
console.log("[AUTH] Starting practitioner signup process", {
|
|
800
|
-
email: data.email,
|
|
801
|
-
hasToken: !!data.token,
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
// Create Firebase user
|
|
805
|
-
console.log("[AUTH] Creating Firebase user");
|
|
806
|
-
let firebaseUser;
|
|
807
|
-
try {
|
|
808
|
-
const result = await createUserWithEmailAndPassword(
|
|
809
|
-
this.auth,
|
|
810
|
-
data.email,
|
|
811
|
-
data.password
|
|
812
|
-
);
|
|
813
|
-
firebaseUser = result.user;
|
|
814
|
-
console.log("[AUTH] Firebase user created successfully", {
|
|
815
|
-
uid: firebaseUser.uid,
|
|
816
|
-
});
|
|
817
|
-
} catch (firebaseError) {
|
|
818
|
-
console.error("[AUTH] Firebase user creation failed:", firebaseError);
|
|
819
|
-
throw firebaseError;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (data.token) {
|
|
823
|
-
// Claiming existing profile with token
|
|
824
|
-
console.log("[AUTH] Claiming draft profile with token");
|
|
825
|
-
|
|
826
|
-
const functions = await this.getFunctions();
|
|
827
|
-
const validateTokenAndClaimProfile = httpsCallable<
|
|
828
|
-
{
|
|
829
|
-
token: string;
|
|
830
|
-
},
|
|
831
|
-
PractitionerResponse
|
|
832
|
-
>(functions, "validateTokenAndClaimProfile");
|
|
833
|
-
|
|
834
|
-
const result = await validateTokenAndClaimProfile({
|
|
835
|
-
token: data.token,
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
return result.data as PractitionerResponse;
|
|
839
|
-
} else {
|
|
840
|
-
// Creating new profile
|
|
841
|
-
console.log("[AUTH] Creating new practitioner profile");
|
|
842
|
-
|
|
843
|
-
// Prepare basic info based on form data
|
|
844
|
-
const profileData = data.profileData || {};
|
|
845
|
-
if (!profileData.basicInfo) {
|
|
846
|
-
profileData.basicInfo = {
|
|
847
|
-
firstName: data.firstName,
|
|
848
|
-
lastName: data.lastName,
|
|
849
|
-
email: data.email,
|
|
850
|
-
phoneNumber: "",
|
|
851
|
-
title: "Practitioner",
|
|
852
|
-
profileImageUrl: "",
|
|
853
|
-
dateOfBirth: new Date(),
|
|
854
|
-
gender: "other",
|
|
855
|
-
languages: ["English"],
|
|
856
|
-
bio: "",
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const functions = await this.getFunctions();
|
|
861
|
-
const createPractitionerProfile = httpsCallable<
|
|
862
|
-
{
|
|
863
|
-
profileData: Partial<CreatePractitionerData>;
|
|
864
|
-
},
|
|
865
|
-
PractitionerResponse
|
|
866
|
-
>(functions, "createPractitionerProfile");
|
|
867
|
-
|
|
868
|
-
const result = await createPractitionerProfile({
|
|
869
|
-
profileData,
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
return result.data as PractitionerResponse;
|
|
873
|
-
}
|
|
874
|
-
} catch (error) {
|
|
875
|
-
if (error instanceof z.ZodError) {
|
|
876
|
-
console.error(
|
|
877
|
-
"[AUTH] Zod validation error in signUpPractitioner:",
|
|
878
|
-
JSON.stringify(error.errors, null, 2)
|
|
879
|
-
);
|
|
880
|
-
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const firebaseError = error as FirebaseError;
|
|
884
|
-
if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
|
|
885
|
-
console.error("[AUTH] Email already in use:", data.email);
|
|
886
|
-
throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
console.error("[AUTH] Unhandled error in signUpPractitioner:", error);
|
|
890
|
-
throw error;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Signs in a user with email and password specifically for practitioner role
|
|
896
|
-
*/
|
|
897
|
-
async signInPractitioner(
|
|
898
|
-
email: string,
|
|
899
|
-
password: string
|
|
900
|
-
): Promise<{
|
|
901
|
-
user: User;
|
|
902
|
-
practitioner: Practitioner;
|
|
903
|
-
}> {
|
|
904
|
-
try {
|
|
905
|
-
console.log("[AUTH] Starting practitioner signin process", {
|
|
906
|
-
email: email,
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
// Sign in with email/password
|
|
910
|
-
const { user: firebaseUser } = await signInWithEmailAndPassword(
|
|
911
|
-
this.auth,
|
|
912
|
-
email,
|
|
913
|
-
password
|
|
914
|
-
);
|
|
915
|
-
|
|
916
|
-
// Get user data
|
|
917
|
-
const user = await this.userService.getUserById(firebaseUser.uid);
|
|
918
|
-
console.log("[AUTH] User retrieved", { uid: user.uid });
|
|
919
|
-
|
|
920
|
-
// Check if user has practitioner role
|
|
921
|
-
if (!user.roles?.includes(UserRole.PRACTITIONER)) {
|
|
922
|
-
console.error("[AUTH] User is not a practitioner:", user.uid);
|
|
923
|
-
throw AUTH_ERRORS.INVALID_ROLE;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Check and get practitioner profile
|
|
927
|
-
if (!user.practitionerProfile) {
|
|
928
|
-
console.error("[AUTH] User has no practitioner profile:", user.uid);
|
|
929
|
-
throw AUTH_ERRORS.NOT_FOUND;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Get practitioner profile using local service
|
|
933
|
-
// This will be updated to use cloud functions in future PRs
|
|
934
|
-
const practitionerService = new PractitionerService(
|
|
935
|
-
this.db,
|
|
936
|
-
this.auth,
|
|
937
|
-
this.app
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
const practitioner = await practitionerService.getPractitioner(
|
|
941
|
-
user.practitionerProfile
|
|
942
|
-
);
|
|
943
|
-
|
|
944
|
-
if (!practitioner) {
|
|
945
|
-
console.error(
|
|
946
|
-
"[AUTH] Practitioner profile not found:",
|
|
947
|
-
user.practitionerProfile
|
|
948
|
-
);
|
|
949
|
-
throw AUTH_ERRORS.NOT_FOUND;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
return {
|
|
953
|
-
user,
|
|
954
|
-
practitioner,
|
|
955
|
-
};
|
|
956
|
-
} catch (error) {
|
|
957
|
-
console.error("[AUTH] Error in signInPractitioner:", error);
|
|
958
|
-
throw error;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
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/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
|
+
// Sign out the user immediately for security
|
|
318
|
+
await this.auth.signOut();
|
|
319
|
+
throw AUTH_ERRORS.UNAUTHORIZED_ROLE;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check and get admin profile
|
|
323
|
+
if (!user.adminProfile) {
|
|
324
|
+
console.error("[AUTH] User has no admin profile:", user.uid);
|
|
325
|
+
throw AUTH_ERRORS.NOT_FOUND;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// This part would ideally use cloud functions to get the admin profile and clinic group
|
|
329
|
+
// But for now, we'll continue using the existing services
|
|
330
|
+
|
|
331
|
+
// Initialize services
|
|
332
|
+
const clinicAdminService = new ClinicAdminService(
|
|
333
|
+
this.db,
|
|
334
|
+
this.auth,
|
|
335
|
+
this.app
|
|
336
|
+
);
|
|
337
|
+
const clinicGroupService = new ClinicGroupService(
|
|
338
|
+
this.db,
|
|
339
|
+
this.auth,
|
|
340
|
+
this.app,
|
|
341
|
+
clinicAdminService
|
|
342
|
+
);
|
|
343
|
+
clinicAdminService.setServices(clinicGroupService, null as any);
|
|
344
|
+
|
|
345
|
+
// Get admin profile
|
|
346
|
+
const adminProfile = await clinicAdminService.getClinicAdmin(
|
|
347
|
+
user.adminProfile
|
|
348
|
+
);
|
|
349
|
+
if (!adminProfile) {
|
|
350
|
+
console.error("[AUTH] Admin profile not found:", user.adminProfile);
|
|
351
|
+
throw AUTH_ERRORS.NOT_FOUND;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Get clinic group
|
|
355
|
+
const clinicGroup = await clinicGroupService.getClinicGroup(
|
|
356
|
+
adminProfile.clinicGroupId
|
|
357
|
+
);
|
|
358
|
+
if (!clinicGroup) {
|
|
359
|
+
console.error(
|
|
360
|
+
"[AUTH] Clinic group not found:",
|
|
361
|
+
adminProfile.clinicGroupId
|
|
362
|
+
);
|
|
363
|
+
throw AUTH_ERRORS.NOT_FOUND;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
user,
|
|
368
|
+
clinicAdmin: adminProfile,
|
|
369
|
+
clinicGroup,
|
|
370
|
+
};
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error("[AUTH] Error in signInClinicAdmin:", error);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Prijavljuje korisnika sa Facebook-om
|
|
379
|
+
*/
|
|
380
|
+
async signInWithFacebook(): Promise<User> {
|
|
381
|
+
const provider = new FacebookAuthProvider();
|
|
382
|
+
provider.addScope("email");
|
|
383
|
+
const { user: firebaseUser } = await signInWithPopup(this.auth, provider);
|
|
384
|
+
|
|
385
|
+
// Check for existing user
|
|
386
|
+
try {
|
|
387
|
+
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
388
|
+
return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
// User doesn't exist, create anonymous patient
|
|
391
|
+
const functions = await this.getFunctions();
|
|
392
|
+
const createAnonymousPatientProfile = httpsCallable<
|
|
393
|
+
{},
|
|
394
|
+
PatientProfileResponse
|
|
395
|
+
>(functions, "createAnonymousPatientProfile");
|
|
396
|
+
|
|
397
|
+
const result = await createAnonymousPatientProfile({});
|
|
398
|
+
return (result.data as PatientProfileResponse).user;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Prijavljuje korisnika sa Google nalogom
|
|
404
|
+
*/
|
|
405
|
+
async signInWithGoogle(
|
|
406
|
+
initialRole: UserRole = UserRole.PATIENT
|
|
407
|
+
): Promise<User> {
|
|
408
|
+
this.googleProvider.addScope("email");
|
|
409
|
+
const { user: firebaseUser } = await signInWithPopup(
|
|
410
|
+
this.auth,
|
|
411
|
+
this.googleProvider
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Check for existing user
|
|
415
|
+
try {
|
|
416
|
+
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
417
|
+
return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
// User doesn't exist, create anonymous patient
|
|
420
|
+
const functions = await this.getFunctions();
|
|
421
|
+
const createAnonymousPatientProfile = httpsCallable<
|
|
422
|
+
{},
|
|
423
|
+
PatientProfileResponse
|
|
424
|
+
>(functions, "createAnonymousPatientProfile");
|
|
425
|
+
|
|
426
|
+
const result = await createAnonymousPatientProfile({});
|
|
427
|
+
return result.data.user;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Prijavljuje korisnika sa Apple-om
|
|
433
|
+
*/
|
|
434
|
+
async signInWithApple(): Promise<User> {
|
|
435
|
+
const provider = new OAuthProvider("apple.com");
|
|
436
|
+
provider.addScope("email");
|
|
437
|
+
provider.addScope("name");
|
|
438
|
+
const { user: firebaseUser } = await signInWithPopup(this.auth, provider);
|
|
439
|
+
|
|
440
|
+
// Check for existing user
|
|
441
|
+
try {
|
|
442
|
+
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
443
|
+
return this.userService.updateUserLoginTimestamp(firebaseUser.uid);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
// User doesn't exist, create anonymous patient
|
|
446
|
+
const functions = await this.getFunctions();
|
|
447
|
+
const createAnonymousPatientProfile = httpsCallable(
|
|
448
|
+
functions,
|
|
449
|
+
"createAnonymousPatientProfile"
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const result = await createAnonymousPatientProfile({});
|
|
453
|
+
return result.data.user;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Prijavljuje korisnika anonimno
|
|
459
|
+
*/
|
|
460
|
+
async signInAnonymously(): Promise<User> {
|
|
461
|
+
const { user: firebaseUser } = await firebaseSignInAnonymously(this.auth);
|
|
462
|
+
|
|
463
|
+
// Create anonymous patient profile using cloud function
|
|
464
|
+
const functions = await this.getFunctions();
|
|
465
|
+
const createAnonymousPatientProfile = httpsCallable(
|
|
466
|
+
functions,
|
|
467
|
+
"createAnonymousPatientProfile"
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const result = await createAnonymousPatientProfile({});
|
|
471
|
+
return result.data.user;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Odjavljuje trenutnog korisnika
|
|
476
|
+
*/
|
|
477
|
+
async signOut(): Promise<void> {
|
|
478
|
+
await firebaseSignOut(this.auth);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Vraća trenutno prijavljenog korisnika
|
|
483
|
+
*/
|
|
484
|
+
async getCurrentUser(): Promise<User | null> {
|
|
485
|
+
const firebaseUser = this.auth.currentUser;
|
|
486
|
+
if (!firebaseUser) return null;
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
return this.userService.getUserById(firebaseUser.uid);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error("Error getting current user:", error);
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Registruje callback za promene stanja autentifikacije
|
|
498
|
+
*/
|
|
499
|
+
onAuthStateChange(callback: (user: FirebaseUser | null) => void): () => void {
|
|
500
|
+
return onAuthStateChanged(this.auth, callback);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Upgrades an anonymous user to a regular user with email and password
|
|
505
|
+
*/
|
|
506
|
+
async upgradeAnonymousUser(email: string, password: string): Promise<User> {
|
|
507
|
+
try {
|
|
508
|
+
await emailSchema.parseAsync(email);
|
|
509
|
+
await passwordSchema.parseAsync(password);
|
|
510
|
+
|
|
511
|
+
const currentUser = this.auth.currentUser;
|
|
512
|
+
if (!currentUser) {
|
|
513
|
+
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
514
|
+
}
|
|
515
|
+
if (!currentUser.isAnonymous) {
|
|
516
|
+
throw new AuthError(
|
|
517
|
+
"User is not anonymous",
|
|
518
|
+
"AUTH/NOT_ANONYMOUS_USER",
|
|
519
|
+
400
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Create email credential
|
|
524
|
+
const credential = EmailAuthProvider.credential(email, password);
|
|
525
|
+
|
|
526
|
+
// Link credential to current user
|
|
527
|
+
await linkWithCredential(currentUser, credential);
|
|
528
|
+
|
|
529
|
+
// Call cloud function to update backend
|
|
530
|
+
const functions = await this.getFunctions();
|
|
531
|
+
const upgradeAnonymousPatient = httpsCallable<
|
|
532
|
+
{
|
|
533
|
+
email: string;
|
|
534
|
+
profileData: {
|
|
535
|
+
email: string;
|
|
536
|
+
};
|
|
537
|
+
},
|
|
538
|
+
PatientProfileResponse
|
|
539
|
+
>(functions, "upgradeAnonymousPatient");
|
|
540
|
+
|
|
541
|
+
const result = await upgradeAnonymousPatient({
|
|
542
|
+
email,
|
|
543
|
+
profileData: {
|
|
544
|
+
email,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
return result.data.user;
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (error instanceof z.ZodError) {
|
|
551
|
+
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
552
|
+
}
|
|
553
|
+
const firebaseError = error as FirebaseError;
|
|
554
|
+
if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
|
|
555
|
+
throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Upgrades an anonymous user to a regular user by signing in with a Google account.
|
|
563
|
+
*/
|
|
564
|
+
async upgradeAnonymousUserWithGoogle(): Promise<User> {
|
|
565
|
+
try {
|
|
566
|
+
const currentUser = this.auth.currentUser;
|
|
567
|
+
if (!currentUser) {
|
|
568
|
+
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
569
|
+
}
|
|
570
|
+
if (!currentUser.isAnonymous) {
|
|
571
|
+
throw new AuthError(
|
|
572
|
+
"User is not anonymous",
|
|
573
|
+
"AUTH/NOT_ANONYMOUS_USER",
|
|
574
|
+
400
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.googleProvider.addScope("email");
|
|
579
|
+
const userCredential = await linkWithPopup(
|
|
580
|
+
currentUser,
|
|
581
|
+
this.googleProvider
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
585
|
+
if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
586
|
+
|
|
587
|
+
// Call cloud function to update backend
|
|
588
|
+
const functions = await this.getFunctions();
|
|
589
|
+
const upgradeAnonymousPatient = httpsCallable(
|
|
590
|
+
functions,
|
|
591
|
+
"upgradeAnonymousPatient"
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const result = await upgradeAnonymousPatient({
|
|
595
|
+
email: userCredential.user.email,
|
|
596
|
+
profileData: {
|
|
597
|
+
email: userCredential.user.email,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
return result.data.user;
|
|
602
|
+
} catch (error: unknown) {
|
|
603
|
+
const firebaseError = error as FirebaseError;
|
|
604
|
+
if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
|
|
605
|
+
throw AUTH_ERRORS.POPUP_CLOSED;
|
|
606
|
+
}
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async upgradeAnonymousUserWithFacebook(): Promise<User> {
|
|
612
|
+
try {
|
|
613
|
+
const currentUser = this.auth.currentUser;
|
|
614
|
+
if (!currentUser) {
|
|
615
|
+
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
616
|
+
}
|
|
617
|
+
if (!currentUser.isAnonymous) {
|
|
618
|
+
throw new AuthError(
|
|
619
|
+
"User is not anonymous",
|
|
620
|
+
"AUTH/NOT_ANONYMOUS_USER",
|
|
621
|
+
400
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this.facebookProvider.addScope("email");
|
|
626
|
+
const userCredential = await linkWithPopup(
|
|
627
|
+
currentUser,
|
|
628
|
+
this.facebookProvider
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
632
|
+
if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
633
|
+
|
|
634
|
+
// Call cloud function to update backend
|
|
635
|
+
const functions = await this.getFunctions();
|
|
636
|
+
const upgradeAnonymousPatient = httpsCallable(
|
|
637
|
+
functions,
|
|
638
|
+
"upgradeAnonymousPatient"
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const result = await upgradeAnonymousPatient({
|
|
642
|
+
email: userCredential.user.email,
|
|
643
|
+
profileData: {
|
|
644
|
+
email: userCredential.user.email,
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
return result.data.user;
|
|
649
|
+
} catch (error: unknown) {
|
|
650
|
+
const firebaseError = error as FirebaseError;
|
|
651
|
+
if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
|
|
652
|
+
throw AUTH_ERRORS.POPUP_CLOSED;
|
|
653
|
+
}
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async upgradeAnonymousUserWithApple(): Promise<User> {
|
|
659
|
+
try {
|
|
660
|
+
const currentUser = this.auth.currentUser;
|
|
661
|
+
if (!currentUser) {
|
|
662
|
+
throw AUTH_ERRORS.NOT_AUTHENTICATED;
|
|
663
|
+
}
|
|
664
|
+
if (!currentUser.isAnonymous) {
|
|
665
|
+
throw new AuthError(
|
|
666
|
+
"User is not anonymous",
|
|
667
|
+
"AUTH/NOT_ANONYMOUS_USER",
|
|
668
|
+
400
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
this.appleProvider.addScope("email");
|
|
673
|
+
this.appleProvider.addScope("name");
|
|
674
|
+
const userCredential = await linkWithPopup(
|
|
675
|
+
currentUser,
|
|
676
|
+
this.appleProvider
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
if (!userCredential) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
680
|
+
if (!userCredential.user.email) throw AUTH_ERRORS.INVALID_CREDENTIAL;
|
|
681
|
+
|
|
682
|
+
// Call cloud function to update backend
|
|
683
|
+
const functions = await this.getFunctions();
|
|
684
|
+
const upgradeAnonymousPatient = httpsCallable(
|
|
685
|
+
functions,
|
|
686
|
+
"upgradeAnonymousPatient"
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
const result = await upgradeAnonymousPatient({
|
|
690
|
+
email: userCredential.user.email,
|
|
691
|
+
profileData: {
|
|
692
|
+
email: userCredential.user.email,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
return result.data.user;
|
|
697
|
+
} catch (error: unknown) {
|
|
698
|
+
const firebaseError = error as FirebaseError;
|
|
699
|
+
if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
|
|
700
|
+
throw AUTH_ERRORS.POPUP_CLOSED;
|
|
701
|
+
}
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Šalje email za resetovanje lozinke korisniku
|
|
708
|
+
* @param email Email adresa korisnika
|
|
709
|
+
* @returns Promise koji se razrešava kada je email poslat
|
|
710
|
+
*/
|
|
711
|
+
async sendPasswordResetEmail(email: string): Promise<void> {
|
|
712
|
+
try {
|
|
713
|
+
await emailSchema.parseAsync(email);
|
|
714
|
+
const functions = await this.getFunctions();
|
|
715
|
+
await sendPasswordResetEmail(this.auth, email);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (error instanceof z.ZodError) {
|
|
718
|
+
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const firebaseError = error as FirebaseError;
|
|
722
|
+
if (firebaseError.code === FirebaseErrorCode.USER_NOT_FOUND) {
|
|
723
|
+
throw AUTH_ERRORS.USER_NOT_FOUND;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Verifikuje kod za resetovanje lozinke iz email linka
|
|
732
|
+
* @param oobCode Kod iz URL-a za resetovanje lozinke
|
|
733
|
+
* @returns Promise koji se razrešava sa email adresom korisnika ako je kod validan
|
|
734
|
+
*/
|
|
735
|
+
async verifyPasswordResetCode(oobCode: string): Promise<string> {
|
|
736
|
+
try {
|
|
737
|
+
const functions = await this.getFunctions();
|
|
738
|
+
return await verifyPasswordResetCode(this.auth, oobCode);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const firebaseError = error as FirebaseError;
|
|
741
|
+
if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
|
|
742
|
+
throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
|
|
743
|
+
} else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
|
|
744
|
+
throw AUTH_ERRORS.INVALID_ACTION_CODE;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Potvrđuje resetovanje lozinke i postavlja novu lozinku
|
|
753
|
+
* @param oobCode Kod iz URL-a za resetovanje lozinke
|
|
754
|
+
* @param newPassword Nova lozinka
|
|
755
|
+
* @returns Promise koji se razrešava kada je lozinka promenjena
|
|
756
|
+
*/
|
|
757
|
+
async confirmPasswordReset(
|
|
758
|
+
oobCode: string,
|
|
759
|
+
newPassword: string
|
|
760
|
+
): Promise<void> {
|
|
761
|
+
try {
|
|
762
|
+
await passwordSchema.parseAsync(newPassword);
|
|
763
|
+
const functions = await this.getFunctions();
|
|
764
|
+
await confirmPasswordReset(this.auth, oobCode, newPassword);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (error instanceof z.ZodError) {
|
|
767
|
+
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const firebaseError = error as FirebaseError;
|
|
771
|
+
if (firebaseError.code === FirebaseErrorCode.EXPIRED_ACTION_CODE) {
|
|
772
|
+
throw AUTH_ERRORS.EXPIRED_ACTION_CODE;
|
|
773
|
+
} else if (firebaseError.code === FirebaseErrorCode.INVALID_ACTION_CODE) {
|
|
774
|
+
throw AUTH_ERRORS.INVALID_ACTION_CODE;
|
|
775
|
+
} else if (firebaseError.code === FirebaseErrorCode.WEAK_PASSWORD) {
|
|
776
|
+
throw AUTH_ERRORS.WEAK_PASSWORD;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Registers a new practitioner user with email and password
|
|
785
|
+
* Can either create a new practitioner profile or claim an existing draft profile with a token
|
|
786
|
+
*/
|
|
787
|
+
async signUpPractitioner(data: {
|
|
788
|
+
email: string;
|
|
789
|
+
password: string;
|
|
790
|
+
firstName: string;
|
|
791
|
+
lastName: string;
|
|
792
|
+
token?: string;
|
|
793
|
+
profileData?: Partial<CreatePractitionerData>;
|
|
794
|
+
}): Promise<{
|
|
795
|
+
user: User;
|
|
796
|
+
practitioner: Practitioner;
|
|
797
|
+
}> {
|
|
798
|
+
try {
|
|
799
|
+
console.log("[AUTH] Starting practitioner signup process", {
|
|
800
|
+
email: data.email,
|
|
801
|
+
hasToken: !!data.token,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Create Firebase user
|
|
805
|
+
console.log("[AUTH] Creating Firebase user");
|
|
806
|
+
let firebaseUser;
|
|
807
|
+
try {
|
|
808
|
+
const result = await createUserWithEmailAndPassword(
|
|
809
|
+
this.auth,
|
|
810
|
+
data.email,
|
|
811
|
+
data.password
|
|
812
|
+
);
|
|
813
|
+
firebaseUser = result.user;
|
|
814
|
+
console.log("[AUTH] Firebase user created successfully", {
|
|
815
|
+
uid: firebaseUser.uid,
|
|
816
|
+
});
|
|
817
|
+
} catch (firebaseError) {
|
|
818
|
+
console.error("[AUTH] Firebase user creation failed:", firebaseError);
|
|
819
|
+
throw firebaseError;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (data.token) {
|
|
823
|
+
// Claiming existing profile with token
|
|
824
|
+
console.log("[AUTH] Claiming draft profile with token");
|
|
825
|
+
|
|
826
|
+
const functions = await this.getFunctions();
|
|
827
|
+
const validateTokenAndClaimProfile = httpsCallable<
|
|
828
|
+
{
|
|
829
|
+
token: string;
|
|
830
|
+
},
|
|
831
|
+
PractitionerResponse
|
|
832
|
+
>(functions, "validateTokenAndClaimProfile");
|
|
833
|
+
|
|
834
|
+
const result = await validateTokenAndClaimProfile({
|
|
835
|
+
token: data.token,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
return result.data as PractitionerResponse;
|
|
839
|
+
} else {
|
|
840
|
+
// Creating new profile
|
|
841
|
+
console.log("[AUTH] Creating new practitioner profile");
|
|
842
|
+
|
|
843
|
+
// Prepare basic info based on form data
|
|
844
|
+
const profileData = data.profileData || {};
|
|
845
|
+
if (!profileData.basicInfo) {
|
|
846
|
+
profileData.basicInfo = {
|
|
847
|
+
firstName: data.firstName,
|
|
848
|
+
lastName: data.lastName,
|
|
849
|
+
email: data.email,
|
|
850
|
+
phoneNumber: "",
|
|
851
|
+
title: "Practitioner",
|
|
852
|
+
profileImageUrl: "",
|
|
853
|
+
dateOfBirth: new Date(),
|
|
854
|
+
gender: "other",
|
|
855
|
+
languages: ["English"],
|
|
856
|
+
bio: "",
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const functions = await this.getFunctions();
|
|
861
|
+
const createPractitionerProfile = httpsCallable<
|
|
862
|
+
{
|
|
863
|
+
profileData: Partial<CreatePractitionerData>;
|
|
864
|
+
},
|
|
865
|
+
PractitionerResponse
|
|
866
|
+
>(functions, "createPractitionerProfile");
|
|
867
|
+
|
|
868
|
+
const result = await createPractitionerProfile({
|
|
869
|
+
profileData,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
return result.data as PractitionerResponse;
|
|
873
|
+
}
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (error instanceof z.ZodError) {
|
|
876
|
+
console.error(
|
|
877
|
+
"[AUTH] Zod validation error in signUpPractitioner:",
|
|
878
|
+
JSON.stringify(error.errors, null, 2)
|
|
879
|
+
);
|
|
880
|
+
throw AUTH_ERRORS.VALIDATION_ERROR;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const firebaseError = error as FirebaseError;
|
|
884
|
+
if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
|
|
885
|
+
console.error("[AUTH] Email already in use:", data.email);
|
|
886
|
+
throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
console.error("[AUTH] Unhandled error in signUpPractitioner:", error);
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Signs in a user with email and password specifically for practitioner role
|
|
896
|
+
*/
|
|
897
|
+
async signInPractitioner(
|
|
898
|
+
email: string,
|
|
899
|
+
password: string
|
|
900
|
+
): Promise<{
|
|
901
|
+
user: User;
|
|
902
|
+
practitioner: Practitioner;
|
|
903
|
+
}> {
|
|
904
|
+
try {
|
|
905
|
+
console.log("[AUTH] Starting practitioner signin process", {
|
|
906
|
+
email: email,
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Sign in with email/password
|
|
910
|
+
const { user: firebaseUser } = await signInWithEmailAndPassword(
|
|
911
|
+
this.auth,
|
|
912
|
+
email,
|
|
913
|
+
password
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
// Get user data
|
|
917
|
+
const user = await this.userService.getUserById(firebaseUser.uid);
|
|
918
|
+
console.log("[AUTH] User retrieved", { uid: user.uid });
|
|
919
|
+
|
|
920
|
+
// Check if user has practitioner role
|
|
921
|
+
if (!user.roles?.includes(UserRole.PRACTITIONER)) {
|
|
922
|
+
console.error("[AUTH] User is not a practitioner:", user.uid);
|
|
923
|
+
throw AUTH_ERRORS.INVALID_ROLE;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Check and get practitioner profile
|
|
927
|
+
if (!user.practitionerProfile) {
|
|
928
|
+
console.error("[AUTH] User has no practitioner profile:", user.uid);
|
|
929
|
+
throw AUTH_ERRORS.NOT_FOUND;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Get practitioner profile using local service
|
|
933
|
+
// This will be updated to use cloud functions in future PRs
|
|
934
|
+
const practitionerService = new PractitionerService(
|
|
935
|
+
this.db,
|
|
936
|
+
this.auth,
|
|
937
|
+
this.app
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
const practitioner = await practitionerService.getPractitioner(
|
|
941
|
+
user.practitionerProfile
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
if (!practitioner) {
|
|
945
|
+
console.error(
|
|
946
|
+
"[AUTH] Practitioner profile not found:",
|
|
947
|
+
user.practitionerProfile
|
|
948
|
+
);
|
|
949
|
+
throw AUTH_ERRORS.NOT_FOUND;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
user,
|
|
954
|
+
practitioner,
|
|
955
|
+
};
|
|
956
|
+
} catch (error) {
|
|
957
|
+
console.error("[AUTH] Error in signInPractitioner:", error);
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|