@blackcode_sa/metaestetics-api 1.14.17 → 1.14.23

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.14.17",
4
+ "version": "1.14.23",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -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><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>
81
+ <p>To create your account:</p>
92
82
  <ol>
93
83
  <li>Visit {{registrationUrl}}</li>
94
- <li>Click "Claim Existing Profile with Token"</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
 
@@ -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); // Wait 300ms for AsyncStorage to stabilize
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,69 +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: Verify auth.currentUser is still set before Firestore query
1292
- // AsyncStorage persistence can cause it to become NULL even after being set
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] Waiting for auth state to recover...');
1298
- // Try to wait for it to come back
1184
+ const credential = GoogleAuthProvider.credential(idToken);
1185
+ await signInWithCredential(this.auth, credential);
1299
1186
  await this.waitForAuthStateToSettle(firebaseUser.uid, 2000);
1300
1187
  }
1301
1188
 
1302
1189
  const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1303
1190
  const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1304
1191
 
1305
- console.log('[AUTH] Draft profiles check result:', {
1306
- email: normalizedEmail,
1307
- draftProfilesCount: draftProfiles.length,
1308
- draftProfileIds: draftProfiles.map(p => p.id),
1309
- });
1310
-
1311
1192
  if (draftProfiles.length === 0) {
1312
- // No draft profiles - sign out and throw error
1313
- console.log('[AUTH] No draft profiles found, signing out and throwing error');
1314
1193
  try {
1315
1194
  await firebaseSignOut(this.auth);
1316
1195
  } catch (signOutError) {
1317
- console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1196
+ console.warn('[AUTH] Error signing out:', signOutError);
1318
1197
  }
1319
1198
  throw new AuthError(
1320
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.',
@@ -1323,35 +1202,21 @@ export class AuthService extends BaseService {
1323
1202
  );
1324
1203
  }
1325
1204
 
1326
- // Draft profiles exist - CREATE USER DOCUMENT IMMEDIATELY (like token flow)
1327
- // This matches the token flow pattern: create User doc right after sign-in, before any delays
1328
- console.log('[AUTH] Draft profiles found, creating User document IMMEDIATELY after sign-in');
1329
- console.log('[AUTH] auth.currentUser at User creation time:', this.auth.currentUser?.uid || 'NULL');
1330
-
1331
1205
  try {
1332
- // Create User document immediately (same pattern as token flow)
1333
1206
  const newUser = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1334
1207
  skipProfileCreation: true,
1335
1208
  });
1336
- console.log('[AUTH] ✅ User document created successfully:', newUser.uid);
1337
1209
 
1338
- // Return user + draft profiles (user doc already exists, just need to claim profiles)
1339
1210
  return {
1340
1211
  user: newUser,
1341
1212
  practitioner: null,
1342
1213
  draftProfiles: draftProfiles,
1343
1214
  };
1344
1215
  } catch (createUserError: any) {
1345
- console.error('[AUTH] ❌ Failed to create User document:', {
1346
- errorCode: createUserError?.code,
1347
- errorMessage: createUserError?.message,
1348
- uid: firebaseUser.uid,
1349
- });
1350
- // Sign out on failure
1351
1216
  try {
1352
1217
  await firebaseSignOut(this.auth);
1353
1218
  } catch (signOutError) {
1354
- console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1219
+ console.warn('[AUTH] Error signing out:', signOutError);
1355
1220
  }
1356
1221
  throw createUserError;
1357
1222
  }
@@ -1383,11 +1248,7 @@ export class AuthService extends BaseService {
1383
1248
  };
1384
1249
  }
1385
1250
 
1386
- // Case 2: User exists with email/password - need to link Google provider
1387
- // Firebase doesn't allow linking without being signed in first
1388
- // So we need to ask user to sign in with email/password first, then link
1389
1251
  if (hasEmailMethod && !hasGoogleMethod) {
1390
- console.log('[AUTH] User exists with email/password only');
1391
1252
  throw new AuthError(
1392
1253
  'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1393
1254
  'AUTH/EMAIL_ALREADY_EXISTS',
@@ -1395,8 +1256,6 @@ export class AuthService extends BaseService {
1395
1256
  );
1396
1257
  }
1397
1258
 
1398
- // Case 3: New user - sign in with Google and check for draft profiles
1399
- console.log('[AUTH] Signing in with Google credential');
1400
1259
  const credential = GoogleAuthProvider.credential(idToken);
1401
1260
 
1402
1261
  let firebaseUser: FirebaseUser;
@@ -1415,39 +1274,26 @@ export class AuthService extends BaseService {
1415
1274
  throw error;
1416
1275
  }
1417
1276
 
1418
- // CRITICAL: Wait for auth state to settle before making Firestore queries
1419
- // In React Native with AsyncStorage persistence, auth.currentUser can be NULL
1420
- // immediately after signInWithCredential due to async persistence race condition
1421
- console.log('[AUTH] Waiting for auth state to settle after sign-in...');
1422
1277
  await this.waitForAuthStateToSettle(firebaseUser.uid);
1423
- console.log('[AUTH] ✅ Auth state settled, proceeding with Firestore queries');
1424
1278
 
1425
- // Check for existing User document (in case user had email/password account that was just linked)
1426
1279
  let existingUser: User | null = null;
1427
1280
  try {
1428
1281
  const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
1429
1282
  if (existingUserDoc) {
1430
1283
  existingUser = existingUserDoc;
1431
- console.log('[AUTH] Found existing User document');
1432
1284
  }
1433
1285
  } catch (error) {
1434
- console.error('[AUTH] Error checking for existing user:', error);
1435
1286
  // Continue with new user creation
1436
1287
  }
1437
1288
 
1438
- // Check for draft profiles
1439
- console.log('[AUTH] Checking for draft profiles');
1440
1289
  let draftProfiles: Practitioner[] = [];
1441
1290
  try {
1442
1291
  draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1443
- console.log('[AUTH] Draft profiles check complete', { count: draftProfiles.length });
1444
1292
  } catch (draftCheckError: any) {
1445
- console.error('[AUTH] Error checking draft profiles:', draftCheckError);
1446
- // If checking draft profiles fails, sign out and throw appropriate error
1447
1293
  try {
1448
1294
  await firebaseSignOut(this.auth);
1449
1295
  } catch (signOutError) {
1450
- console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1296
+ console.warn('[AUTH] Error signing out:', signOutError);
1451
1297
  }
1452
1298
  throw new AuthError(
1453
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.',
@@ -1458,72 +1304,43 @@ export class AuthService extends BaseService {
1458
1304
 
1459
1305
  let user: User;
1460
1306
  if (existingUser) {
1461
- // User exists - use existing account
1462
1307
  user = existingUser;
1463
- console.log('[AUTH] Using existing user account');
1464
1308
  } else {
1465
- // For new users: Only create account if there are draft profiles OR if user already has a practitioner profile
1466
- // Since doctors can only join via clinic invitation, we should not create accounts without invitations
1467
1309
  if (draftProfiles.length === 0) {
1468
- console.log('[AUTH] No draft profiles found, signing out and throwing error');
1469
- // Sign out the Firebase user since we're not creating an account
1470
- // Wrap in try-catch to handle any sign-out errors gracefully
1471
1310
  try {
1472
1311
  await firebaseSignOut(this.auth);
1473
1312
  } catch (signOutError) {
1474
- console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1475
- // Continue anyway - the important part is we're not creating the account
1313
+ console.warn('[AUTH] Error signing out:', signOutError);
1476
1314
  }
1477
- const noDraftError = new AuthError(
1315
+ throw new AuthError(
1478
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.',
1479
1317
  'AUTH/NO_DRAFT_PROFILES',
1480
1318
  404,
1481
1319
  );
1482
- console.log('[AUTH] Throwing NO_DRAFT_PROFILES error:', noDraftError.code);
1483
- throw noDraftError;
1484
1320
  }
1485
1321
 
1486
- // Create new user document only if draft profiles exist
1487
1322
  user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1488
1323
  skipProfileCreation: true,
1489
1324
  });
1490
- console.log('[AUTH] Created new user account with draft profiles available');
1491
1325
  }
1492
1326
 
1493
- // Check if user already has practitioner profile
1494
1327
  let practitioner: Practitioner | null = null;
1495
1328
  if (user.practitionerProfile) {
1496
1329
  practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1497
1330
  }
1498
1331
 
1499
- console.log('[AUTH] Google Sign-In complete', {
1500
- userId: user.uid,
1501
- hasPractitioner: !!practitioner,
1502
- draftProfilesCount: draftProfiles.length,
1503
- });
1504
-
1505
1332
  return {
1506
1333
  user,
1507
1334
  practitioner,
1508
1335
  draftProfiles,
1509
1336
  };
1510
1337
  } catch (error: any) {
1511
- console.error('[AUTH] Error in signUpPractitionerWithGoogle:', error);
1512
- console.error('[AUTH] Error type:', error?.constructor?.name);
1513
- console.error('[AUTH] Error instanceof AuthError:', error instanceof AuthError);
1514
- console.error('[AUTH] Error code:', error?.code);
1515
- console.error('[AUTH] Error message:', error?.message);
1516
-
1517
- // Preserve AuthError instances (like NO_DRAFT_PROFILES) without wrapping
1518
1338
  if (error instanceof AuthError) {
1519
- console.log('[AUTH] Preserving AuthError:', error.code);
1520
1339
  throw error;
1521
1340
  }
1522
1341
 
1523
- // Check if error message contains NO_DRAFT_PROFILES before wrapping
1524
1342
  const errorMessage = error?.message || error?.toString() || '';
1525
1343
  if (errorMessage.includes('NO_DRAFT_PROFILES') || errorMessage.includes('clinic invitation')) {
1526
- console.log('[AUTH] Detected clinic invitation error in message, converting to AuthError');
1527
1344
  throw new AuthError(
1528
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.',
1529
1346
  'AUTH/NO_DRAFT_PROFILES',
@@ -1531,14 +1348,9 @@ export class AuthService extends BaseService {
1531
1348
  );
1532
1349
  }
1533
1350
 
1534
- // For other errors, wrap them but preserve the original message if it's helpful
1535
1351
  const wrappedError = handleFirebaseError(error);
1536
- console.log('[AUTH] Wrapped error:', wrappedError.message);
1537
1352
 
1538
- // If the wrapped error message is generic, try to preserve more context
1539
1353
  if (wrappedError.message.includes('permissions') || wrappedError.message.includes('Account creation failed')) {
1540
- // This might be a permissions error during sign-out or user creation
1541
- // If we got here and there were no draft profiles, it's likely the same issue
1542
1354
  throw new AuthError(
1543
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.',
1544
1356
  'AUTH/NO_DRAFT_PROFILES',
@@ -483,6 +483,34 @@ export class PractitionerService extends BaseService {
483
483
  throw new Error("Practitioner is not associated with this clinic");
484
484
  }
485
485
 
486
+ // Security check: Verify that the clinic belongs to the clinic group of the user creating the token
487
+ // createdBy can be either clinicGroupId or clinicId
488
+ let expectedClinicGroupId: string | null = null;
489
+
490
+ // First, check if createdBy matches the clinic's clinicGroupId directly
491
+ if (clinic.clinicGroupId === createdBy) {
492
+ // createdBy is the clinicGroupId, which matches - this is valid
493
+ expectedClinicGroupId = createdBy;
494
+ } else {
495
+ // createdBy might be a clinicId, check if that clinic belongs to the same group
496
+ try {
497
+ const creatorClinic = await this.getClinicService().getClinic(createdBy);
498
+ if (creatorClinic && creatorClinic.clinicGroupId === clinic.clinicGroupId) {
499
+ // Both clinics belong to the same group - valid
500
+ expectedClinicGroupId = clinic.clinicGroupId;
501
+ } else {
502
+ throw new Error("Clinic does not belong to your clinic group");
503
+ }
504
+ } catch (error: any) {
505
+ // If createdBy is not a valid clinicId, or clinics don't match, reject
506
+ if (error.message === "Clinic does not belong to your clinic group") {
507
+ throw error;
508
+ }
509
+ // If getClinic fails, createdBy might be a clinicGroupId that doesn't match
510
+ throw new Error("Clinic does not belong to your clinic group");
511
+ }
512
+ }
513
+
486
514
  // Default expiration is 7 days from now if not specified
487
515
  const expiration =
488
516
  validatedData.expiresAt ||
@@ -522,21 +550,29 @@ export class PractitionerService extends BaseService {
522
550
  /**
523
551
  * Gets active tokens for a practitioner
524
552
  * @param practitionerId ID of the practitioner
553
+ * @param clinicId Optional clinic ID to filter tokens by. If provided, only returns tokens for this clinic.
525
554
  * @returns Array of active tokens
526
555
  */
527
556
  async getPractitionerActiveTokens(
528
- practitionerId: string
557
+ practitionerId: string,
558
+ clinicId?: string
529
559
  ): Promise<PractitionerToken[]> {
530
560
  const tokensRef = collection(
531
561
  this.db,
532
562
  `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
533
563
  );
534
564
 
535
- const q = query(
536
- tokensRef,
565
+ const conditions = [
537
566
  where("status", "==", PractitionerTokenStatus.ACTIVE),
538
567
  where("expiresAt", ">", Timestamp.now())
539
- );
568
+ ];
569
+
570
+ // Filter by clinic if provided
571
+ if (clinicId) {
572
+ conditions.push(where("clinicId", "==", clinicId));
573
+ }
574
+
575
+ const q = query(tokensRef, ...conditions);
540
576
 
541
577
  const querySnapshot = await getDocs(q);
542
578
  return querySnapshot.docs.map((doc) => doc.data() as PractitionerToken);
@@ -628,6 +664,45 @@ export class PractitionerService extends BaseService {
628
664
  });
629
665
  }
630
666
 
667
+ /**
668
+ * Revokes a token by setting its status to REVOKED
669
+ * @param tokenId ID of the token
670
+ * @param practitionerId ID of the practitioner
671
+ * @param clinicId ID of the clinic that owns the token. Used to verify ownership before revoking.
672
+ * @throws Error if token doesn't exist or doesn't belong to the specified clinic
673
+ */
674
+ async revokeToken(
675
+ tokenId: string,
676
+ practitionerId: string,
677
+ clinicId: string
678
+ ): Promise<void> {
679
+ const tokenRef = doc(
680
+ this.db,
681
+ `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
682
+ );
683
+
684
+ // First, verify the token exists and belongs to the clinic
685
+ const tokenDoc = await getDoc(tokenRef);
686
+ if (!tokenDoc.exists()) {
687
+ throw new Error("Token not found");
688
+ }
689
+
690
+ const tokenData = tokenDoc.data() as PractitionerToken;
691
+ if (tokenData.clinicId !== clinicId) {
692
+ throw new Error("Token does not belong to the specified clinic");
693
+ }
694
+
695
+ // Only revoke if token is still active
696
+ if (tokenData.status !== PractitionerTokenStatus.ACTIVE) {
697
+ throw new Error("Token is not active and cannot be revoked");
698
+ }
699
+
700
+ await updateDoc(tokenRef, {
701
+ status: PractitionerTokenStatus.REVOKED,
702
+ updatedAt: serverTimestamp(),
703
+ });
704
+ }
705
+
631
706
  /**
632
707
  * Dohvata zdravstvenog radnika po ID-u
633
708
  */