@edgedev/firebase 2.0.33 → 2.0.35

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/edgeFirebase.ts CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  } from "firebase/auth";
56
56
 
57
57
 
58
- import { getStorage, ref as storageRef, uploadBytes, getDownloadURL, connectStorageEmulator} from "firebase/storage";
58
+ import { getStorage, ref, uploadBytes, getDownloadURL, connectStorageEmulator, listAll, deleteObject} from "firebase/storage";
59
59
 
60
60
  import { getFunctions, httpsCallable, connectFunctionsEmulator } from "firebase/functions";
61
61
 
@@ -245,8 +245,9 @@ export const EdgeFirebase = class {
245
245
  connectFunctionsEmulator(this.functions, "127.0.0.1", this.firebaseConfig.emulatorFunctions)
246
246
  }
247
247
  if (this.firebaseConfig.emulatorStorage) {
248
- connectStorageEmulator(this.storage, "127.0.0.1", this.firebaseConfig.emulatorStorage)
248
+ connectStorageEmulator(this.storage, "127.0.0.1", this.firebaseConfig.emulatorStorage)
249
249
  }
250
+ this.setOnAuthStateChanged();
250
251
  }
251
252
 
252
253
  private firebaseConfig = null;
@@ -2126,8 +2127,8 @@ export const EdgeFirebase = class {
2126
2127
 
2127
2128
 
2128
2129
  // File functions
2129
- public uploadFileToStorage = async (filePath: string, file: Blob): Promise<actionResponse> => {
2130
- try {
2130
+ public uploadFile = async (filePath: string, file: Blob): Promise<actionResponse> => {
2131
+
2131
2132
  // Validate if file is provided
2132
2133
  if (!file) {
2133
2134
  return this.sendResponse({
@@ -2147,46 +2148,83 @@ export const EdgeFirebase = class {
2147
2148
  });
2148
2149
  }
2149
2150
 
2150
- // Initialize Firebase Storage
2151
-
2152
- // Define a temporary path for the file upload
2153
- // You might want to include some unique identifier in the temporary path
2154
- const tempFilePath = `temp/${this.user.uid}/${filePath.replaceAll('/', '-|-')}`;
2155
-
2156
- // Create a reference to the temporary file location
2157
- const fileRef = storageRef(this.storage, tempFilePath);
2158
-
2159
- // Upload the file to the temporary location
2160
- await uploadBytes(fileRef, file);
2161
-
2162
- // Prepare data for the callable function
2163
- const data = {
2164
- uid: this.user.uid,
2165
- filePath: filePath, // The final desired path for the file
2166
- };
2167
-
2168
- // Call the Firebase Function to handle the file move and any additional processing
2169
- const callable = httpsCallable(this.functions, 'edgeFirebase-uploadFile');
2170
- const functionResult = await callable(data);
2171
-
2172
- const resultData = functionResult.data as { success: boolean; message: string; finalDownloadURL?: string };
2173
-
2174
- if (!resultData.success) {
2175
- throw new Error(resultData.message);
2151
+ try {
2152
+ const tempFilePath = `${filePath.replaceAll('/', '-')}` + '/' + file.name;
2153
+ const fileRef = ref(this.storage, tempFilePath);
2154
+ await uploadBytes(fileRef, file);
2155
+ return this.sendResponse({
2156
+ success: true,
2157
+ message: "File uploaded successfully.",
2158
+ meta: {}
2159
+ });
2160
+ } catch (error) {
2161
+ return this.sendResponse({
2162
+ success: false,
2163
+ message: "An error occurred during file upload.",
2164
+ meta: {}
2165
+ });
2176
2166
  }
2177
- // Return success response
2167
+ };
2168
+
2169
+ public deleteFile = async (filePath: string): Promise<actionResponse> => {
2170
+ const hasDeletePermission = await this.permissionCheck("write", filePath);
2171
+ if (!hasDeletePermission) {
2172
+ return this.sendResponse({
2173
+ success: false,
2174
+ message: "You do not have permission to delete files in this path.",
2175
+ meta: {}
2176
+ });
2177
+ }
2178
+ try {
2179
+ const fileRef = ref(this.storage, filePath);
2180
+ await deleteObject(fileRef);
2178
2181
  return this.sendResponse({
2179
2182
  success: true,
2180
- message: "File processed successfully.",
2183
+ message: "File deleted successfully.",
2181
2184
  meta: {}
2182
2185
  });
2183
2186
  } catch (error) {
2184
- console.error(error);
2185
2187
  return this.sendResponse({
2186
2188
  success: false,
2187
- message: "An error occurred during file processing.",
2189
+ message: "An error occurred during file deletion.",
2188
2190
  meta: {}
2189
2191
  });
2190
2192
  }
2191
2193
  };
2194
+
2195
+ public listFiles = async (filePath: string): Promise<actionResponse> => {
2196
+ const hasReadPermission = await this.permissionCheck("read", filePath);
2197
+ if (!hasReadPermission) {
2198
+ return this.sendResponse({
2199
+ success: false,
2200
+ message: "You do not have permission to list files in this path.",
2201
+ meta: {}
2202
+ });
2203
+ }
2204
+
2205
+ try {
2206
+ const listRef = ref(this.storage, filePath.replaceAll('/', '-'));
2207
+ const listResult = await listAll(listRef);
2208
+ const filesPromises = listResult.items.map(async (item) => {
2209
+ const downloadURL = await getDownloadURL(item);
2210
+ return {
2211
+ fileName: item.name,
2212
+ fullPath: item.fullPath, // Assuming fullPath is a property available on the item
2213
+ downloadURL: downloadURL
2214
+ };
2215
+ });
2216
+ const files = await Promise.all(filesPromises);
2217
+ return this.sendResponse({
2218
+ success: true,
2219
+ message: "Files listed successfully.",
2220
+ meta: { files }
2221
+ });
2222
+ } catch (error) {
2223
+ return this.sendResponse({
2224
+ success: false,
2225
+ message: "An error occurred while listing files.",
2226
+ meta: {}
2227
+ });
2228
+ }
2229
+ }
2192
2230
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/firebase",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
4
4
  "description": "Vue 3 / Nuxt 3 Plugin or Nuxt 3 plugin for firebase authentication and firestore.",
5
5
  "main": "index.ts",
6
6
  "scripts": {
package/src/.env.dev CHANGED
@@ -4,5 +4,4 @@ STRIPE_RETURN_URL=
4
4
  OPENAI_API_KEY=
5
5
  TWILIO_SID=
6
6
  TWILIO_AUTH_TOKEN=
7
- TWILIO_SYSTEM_NUMBER=
8
- STORAGE_BUCKET=
7
+ TWILIO_SYSTEM_NUMBER=
package/src/.env.prod CHANGED
@@ -4,5 +4,4 @@ STRIPE_RETURN_URL=
4
4
  OPENAI_API_KEY=
5
5
  TWILIO_SID=
6
6
  TWILIO_AUTH_TOKEN=
7
- TWILIO_SYSTEM_NUMBER=
8
- STORAGE_BUCKET=
7
+ TWILIO_SYSTEM_NUMBER=
@@ -1,12 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/no-var-requires */
2
2
  /* eslint-disable no-undef */
3
- const { onCall, HttpsError, logger, getFirestore, functions, admin, twilio, db, onSchedule, onDocumentUpdated, pubsub } = require('./config.js')
3
+ const { onCall, HttpsError, logger, getFirestore, functions, admin, twilio, db, onSchedule, onDocumentUpdated, pubsub, Storage } = require('./config.js')
4
4
 
5
5
  const authToken = process.env.TWILIO_AUTH_TOKEN
6
6
  const accountSid = process.env.TWILIO_SID
7
7
  const systemNumber = process.env.TWILIO_SYSTEM_NUMBER
8
8
 
9
-
10
9
  function formatPhoneNumber(phone) {
11
10
  // Remove non-numeric characters from the phone number
12
11
  const numericPhone = phone.replace(/\D/g, '')
@@ -14,119 +13,31 @@ function formatPhoneNumber(phone) {
14
13
  return `+1${numericPhone}`
15
14
  }
16
15
 
16
+ const permissionCheck = async (userId, action, originalFilePath) => {
17
+ // Fetch user document
18
+ const collectionPath = originalFilePath.replace(/\//g, '-')
19
+ const userDoc = await db.collection('users').doc(userId).get()
20
+ const userData = userDoc.data()
17
21
 
22
+ // Fetch roles from user data
23
+ const roles = Object.values(userData.roles || {})
18
24
 
19
- // File functions:
20
-
21
- //TODO: NEED TO WRITE WRAPPERS FOR THESE IN THE edgeFirebase.js file... UPLOAD has to do alot more... upload needs to acutall upload the file to firestorage... get the path, then pass it to the uploadFile function...
22
- //TODO: the uploadFile funntion needs to delete the file if they don't have permission to write to the path...
23
-
24
- const bucketName = process.env.BUCKET_NAME
25
- const storage = new Storage();
26
- const bucket = storage.bucket(bucketName)
25
+ for (const role of roles) {
26
+ // Check if the role's collectionPath is a prefix of the collectionPath
27
+ if (collectionPath.startsWith(role.collectionPath)) {
28
+ // Fetch collection data
29
+ const collectionDoc = await db.collection('collection-data').doc(role.collectionPath).get()
30
+ const collectionData = collectionDoc.exists ? collectionDoc.data() : await db.collection('collection-data').doc('-default-').get().then(doc => doc.data())
27
31
 
28
- exports.uploadFile = onCall(async (request) => {
29
- const auth = request.auth
30
- if (data.uid !== auth.uid) {
31
- throw new functions.https.HttpsError('permission-denied', 'You do not have permission to upload files for this user.');
32
- }
33
- const tempFilesPath = `temp/${auth.uid}/`;
34
- const [files] = await bucket.getFiles({ prefix: tempFilesPath });
35
- for (const file of files) {
36
- const originalFilePath = file.name.replace(/-\|-/g, '/');
37
- const hasWritePermission = await permissionCheck(auth.uid, "write", originalFilePath);
38
- if (hasWritePermission) {
39
- // Move file to the new path
40
- await bucket.file(file.name).move(originalFilePath);
41
- } else {
42
- // Delete the file if no write permission
43
- await bucket.file(file.name).delete();
32
+ // Check if action is permitted
33
+ if (collectionData && collectionData[role.role] && collectionData[role.role][action]) {
34
+ return true
35
+ }
44
36
  }
45
37
  }
46
- });
47
-
48
- exports.downloadFile = onCall(async (request) => {
49
- const data = request.data;
50
- const auth = request.auth
51
- if (data.uid !== auth.uid) {
52
- throw new functions.https.HttpsError('permission-denied', 'You do not have permission to upload files for this user.');
53
- }
54
- // Permission check for downloading the specified file
55
- const canRead = await permissionCheck(auth.uid, "read", data.filePath);
56
- if (!canRead) {
57
- throw new HttpsError('permission-denied', 'You do not have permission to download this file.');
58
- }
59
-
60
- const options = {
61
- version: 'v4',
62
- action: 'read',
63
- expires: Date.now() + 5 * 60 * 1000, // 5 minutes
64
- };
65
-
66
- try {
67
- const [url] = await bucket.file(data.filePath).getSignedUrl(options);
68
- return { success: true, url };
69
- } catch (error) {
70
- logger.error(error);
71
- throw new HttpsError('internal', 'Unable to generate download URL.');
72
- }
73
- });
74
-
75
- exports.listFiles = onCall(async (request) => {
76
- // Validate user authentication
77
- const data = request.data
78
- const auth = request.auth
79
- if (data.uid !== auth.uid) {
80
- throw new functions.https.HttpsError('permission-denied', 'You do not have permission to upload files for this user.');
81
- }
82
-
83
- // Permission check for reading the specified directory
84
- const canRead = await permissionCheck(auth.uid, "read", data.directoryPath);
85
- if (!canRead) {
86
- throw new HttpsError('permission-denied', 'You do not have permission to list files in this directory.');
87
- }
88
-
89
- try {
90
- const [files] = await bucket.getFiles({ prefix: data.directoryPath });
91
- const fileList = files.map(file => file.name);
92
- return { success: true, fileList };
93
- } catch (error) {
94
- logger.error(error);
95
- throw new HttpsError('internal', 'Unable to list files.');
96
- }
97
- });
98
-
99
-
100
- exports.deleteFile = onCall(async (request) => {
101
- // Validate user authentication
102
- const data = request.data
103
- const auth = request.auth
104
- if (data.uid !== auth.uid) {
105
- throw new functions.https.HttpsError('permission-denied', 'You do not have permission to upload files for this user.');
106
- }
107
-
108
- // Extract filePath from the request data
109
- const filePath = data.filePath;
110
-
111
- // Perform permission check for deleting the specified file
112
- const canDelete = await permissionCheck(auth.uid, "delete", filePath);
113
- if (!canDelete) {
114
- throw new functions.https.HttpsError('permission-denied', 'You do not have permission to delete this file.');
115
- }
116
-
117
- try {
118
- // Specify your bucket name
119
- await storage.bucket(bucketName).file(filePath).delete();
120
-
121
- return { success: true, message: "File successfully deleted." };
122
- } catch (error) {
123
- console.error("Error deleting file:", error);
124
- throw new functions.https.HttpsError('internal', 'Failed to delete file.');
125
- }
126
- });
127
-
128
- //end file functions
129
-
38
+ // If no permission found, return false
39
+ return false
40
+ }
130
41
 
131
42
  exports.topicQueue = onSchedule({ schedule: 'every 1 minutes', timeoutSeconds: 180 }, async (event) => {
132
43
  const queuedTopicsRef = db.collection('topic-queue')
@@ -178,7 +89,7 @@ exports.sendVerificationCode = onCall(async (request) => {
178
89
  const data = request.data
179
90
  let code = (Math.floor(Math.random() * 1000000) + 1000000).toString().substring(1)
180
91
  const phone = formatPhoneNumber(data.phone)
181
-
92
+
182
93
  if (phone === '+19999999999') {
183
94
  code = '123456'
184
95
  }
@@ -374,32 +285,6 @@ exports.checkOrgIdExists = onCall(async (request) => {
374
285
  return { exists: orgDoc.exists }
375
286
  })
376
287
 
377
- const permissionCheck = async (userId, action, collectionPath) => {
378
- // Fetch user document
379
- const userDoc = await db.collection('users').doc(userId).get()
380
- const userData = userDoc.data()
381
-
382
- // Fetch roles from user data
383
- const roles = userData.roles || []
384
-
385
- // Check each role for permission
386
- for (const role of roles) {
387
- if (role.collectionPath === collectionPath) {
388
- // Fetch collection data
389
- const collectionDoc = await db.collection('collection-data').doc(collectionPath).get()
390
- const collectionData = collectionDoc.exists ? collectionDoc.data() : await db.collection('collection-data').doc('-default-').get().then(doc => doc.data())
391
-
392
- // Check if action is permitted
393
- if (collectionData && collectionData[role.role] && collectionData[role.role][action]) {
394
- return true
395
- }
396
- }
397
- }
398
-
399
- // If no permission found, return false
400
- return false
401
- }
402
-
403
288
  exports.deleteSelf = onCall(async (request) => {
404
289
  if (request.data.uid === request.auth.uid) {
405
290
  try {
package/src/storage.rules CHANGED
@@ -2,17 +2,44 @@ rules_version = '2';
2
2
  // #EDGE FIREBASE RULES START
3
3
  service firebase.storage {
4
4
  match /b/{bucket}/o {
5
- // Deny read access to all paths
6
- match /{allPaths=**} {
7
- allow read: if false;
5
+ // Match the file path structure you're using, simulating the Firestore document path structure for permissions.
6
+ function getRolePermission(role, collection, permissionCheck) {
7
+ let pathCollectionPermissions = firestore.get(/databases/(default)/documents/collection-data/$(collection)).data;
8
+ let defaultPermissions = firestore.get(/databases/(default)/documents/collection-data/-default-).data;
9
+ return (role in pathCollectionPermissions && pathCollectionPermissions[role][permissionCheck]) ||
10
+ (role in defaultPermissions && defaultPermissions[role][permissionCheck]);
8
11
  }
9
- // Allow write access only to paths matching /temp/{userId}/{anyPath=**}
10
- // {userId} is a placeholder for the actual user ID
11
- // {anyPath=**} matches any file path under /temp/{userId}/
12
- match /temp/{userId}/{anyPath=**} {
13
- allow write: if request.auth != null && request.auth.uid == userId;
14
- // Keep read access denied
15
- allow read: if false;
12
+ function checkPermission(permissionCheck, collectionPath) {
13
+ let user = firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data;
14
+ let ruleHelper = firestore.get(/databases/(default)/documents/rule-helpers/$(request.auth.uid)).data;
15
+ return request.auth != null &&
16
+ collectionPath in ruleHelper &&
17
+ "permissionCheckPath" in ruleHelper[collectionPath] &&
18
+ (
19
+ ruleHelper[collectionPath].permissionCheckPath == "-" ||
20
+ collectionPath.matches("^" + ruleHelper[collectionPath].permissionCheckPath + ".*$")
21
+ ) &&
22
+ (
23
+ (
24
+ "roles" in user &&
25
+ ruleHelper[collectionPath].permissionCheckPath in user.roles &&
26
+ getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role, ruleHelper[collectionPath].permissionCheckPath, permissionCheck)
27
+ ) ||
28
+ (
29
+ "specialPermissions" in user &&
30
+ ruleHelper[collectionPath].permissionCheckPath in user.specialPermissions &&
31
+ permissionCheck in user.specialPermissions[ruleHelper[collectionPath].permissionCheckPath] &&
32
+ user.specialPermissions[ruleHelper[collectionPath].permissionCheckPath][permissionCheck]
33
+ )
34
+ );
35
+ }
36
+ match /{dir}/{fileId} {
37
+ // General read permission check based on Firestore data
38
+ allow read: if checkPermission("read", dir);
39
+ // General write permission check, including creating and updating files
40
+ allow write: if checkPermission("write", dir);
41
+ // General delete permission check
42
+ allow delete: if checkPermission("write", dir);
16
43
  }
17
44
  }
18
45
  }