@blackcode_sa/metaestetics-api 1.7.4 → 1.7.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.7.4",
4
+ "version": "1.7.6",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -991,15 +991,21 @@ export class AuthService extends BaseService {
991
991
 
992
992
  // We need to create a full PractitionerBasicInfo object
993
993
  const basicInfo: PractitionerBasicInfo = {
994
- firstName: data.firstName,
995
- lastName: data.lastName,
994
+ firstName:
995
+ data.firstName.length >= 2
996
+ ? data.firstName
997
+ : data.firstName.padEnd(2, " "),
998
+ lastName:
999
+ data.lastName.length >= 2
1000
+ ? data.lastName
1001
+ : data.lastName.padEnd(2, " "),
996
1002
  email: data.email,
997
- phoneNumber: data.profileData.basicInfo?.phoneNumber || "",
1003
+ phoneNumber: "+1234567890", // Default valid phone number
998
1004
  profileImageUrl: data.profileData.basicInfo?.profileImageUrl || "",
999
1005
  gender: data.profileData.basicInfo?.gender || "other", // Default to "other" if not provided
1000
1006
  bio: data.profileData.basicInfo?.bio || "",
1001
- title: "Practitioner", // Default title
1002
- dateOfBirth: new Date(), // Default to today
1007
+ title: data.profileData.basicInfo?.title || "Practitioner", // Default title
1008
+ dateOfBirth: Timestamp.fromDate(new Date()), // Use Timestamp instead of Date
1003
1009
  languages: ["English"], // Default language
1004
1010
  };
1005
1011
 
@@ -1008,9 +1014,9 @@ export class AuthService extends BaseService {
1008
1014
  .certification || {
1009
1015
  level: CertificationLevel.AESTHETICIAN,
1010
1016
  specialties: [],
1011
- licenseNumber: "Pending",
1012
- issuingAuthority: "Pending",
1013
- issueDate: new Date(),
1017
+ licenseNumber: "Pending123", // At least 3 characters
1018
+ issuingAuthority: "Default Authority", // At least 2 characters
1019
+ issueDate: Timestamp.fromDate(new Date()), // Use Timestamp instead of Date
1014
1020
  verificationStatus: "pending",
1015
1021
  };
1016
1022
 
@@ -0,0 +1,330 @@
1
+ import { Auth } from "firebase/auth";
2
+ import { Firestore, Timestamp } from "firebase/firestore";
3
+ import { FirebaseApp } from "firebase/app";
4
+ import {
5
+ ref,
6
+ uploadBytes,
7
+ getDownloadURL,
8
+ deleteObject,
9
+ listAll,
10
+ FirebaseStorage,
11
+ } from "firebase/storage";
12
+ import {
13
+ doc,
14
+ getDoc,
15
+ setDoc,
16
+ updateDoc,
17
+ arrayUnion,
18
+ arrayRemove,
19
+ collection,
20
+ query,
21
+ where,
22
+ limit,
23
+ getDocs,
24
+ deleteDoc,
25
+ orderBy,
26
+ } from "firebase/firestore";
27
+ import { BaseService } from "../base.service";
28
+
29
+ /**
30
+ * Enum for media access levels
31
+ */
32
+ export enum MediaAccessLevel {
33
+ PUBLIC = "public",
34
+ PRIVATE = "private",
35
+ CONFIDENTIAL = "confidential",
36
+ }
37
+
38
+ /**
39
+ * Media file metadata interface
40
+ */
41
+ export interface MediaMetadata {
42
+ id: string;
43
+ name: string;
44
+ url: string;
45
+ contentType: string;
46
+ size: number;
47
+ createdAt: Timestamp;
48
+ accessLevel: MediaAccessLevel;
49
+ ownerId: string;
50
+ collectionName: string;
51
+ path: string;
52
+ updatedAt?: Timestamp;
53
+ }
54
+
55
+ const MEDIA_METADATA_COLLECTION = "media_metadata";
56
+
57
+ export class MediaService extends BaseService {
58
+ constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
59
+ super(db, auth, app);
60
+ }
61
+
62
+ /**
63
+ * Upload a media file, store its metadata, and return the metadata including the URL.
64
+ * @param file - The file to upload.
65
+ * @param ownerId - ID of the owner (user, patient, clinic, etc.).
66
+ * @param accessLevel - Access level (public, private, confidential).
67
+ * @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
68
+ * @param originalFileName - Optional: the original name of the file, if not using file.name.
69
+ * @returns Promise with the media metadata.
70
+ */
71
+ async uploadMedia(
72
+ file: File,
73
+ ownerId: string,
74
+ accessLevel: MediaAccessLevel,
75
+ collectionName: string,
76
+ originalFileName?: string
77
+ ): Promise<MediaMetadata> {
78
+ const mediaId = this.generateId();
79
+ const fileNameToUse = originalFileName || file.name;
80
+ // Sanitize fileNameToUse if necessary, e.g., remove special characters or ensure a safe extension
81
+ const uniqueFileName = `${mediaId}-${fileNameToUse}`;
82
+ const filePath = `media/${accessLevel}/${ownerId}/${uniqueFileName}`;
83
+
84
+ console.log(`[MediaService] Uploading file to: ${filePath}`);
85
+
86
+ const storageRef = ref(this.storage, filePath);
87
+
88
+ try {
89
+ const uploadResult = await uploadBytes(storageRef, file, {
90
+ contentType: file.type,
91
+ });
92
+ console.log("[MediaService] File uploaded successfully", uploadResult);
93
+
94
+ const downloadURL = await getDownloadURL(uploadResult.ref);
95
+ console.log("[MediaService] Got download URL:", downloadURL);
96
+
97
+ const metadata: MediaMetadata = {
98
+ id: mediaId,
99
+ name: fileNameToUse,
100
+ url: downloadURL,
101
+ contentType: file.type,
102
+ size: file.size,
103
+ createdAt: Timestamp.now(),
104
+ accessLevel: accessLevel,
105
+ ownerId: ownerId,
106
+ collectionName: collectionName,
107
+ path: filePath, // Store the full storage path
108
+ };
109
+
110
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
111
+ await setDoc(metadataDocRef, metadata);
112
+ console.log("[MediaService] Metadata stored in Firestore:", mediaId);
113
+
114
+ return metadata;
115
+ } catch (error) {
116
+ console.error("[MediaService] Error during media upload:", error);
117
+ // Consider more specific error handling or re-throwing a custom error
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get media metadata from Firestore by its ID.
124
+ * @param mediaId - ID of the media.
125
+ * @returns Promise with the media metadata or null if not found.
126
+ */
127
+ async getMediaMetadata(mediaId: string): Promise<MediaMetadata | null> {
128
+ console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
129
+ const docRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
130
+ const docSnap = await getDoc(docRef);
131
+
132
+ if (docSnap.exists()) {
133
+ console.log("[MediaService] Metadata found:", docSnap.data());
134
+ return docSnap.data() as MediaMetadata;
135
+ }
136
+ console.log("[MediaService] No metadata found for ID:", mediaId);
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Get media metadata from Firestore by its public URL.
142
+ * @param url - The public URL of the media file.
143
+ * @returns Promise with the media metadata or null if not found.
144
+ */
145
+ async getMediaMetadataByUrl(url: string): Promise<MediaMetadata | null> {
146
+ console.log(`[MediaService] Getting media metadata by URL: ${url}`);
147
+ const q = query(
148
+ collection(this.db, MEDIA_METADATA_COLLECTION),
149
+ where("url", "==", url),
150
+ limit(1)
151
+ );
152
+
153
+ try {
154
+ const querySnapshot = await getDocs(q);
155
+ if (!querySnapshot.empty) {
156
+ const metadata = querySnapshot.docs[0].data() as MediaMetadata;
157
+ console.log("[MediaService] Metadata found by URL:", metadata);
158
+ return metadata;
159
+ }
160
+ console.log("[MediaService] No metadata found for URL:", url);
161
+ return null;
162
+ } catch (error) {
163
+ console.error("[MediaService] Error fetching metadata by URL:", error);
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Delete media from storage and remove metadata from Firestore.
170
+ * @param mediaId - ID of the media to delete.
171
+ */
172
+ async deleteMedia(mediaId: string): Promise<void> {
173
+ console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
174
+ const metadata = await this.getMediaMetadata(mediaId);
175
+
176
+ if (!metadata) {
177
+ console.warn(
178
+ `[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
179
+ );
180
+ // Optionally throw an error or return a status
181
+ return;
182
+ }
183
+
184
+ const storageFileRef = ref(this.storage, metadata.path);
185
+
186
+ try {
187
+ // Delete the file from Firebase Storage
188
+ await deleteObject(storageFileRef);
189
+ console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
190
+
191
+ // Delete the metadata from Firestore
192
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
193
+ await deleteDoc(metadataDocRef);
194
+ console.log(
195
+ `[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
196
+ );
197
+ } catch (error) {
198
+ console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
199
+ // Handle specific errors, e.g., file not found in storage, permissions issues
200
+ // If Firestore delete fails after storage delete, there might be an orphaned metadata entry.
201
+ // Consider how to handle such inconsistencies or if it's acceptable.
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Update media access level. This is a complex operation if it involves moving the file.
208
+ * For now, this will only update the metadata. True access control might need storage path changes and Firestore rule updates.
209
+ * @param mediaId - ID of the media to update.
210
+ * @param newAccessLevel - New access level.
211
+ * @returns Promise with the updated media metadata.
212
+ */
213
+ async updateMediaAccessLevel(
214
+ mediaId: string,
215
+ newAccessLevel: MediaAccessLevel
216
+ ): Promise<MediaMetadata | null> {
217
+ console.log(
218
+ `[MediaService] Updating access level for media ID: ${mediaId} to ${newAccessLevel}`
219
+ );
220
+ const metadata = await this.getMediaMetadata(mediaId);
221
+ if (!metadata) {
222
+ console.warn(
223
+ `[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
224
+ );
225
+ return null;
226
+ }
227
+
228
+ // IMPORTANT: This only updates the metadata field.
229
+ // If your Firebase Storage rules depend on the path, this won't change file accessibility directly.
230
+ // A full implementation might involve moving the file in storage to a new path (e.g., media/newAccessLevel/...),
231
+ // updating the URL, and then updating the metadata. This is significantly more complex.
232
+
233
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
234
+ const updateData = {
235
+ accessLevel: newAccessLevel,
236
+ updatedAt: Timestamp.now(),
237
+ };
238
+
239
+ try {
240
+ await updateDoc(metadataDocRef, updateData);
241
+ console.log(`[MediaService] Media access level updated for ${mediaId}.`);
242
+ // Return the updated metadata
243
+ return { ...metadata, ...updateData } as MediaMetadata;
244
+ } catch (error) {
245
+ console.error(
246
+ `[MediaService] Error updating media access level for ${mediaId}:`,
247
+ error
248
+ );
249
+ throw error;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * List all media for an owner, optionally filtered by collection and access level.
255
+ * @param ownerId - ID of the owner.
256
+ * @param collectionName - Optional: Filter by collection name.
257
+ * @param accessLevel - Optional: Filter by access level.
258
+ * @param count - Optional: Number of items to fetch.
259
+ * @param startAfterId - Optional: ID of the document to start after (for pagination).
260
+ */
261
+ async listMedia(
262
+ ownerId: string,
263
+ collectionName?: string,
264
+ accessLevel?: MediaAccessLevel,
265
+ count?: number,
266
+ startAfterId?: string // Using ID for pagination simplicity with Firestore
267
+ ): Promise<MediaMetadata[]> {
268
+ console.log(`[MediaService] Listing media for owner: ${ownerId}`);
269
+ let qConstraints: any[] = [where("ownerId", "==", ownerId)];
270
+
271
+ if (collectionName) {
272
+ qConstraints.push(where("collectionName", "==", collectionName));
273
+ }
274
+ if (accessLevel) {
275
+ qConstraints.push(where("accessLevel", "==", accessLevel));
276
+ }
277
+
278
+ qConstraints.push(orderBy("createdAt", "desc")); // Example ordering
279
+
280
+ if (count) {
281
+ qConstraints.push(limit(count));
282
+ }
283
+
284
+ if (startAfterId) {
285
+ const startAfterDoc = await this.getMediaMetadata(startAfterId);
286
+ if (startAfterDoc) {
287
+ // Firestore's startAfter needs a DocumentSnapshot or field values from it.
288
+ // For simplicity, if using cursor-based pagination with getDocs, you'd pass the snapshot.
289
+ // If building query manually, it's more complex with orderBy fields.
290
+ // This part needs refinement based on actual pagination strategy.
291
+ // For now, let's assume we'd fetch the doc snapshot if needed or simplify.
292
+ // console.log("[MediaService] Pagination: starting after document corresponding to ID", startAfterId);
293
+ // This simple approach of passing ID won't directly work with Firestore's startAfter without fetching the document snapshot first.
294
+ // A more robust solution would involve passing the actual DocumentSnapshot or specific field values of the last retrieved document.
295
+ }
296
+ }
297
+
298
+ const finalQuery = query(
299
+ collection(this.db, MEDIA_METADATA_COLLECTION),
300
+ ...qConstraints
301
+ );
302
+
303
+ try {
304
+ const querySnapshot = await getDocs(finalQuery);
305
+ const mediaList = querySnapshot.docs.map(
306
+ (doc) => doc.data() as MediaMetadata
307
+ );
308
+ console.log(`[MediaService] Found ${mediaList.length} media items.`);
309
+ return mediaList;
310
+ } catch (error) {
311
+ console.error("[MediaService] Error listing media:", error);
312
+ throw error;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Get download URL for media. (Convenience, as URL is in metadata)
318
+ * @param mediaId - ID of the media.
319
+ */
320
+ async getMediaDownloadUrl(mediaId: string): Promise<string | null> {
321
+ console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
322
+ const metadata = await this.getMediaMetadata(mediaId);
323
+ if (metadata && metadata.url) {
324
+ console.log(`[MediaService] URL found: ${metadata.url}`);
325
+ return metadata.url;
326
+ }
327
+ console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
328
+ return null;
329
+ }
330
+ }
@@ -1,5 +1,5 @@
1
1
  import { User as FirebaseAuthUser } from "firebase/auth";
2
- import { Timestamp, FieldValue } from "firebase/firestore";
2
+ import { Timestamp, FieldValue, serverTimestamp } from "firebase/firestore";
3
3
 
4
4
  // User tipovi
5
5
  export enum UserRole {
@@ -14,9 +14,9 @@ export interface User {
14
14
  email: string | null;
15
15
  roles: UserRole[];
16
16
  isAnonymous: boolean;
17
- createdAt: Timestamp | FieldValue;
18
- updatedAt: Timestamp | FieldValue;
19
- lastLoginAt: Timestamp | FieldValue;
17
+ createdAt: Timestamp | FieldValue | (() => FieldValue);
18
+ updatedAt: Timestamp | FieldValue | (() => FieldValue);
19
+ lastLoginAt: Timestamp | FieldValue | (() => FieldValue);
20
20
  patientProfile?: string;
21
21
  practitionerProfile?: string;
22
22
  adminProfile?: string;
@@ -27,9 +27,9 @@ export interface CreateUserData {
27
27
  email: string | null;
28
28
  roles: UserRole[];
29
29
  isAnonymous: boolean;
30
- createdAt: FieldValue;
31
- updatedAt: FieldValue;
32
- lastLoginAt: FieldValue;
30
+ createdAt: FieldValue | Timestamp | (() => FieldValue);
31
+ updatedAt: FieldValue | Timestamp | (() => FieldValue);
32
+ lastLoginAt: FieldValue | Timestamp | (() => FieldValue);
33
33
  }
34
34
 
35
35
  export const USERS_COLLECTION = "users";
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { UserRole } from "../types";
3
- import { Timestamp, FieldValue } from "firebase/firestore";
3
+ import { Timestamp, FieldValue, serverTimestamp } from "firebase/firestore";
4
4
 
5
5
  export const emailSchema = z
6
6
  .string()
@@ -24,21 +24,41 @@ export const userRolesSchema = z
24
24
  .min(1, "User must have at least one role")
25
25
  .max(3, "User cannot have more than 3 roles");
26
26
 
27
- export const timestampSchema = z.custom<Timestamp | FieldValue>((data) => {
28
- // If it's a serverTimestamp (FieldValue), it's valid
29
- if (data && typeof data === "object" && "isEqual" in data) {
27
+ export const timestampSchema = z.custom<
28
+ Timestamp | FieldValue | (() => FieldValue)
29
+ >((data) => {
30
+ // If data is null or undefined, it's not valid
31
+ if (data == null) {
32
+ return false;
33
+ }
34
+
35
+ // If it's serverTimestamp (FieldValue), it's valid
36
+ if (typeof data === "object" && "isEqual" in data) {
30
37
  return true;
31
38
  }
32
39
 
33
40
  // If it's a Timestamp object, validate its structure
34
- return (
35
- data &&
41
+ if (
36
42
  typeof data === "object" &&
37
43
  "toDate" in data &&
38
44
  "seconds" in data &&
39
45
  "nanoseconds" in data
40
- );
41
- }, "Must be a Timestamp object or serverTimestamp");
46
+ ) {
47
+ return true;
48
+ }
49
+
50
+ // If it's the serverTimestamp function reference
51
+ if (data === serverTimestamp) {
52
+ return true;
53
+ }
54
+
55
+ // If it's a function that returns FieldValue (serverTimestamp call)
56
+ if (typeof data === "function") {
57
+ return true;
58
+ }
59
+
60
+ return false;
61
+ }, "Must be a Timestamp object, FieldValue or serverTimestamp function");
42
62
 
43
63
  /**
44
64
  * Validaciona šema za clinic admin opcije pri kreiranju