@blackcode_sa/metaestetics-api 1.13.21 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -11738,6 +11738,240 @@ var PractitionerService = class extends BaseService {
11738
11738
  return null;
11739
11739
  }
11740
11740
  }
11741
+ /**
11742
+ * Finds all draft practitioner profiles by email address
11743
+ * Used when a doctor signs in with Google to show all clinic invitations
11744
+ *
11745
+ * @param email - Email address to search for
11746
+ * @returns Array of draft practitioner profiles with clinic information
11747
+ *
11748
+ * @remarks
11749
+ * Requires Firestore composite index on:
11750
+ * - Collection: practitioners
11751
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
11752
+ */
11753
+ async getDraftProfilesByEmail(email) {
11754
+ try {
11755
+ const normalizedEmail = email.toLowerCase().trim();
11756
+ console.log("[PRACTITIONER] Searching for all draft practitioners by email", {
11757
+ email: normalizedEmail
11758
+ });
11759
+ const q = query13(
11760
+ collection13(this.db, PRACTITIONERS_COLLECTION),
11761
+ where13("basicInfo.email", "==", normalizedEmail),
11762
+ where13("status", "==", "draft" /* DRAFT */),
11763
+ where13("userRef", "==", "")
11764
+ );
11765
+ const querySnapshot = await getDocs13(q);
11766
+ if (querySnapshot.empty) {
11767
+ console.log("[PRACTITIONER] No draft practitioners found for email", {
11768
+ email: normalizedEmail
11769
+ });
11770
+ return [];
11771
+ }
11772
+ const draftPractitioners = querySnapshot.docs.map(
11773
+ (doc47) => doc47.data()
11774
+ );
11775
+ console.log("[PRACTITIONER] Found draft practitioners", {
11776
+ email: normalizedEmail,
11777
+ count: draftPractitioners.length,
11778
+ practitionerIds: draftPractitioners.map((p) => p.id)
11779
+ });
11780
+ return draftPractitioners;
11781
+ } catch (error) {
11782
+ console.error(
11783
+ "[PRACTITIONER] Error finding draft practitioners by email:",
11784
+ error
11785
+ );
11786
+ return [];
11787
+ }
11788
+ }
11789
+ /**
11790
+ * Claims a draft practitioner profile and links it to a user account
11791
+ * Used when a doctor selects which clinic(s) to join after Google Sign-In
11792
+ *
11793
+ * @param practitionerId - ID of the draft practitioner profile to claim
11794
+ * @param userId - ID of the user account to link the profile to
11795
+ * @returns The claimed practitioner profile
11796
+ */
11797
+ async claimDraftProfileWithGoogle(practitionerId, userId) {
11798
+ try {
11799
+ console.log("[PRACTITIONER] Claiming draft profile with Google", {
11800
+ practitionerId,
11801
+ userId
11802
+ });
11803
+ const practitioner = await this.getPractitioner(practitionerId);
11804
+ if (!practitioner) {
11805
+ throw new Error(`Practitioner ${practitionerId} not found`);
11806
+ }
11807
+ if (practitioner.status !== "draft" /* DRAFT */) {
11808
+ throw new Error("This practitioner profile has already been claimed");
11809
+ }
11810
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
11811
+ if (existingPractitioner) {
11812
+ console.log("[PRACTITIONER] User already has profile, merging clinics");
11813
+ const mergedClinics = Array.from(/* @__PURE__ */ new Set([
11814
+ ...existingPractitioner.clinics,
11815
+ ...practitioner.clinics
11816
+ ]));
11817
+ const mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
11818
+ for (const workingHours of practitioner.clinicWorkingHours) {
11819
+ if (!mergedWorkingHours.find((wh) => wh.clinicId === workingHours.clinicId)) {
11820
+ mergedWorkingHours.push(workingHours);
11821
+ }
11822
+ }
11823
+ const mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
11824
+ for (const clinicInfo of practitioner.clinicsInfo) {
11825
+ if (!mergedClinicsInfo.find((ci) => ci.id === clinicInfo.id)) {
11826
+ mergedClinicsInfo.push(clinicInfo);
11827
+ }
11828
+ }
11829
+ const updatedPractitioner2 = await this.updatePractitioner(existingPractitioner.id, {
11830
+ clinics: mergedClinics,
11831
+ clinicWorkingHours: mergedWorkingHours,
11832
+ clinicsInfo: mergedClinicsInfo
11833
+ });
11834
+ await deleteDoc4(doc20(this.db, PRACTITIONERS_COLLECTION, practitionerId));
11835
+ const activeTokens2 = await this.getPractitionerActiveTokens(practitionerId);
11836
+ for (const token of activeTokens2) {
11837
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
11838
+ }
11839
+ return updatedPractitioner2;
11840
+ }
11841
+ const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
11842
+ userRef: userId,
11843
+ status: "active" /* ACTIVE */
11844
+ });
11845
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
11846
+ for (const token of activeTokens) {
11847
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
11848
+ }
11849
+ console.log("[PRACTITIONER] Draft profile claimed successfully", {
11850
+ practitionerId: updatedPractitioner.id,
11851
+ userId
11852
+ });
11853
+ return updatedPractitioner;
11854
+ } catch (error) {
11855
+ console.error(
11856
+ "[PRACTITIONER] Error claiming draft profile with Google:",
11857
+ error
11858
+ );
11859
+ throw error;
11860
+ }
11861
+ }
11862
+ /**
11863
+ * Claims multiple draft practitioner profiles and merges them into one profile
11864
+ * Used when a doctor selects multiple clinics to join after Google Sign-In
11865
+ *
11866
+ * @param practitionerIds - Array of draft practitioner profile IDs to claim
11867
+ * @param userId - ID of the user account to link the profiles to
11868
+ * @returns The claimed practitioner profile (first one becomes main, others merged)
11869
+ */
11870
+ async claimMultipleDraftProfilesWithGoogle(practitionerIds, userId) {
11871
+ try {
11872
+ if (practitionerIds.length === 0) {
11873
+ throw new Error("No practitioner IDs provided");
11874
+ }
11875
+ console.log("[PRACTITIONER] Claiming multiple draft profiles with Google", {
11876
+ practitionerIds,
11877
+ userId,
11878
+ count: practitionerIds.length
11879
+ });
11880
+ const draftProfiles = await Promise.all(
11881
+ practitionerIds.map((id) => this.getPractitioner(id))
11882
+ );
11883
+ const validDrafts = draftProfiles.filter((p) => {
11884
+ if (!p) return false;
11885
+ if (p.status !== "draft" /* DRAFT */) {
11886
+ throw new Error(`Practitioner ${p.id} has already been claimed`);
11887
+ }
11888
+ return true;
11889
+ });
11890
+ if (validDrafts.length === 0) {
11891
+ throw new Error("No valid draft profiles found");
11892
+ }
11893
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
11894
+ if (existingPractitioner) {
11895
+ let mergedClinics2 = new Set(existingPractitioner.clinics);
11896
+ let mergedWorkingHours2 = [...existingPractitioner.clinicWorkingHours];
11897
+ let mergedClinicsInfo2 = [...existingPractitioner.clinicsInfo];
11898
+ for (const draft of validDrafts) {
11899
+ draft.clinics.forEach((clinicId) => mergedClinics2.add(clinicId));
11900
+ for (const workingHours of draft.clinicWorkingHours) {
11901
+ if (!mergedWorkingHours2.find((wh) => wh.clinicId === workingHours.clinicId)) {
11902
+ mergedWorkingHours2.push(workingHours);
11903
+ }
11904
+ }
11905
+ for (const clinicInfo of draft.clinicsInfo) {
11906
+ if (!mergedClinicsInfo2.find((ci) => ci.id === clinicInfo.id)) {
11907
+ mergedClinicsInfo2.push(clinicInfo);
11908
+ }
11909
+ }
11910
+ }
11911
+ const updatedPractitioner2 = await this.updatePractitioner(existingPractitioner.id, {
11912
+ clinics: Array.from(mergedClinics2),
11913
+ clinicWorkingHours: mergedWorkingHours2,
11914
+ clinicsInfo: mergedClinicsInfo2
11915
+ });
11916
+ for (const draft of validDrafts) {
11917
+ await deleteDoc4(doc20(this.db, PRACTITIONERS_COLLECTION, draft.id));
11918
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
11919
+ for (const token of activeTokens) {
11920
+ await this.markTokenAsUsed(token.id, draft.id, userId);
11921
+ }
11922
+ }
11923
+ return updatedPractitioner2;
11924
+ }
11925
+ const mainDraft = validDrafts[0];
11926
+ const otherDrafts = validDrafts.slice(1);
11927
+ let mergedClinics = new Set(mainDraft.clinics);
11928
+ let mergedWorkingHours = [...mainDraft.clinicWorkingHours];
11929
+ let mergedClinicsInfo = [...mainDraft.clinicsInfo];
11930
+ for (const draft of otherDrafts) {
11931
+ draft.clinics.forEach((clinicId) => mergedClinics.add(clinicId));
11932
+ for (const workingHours of draft.clinicWorkingHours) {
11933
+ if (!mergedWorkingHours.find((wh) => wh.clinicId === workingHours.clinicId)) {
11934
+ mergedWorkingHours.push(workingHours);
11935
+ }
11936
+ }
11937
+ for (const clinicInfo of draft.clinicsInfo) {
11938
+ if (!mergedClinicsInfo.find((ci) => ci.id === clinicInfo.id)) {
11939
+ mergedClinicsInfo.push(clinicInfo);
11940
+ }
11941
+ }
11942
+ }
11943
+ const updatedPractitioner = await this.updatePractitioner(mainDraft.id, {
11944
+ userRef: userId,
11945
+ status: "active" /* ACTIVE */,
11946
+ clinics: Array.from(mergedClinics),
11947
+ clinicWorkingHours: mergedWorkingHours,
11948
+ clinicsInfo: mergedClinicsInfo
11949
+ });
11950
+ const mainActiveTokens = await this.getPractitionerActiveTokens(mainDraft.id);
11951
+ for (const token of mainActiveTokens) {
11952
+ await this.markTokenAsUsed(token.id, mainDraft.id, userId);
11953
+ }
11954
+ for (const draft of otherDrafts) {
11955
+ await deleteDoc4(doc20(this.db, PRACTITIONERS_COLLECTION, draft.id));
11956
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
11957
+ for (const token of activeTokens) {
11958
+ await this.markTokenAsUsed(token.id, draft.id, userId);
11959
+ }
11960
+ }
11961
+ console.log("[PRACTITIONER] Multiple draft profiles claimed successfully", {
11962
+ practitionerId: updatedPractitioner.id,
11963
+ userId,
11964
+ mergedCount: validDrafts.length
11965
+ });
11966
+ return updatedPractitioner;
11967
+ } catch (error) {
11968
+ console.error(
11969
+ "[PRACTITIONER] Error claiming multiple draft profiles with Google:",
11970
+ error
11971
+ );
11972
+ throw error;
11973
+ }
11974
+ }
11741
11975
  /**
11742
11976
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
11743
11977
  */
@@ -15874,6 +16108,135 @@ var AuthService = class extends BaseService {
15874
16108
  throw handleFirebaseError(error);
15875
16109
  }
15876
16110
  }
16111
+ /**
16112
+ * Signs up or signs in a practitioner with Google authentication.
16113
+ * Checks for existing practitioner account or draft profiles.
16114
+ *
16115
+ * @param idToken - The Google ID token obtained from the mobile app
16116
+ * @returns Object containing user, practitioner (if exists), and draft profiles (if any)
16117
+ */
16118
+ async signUpPractitionerWithGoogle(idToken) {
16119
+ try {
16120
+ console.log("[AUTH] Starting practitioner Google Sign-In/Sign-Up");
16121
+ let email;
16122
+ try {
16123
+ const payloadBase64 = idToken.split(".")[1];
16124
+ const payloadJson = globalThis.atob ? globalThis.atob(payloadBase64) : Buffer.from(payloadBase64, "base64").toString("utf8");
16125
+ const payload = JSON.parse(payloadJson);
16126
+ email = payload.email;
16127
+ } catch (decodeError) {
16128
+ console.error("[AUTH] Failed to decode email from Google ID token:", decodeError);
16129
+ throw new AuthError(
16130
+ "Unable to read email from Google token. Please try again.",
16131
+ "AUTH/INVALID_GOOGLE_TOKEN",
16132
+ 400
16133
+ );
16134
+ }
16135
+ if (!email) {
16136
+ throw new AuthError(
16137
+ "Unable to read email from Google token. Please try again.",
16138
+ "AUTH/INVALID_GOOGLE_TOKEN",
16139
+ 400
16140
+ );
16141
+ }
16142
+ const normalizedEmail = email.toLowerCase().trim();
16143
+ console.log("[AUTH] Extracted email from Google token:", normalizedEmail);
16144
+ const methods = await fetchSignInMethodsForEmail2(this.auth, normalizedEmail);
16145
+ const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
16146
+ const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
16147
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
16148
+ if (hasGoogleMethod) {
16149
+ console.log("[AUTH] User exists with Google provider, signing in");
16150
+ const credential2 = GoogleAuthProvider.credential(idToken);
16151
+ const { user: firebaseUser2 } = await signInWithCredential(this.auth, credential2);
16152
+ const existingUser2 = await this.userService.getUserById(firebaseUser2.uid);
16153
+ if (!existingUser2) {
16154
+ await firebaseSignOut(this.auth);
16155
+ throw new AuthError(
16156
+ "No account found. Please contact support.",
16157
+ "AUTH/USER_NOT_FOUND",
16158
+ 404
16159
+ );
16160
+ }
16161
+ let practitioner2 = null;
16162
+ if (existingUser2.practitionerProfile) {
16163
+ practitioner2 = await practitionerService.getPractitioner(existingUser2.practitionerProfile);
16164
+ }
16165
+ const draftProfiles2 = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
16166
+ return {
16167
+ user: existingUser2,
16168
+ practitioner: practitioner2,
16169
+ draftProfiles: draftProfiles2
16170
+ };
16171
+ }
16172
+ if (hasEmailMethod && !hasGoogleMethod) {
16173
+ console.log("[AUTH] User exists with email/password only");
16174
+ throw new AuthError(
16175
+ "An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.",
16176
+ "AUTH/EMAIL_ALREADY_EXISTS",
16177
+ 409
16178
+ );
16179
+ }
16180
+ console.log("[AUTH] Signing in with Google credential");
16181
+ const credential = GoogleAuthProvider.credential(idToken);
16182
+ let firebaseUser;
16183
+ try {
16184
+ const result = await signInWithCredential(this.auth, credential);
16185
+ firebaseUser = result.user;
16186
+ } catch (error) {
16187
+ if (error.code === "auth/account-exists-with-different-credential") {
16188
+ throw new AuthError(
16189
+ "An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.",
16190
+ "AUTH/EMAIL_ALREADY_EXISTS",
16191
+ 409
16192
+ );
16193
+ }
16194
+ throw error;
16195
+ }
16196
+ let existingUser = null;
16197
+ try {
16198
+ const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
16199
+ if (existingUserDoc) {
16200
+ existingUser = existingUserDoc;
16201
+ console.log("[AUTH] Found existing User document");
16202
+ }
16203
+ } catch (error) {
16204
+ console.error("[AUTH] Error checking for existing user:", error);
16205
+ }
16206
+ console.log("[AUTH] Checking for draft profiles");
16207
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
16208
+ let user;
16209
+ if (existingUser) {
16210
+ user = existingUser;
16211
+ console.log("[AUTH] Using existing user account");
16212
+ } else {
16213
+ user = await this.userService.createUser(firebaseUser, ["practitioner" /* PRACTITIONER */], {
16214
+ skipProfileCreation: true
16215
+ });
16216
+ console.log("[AUTH] Created new user account");
16217
+ }
16218
+ let practitioner = null;
16219
+ if (user.practitionerProfile) {
16220
+ practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
16221
+ }
16222
+ console.log("[AUTH] Google Sign-In complete", {
16223
+ userId: user.uid,
16224
+ hasPractitioner: !!practitioner,
16225
+ draftProfilesCount: draftProfiles.length
16226
+ });
16227
+ return {
16228
+ user,
16229
+ practitioner,
16230
+ draftProfiles
16231
+ };
16232
+ } catch (error) {
16233
+ console.error("[AUTH] Error in signUpPractitionerWithGoogle:", error);
16234
+ if (error instanceof AuthError) {
16235
+ throw error;
16236
+ }
16237
+ throw handleFirebaseError(error);
16238
+ }
16239
+ }
15877
16240
  /**
15878
16241
  * Links a Google account to the currently signed-in user using an ID token.
15879
16242
  * This is used to upgrade an anonymous user or to allow an existing user
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.13.21",
4
+ "version": "1.14.0",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -78,10 +78,21 @@ export const practitionerInvitationTemplate = `
78
78
 
79
79
  <p>This token will expire on <strong>{{expirationDate}}</strong>.</p>
80
80
 
81
- <p>To create your account:</p>
81
+ <p><strong>You have two options to create your account:</strong></p>
82
+
83
+ <p><strong>Option 1: Sign in with Google (Recommended)</strong></p>
84
+ <ol>
85
+ <li>Open the MetaEsthetics Doctor App</li>
86
+ <li>Click "Sign in with Google" on the login screen</li>
87
+ <li>Select your Google account (use the email address: {{practitionerEmail}})</li>
88
+ <li>You'll see an invitation to join {{clinicName}} - simply select it and join!</li>
89
+ </ol>
90
+
91
+ <p><strong>Option 2: Use Email/Password with Token</strong></p>
82
92
  <ol>
83
93
  <li>Visit {{registrationUrl}}</li>
84
- <li>Enter your email and create a password</li>
94
+ <li>Click "Claim Existing Profile with Token"</li>
95
+ <li>Enter your email ({{practitionerEmail}}) and create a password</li>
85
96
  <li>When prompted, enter the token above</li>
86
97
  </ol>
87
98
 
@@ -1002,6 +1002,179 @@ export class AuthService extends BaseService {
1002
1002
  }
1003
1003
  }
1004
1004
 
1005
+ /**
1006
+ * Signs up or signs in a practitioner with Google authentication.
1007
+ * Checks for existing practitioner account or draft profiles.
1008
+ *
1009
+ * @param idToken - The Google ID token obtained from the mobile app
1010
+ * @returns Object containing user, practitioner (if exists), and draft profiles (if any)
1011
+ */
1012
+ async signUpPractitionerWithGoogle(
1013
+ idToken: string
1014
+ ): Promise<{
1015
+ user: User;
1016
+ practitioner: Practitioner | null;
1017
+ draftProfiles: Practitioner[];
1018
+ }> {
1019
+ try {
1020
+ console.log('[AUTH] Starting practitioner Google Sign-In/Sign-Up');
1021
+
1022
+ // Extract email from Google ID token
1023
+ let email: string | undefined;
1024
+ try {
1025
+ const payloadBase64 = idToken.split('.')[1];
1026
+ const payloadJson = globalThis.atob
1027
+ ? globalThis.atob(payloadBase64)
1028
+ : Buffer.from(payloadBase64, 'base64').toString('utf8');
1029
+ const payload = JSON.parse(payloadJson);
1030
+ email = payload.email as string | undefined;
1031
+ } catch (decodeError) {
1032
+ console.error('[AUTH] Failed to decode email from Google ID token:', decodeError);
1033
+ throw new AuthError(
1034
+ 'Unable to read email from Google token. Please try again.',
1035
+ 'AUTH/INVALID_GOOGLE_TOKEN',
1036
+ 400,
1037
+ );
1038
+ }
1039
+
1040
+ if (!email) {
1041
+ throw new AuthError(
1042
+ 'Unable to read email from Google token. Please try again.',
1043
+ 'AUTH/INVALID_GOOGLE_TOKEN',
1044
+ 400,
1045
+ );
1046
+ }
1047
+
1048
+ const normalizedEmail = email.toLowerCase().trim();
1049
+ console.log('[AUTH] Extracted email from Google token:', normalizedEmail);
1050
+
1051
+ // Check if user already exists in Firebase Auth
1052
+ const methods = await fetchSignInMethodsForEmail(this.auth, normalizedEmail);
1053
+ const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
1054
+ const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
1055
+
1056
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1057
+
1058
+ // Case 1: User exists with Google provider - sign in and check for practitioner profile
1059
+ if (hasGoogleMethod) {
1060
+ console.log('[AUTH] User exists with Google provider, signing in');
1061
+ const credential = GoogleAuthProvider.credential(idToken);
1062
+ const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1063
+
1064
+ const existingUser = await this.userService.getUserById(firebaseUser.uid);
1065
+ if (!existingUser) {
1066
+ await firebaseSignOut(this.auth);
1067
+ throw new AuthError(
1068
+ 'No account found. Please contact support.',
1069
+ 'AUTH/USER_NOT_FOUND',
1070
+ 404,
1071
+ );
1072
+ }
1073
+
1074
+ // Check if user has practitioner profile
1075
+ let practitioner: Practitioner | null = null;
1076
+ if (existingUser.practitionerProfile) {
1077
+ practitioner = await practitionerService.getPractitioner(existingUser.practitionerProfile);
1078
+ }
1079
+
1080
+ // Check for any new draft profiles
1081
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1082
+
1083
+ return {
1084
+ user: existingUser,
1085
+ practitioner,
1086
+ draftProfiles,
1087
+ };
1088
+ }
1089
+
1090
+ // Case 2: User exists with email/password - need to link Google provider
1091
+ // Firebase doesn't allow linking without being signed in first
1092
+ // So we need to ask user to sign in with email/password first, then link
1093
+ if (hasEmailMethod && !hasGoogleMethod) {
1094
+ console.log('[AUTH] User exists with email/password only');
1095
+ throw new AuthError(
1096
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1097
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1098
+ 409,
1099
+ );
1100
+ }
1101
+
1102
+ // Case 3: New user - sign in with Google and check for draft profiles
1103
+ console.log('[AUTH] Signing in with Google credential');
1104
+ const credential = GoogleAuthProvider.credential(idToken);
1105
+
1106
+ let firebaseUser: FirebaseUser;
1107
+ try {
1108
+ const result = await signInWithCredential(this.auth, credential);
1109
+ firebaseUser = result.user;
1110
+ } catch (error: any) {
1111
+ // If sign-in fails because email already exists with different provider
1112
+ if (error.code === 'auth/account-exists-with-different-credential') {
1113
+ throw new AuthError(
1114
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1115
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1116
+ 409,
1117
+ );
1118
+ }
1119
+ throw error;
1120
+ }
1121
+
1122
+ // Check for existing User document (in case user had email/password account that was just linked)
1123
+ let existingUser: User | null = null;
1124
+ try {
1125
+ const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
1126
+ if (existingUserDoc) {
1127
+ existingUser = existingUserDoc;
1128
+ console.log('[AUTH] Found existing User document');
1129
+ }
1130
+ } catch (error) {
1131
+ console.error('[AUTH] Error checking for existing user:', error);
1132
+ // Continue with new user creation
1133
+ }
1134
+
1135
+ // Check for draft profiles
1136
+ console.log('[AUTH] Checking for draft profiles');
1137
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1138
+
1139
+ let user: User;
1140
+ if (existingUser) {
1141
+ // User exists - use existing account
1142
+ user = existingUser;
1143
+ console.log('[AUTH] Using existing user account');
1144
+ } else {
1145
+ // Create new user document
1146
+ user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1147
+ skipProfileCreation: true,
1148
+ });
1149
+ console.log('[AUTH] Created new user account');
1150
+ }
1151
+
1152
+ // Check if user already has practitioner profile
1153
+ let practitioner: Practitioner | null = null;
1154
+ if (user.practitionerProfile) {
1155
+ practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1156
+ }
1157
+
1158
+ console.log('[AUTH] Google Sign-In complete', {
1159
+ userId: user.uid,
1160
+ hasPractitioner: !!practitioner,
1161
+ draftProfilesCount: draftProfiles.length,
1162
+ });
1163
+
1164
+ return {
1165
+ user,
1166
+ practitioner,
1167
+ draftProfiles,
1168
+ };
1169
+ } catch (error) {
1170
+ console.error('[AUTH] Error in signUpPractitionerWithGoogle:', error);
1171
+ if (error instanceof AuthError) {
1172
+ throw error;
1173
+ }
1174
+ throw handleFirebaseError(error);
1175
+ }
1176
+ }
1177
+
1005
1178
  /**
1006
1179
  * Links a Google account to the currently signed-in user using an ID token.
1007
1180
  * This is used to upgrade an anonymous user or to allow an existing user