@blackcode_sa/metaestetics-api 1.12.27 → 1.12.28
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/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +64 -140
- package/dist/index.mjs +65 -146
- package/package.json +1 -1
- package/src/services/patient/patient.service.ts +117 -282
- package/src/services/patient/utils/sensitive.utils.ts +81 -67
- package/src/services/user/user.service.ts +9 -3
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDoc,
|
|
3
|
-
updateDoc,
|
|
4
|
-
setDoc,
|
|
5
|
-
serverTimestamp,
|
|
6
|
-
Firestore,
|
|
7
|
-
} from "firebase/firestore";
|
|
1
|
+
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore } from 'firebase/firestore';
|
|
8
2
|
import {
|
|
9
3
|
PatientSensitiveInfo,
|
|
10
4
|
CreatePatientSensitiveInfoData,
|
|
11
5
|
UpdatePatientSensitiveInfoData,
|
|
12
|
-
} from
|
|
13
|
-
import { UserRole } from
|
|
14
|
-
import { createPatientSensitiveInfoSchema } from
|
|
15
|
-
import { z } from
|
|
6
|
+
} from '../../../types/patient';
|
|
7
|
+
import { UserRole } from '../../../types';
|
|
8
|
+
import { createPatientSensitiveInfoSchema } from '../../../validations/patient.schema';
|
|
9
|
+
import { z } from 'zod';
|
|
16
10
|
import {
|
|
17
11
|
getSensitiveInfoDocRef,
|
|
18
12
|
initSensitiveInfoDocIfNotExists,
|
|
19
13
|
getPatientDocRef,
|
|
20
|
-
} from
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from "../../media/media.service";
|
|
26
|
-
import { AuthError } from "../../../errors/auth.errors";
|
|
27
|
-
import { getPractitionerProfileByUserRef } from "./practitioner.utils";
|
|
28
|
-
import { getClinicAdminByUserRef } from "../../clinic/utils/admin.utils";
|
|
14
|
+
} from './docs.utils';
|
|
15
|
+
import { MediaService, MediaAccessLevel, MediaResource } from '../../media/media.service';
|
|
16
|
+
import { AuthError } from '../../../errors/auth.errors';
|
|
17
|
+
import { getPractitionerProfileByUserRef } from './practitioner.utils';
|
|
18
|
+
import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
|
|
29
19
|
|
|
30
20
|
/**
|
|
31
21
|
* Checks if the requester has permission to access/modify sensitive info.
|
|
@@ -35,11 +25,11 @@ const checkSensitiveAccessUtil = async (
|
|
|
35
25
|
db: Firestore,
|
|
36
26
|
patientId: string,
|
|
37
27
|
requesterId: string,
|
|
38
|
-
requesterRoles: UserRole[]
|
|
28
|
+
requesterRoles: UserRole[],
|
|
39
29
|
): Promise<void> => {
|
|
40
30
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
41
31
|
if (!patientDoc.exists()) {
|
|
42
|
-
throw new Error(
|
|
32
|
+
throw new Error('Patient profile not found');
|
|
43
33
|
}
|
|
44
34
|
const patientData = patientDoc.data() as any; // Cast to any to access properties
|
|
45
35
|
|
|
@@ -50,14 +40,8 @@ const checkSensitiveAccessUtil = async (
|
|
|
50
40
|
|
|
51
41
|
// 2. Requester is an associated practitioner
|
|
52
42
|
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
53
|
-
const practitionerProfile = await getPractitionerProfileByUserRef(
|
|
54
|
-
|
|
55
|
-
requesterId
|
|
56
|
-
);
|
|
57
|
-
if (
|
|
58
|
-
practitionerProfile &&
|
|
59
|
-
patientData.doctorIds?.includes(practitionerProfile.id)
|
|
60
|
-
) {
|
|
43
|
+
const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
|
|
44
|
+
if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
|
|
61
45
|
return;
|
|
62
46
|
}
|
|
63
47
|
}
|
|
@@ -66,8 +50,8 @@ const checkSensitiveAccessUtil = async (
|
|
|
66
50
|
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
67
51
|
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
68
52
|
if (adminProfile && adminProfile.clinicsManaged) {
|
|
69
|
-
const hasAccess = adminProfile.clinicsManaged.some(
|
|
70
|
-
patientData.clinicIds?.includes(managedClinicId)
|
|
53
|
+
const hasAccess = adminProfile.clinicsManaged.some(managedClinicId =>
|
|
54
|
+
patientData.clinicIds?.includes(managedClinicId),
|
|
71
55
|
);
|
|
72
56
|
if (hasAccess) {
|
|
73
57
|
return;
|
|
@@ -76,9 +60,9 @@ const checkSensitiveAccessUtil = async (
|
|
|
76
60
|
}
|
|
77
61
|
|
|
78
62
|
throw new AuthError(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
403
|
|
63
|
+
'Unauthorized access to sensitive information.',
|
|
64
|
+
'AUTH/UNAUTHORIZED_ACCESS',
|
|
65
|
+
403,
|
|
82
66
|
);
|
|
83
67
|
};
|
|
84
68
|
|
|
@@ -92,14 +76,14 @@ const checkSensitiveAccessUtil = async (
|
|
|
92
76
|
const handlePhotoUrlUpload = async (
|
|
93
77
|
photoUrl: MediaResource | undefined | null,
|
|
94
78
|
patientId: string,
|
|
95
|
-
mediaService: MediaService
|
|
79
|
+
mediaService: MediaService,
|
|
96
80
|
): Promise<string | null> => {
|
|
97
81
|
if (!photoUrl) {
|
|
98
82
|
return null;
|
|
99
83
|
}
|
|
100
84
|
|
|
101
85
|
// If it's already a URL string, return it as is
|
|
102
|
-
if (typeof photoUrl ===
|
|
86
|
+
if (typeof photoUrl === 'string') {
|
|
103
87
|
return photoUrl;
|
|
104
88
|
}
|
|
105
89
|
|
|
@@ -109,8 +93,8 @@ const handlePhotoUrlUpload = async (
|
|
|
109
93
|
photoUrl,
|
|
110
94
|
patientId, // Using patientId as ownerId
|
|
111
95
|
MediaAccessLevel.PRIVATE, // Sensitive info should be private
|
|
112
|
-
|
|
113
|
-
photoUrl instanceof File ? photoUrl.name : `sensitive_photo_${patientId}
|
|
96
|
+
'patient_sensitive_photos',
|
|
97
|
+
photoUrl instanceof File ? photoUrl.name : `sensitive_photo_${patientId}`,
|
|
114
98
|
);
|
|
115
99
|
return mediaMetadata.url;
|
|
116
100
|
}
|
|
@@ -124,25 +108,18 @@ export const createSensitiveInfoUtil = async (
|
|
|
124
108
|
data: CreatePatientSensitiveInfoData,
|
|
125
109
|
requesterId: string,
|
|
126
110
|
requesterRoles: UserRole[],
|
|
127
|
-
mediaService?: MediaService
|
|
111
|
+
mediaService?: MediaService,
|
|
128
112
|
): Promise<PatientSensitiveInfo> => {
|
|
129
113
|
try {
|
|
130
114
|
// Security check
|
|
131
|
-
await checkSensitiveAccessUtil(
|
|
132
|
-
db,
|
|
133
|
-
data.patientId,
|
|
134
|
-
requesterId,
|
|
135
|
-
requesterRoles
|
|
136
|
-
);
|
|
115
|
+
await checkSensitiveAccessUtil(db, data.patientId, requesterId, requesterRoles);
|
|
137
116
|
|
|
138
117
|
const validatedData = createPatientSensitiveInfoSchema.parse(data);
|
|
139
118
|
|
|
140
119
|
// Proveriti da li dokument već postoji
|
|
141
|
-
const sensitiveDoc = await getDoc(
|
|
142
|
-
getSensitiveInfoDocRef(db, data.patientId)
|
|
143
|
-
);
|
|
120
|
+
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, data.patientId));
|
|
144
121
|
if (sensitiveDoc.exists()) {
|
|
145
|
-
throw new Error(
|
|
122
|
+
throw new Error('Sensitive information already exists for this patient');
|
|
146
123
|
}
|
|
147
124
|
|
|
148
125
|
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
@@ -151,9 +128,9 @@ export const createSensitiveInfoUtil = async (
|
|
|
151
128
|
processedPhotoUrl = await handlePhotoUrlUpload(
|
|
152
129
|
validatedData.photoUrl,
|
|
153
130
|
data.patientId,
|
|
154
|
-
mediaService
|
|
131
|
+
mediaService,
|
|
155
132
|
);
|
|
156
|
-
} else if (typeof validatedData.photoUrl ===
|
|
133
|
+
} else if (typeof validatedData.photoUrl === 'string') {
|
|
157
134
|
processedPhotoUrl = validatedData.photoUrl;
|
|
158
135
|
}
|
|
159
136
|
|
|
@@ -168,13 +145,13 @@ export const createSensitiveInfoUtil = async (
|
|
|
168
145
|
|
|
169
146
|
const createdDoc = await getDoc(getSensitiveInfoDocRef(db, data.patientId));
|
|
170
147
|
if (!createdDoc.exists()) {
|
|
171
|
-
throw new Error(
|
|
148
|
+
throw new Error('Failed to create sensitive information');
|
|
172
149
|
}
|
|
173
150
|
|
|
174
151
|
return createdDoc.data() as PatientSensitiveInfo;
|
|
175
152
|
} catch (error) {
|
|
176
153
|
if (error instanceof z.ZodError) {
|
|
177
|
-
throw new Error(
|
|
154
|
+
throw new Error('Invalid sensitive info data: ' + error.message);
|
|
178
155
|
}
|
|
179
156
|
throw error;
|
|
180
157
|
}
|
|
@@ -184,7 +161,7 @@ export const getSensitiveInfoUtil = async (
|
|
|
184
161
|
db: Firestore,
|
|
185
162
|
patientId: string,
|
|
186
163
|
requesterId: string,
|
|
187
|
-
requesterRoles: UserRole[]
|
|
164
|
+
requesterRoles: UserRole[],
|
|
188
165
|
): Promise<PatientSensitiveInfo | null> => {
|
|
189
166
|
// Security check
|
|
190
167
|
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
@@ -193,9 +170,7 @@ export const getSensitiveInfoUtil = async (
|
|
|
193
170
|
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
194
171
|
|
|
195
172
|
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
196
|
-
return sensitiveDoc.exists()
|
|
197
|
-
? (sensitiveDoc.data() as PatientSensitiveInfo)
|
|
198
|
-
: null;
|
|
173
|
+
return sensitiveDoc.exists() ? (sensitiveDoc.data() as PatientSensitiveInfo) : null;
|
|
199
174
|
};
|
|
200
175
|
|
|
201
176
|
export const updateSensitiveInfoUtil = async (
|
|
@@ -204,7 +179,7 @@ export const updateSensitiveInfoUtil = async (
|
|
|
204
179
|
data: UpdatePatientSensitiveInfoData,
|
|
205
180
|
requesterId: string,
|
|
206
181
|
requesterRoles: UserRole[],
|
|
207
|
-
mediaService?: MediaService
|
|
182
|
+
mediaService?: MediaService,
|
|
208
183
|
): Promise<PatientSensitiveInfo> => {
|
|
209
184
|
// Security check
|
|
210
185
|
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
@@ -216,16 +191,12 @@ export const updateSensitiveInfoUtil = async (
|
|
|
216
191
|
let processedPhotoUrl: string | null | undefined = undefined;
|
|
217
192
|
if (data.photoUrl !== undefined) {
|
|
218
193
|
if (mediaService) {
|
|
219
|
-
processedPhotoUrl = await handlePhotoUrlUpload(
|
|
220
|
-
|
|
221
|
-
patientId,
|
|
222
|
-
mediaService
|
|
223
|
-
);
|
|
224
|
-
} else if (typeof data.photoUrl === "string" || data.photoUrl === null) {
|
|
194
|
+
processedPhotoUrl = await handlePhotoUrlUpload(data.photoUrl, patientId, mediaService);
|
|
195
|
+
} else if (typeof data.photoUrl === 'string' || data.photoUrl === null) {
|
|
225
196
|
processedPhotoUrl = data.photoUrl;
|
|
226
197
|
} else {
|
|
227
198
|
// If photoUrl is a File/Blob but no mediaService provided, throw error
|
|
228
|
-
throw new Error(
|
|
199
|
+
throw new Error('MediaService required to process photo upload');
|
|
229
200
|
}
|
|
230
201
|
}
|
|
231
202
|
|
|
@@ -239,7 +210,50 @@ export const updateSensitiveInfoUtil = async (
|
|
|
239
210
|
|
|
240
211
|
const updatedDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
241
212
|
if (!updatedDoc.exists()) {
|
|
242
|
-
throw new Error(
|
|
213
|
+
throw new Error('Failed to retrieve updated sensitive information');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return updatedDoc.data() as PatientSensitiveInfo;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const claimPatientSensitiveInfoUtil = async (
|
|
220
|
+
db: Firestore,
|
|
221
|
+
patientId: string,
|
|
222
|
+
userId: string,
|
|
223
|
+
): Promise<PatientSensitiveInfo> => {
|
|
224
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
225
|
+
if (!patientDoc.exists()) {
|
|
226
|
+
throw new Error('Patient profile not found');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const patientData = patientDoc.data() as any;
|
|
230
|
+
|
|
231
|
+
if (!patientData.isManual) {
|
|
232
|
+
throw new Error('Only manually created patient profiles can be claimed');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (patientData.userRef) {
|
|
236
|
+
throw new Error('Patient profile has already been claimed');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
240
|
+
if (!sensitiveDoc.exists()) {
|
|
241
|
+
throw new Error('Patient sensitive information not found');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sensitiveData = sensitiveDoc.data() as PatientSensitiveInfo;
|
|
245
|
+
if (sensitiveData.userRef) {
|
|
246
|
+
throw new Error('Patient sensitive information has already been claimed');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await updateDoc(getSensitiveInfoDocRef(db, patientId), {
|
|
250
|
+
userRef: userId,
|
|
251
|
+
updatedAt: serverTimestamp(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const updatedDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
255
|
+
if (!updatedDoc.exists()) {
|
|
256
|
+
throw new Error('Failed to retrieve updated sensitive information');
|
|
243
257
|
}
|
|
244
258
|
|
|
245
259
|
return updatedDoc.data() as PatientSensitiveInfo;
|
|
@@ -178,16 +178,22 @@ export class UserService extends BaseService {
|
|
|
178
178
|
throw new Error('User already has a patient profile.');
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
// Claim
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
// Claim sensitive info first (this adds userRef to sensitive info)
|
|
182
|
+
const sensitiveInfo = await patientService.claimPatientSensitiveInfo(
|
|
183
|
+
patientProfile.id,
|
|
184
|
+
userId,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Construct full display name
|
|
184
188
|
const fullDisplayName = sensitiveInfo
|
|
185
189
|
? `${sensitiveInfo.firstName} ${sensitiveInfo.lastName}`
|
|
186
190
|
: patientProfile.displayName;
|
|
187
191
|
|
|
192
|
+
// Update patient profile: link userRef, set isManual to false, and update displayName
|
|
188
193
|
await patientService.updatePatientProfile(patientProfile.id, {
|
|
189
194
|
userRef: userId,
|
|
190
195
|
isManual: false,
|
|
196
|
+
isVerified: true,
|
|
191
197
|
displayName: fullDisplayName,
|
|
192
198
|
});
|
|
193
199
|
|