@blackcode_sa/metaestetics-api 1.15.17-staging.4 → 1.15.17-staging.6

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.15.17-staging.4",
4
+ "version": "1.15.17-staging.6",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -59,7 +59,7 @@ export class ClinicAggregationService {
59
59
  try {
60
60
  await groupRef.update({
61
61
  clinicsInfo: admin.firestore.FieldValue.arrayUnion(clinicInfo),
62
- clinicIds: admin.firestore.FieldValue.arrayUnion(clinicId),
62
+ clinics: admin.firestore.FieldValue.arrayUnion(clinicId),
63
63
  updatedAt: admin.firestore.FieldValue.serverTimestamp(),
64
64
  });
65
65
 
@@ -319,11 +319,12 @@ export class AuthService extends BaseService {
319
319
  // Now update the admin with the group ID
320
320
  console.log('[AUTH] Updating admin with clinic group ID');
321
321
  await clinicAdminService.updateClinicAdmin(adminProfile.id, {
322
- // Use admin profile ID, not user UID
323
322
  clinicGroupId: clinicGroup.id,
324
323
  });
325
324
  console.log('[AUTH] Admin updated with clinic group ID successfully');
326
325
 
326
+ // adminsInfo is populated by onCreateClinicAdmin Firestore trigger
327
+
327
328
  // Get the updated admin profile
328
329
  adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
329
330
  } catch (groupCreationError) {
@@ -341,57 +342,23 @@ export class AuthService extends BaseService {
341
342
  token: data.inviteToken,
342
343
  });
343
344
 
344
- // Find the token in the database
345
- console.log('[AUTH] Searching for token in clinic groups');
346
- const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
347
- const q = query(groupsRef);
348
- const querySnapshot = await getDocs(q);
349
-
350
- let foundGroup: ClinicGroup | null = null;
351
- let foundToken: AdminToken | null = null;
352
-
353
- console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
354
- for (const docSnapshot of querySnapshot.docs) {
355
- const group = docSnapshot.data() as ClinicGroup;
356
- console.log('[AUTH] Checking group', {
357
- groupId: group.id,
358
- groupName: group.name,
359
- });
360
-
361
- // Find the token in the group's tokens
362
- const token = group.adminTokens.find(t => {
363
- const isMatch =
364
- t.token === data.inviteToken &&
365
- t.status === AdminTokenStatus.ACTIVE &&
366
- new Date(t.expiresAt.toDate()) > new Date();
367
-
368
- console.log('[AUTH] Checking token', {
369
- tokenId: t.id,
370
- tokenMatch: t.token === data.inviteToken,
371
- tokenStatus: t.status,
372
- tokenActive: t.status === AdminTokenStatus.ACTIVE,
373
- tokenExpiry: new Date(t.expiresAt.toDate()),
374
- tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
375
- isMatch,
376
- });
377
-
378
- return isMatch;
379
- });
345
+ // Find the token using collection group query (O(1) instead of scanning all groups)
346
+ console.log('[AUTH] Searching for token via collection group query');
347
+ const tokenResult = await clinicGroupService.findAdminTokenByValue(data.inviteToken);
380
348
 
381
- if (token) {
382
- foundGroup = group;
383
- foundToken = token;
384
- console.log('[AUTH] Found matching token in group', {
385
- groupId: group.id,
386
- tokenId: token.id,
387
- });
388
- break;
389
- }
349
+ if (!tokenResult) {
350
+ console.error('[AUTH] No valid active token found');
351
+ throw new Error('Invalid or expired invite token');
390
352
  }
391
353
 
392
- if (!foundGroup || !foundToken) {
393
- console.error('[AUTH] No valid token found in any clinic group');
394
- throw new Error('Invalid or expired invite token');
354
+ console.log('[AUTH] Found matching token', {
355
+ tokenId: tokenResult.token.id,
356
+ clinicGroupId: tokenResult.clinicGroupId,
357
+ });
358
+
359
+ const foundGroup = await clinicGroupService.getClinicGroup(tokenResult.clinicGroupId);
360
+ if (!foundGroup) {
361
+ throw new Error('Clinic group not found for token');
395
362
  }
396
363
 
397
364
  clinicGroup = foundGroup;
@@ -198,13 +198,20 @@ export class ClinicGroupService extends BaseService {
198
198
  return ClinicGroupUtils.getActiveAdminTokens(this.db, groupId, adminId, this.app);
199
199
  }
200
200
 
201
- // TODO: Add a method to get all admin tokens for a clinic group (not just active ones)
202
-
203
- // TODO: Refactor admin token methods not to add tokens to the clinicGroup document,
204
- // but to add them to a subcollection called adminTokens that belongs to a specific clinicGroup document
201
+ /**
202
+ * Gets ALL admin tokens for a clinic group (all statuses)
203
+ */
204
+ async getAllAdminTokens(groupId: string, adminId: string): Promise<AdminToken[]> {
205
+ return ClinicGroupUtils.getAllAdminTokens(this.db, groupId, adminId, this.app);
206
+ }
205
207
 
206
- // TODO: Add granular control over admin permissions, e.g. only allow admins to manage certain clinics to tokens directly
207
- // TODO: Generally refactor admin tokens and invites, also create cloud function to send invites and send updates when sombody uses the token
208
+ /**
209
+ * Finds an admin token by its value across all clinic groups.
210
+ * Uses a collection group query for O(1) lookup.
211
+ */
212
+ async findAdminTokenByValue(tokenValue: string): Promise<{ token: AdminToken; clinicGroupId: string } | null> {
213
+ return ClinicGroupUtils.findAdminTokenByValue(this.db, tokenValue);
214
+ }
208
215
 
209
216
  /**
210
217
  * Updates the onboarding status for a clinic group
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  collection,
3
+ collectionGroup,
3
4
  doc,
4
5
  getDoc,
5
6
  getDocs,
6
7
  query,
7
8
  where,
9
+ orderBy,
10
+ limit,
8
11
  updateDoc,
9
12
  setDoc,
10
13
  deleteDoc,
@@ -161,7 +164,7 @@ export async function createClinicGroup(
161
164
  clinicsInfo: [],
162
165
  admins: [ownerId],
163
166
  adminsInfo: [],
164
- adminTokens: [],
167
+ adminTokens: [], // @deprecated — tokens now in subcollection, kept for backward compat
165
168
  ownerId,
166
169
  createdAt: now,
167
170
  updatedAt: now,
@@ -369,9 +372,10 @@ export async function addAdminToGroup(
369
372
  adminId,
370
373
  groupId,
371
374
  });
372
- return; // Admin is already in the group
375
+ return;
373
376
  }
374
377
 
378
+ // Only add the admin ID — adminsInfo is populated by onCreateClinicAdmin trigger
375
379
  console.log("[CLINIC_GROUP] Updating group with new admin");
376
380
  await updateClinicGroup(
377
381
  db,
@@ -459,6 +463,18 @@ export async function deactivateClinicGroup(
459
463
  * @param data - Token data
460
464
  * @returns The created admin token
461
465
  */
466
+ // --- Admin Token Subcollection ---
467
+ // Tokens are stored in: clinic_groups/{groupId}/adminTokens/{tokenId}
468
+
469
+ const ADMIN_TOKENS_SUBCOLLECTION = "adminTokens";
470
+
471
+ function adminTokensRef(db: Firestore, groupId: string) {
472
+ return collection(db, CLINIC_GROUPS_COLLECTION, groupId, ADMIN_TOKENS_SUBCOLLECTION);
473
+ }
474
+
475
+ /**
476
+ * Creates an admin token in the subcollection.
477
+ */
462
478
  export async function createAdminToken(
463
479
  db: Firestore,
464
480
  groupId: string,
@@ -471,121 +487,105 @@ export async function createAdminToken(
471
487
  throw new Error("Clinic group not found");
472
488
  }
473
489
 
474
- // Proveravamo da li admin pripada grupi
475
490
  if (!group.admins.includes(creatorAdminId)) {
476
491
  throw new Error("Admin does not belong to this clinic group");
477
492
  }
478
493
 
479
494
  const now = Timestamp.now();
480
- const expiresInDays = data?.expiresInDays || 7; // Default 7 days
495
+ const expiresInDays = data?.expiresInDays || 7;
481
496
  const email = data?.email || null;
482
497
  const expiresAt = new Timestamp(
483
498
  now.seconds + expiresInDays * 24 * 60 * 60,
484
499
  now.nanoseconds
485
500
  );
486
501
 
502
+ const tokenRef = doc(adminTokensRef(db, groupId));
487
503
  const token: AdminToken = {
488
- id: generateId(),
504
+ id: tokenRef.id,
489
505
  token: generateId(),
490
506
  status: AdminTokenStatus.ACTIVE,
491
507
  email,
508
+ clinicGroupId: groupId,
492
509
  createdAt: now,
493
510
  expiresAt,
494
511
  };
495
512
 
496
- // Dodajemo token u grupu
497
- // Ovo treba promeniti, staviti admin tokene u sub-kolekciju u klinickoj grupi
498
- await updateClinicGroup(
499
- db,
500
- groupId,
501
- {
502
- adminTokens: [...group.adminTokens, token],
503
- },
504
- app
505
- );
506
-
513
+ await setDoc(tokenRef, token);
507
514
  return token;
508
515
  }
509
516
 
510
517
  /**
511
- * Verifies and uses an admin token
512
- * @param db - Firestore database instance
513
- * @param groupId - ID of the clinic group
514
- * @param token - Token to verify
515
- * @param userRef - User reference
516
- * @param app - Firebase app instance
517
- * @returns Whether the token was successfully used
518
+ * Verifies and marks an admin token as used.
519
+ * Uses a collection group query to find the token across all groups.
518
520
  */
519
521
  export async function verifyAndUseAdminToken(
520
522
  db: Firestore,
521
523
  groupId: string,
522
- token: string,
524
+ tokenValue: string,
523
525
  userRef: string,
524
526
  app: FirebaseApp
525
527
  ): Promise<boolean> {
526
- const group = await getClinicGroup(db, groupId);
527
- if (!group) {
528
- throw new Error("Clinic group not found");
529
- }
528
+ // Query the subcollection for the matching token
529
+ const tokensQuery = query(
530
+ adminTokensRef(db, groupId),
531
+ where("token", "==", tokenValue),
532
+ limit(1)
533
+ );
534
+ const snapshot = await getDocs(tokensQuery);
530
535
 
531
- const adminToken = group.adminTokens.find((t) => t.token === token);
532
- if (!adminToken) {
536
+ if (snapshot.empty) {
533
537
  throw new Error("Admin token not found");
534
538
  }
535
539
 
540
+ const tokenDoc = snapshot.docs[0];
541
+ const adminToken = tokenDoc.data() as AdminToken;
542
+
536
543
  if (adminToken.status !== AdminTokenStatus.ACTIVE) {
537
544
  throw new Error("Admin token is not active");
538
545
  }
539
546
 
540
547
  const now = Timestamp.now();
541
548
  if (adminToken.expiresAt.seconds < now.seconds) {
542
- // Token je istekao, ažuriramo status
543
- const updatedTokens = group.adminTokens.map((t) =>
544
- t.id === adminToken.id ? { ...t, status: AdminTokenStatus.EXPIRED } : t
545
- );
546
-
547
- await updateClinicGroup(
548
- db,
549
- groupId,
550
- {
551
- adminTokens: updatedTokens,
552
- },
553
- app
554
- );
555
-
549
+ await updateDoc(tokenDoc.ref, { status: AdminTokenStatus.EXPIRED });
556
550
  throw new Error("Admin token has expired");
557
551
  }
558
552
 
559
- // Token je validan, ažuriramo status
560
- const updatedTokens = group.adminTokens.map((t) =>
561
- t.id === adminToken.id
562
- ? {
563
- ...t,
564
- status: AdminTokenStatus.USED,
565
- usedByUserRef: userRef,
566
- }
567
- : t
568
- );
553
+ // Mark as used
554
+ await updateDoc(tokenDoc.ref, {
555
+ status: AdminTokenStatus.USED,
556
+ usedByUserRef: userRef,
557
+ });
569
558
 
570
- await updateClinicGroup(
571
- db,
572
- groupId,
573
- {
574
- adminTokens: updatedTokens,
575
- },
576
- app
559
+ return true;
560
+ }
561
+
562
+ /**
563
+ * Finds an admin token by its token string across ALL clinic groups.
564
+ * Uses a collection group query — much more efficient than scanning all groups.
565
+ */
566
+ export async function findAdminTokenByValue(
567
+ db: Firestore,
568
+ tokenValue: string
569
+ ): Promise<{ token: AdminToken; clinicGroupId: string } | null> {
570
+ const tokensQuery = query(
571
+ collectionGroup(db, ADMIN_TOKENS_SUBCOLLECTION),
572
+ where("token", "==", tokenValue),
573
+ where("status", "==", AdminTokenStatus.ACTIVE),
574
+ limit(1)
577
575
  );
576
+ const snapshot = await getDocs(tokensQuery);
578
577
 
579
- return true;
578
+ if (snapshot.empty) return null;
579
+
580
+ const tokenData = snapshot.docs[0].data() as AdminToken;
581
+ return {
582
+ token: tokenData,
583
+ clinicGroupId: tokenData.clinicGroupId,
584
+ };
580
585
  }
581
586
 
582
587
  /**
583
- * Deletes an admin token
584
- * @param db - Firestore database instance
585
- * @param groupId - ID of the clinic group
586
- * @param tokenId - ID of the token to delete
587
- * @param adminId - ID of the admin making the deletion
588
- * @param app - Firebase app instance
588
+ * Deletes an admin token from the subcollection.
589
589
  */
590
590
  export async function deleteAdminToken(
591
591
  db: Firestore,
@@ -599,33 +599,43 @@ export async function deleteAdminToken(
599
599
  throw new Error("Clinic group not found");
600
600
  }
601
601
 
602
- // Proveravamo da li admin pripada grupi
603
602
  if (!group.admins.includes(adminId)) {
604
603
  throw new Error("Admin does not belong to this clinic group");
605
604
  }
606
605
 
607
- // Uklanjamo token
608
- const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
606
+ await deleteDoc(doc(adminTokensRef(db, groupId), tokenId));
607
+ }
609
608
 
610
- await updateClinicGroup(
611
- db,
612
- groupId,
613
- {
614
- adminTokens: updatedTokens,
615
- },
616
- app
609
+ /**
610
+ * Gets active admin tokens for a clinic group from the subcollection.
611
+ */
612
+ export async function getActiveAdminTokens(
613
+ db: Firestore,
614
+ groupId: string,
615
+ adminId: string,
616
+ app: FirebaseApp
617
+ ): Promise<AdminToken[]> {
618
+ const group = await getClinicGroup(db, groupId);
619
+ if (!group) {
620
+ throw new Error("Clinic group not found");
621
+ }
622
+
623
+ if (!group.admins.includes(adminId)) {
624
+ throw new Error("Admin does not belong to this clinic group");
625
+ }
626
+
627
+ const tokensQuery = query(
628
+ adminTokensRef(db, groupId),
629
+ where("status", "==", AdminTokenStatus.ACTIVE)
617
630
  );
631
+ const snapshot = await getDocs(tokensQuery);
632
+ return snapshot.docs.map((d) => d.data() as AdminToken);
618
633
  }
619
634
 
620
635
  /**
621
- * Gets active admin tokens for a clinic group
622
- * @param db - Firestore database instance
623
- * @param groupId - ID of the clinic group
624
- * @param adminId - ID of the admin requesting the tokens
625
- * @param app - Firebase app instance (not used but included for consistency)
626
- * @returns Array of active admin tokens
636
+ * Gets ALL admin tokens for a clinic group (all statuses).
627
637
  */
628
- export async function getActiveAdminTokens(
638
+ export async function getAllAdminTokens(
629
639
  db: Firestore,
630
640
  groupId: string,
631
641
  adminId: string,
@@ -636,11 +646,10 @@ export async function getActiveAdminTokens(
636
646
  throw new Error("Clinic group not found");
637
647
  }
638
648
 
639
- // Proveravamo da li admin pripada grupi
640
649
  if (!group.admins.includes(adminId)) {
641
650
  throw new Error("Admin does not belong to this clinic group");
642
651
  }
643
652
 
644
- // Vraćamo samo aktivne tokene
645
- return group.adminTokens.filter((t) => t.status === AdminTokenStatus.ACTIVE);
653
+ const snapshot = await getDocs(adminTokensRef(db, groupId));
654
+ return snapshot.docs.map((d) => d.data() as AdminToken);
646
655
  }
@@ -160,6 +160,7 @@ export interface AdminToken {
160
160
  email?: string | null;
161
161
  status: AdminTokenStatus;
162
162
  usedByUserRef?: string;
163
+ clinicGroupId: string;
163
164
  createdAt: Timestamp;
164
165
  expiresAt: Timestamp;
165
166
  }
@@ -308,7 +309,8 @@ export interface ClinicGroup {
308
309
  clinicsInfo: ClinicInfo[];
309
310
  admins: string[];
310
311
  adminsInfo: AdminInfo[];
311
- adminTokens: AdminToken[];
312
+ /** @deprecated Tokens now stored in subcollection clinic_groups/{id}/adminTokens */
313
+ adminTokens?: AdminToken[];
312
314
  ownerId: string | null;
313
315
  createdAt: Timestamp;
314
316
  updatedAt: Timestamp;
@@ -143,8 +143,9 @@ export const adminTokenSchema = z.object({
143
143
  email: z.string().email().optional().nullable(),
144
144
  status: z.nativeEnum(AdminTokenStatus),
145
145
  usedByUserRef: z.string().optional(),
146
- createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
147
- expiresAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
146
+ clinicGroupId: z.string(),
147
+ createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)),
148
+ expiresAt: z.instanceof(Date).or(z.instanceof(Timestamp)),
148
149
  });
149
150
 
150
151
  /**
@@ -233,7 +234,7 @@ export const clinicGroupSchema = z.object({
233
234
  clinicsInfo: z.array(clinicInfoSchema),
234
235
  admins: z.array(z.string()),
235
236
  adminsInfo: z.array(adminInfoSchema),
236
- adminTokens: z.array(adminTokenSchema),
237
+ adminTokens: z.array(adminTokenSchema).optional(),
237
238
  ownerId: z.string().nullable(),
238
239
  createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
239
240
  updatedAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp