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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.5",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -341,57 +341,23 @@ export class AuthService extends BaseService {
341
341
  token: data.inviteToken,
342
342
  });
343
343
 
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
- });
344
+ // Find the token using collection group query (O(1) instead of scanning all groups)
345
+ console.log('[AUTH] Searching for token via collection group query');
346
+ const tokenResult = await clinicGroupService.findAdminTokenByValue(data.inviteToken);
377
347
 
378
- return isMatch;
379
- });
380
-
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
- }
348
+ if (!tokenResult) {
349
+ console.error('[AUTH] No valid active token found');
350
+ throw new Error('Invalid or expired invite token');
390
351
  }
391
352
 
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');
353
+ console.log('[AUTH] Found matching token', {
354
+ tokenId: tokenResult.token.id,
355
+ clinicGroupId: tokenResult.clinicGroupId,
356
+ });
357
+
358
+ const foundGroup = await clinicGroupService.getClinicGroup(tokenResult.clinicGroupId);
359
+ if (!foundGroup) {
360
+ throw new Error('Clinic group not found for token');
395
361
  }
396
362
 
397
363
  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,
@@ -459,6 +462,18 @@ export async function deactivateClinicGroup(
459
462
  * @param data - Token data
460
463
  * @returns The created admin token
461
464
  */
465
+ // --- Admin Token Subcollection ---
466
+ // Tokens are stored in: clinic_groups/{groupId}/adminTokens/{tokenId}
467
+
468
+ const ADMIN_TOKENS_SUBCOLLECTION = "adminTokens";
469
+
470
+ function adminTokensRef(db: Firestore, groupId: string) {
471
+ return collection(db, CLINIC_GROUPS_COLLECTION, groupId, ADMIN_TOKENS_SUBCOLLECTION);
472
+ }
473
+
474
+ /**
475
+ * Creates an admin token in the subcollection.
476
+ */
462
477
  export async function createAdminToken(
463
478
  db: Firestore,
464
479
  groupId: string,
@@ -471,121 +486,105 @@ export async function createAdminToken(
471
486
  throw new Error("Clinic group not found");
472
487
  }
473
488
 
474
- // Proveravamo da li admin pripada grupi
475
489
  if (!group.admins.includes(creatorAdminId)) {
476
490
  throw new Error("Admin does not belong to this clinic group");
477
491
  }
478
492
 
479
493
  const now = Timestamp.now();
480
- const expiresInDays = data?.expiresInDays || 7; // Default 7 days
494
+ const expiresInDays = data?.expiresInDays || 7;
481
495
  const email = data?.email || null;
482
496
  const expiresAt = new Timestamp(
483
497
  now.seconds + expiresInDays * 24 * 60 * 60,
484
498
  now.nanoseconds
485
499
  );
486
500
 
501
+ const tokenRef = doc(adminTokensRef(db, groupId));
487
502
  const token: AdminToken = {
488
- id: generateId(),
503
+ id: tokenRef.id,
489
504
  token: generateId(),
490
505
  status: AdminTokenStatus.ACTIVE,
491
506
  email,
507
+ clinicGroupId: groupId,
492
508
  createdAt: now,
493
509
  expiresAt,
494
510
  };
495
511
 
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
-
512
+ await setDoc(tokenRef, token);
507
513
  return token;
508
514
  }
509
515
 
510
516
  /**
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
517
+ * Verifies and marks an admin token as used.
518
+ * Uses a collection group query to find the token across all groups.
518
519
  */
519
520
  export async function verifyAndUseAdminToken(
520
521
  db: Firestore,
521
522
  groupId: string,
522
- token: string,
523
+ tokenValue: string,
523
524
  userRef: string,
524
525
  app: FirebaseApp
525
526
  ): Promise<boolean> {
526
- const group = await getClinicGroup(db, groupId);
527
- if (!group) {
528
- throw new Error("Clinic group not found");
529
- }
527
+ // Query the subcollection for the matching token
528
+ const tokensQuery = query(
529
+ adminTokensRef(db, groupId),
530
+ where("token", "==", tokenValue),
531
+ limit(1)
532
+ );
533
+ const snapshot = await getDocs(tokensQuery);
530
534
 
531
- const adminToken = group.adminTokens.find((t) => t.token === token);
532
- if (!adminToken) {
535
+ if (snapshot.empty) {
533
536
  throw new Error("Admin token not found");
534
537
  }
535
538
 
539
+ const tokenDoc = snapshot.docs[0];
540
+ const adminToken = tokenDoc.data() as AdminToken;
541
+
536
542
  if (adminToken.status !== AdminTokenStatus.ACTIVE) {
537
543
  throw new Error("Admin token is not active");
538
544
  }
539
545
 
540
546
  const now = Timestamp.now();
541
547
  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
-
548
+ await updateDoc(tokenDoc.ref, { status: AdminTokenStatus.EXPIRED });
556
549
  throw new Error("Admin token has expired");
557
550
  }
558
551
 
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
- );
552
+ // Mark as used
553
+ await updateDoc(tokenDoc.ref, {
554
+ status: AdminTokenStatus.USED,
555
+ usedByUserRef: userRef,
556
+ });
569
557
 
570
- await updateClinicGroup(
571
- db,
572
- groupId,
573
- {
574
- adminTokens: updatedTokens,
575
- },
576
- app
558
+ return true;
559
+ }
560
+
561
+ /**
562
+ * Finds an admin token by its token string across ALL clinic groups.
563
+ * Uses a collection group query — much more efficient than scanning all groups.
564
+ */
565
+ export async function findAdminTokenByValue(
566
+ db: Firestore,
567
+ tokenValue: string
568
+ ): Promise<{ token: AdminToken; clinicGroupId: string } | null> {
569
+ const tokensQuery = query(
570
+ collectionGroup(db, ADMIN_TOKENS_SUBCOLLECTION),
571
+ where("token", "==", tokenValue),
572
+ where("status", "==", AdminTokenStatus.ACTIVE),
573
+ limit(1)
577
574
  );
575
+ const snapshot = await getDocs(tokensQuery);
578
576
 
579
- return true;
577
+ if (snapshot.empty) return null;
578
+
579
+ const tokenData = snapshot.docs[0].data() as AdminToken;
580
+ return {
581
+ token: tokenData,
582
+ clinicGroupId: tokenData.clinicGroupId,
583
+ };
580
584
  }
581
585
 
582
586
  /**
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
587
+ * Deletes an admin token from the subcollection.
589
588
  */
590
589
  export async function deleteAdminToken(
591
590
  db: Firestore,
@@ -599,33 +598,43 @@ export async function deleteAdminToken(
599
598
  throw new Error("Clinic group not found");
600
599
  }
601
600
 
602
- // Proveravamo da li admin pripada grupi
603
601
  if (!group.admins.includes(adminId)) {
604
602
  throw new Error("Admin does not belong to this clinic group");
605
603
  }
606
604
 
607
- // Uklanjamo token
608
- const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
605
+ await deleteDoc(doc(adminTokensRef(db, groupId), tokenId));
606
+ }
609
607
 
610
- await updateClinicGroup(
611
- db,
612
- groupId,
613
- {
614
- adminTokens: updatedTokens,
615
- },
616
- app
608
+ /**
609
+ * Gets active admin tokens for a clinic group from the subcollection.
610
+ */
611
+ export async function getActiveAdminTokens(
612
+ db: Firestore,
613
+ groupId: string,
614
+ adminId: string,
615
+ app: FirebaseApp
616
+ ): Promise<AdminToken[]> {
617
+ const group = await getClinicGroup(db, groupId);
618
+ if (!group) {
619
+ throw new Error("Clinic group not found");
620
+ }
621
+
622
+ if (!group.admins.includes(adminId)) {
623
+ throw new Error("Admin does not belong to this clinic group");
624
+ }
625
+
626
+ const tokensQuery = query(
627
+ adminTokensRef(db, groupId),
628
+ where("status", "==", AdminTokenStatus.ACTIVE)
617
629
  );
630
+ const snapshot = await getDocs(tokensQuery);
631
+ return snapshot.docs.map((d) => d.data() as AdminToken);
618
632
  }
619
633
 
620
634
  /**
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
635
+ * Gets ALL admin tokens for a clinic group (all statuses).
627
636
  */
628
- export async function getActiveAdminTokens(
637
+ export async function getAllAdminTokens(
629
638
  db: Firestore,
630
639
  groupId: string,
631
640
  adminId: string,
@@ -636,11 +645,10 @@ export async function getActiveAdminTokens(
636
645
  throw new Error("Clinic group not found");
637
646
  }
638
647
 
639
- // Proveravamo da li admin pripada grupi
640
648
  if (!group.admins.includes(adminId)) {
641
649
  throw new Error("Admin does not belong to this clinic group");
642
650
  }
643
651
 
644
- // Vraćamo samo aktivne tokene
645
- return group.adminTokens.filter((t) => t.status === AdminTokenStatus.ACTIVE);
652
+ const snapshot = await getDocs(adminTokensRef(db, groupId));
653
+ return snapshot.docs.map((d) => d.data() as AdminToken);
646
654
  }
@@ -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