@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/dist/admin/index.d.mts +3 -1
- package/dist/admin/index.d.ts +3 -1
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +102 -116
- package/dist/index.mjs +242 -256
- package/package.json +1 -1
- package/src/services/auth/auth.service.ts +14 -48
- package/src/services/clinic/clinic-group.service.ts +13 -6
- package/src/services/clinic/utils/clinic-group.utils.ts +95 -87
- package/src/types/clinic/index.ts +3 -1
- package/src/validations/clinic.schema.ts +4 -3
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
|
+
"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
|
|
345
|
-
console.log('[AUTH] Searching for token
|
|
346
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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;
|
|
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:
|
|
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
|
-
|
|
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
|
|
512
|
-
*
|
|
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
|
-
|
|
523
|
+
tokenValue: string,
|
|
523
524
|
userRef: string,
|
|
524
525
|
app: FirebaseApp
|
|
525
526
|
): Promise<boolean> {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
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
|
-
|
|
608
|
-
|
|
605
|
+
await deleteDoc(doc(adminTokensRef(db, groupId), tokenId));
|
|
606
|
+
}
|
|
609
607
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
645
|
-
return
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
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
|