@blackcode_sa/metaestetics-api 1.14.18 → 1.14.26
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.js +2 -13
- package/dist/admin/index.mjs +2 -13
- package/dist/backoffice/index.d.mts +9 -4
- package/dist/backoffice/index.d.ts +9 -4
- package/dist/backoffice/index.js +18 -8
- package/dist/backoffice/index.mjs +18 -8
- package/dist/index.d.mts +20 -18
- package/dist/index.d.ts +20 -18
- package/dist/index.js +82 -179
- package/dist/index.mjs +135 -232
- package/package.json +4 -1
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +2 -13
- package/src/backoffice/services/brand.service.ts +21 -4
- package/src/backoffice/types/brand.types.ts +2 -0
- package/src/services/auth/auth.service.ts +7 -203
- package/src/services/practitioner/practitioner.service.ts +79 -4
- package/src/services/user/user.service.ts +1 -51
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackcode_sa/metaestetics-api",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.14.
|
|
4
|
+
"version": "1.14.26",
|
|
5
5
|
"description": "Firebase authentication service with anonymous upgrade support",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -117,5 +117,8 @@
|
|
|
117
117
|
"setupFilesAfterEnv": [
|
|
118
118
|
"<rootDir>/jest.setup.ts"
|
|
119
119
|
]
|
|
120
|
+
},
|
|
121
|
+
"overrides": {
|
|
122
|
+
"node-forge": ">=1.3.2"
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -78,21 +78,10 @@ export const practitionerInvitationTemplate = `
|
|
|
78
78
|
|
|
79
79
|
<p>This token will expire on <strong>{{expirationDate}}</strong>.</p>
|
|
80
80
|
|
|
81
|
-
<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>
|
|
81
|
+
<p>To create your account:</p>
|
|
92
82
|
<ol>
|
|
93
83
|
<li>Visit {{registrationUrl}}</li>
|
|
94
|
-
<li>
|
|
95
|
-
<li>Enter your email ({{practitionerEmail}}) and create a password</li>
|
|
84
|
+
<li>Enter your email and create a password</li>
|
|
96
85
|
<li>When prompted, enter the token above</li>
|
|
97
86
|
</ol>
|
|
98
87
|
|
|
@@ -46,12 +46,18 @@ export class BrandService extends BaseService {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Gets a paginated list of active brands, optionally filtered by name.
|
|
49
|
+
* Gets a paginated list of active brands, optionally filtered by name and category.
|
|
50
50
|
* @param rowsPerPage - The number of brands to fetch.
|
|
51
51
|
* @param searchTerm - An optional string to filter brand names by (starts-with search).
|
|
52
52
|
* @param lastVisible - An optional document snapshot to use as a cursor for pagination.
|
|
53
|
+
* @param category - An optional category to filter brands by.
|
|
53
54
|
*/
|
|
54
|
-
async getAll(
|
|
55
|
+
async getAll(
|
|
56
|
+
rowsPerPage: number,
|
|
57
|
+
searchTerm?: string,
|
|
58
|
+
lastVisible?: any,
|
|
59
|
+
category?: string
|
|
60
|
+
) {
|
|
55
61
|
const constraints: QueryConstraint[] = [
|
|
56
62
|
where("isActive", "==", true),
|
|
57
63
|
orderBy("name_lowercase"),
|
|
@@ -65,6 +71,10 @@ export class BrandService extends BaseService {
|
|
|
65
71
|
);
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
if (category) {
|
|
75
|
+
constraints.push(where("category", "==", category));
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
if (lastVisible) {
|
|
69
79
|
constraints.push(startAfter(lastVisible));
|
|
70
80
|
}
|
|
@@ -87,10 +97,11 @@ export class BrandService extends BaseService {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
/**
|
|
90
|
-
* Gets the total count of active brands, optionally filtered by name.
|
|
100
|
+
* Gets the total count of active brands, optionally filtered by name and category.
|
|
91
101
|
* @param searchTerm - An optional string to filter brand names by (starts-with search).
|
|
102
|
+
* @param category - An optional category to filter brands by.
|
|
92
103
|
*/
|
|
93
|
-
async getBrandsCount(searchTerm?: string) {
|
|
104
|
+
async getBrandsCount(searchTerm?: string, category?: string) {
|
|
94
105
|
const constraints: QueryConstraint[] = [where("isActive", "==", true)];
|
|
95
106
|
|
|
96
107
|
if (searchTerm) {
|
|
@@ -101,6 +112,10 @@ export class BrandService extends BaseService {
|
|
|
101
112
|
);
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
if (category) {
|
|
116
|
+
constraints.push(where("category", "==", category));
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
const q = query(this.getBrandsRef(), ...constraints);
|
|
105
120
|
const snapshot = await getCountFromServer(q);
|
|
106
121
|
return snapshot.data().count;
|
|
@@ -184,6 +199,7 @@ export class BrandService extends BaseService {
|
|
|
184
199
|
"id",
|
|
185
200
|
"name",
|
|
186
201
|
"manufacturer",
|
|
202
|
+
"category",
|
|
187
203
|
"website",
|
|
188
204
|
"description",
|
|
189
205
|
"isActive",
|
|
@@ -230,6 +246,7 @@ export class BrandService extends BaseService {
|
|
|
230
246
|
brand.id ?? "",
|
|
231
247
|
brand.name ?? "",
|
|
232
248
|
brand.manufacturer ?? "",
|
|
249
|
+
brand.category ?? "",
|
|
233
250
|
brand.website ?? "",
|
|
234
251
|
brand.description ?? "",
|
|
235
252
|
String(brand.isActive ?? ""),
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @property manufacturer - Naziv proizvođača
|
|
8
8
|
* @property description - Detaljan opis brenda i njegovih proizvoda
|
|
9
9
|
* @property website - Web stranica brenda
|
|
10
|
+
* @property category - Kategorija brenda (npr. "laser", "peeling", "injectables") - za filtriranje
|
|
10
11
|
* @property isActive - Da li je brend aktivan u sistemu
|
|
11
12
|
* @property createdAt - Datum kreiranja
|
|
12
13
|
* @property updatedAt - Datum poslednjeg ažuriranja
|
|
@@ -21,6 +22,7 @@ export interface Brand {
|
|
|
21
22
|
isActive: boolean;
|
|
22
23
|
website?: string;
|
|
23
24
|
description?: string;
|
|
25
|
+
category?: string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
Timestamp,
|
|
36
36
|
runTransaction,
|
|
37
37
|
Firestore,
|
|
38
|
-
serverTimestamp,
|
|
39
38
|
} from 'firebase/firestore';
|
|
40
39
|
import { FirebaseApp } from 'firebase/app';
|
|
41
40
|
import { User, UserRole, USERS_COLLECTION } from '../../types';
|
|
@@ -91,91 +90,40 @@ export class AuthService extends BaseService {
|
|
|
91
90
|
constructor(db: Firestore, auth: Auth, app: FirebaseApp, userService: UserService) {
|
|
92
91
|
super(db, auth, app);
|
|
93
92
|
this.userService = userService || new UserService(db, auth, app);
|
|
94
|
-
|
|
95
|
-
// Initialize auth state change listener for debugging
|
|
96
|
-
// This helps track when auth.currentUser changes, which is critical for debugging
|
|
97
|
-
// the permission-denied issue during user document creation
|
|
98
|
-
onAuthStateChanged(this.auth, (user) => {
|
|
99
|
-
const timestamp = new Date().toISOString();
|
|
100
|
-
const stackTrace = new Error().stack?.split('\n').slice(2, 5).join('\n') || 'N/A';
|
|
101
|
-
console.log(`[AUTH STATE CHANGE] ${timestamp}`);
|
|
102
|
-
console.log(`[AUTH STATE CHANGE] User: ${user?.uid || 'NULL'} (email: ${user?.email || 'N/A'})`);
|
|
103
|
-
console.log(`[AUTH STATE CHANGE] auth.currentUser: ${this.auth.currentUser?.uid || 'NULL'}`);
|
|
104
|
-
console.log(`[AUTH STATE CHANGE] Stack trace (first 3 frames):\n${stackTrace}`);
|
|
105
|
-
console.log('[AUTH STATE CHANGE] ---');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Log initial auth state
|
|
109
|
-
console.log('[AUTH] AuthService initialized');
|
|
110
|
-
console.log('[AUTH] Initial auth.currentUser:', this.auth.currentUser?.uid || 'NULL');
|
|
111
93
|
}
|
|
112
94
|
|
|
113
95
|
/**
|
|
114
96
|
* Waits for Firebase Auth state to settle after sign-in.
|
|
115
|
-
*
|
|
116
|
-
* In React Native with AsyncStorage persistence, there's a critical issue:
|
|
117
|
-
* 1. signInWithCredential() sets auth.currentUser in memory immediately
|
|
118
|
-
* 2. But AsyncStorage persistence happens asynchronously
|
|
119
|
-
* 3. If AsyncStorage reads an old NULL value, it can OVERWRITE the current auth state
|
|
120
|
-
* 4. This causes auth.currentUser to become NULL even after it was set
|
|
121
|
-
*
|
|
122
|
-
* This method uses onAuthStateChanged to wait for the auth state to be SET and STABLE.
|
|
123
|
-
* It ensures the auth state persists through AsyncStorage operations.
|
|
124
|
-
*
|
|
125
|
-
* @param expectedUid - The UID we expect to see in auth.currentUser
|
|
126
|
-
* @param timeoutMs - Maximum time to wait (default 5 seconds)
|
|
127
|
-
* @returns Promise that resolves when auth state is ready and stable
|
|
97
|
+
* In React Native with AsyncStorage persistence, auth state may not be immediately available.
|
|
128
98
|
*/
|
|
129
99
|
private async waitForAuthStateToSettle(expectedUid: string, timeoutMs: number = 5000): Promise<void> {
|
|
130
|
-
// If already correct, still wait a bit to ensure it's stable (not just set in memory)
|
|
131
100
|
if (this.auth.currentUser?.uid === expectedUid) {
|
|
132
|
-
console.log('[AUTH] Auth state appears set, waiting for stability...');
|
|
133
|
-
// Wait a small amount to ensure AsyncStorage persistence completes
|
|
134
101
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
135
|
-
// Check again after wait
|
|
136
102
|
if (this.auth.currentUser?.uid === expectedUid) {
|
|
137
|
-
console.log('[AUTH] ✅ Auth state stable for:', expectedUid);
|
|
138
103
|
return;
|
|
139
104
|
}
|
|
140
105
|
}
|
|
141
|
-
|
|
142
|
-
console.log('[AUTH] Waiting for auth state to settle for:', expectedUid);
|
|
143
|
-
console.log('[AUTH] Current auth.currentUser:', this.auth.currentUser?.uid || 'NULL');
|
|
144
106
|
|
|
145
107
|
return new Promise((resolve, reject) => {
|
|
146
108
|
const startTime = Date.now();
|
|
147
109
|
let resolved = false;
|
|
148
110
|
|
|
149
|
-
// Use onAuthStateChanged to wait for auth state to be SET
|
|
150
|
-
// This is more reliable than polling auth.currentUser
|
|
151
111
|
const unsubscribe = onAuthStateChanged(this.auth, (user) => {
|
|
152
112
|
if (resolved) return;
|
|
153
113
|
|
|
154
114
|
const currentUid = user?.uid || null;
|
|
155
|
-
console.log('[AUTH] onAuthStateChanged fired:', currentUid || 'NULL');
|
|
156
115
|
|
|
157
116
|
if (currentUid === expectedUid) {
|
|
158
|
-
// Auth state is set, but wait a bit more to ensure it's stable
|
|
159
|
-
// AsyncStorage might still be writing/reading
|
|
160
117
|
setTimeout(() => {
|
|
161
118
|
if (resolved) return;
|
|
162
119
|
|
|
163
|
-
// Final check - is it still set?
|
|
164
120
|
if (this.auth.currentUser?.uid === expectedUid) {
|
|
165
121
|
resolved = true;
|
|
166
122
|
unsubscribe();
|
|
167
123
|
clearTimeout(timeout);
|
|
168
|
-
const elapsed = Date.now() - startTime;
|
|
169
|
-
console.log(`[AUTH] ✅ Auth state settled and stable after ${elapsed}ms for:`, expectedUid);
|
|
170
124
|
resolve();
|
|
171
|
-
} else {
|
|
172
|
-
console.warn('[AUTH] ⚠️ Auth state became NULL after being set, waiting more...');
|
|
173
125
|
}
|
|
174
|
-
}, 300);
|
|
175
|
-
} else if (currentUid === null && Date.now() - startTime > 1000) {
|
|
176
|
-
// Auth state became NULL after being set - this is the bug we're trying to fix
|
|
177
|
-
console.error('[AUTH] ❌ Auth state became NULL after being set!');
|
|
178
|
-
console.error('[AUTH] This indicates AsyncStorage persistence issue');
|
|
126
|
+
}, 300);
|
|
179
127
|
}
|
|
180
128
|
});
|
|
181
129
|
|
|
@@ -183,9 +131,6 @@ export class AuthService extends BaseService {
|
|
|
183
131
|
if (resolved) return;
|
|
184
132
|
resolved = true;
|
|
185
133
|
unsubscribe();
|
|
186
|
-
console.error('[AUTH] ❌ Timeout waiting for auth state to settle');
|
|
187
|
-
console.error('[AUTH] Expected UID:', expectedUid);
|
|
188
|
-
console.error('[AUTH] Actual auth.currentUser:', this.auth.currentUser?.uid || 'NULL');
|
|
189
134
|
reject(new Error(`Timeout waiting for auth state to settle. Expected: ${expectedUid}, Got: ${this.auth.currentUser?.uid || 'NULL'}`));
|
|
190
135
|
}, timeoutMs);
|
|
191
136
|
});
|
|
@@ -935,31 +880,16 @@ export class AuthService extends BaseService {
|
|
|
935
880
|
throw new AuthError('No practitioner profiles selected to claim', 'AUTH/NO_PROFILES_SELECTED', 400);
|
|
936
881
|
}
|
|
937
882
|
|
|
938
|
-
// Check auth state BEFORE sign-in
|
|
939
|
-
console.log('[AUTH] currentUser BEFORE sign-in:', this.auth.currentUser?.uid || 'NULL');
|
|
940
|
-
|
|
941
|
-
// Sign in with Google credential
|
|
942
|
-
console.log('[AUTH] Signing in with Google credential...');
|
|
943
883
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
944
884
|
const result = await signInWithCredential(this.auth, credential);
|
|
945
885
|
const firebaseUser = result.user;
|
|
946
|
-
|
|
947
|
-
// Check auth state IMMEDIATELY AFTER sign-in
|
|
948
|
-
console.log('[AUTH] currentUser IMMEDIATELY AFTER sign-in:', this.auth.currentUser?.uid || 'NULL');
|
|
949
|
-
console.log('[AUTH] User returned from signInWithCredential:', firebaseUser.uid);
|
|
950
886
|
|
|
951
887
|
const practitionerService = new PractitionerService(this.db, this.auth, this.app);
|
|
952
888
|
|
|
953
|
-
// Step 1: Get User document (should already exist from signUpPractitionerWithGoogle)
|
|
954
|
-
// The User document is created IMMEDIATELY after Google sign-in in signUpPractitionerWithGoogle
|
|
955
|
-
// This matches the token flow pattern - no delays between sign-in and User doc creation
|
|
956
889
|
let user: User;
|
|
957
890
|
try {
|
|
958
891
|
user = await this.userService.getUserById(firebaseUser.uid);
|
|
959
|
-
console.log('[AUTH] User document found:', user.uid);
|
|
960
892
|
} catch (userError) {
|
|
961
|
-
console.error('[AUTH] ❌ User document should already exist! It should have been created in signUpPractitionerWithGoogle.');
|
|
962
|
-
console.error('[AUTH] This indicates a bug - User doc creation failed in the initial Google sign-in flow.');
|
|
963
893
|
throw new AuthError(
|
|
964
894
|
'User account not properly initialized. Please try signing in again.',
|
|
965
895
|
'AUTH/USER_NOT_INITIALIZED',
|
|
@@ -967,39 +897,27 @@ export class AuthService extends BaseService {
|
|
|
967
897
|
);
|
|
968
898
|
}
|
|
969
899
|
|
|
970
|
-
// Step 3: Claim the draft profiles
|
|
971
900
|
let practitioner: Practitioner;
|
|
972
901
|
if (practitionerIds.length === 1) {
|
|
973
|
-
console.log('[AUTH] Claiming single draft profile:', practitionerIds[0]);
|
|
974
902
|
practitioner = await practitionerService.claimDraftProfileWithGoogle(
|
|
975
903
|
practitionerIds[0],
|
|
976
904
|
firebaseUser.uid
|
|
977
905
|
);
|
|
978
906
|
} else {
|
|
979
|
-
console.log('[AUTH] Claiming multiple draft profiles:', practitionerIds);
|
|
980
907
|
practitioner = await practitionerService.claimMultipleDraftProfilesWithGoogle(
|
|
981
908
|
practitionerIds,
|
|
982
909
|
firebaseUser.uid
|
|
983
910
|
);
|
|
984
911
|
}
|
|
985
|
-
console.log('[AUTH] Draft profiles claimed:', practitioner.id);
|
|
986
912
|
|
|
987
|
-
// Step 4: Link practitioner to user
|
|
988
913
|
if (!user.practitionerProfile || user.practitionerProfile !== practitioner.id) {
|
|
989
|
-
console.log('[AUTH] Linking practitioner to user');
|
|
990
914
|
await this.userService.updateUser(firebaseUser.uid, {
|
|
991
915
|
practitionerProfile: practitioner.id,
|
|
992
916
|
});
|
|
993
917
|
}
|
|
994
918
|
|
|
995
|
-
// Fetch updated user (with practitionerProfile reference)
|
|
996
919
|
const updatedUser = await this.userService.getUserById(firebaseUser.uid);
|
|
997
920
|
|
|
998
|
-
console.log('[AUTH] Draft profiles claimed successfully', {
|
|
999
|
-
userId: updatedUser.uid,
|
|
1000
|
-
practitionerId: practitioner.id,
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
921
|
return {
|
|
1004
922
|
user: updatedUser,
|
|
1005
923
|
practitioner,
|
|
@@ -1180,13 +1098,6 @@ export class AuthService extends BaseService {
|
|
|
1180
1098
|
const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
|
|
1181
1099
|
console.log('[AUTH] Firebase user signed in:', firebaseUser.uid);
|
|
1182
1100
|
|
|
1183
|
-
// CRITICAL: Wait for auth state to settle before making Firestore queries
|
|
1184
|
-
// In React Native with AsyncStorage persistence, auth.currentUser can be NULL
|
|
1185
|
-
// immediately after signInWithCredential due to async persistence race condition
|
|
1186
|
-
console.log('[AUTH] Waiting for auth state to settle after sign-in...');
|
|
1187
|
-
await this.waitForAuthStateToSettle(firebaseUser.uid);
|
|
1188
|
-
console.log('[AUTH] ✅ Auth state settled, proceeding with Firestore queries');
|
|
1189
|
-
|
|
1190
1101
|
// 4) Load our domain user document.
|
|
1191
1102
|
const existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
1192
1103
|
if (existingUser) {
|
|
@@ -1252,77 +1163,37 @@ export class AuthService extends BaseService {
|
|
|
1252
1163
|
}
|
|
1253
1164
|
|
|
1254
1165
|
const normalizedEmail = email.toLowerCase().trim();
|
|
1255
|
-
console.log('[AUTH] Extracted email from Google token:', normalizedEmail);
|
|
1256
1166
|
|
|
1257
|
-
// Check if user already exists in Firebase Auth
|
|
1258
1167
|
const methods = await fetchSignInMethodsForEmail(this.auth, normalizedEmail);
|
|
1259
1168
|
const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
|
|
1260
1169
|
const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
|
|
1261
1170
|
|
|
1262
1171
|
const practitionerService = new PractitionerService(this.db, this.auth, this.app);
|
|
1263
1172
|
|
|
1264
|
-
// Case 1: User exists with Google provider - sign in and check for practitioner profile
|
|
1265
1173
|
if (hasGoogleMethod) {
|
|
1266
|
-
console.log('[AUTH] User exists with Google provider, signing in');
|
|
1267
1174
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
1268
1175
|
const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
|
|
1269
1176
|
|
|
1270
|
-
// CRITICAL: Wait for auth state to settle before making Firestore queries
|
|
1271
|
-
// In React Native with AsyncStorage persistence, auth.currentUser can be NULL
|
|
1272
|
-
// immediately after signInWithCredential due to async persistence race condition
|
|
1273
|
-
console.log('[AUTH] Waiting for auth state to settle after sign-in...');
|
|
1274
1177
|
await this.waitForAuthStateToSettle(firebaseUser.uid);
|
|
1275
|
-
console.log('[AUTH] ✅ Auth state settled, proceeding with Firestore queries');
|
|
1276
1178
|
|
|
1277
1179
|
let existingUser: User | null = null;
|
|
1278
1180
|
try {
|
|
1279
1181
|
existingUser = await this.userService.getUserById(firebaseUser.uid);
|
|
1280
|
-
console.log('[AUTH] User document found:', existingUser.uid);
|
|
1281
1182
|
} catch (userError: any) {
|
|
1282
|
-
// User document doesn't exist in Firestore
|
|
1283
|
-
console.log('[AUTH] User document not found in Firestore, checking for draft profiles', {
|
|
1284
|
-
errorCode: userError?.code,
|
|
1285
|
-
errorMessage: userError?.message,
|
|
1286
|
-
errorType: userError?.constructor?.name,
|
|
1287
|
-
isAuthError: userError instanceof AuthError,
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
// Check for draft profiles before signing out
|
|
1291
|
-
// CRITICAL: Firestore queries can trigger AsyncStorage reads which overwrite auth.currentUser
|
|
1292
|
-
// If auth.currentUser is NULL, re-sign in immediately to restore it before the query
|
|
1293
1183
|
if (!this.auth.currentUser || this.auth.currentUser.uid !== firebaseUser.uid) {
|
|
1294
|
-
console.error('[AUTH] ❌ auth.currentUser became NULL before draft profile query!');
|
|
1295
|
-
console.error('[AUTH] Expected UID:', firebaseUser.uid);
|
|
1296
|
-
console.error('[AUTH] Actual auth.currentUser:', this.auth.currentUser?.uid || 'NULL');
|
|
1297
|
-
console.log('[AUTH] Re-signing in to restore auth state before query...');
|
|
1298
|
-
|
|
1299
|
-
// Re-sign in with the credential to restore auth.currentUser
|
|
1300
|
-
// This is the most reliable way to ensure auth state is set before Firestore queries
|
|
1301
1184
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
1302
1185
|
await signInWithCredential(this.auth, credential);
|
|
1303
|
-
|
|
1304
|
-
// Wait for auth state to settle after re-sign-in
|
|
1305
1186
|
await this.waitForAuthStateToSettle(firebaseUser.uid, 2000);
|
|
1306
|
-
|
|
1307
|
-
console.log('[AUTH] ✅ Auth state restored, proceeding with query');
|
|
1308
1187
|
}
|
|
1309
1188
|
|
|
1310
1189
|
const practitionerService = new PractitionerService(this.db, this.auth, this.app);
|
|
1311
1190
|
const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
|
|
1312
1191
|
|
|
1313
|
-
console.log('[AUTH] Draft profiles check result:', {
|
|
1314
|
-
email: normalizedEmail,
|
|
1315
|
-
draftProfilesCount: draftProfiles.length,
|
|
1316
|
-
draftProfileIds: draftProfiles.map(p => p.id),
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
1192
|
if (draftProfiles.length === 0) {
|
|
1320
|
-
// No draft profiles - sign out and throw error
|
|
1321
|
-
console.log('[AUTH] No draft profiles found, signing out and throwing error');
|
|
1322
1193
|
try {
|
|
1323
1194
|
await firebaseSignOut(this.auth);
|
|
1324
1195
|
} catch (signOutError) {
|
|
1325
|
-
console.warn('[AUTH] Error signing out
|
|
1196
|
+
console.warn('[AUTH] Error signing out:', signOutError);
|
|
1326
1197
|
}
|
|
1327
1198
|
throw new AuthError(
|
|
1328
1199
|
'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
|
|
@@ -1331,35 +1202,21 @@ export class AuthService extends BaseService {
|
|
|
1331
1202
|
);
|
|
1332
1203
|
}
|
|
1333
1204
|
|
|
1334
|
-
// Draft profiles exist - CREATE USER DOCUMENT IMMEDIATELY (like token flow)
|
|
1335
|
-
// This matches the token flow pattern: create User doc right after sign-in, before any delays
|
|
1336
|
-
console.log('[AUTH] Draft profiles found, creating User document IMMEDIATELY after sign-in');
|
|
1337
|
-
console.log('[AUTH] auth.currentUser at User creation time:', this.auth.currentUser?.uid || 'NULL');
|
|
1338
|
-
|
|
1339
1205
|
try {
|
|
1340
|
-
// Create User document immediately (same pattern as token flow)
|
|
1341
1206
|
const newUser = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
|
|
1342
1207
|
skipProfileCreation: true,
|
|
1343
1208
|
});
|
|
1344
|
-
console.log('[AUTH] ✅ User document created successfully:', newUser.uid);
|
|
1345
1209
|
|
|
1346
|
-
// Return user + draft profiles (user doc already exists, just need to claim profiles)
|
|
1347
1210
|
return {
|
|
1348
1211
|
user: newUser,
|
|
1349
1212
|
practitioner: null,
|
|
1350
1213
|
draftProfiles: draftProfiles,
|
|
1351
1214
|
};
|
|
1352
1215
|
} catch (createUserError: any) {
|
|
1353
|
-
console.error('[AUTH] ❌ Failed to create User document:', {
|
|
1354
|
-
errorCode: createUserError?.code,
|
|
1355
|
-
errorMessage: createUserError?.message,
|
|
1356
|
-
uid: firebaseUser.uid,
|
|
1357
|
-
});
|
|
1358
|
-
// Sign out on failure
|
|
1359
1216
|
try {
|
|
1360
1217
|
await firebaseSignOut(this.auth);
|
|
1361
1218
|
} catch (signOutError) {
|
|
1362
|
-
console.warn('[AUTH] Error signing out
|
|
1219
|
+
console.warn('[AUTH] Error signing out:', signOutError);
|
|
1363
1220
|
}
|
|
1364
1221
|
throw createUserError;
|
|
1365
1222
|
}
|
|
@@ -1391,11 +1248,7 @@ export class AuthService extends BaseService {
|
|
|
1391
1248
|
};
|
|
1392
1249
|
}
|
|
1393
1250
|
|
|
1394
|
-
// Case 2: User exists with email/password - need to link Google provider
|
|
1395
|
-
// Firebase doesn't allow linking without being signed in first
|
|
1396
|
-
// So we need to ask user to sign in with email/password first, then link
|
|
1397
1251
|
if (hasEmailMethod && !hasGoogleMethod) {
|
|
1398
|
-
console.log('[AUTH] User exists with email/password only');
|
|
1399
1252
|
throw new AuthError(
|
|
1400
1253
|
'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
|
|
1401
1254
|
'AUTH/EMAIL_ALREADY_EXISTS',
|
|
@@ -1403,8 +1256,6 @@ export class AuthService extends BaseService {
|
|
|
1403
1256
|
);
|
|
1404
1257
|
}
|
|
1405
1258
|
|
|
1406
|
-
// Case 3: New user - sign in with Google and check for draft profiles
|
|
1407
|
-
console.log('[AUTH] Signing in with Google credential');
|
|
1408
1259
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
1409
1260
|
|
|
1410
1261
|
let firebaseUser: FirebaseUser;
|
|
@@ -1423,39 +1274,26 @@ export class AuthService extends BaseService {
|
|
|
1423
1274
|
throw error;
|
|
1424
1275
|
}
|
|
1425
1276
|
|
|
1426
|
-
// CRITICAL: Wait for auth state to settle before making Firestore queries
|
|
1427
|
-
// In React Native with AsyncStorage persistence, auth.currentUser can be NULL
|
|
1428
|
-
// immediately after signInWithCredential due to async persistence race condition
|
|
1429
|
-
console.log('[AUTH] Waiting for auth state to settle after sign-in...');
|
|
1430
1277
|
await this.waitForAuthStateToSettle(firebaseUser.uid);
|
|
1431
|
-
console.log('[AUTH] ✅ Auth state settled, proceeding with Firestore queries');
|
|
1432
1278
|
|
|
1433
|
-
// Check for existing User document (in case user had email/password account that was just linked)
|
|
1434
1279
|
let existingUser: User | null = null;
|
|
1435
1280
|
try {
|
|
1436
1281
|
const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
|
|
1437
1282
|
if (existingUserDoc) {
|
|
1438
1283
|
existingUser = existingUserDoc;
|
|
1439
|
-
console.log('[AUTH] Found existing User document');
|
|
1440
1284
|
}
|
|
1441
1285
|
} catch (error) {
|
|
1442
|
-
console.error('[AUTH] Error checking for existing user:', error);
|
|
1443
1286
|
// Continue with new user creation
|
|
1444
1287
|
}
|
|
1445
1288
|
|
|
1446
|
-
// Check for draft profiles
|
|
1447
|
-
console.log('[AUTH] Checking for draft profiles');
|
|
1448
1289
|
let draftProfiles: Practitioner[] = [];
|
|
1449
1290
|
try {
|
|
1450
1291
|
draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
|
|
1451
|
-
console.log('[AUTH] Draft profiles check complete', { count: draftProfiles.length });
|
|
1452
1292
|
} catch (draftCheckError: any) {
|
|
1453
|
-
console.error('[AUTH] Error checking draft profiles:', draftCheckError);
|
|
1454
|
-
// If checking draft profiles fails, sign out and throw appropriate error
|
|
1455
1293
|
try {
|
|
1456
1294
|
await firebaseSignOut(this.auth);
|
|
1457
1295
|
} catch (signOutError) {
|
|
1458
|
-
console.warn('[AUTH] Error signing out
|
|
1296
|
+
console.warn('[AUTH] Error signing out:', signOutError);
|
|
1459
1297
|
}
|
|
1460
1298
|
throw new AuthError(
|
|
1461
1299
|
'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
|
|
@@ -1466,72 +1304,43 @@ export class AuthService extends BaseService {
|
|
|
1466
1304
|
|
|
1467
1305
|
let user: User;
|
|
1468
1306
|
if (existingUser) {
|
|
1469
|
-
// User exists - use existing account
|
|
1470
1307
|
user = existingUser;
|
|
1471
|
-
console.log('[AUTH] Using existing user account');
|
|
1472
1308
|
} else {
|
|
1473
|
-
// For new users: Only create account if there are draft profiles OR if user already has a practitioner profile
|
|
1474
|
-
// Since doctors can only join via clinic invitation, we should not create accounts without invitations
|
|
1475
1309
|
if (draftProfiles.length === 0) {
|
|
1476
|
-
console.log('[AUTH] No draft profiles found, signing out and throwing error');
|
|
1477
|
-
// Sign out the Firebase user since we're not creating an account
|
|
1478
|
-
// Wrap in try-catch to handle any sign-out errors gracefully
|
|
1479
1310
|
try {
|
|
1480
1311
|
await firebaseSignOut(this.auth);
|
|
1481
1312
|
} catch (signOutError) {
|
|
1482
|
-
console.warn('[AUTH] Error signing out
|
|
1483
|
-
// Continue anyway - the important part is we're not creating the account
|
|
1313
|
+
console.warn('[AUTH] Error signing out:', signOutError);
|
|
1484
1314
|
}
|
|
1485
|
-
|
|
1315
|
+
throw new AuthError(
|
|
1486
1316
|
'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
|
|
1487
1317
|
'AUTH/NO_DRAFT_PROFILES',
|
|
1488
1318
|
404,
|
|
1489
1319
|
);
|
|
1490
|
-
console.log('[AUTH] Throwing NO_DRAFT_PROFILES error:', noDraftError.code);
|
|
1491
|
-
throw noDraftError;
|
|
1492
1320
|
}
|
|
1493
1321
|
|
|
1494
|
-
// Create new user document only if draft profiles exist
|
|
1495
1322
|
user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
|
|
1496
1323
|
skipProfileCreation: true,
|
|
1497
1324
|
});
|
|
1498
|
-
console.log('[AUTH] Created new user account with draft profiles available');
|
|
1499
1325
|
}
|
|
1500
1326
|
|
|
1501
|
-
// Check if user already has practitioner profile
|
|
1502
1327
|
let practitioner: Practitioner | null = null;
|
|
1503
1328
|
if (user.practitionerProfile) {
|
|
1504
1329
|
practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
|
|
1505
1330
|
}
|
|
1506
1331
|
|
|
1507
|
-
console.log('[AUTH] Google Sign-In complete', {
|
|
1508
|
-
userId: user.uid,
|
|
1509
|
-
hasPractitioner: !!practitioner,
|
|
1510
|
-
draftProfilesCount: draftProfiles.length,
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
1332
|
return {
|
|
1514
1333
|
user,
|
|
1515
1334
|
practitioner,
|
|
1516
1335
|
draftProfiles,
|
|
1517
1336
|
};
|
|
1518
1337
|
} catch (error: any) {
|
|
1519
|
-
console.error('[AUTH] Error in signUpPractitionerWithGoogle:', error);
|
|
1520
|
-
console.error('[AUTH] Error type:', error?.constructor?.name);
|
|
1521
|
-
console.error('[AUTH] Error instanceof AuthError:', error instanceof AuthError);
|
|
1522
|
-
console.error('[AUTH] Error code:', error?.code);
|
|
1523
|
-
console.error('[AUTH] Error message:', error?.message);
|
|
1524
|
-
|
|
1525
|
-
// Preserve AuthError instances (like NO_DRAFT_PROFILES) without wrapping
|
|
1526
1338
|
if (error instanceof AuthError) {
|
|
1527
|
-
console.log('[AUTH] Preserving AuthError:', error.code);
|
|
1528
1339
|
throw error;
|
|
1529
1340
|
}
|
|
1530
1341
|
|
|
1531
|
-
// Check if error message contains NO_DRAFT_PROFILES before wrapping
|
|
1532
1342
|
const errorMessage = error?.message || error?.toString() || '';
|
|
1533
1343
|
if (errorMessage.includes('NO_DRAFT_PROFILES') || errorMessage.includes('clinic invitation')) {
|
|
1534
|
-
console.log('[AUTH] Detected clinic invitation error in message, converting to AuthError');
|
|
1535
1344
|
throw new AuthError(
|
|
1536
1345
|
'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
|
|
1537
1346
|
'AUTH/NO_DRAFT_PROFILES',
|
|
@@ -1539,14 +1348,9 @@ export class AuthService extends BaseService {
|
|
|
1539
1348
|
);
|
|
1540
1349
|
}
|
|
1541
1350
|
|
|
1542
|
-
// For other errors, wrap them but preserve the original message if it's helpful
|
|
1543
1351
|
const wrappedError = handleFirebaseError(error);
|
|
1544
|
-
console.log('[AUTH] Wrapped error:', wrappedError.message);
|
|
1545
1352
|
|
|
1546
|
-
// If the wrapped error message is generic, try to preserve more context
|
|
1547
1353
|
if (wrappedError.message.includes('permissions') || wrappedError.message.includes('Account creation failed')) {
|
|
1548
|
-
// This might be a permissions error during sign-out or user creation
|
|
1549
|
-
// If we got here and there were no draft profiles, it's likely the same issue
|
|
1550
1354
|
throw new AuthError(
|
|
1551
1355
|
'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
|
|
1552
1356
|
'AUTH/NO_DRAFT_PROFILES',
|