@edgedev/firebase 2.0.32 → 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} 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
 
@@ -181,6 +181,7 @@ interface firebaseConfig {
181
181
  measurementId?: string;
182
182
  emulatorFirestore?: string;
183
183
  emulatorFunctions?: string;
184
+ emulatorStorage?: string;
184
185
  }
185
186
 
186
187
  interface actionResponse {
@@ -206,7 +207,8 @@ export const EdgeFirebase = class {
206
207
  measurementId: "",
207
208
  emulatorAuth: "",
208
209
  emulatorFirestore: "",
209
- emulatorFunctions: ""
210
+ emulatorFunctions: "",
211
+ emulatorStorage: "",
210
212
  },
211
213
  isPersistant: false,
212
214
  enablePopupRedirect: false,
@@ -242,6 +244,9 @@ export const EdgeFirebase = class {
242
244
  if (this.firebaseConfig.emulatorFunctions) {
243
245
  connectFunctionsEmulator(this.functions, "127.0.0.1", this.firebaseConfig.emulatorFunctions)
244
246
  }
247
+ if (this.firebaseConfig.emulatorStorage) {
248
+ connectStorageEmulator(this.storage, "127.0.0.1", this.firebaseConfig.emulatorStorage)
249
+ }
245
250
  this.setOnAuthStateChanged();
246
251
  }
247
252
 
@@ -2122,8 +2127,8 @@ export const EdgeFirebase = class {
2122
2127
 
2123
2128
 
2124
2129
  // File functions
2125
- public uploadFileToStorage = async (filePath: string, file: Blob): Promise<actionResponse> => {
2126
- try {
2130
+ public uploadFile = async (filePath: string, file: Blob): Promise<actionResponse> => {
2131
+
2127
2132
  // Validate if file is provided
2128
2133
  if (!file) {
2129
2134
  return this.sendResponse({
@@ -2143,46 +2148,83 @@ export const EdgeFirebase = class {
2143
2148
  });
2144
2149
  }
2145
2150
 
2146
- // Initialize Firebase Storage
2147
-
2148
- // Define a temporary path for the file upload
2149
- // You might want to include some unique identifier in the temporary path
2150
- const tempFilePath = `temp/${this.user.uid}/${filePath.replaceAll('/', '-|-')}`;
2151
-
2152
- // Create a reference to the temporary file location
2153
- const fileRef = storageRef(this.storage, tempFilePath);
2154
-
2155
- // Upload the file to the temporary location
2156
- await uploadBytes(fileRef, file);
2157
-
2158
- // Prepare data for the callable function
2159
- const data = {
2160
- uid: this.user.uid,
2161
- filePath: filePath, // The final desired path for the file
2162
- };
2163
-
2164
- // Call the Firebase Function to handle the file move and any additional processing
2165
- const callable = httpsCallable(this.functions, 'edgeFirebase-uploadFile');
2166
- const functionResult = await callable(data);
2167
-
2168
- const resultData = functionResult.data as { success: boolean; message: string; finalDownloadURL?: string };
2169
-
2170
- if (!resultData.success) {
2171
- 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
+ });
2172
2166
  }
2173
- // 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);
2174
2181
  return this.sendResponse({
2175
2182
  success: true,
2176
- message: "File processed successfully.",
2183
+ message: "File deleted successfully.",
2177
2184
  meta: {}
2178
2185
  });
2179
2186
  } catch (error) {
2180
- console.error(error);
2181
2187
  return this.sendResponse({
2182
2188
  success: false,
2183
- message: "An error occurred during file processing.",
2189
+ message: "An error occurred during file deletion.",
2184
2190
  meta: {}
2185
2191
  });
2186
2192
  }
2187
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
+ }
2188
2230
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/firebase",
3
- "version": "2.0.32",
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=
@@ -7,4 +7,5 @@ VITE_FIREBASE_APP_ID=
7
7
  VITE_FIREBASE_MEASUREMENT_ID=
8
8
  VITE_FIREBASE_EMULATOR_AUTH=9099
9
9
  VITE_FIREBASE_EMULATOR_FIRESTORE=8080
10
- VITE_FIREBASE_EMULATOR_FUNCTIONS=5001
10
+ VITE_FIREBASE_EMULATOR_FUNCTIONS=5001
11
+ VITE_FIREBASE_EMULATOR_STORAGE=9199
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=
@@ -7,4 +7,5 @@ VITE_FIREBASE_APP_ID=
7
7
  VITE_FIREBASE_MEASUREMENT_ID=
8
8
  VITE_FIREBASE_EMULATOR_AUTH=
9
9
  VITE_FIREBASE_EMULATOR_FIRESTORE=
10
- VITE_FIREBASE_EMULATOR_FUNCTIONS=
10
+ VITE_FIREBASE_EMULATOR_FUNCTIONS=
11
+ VITE_FIREBASE_EMULATOR_STORAGE=
@@ -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 {
@@ -60,12 +60,12 @@ if [ ! -f "$project_root/functions/.env.prod" ]; then
60
60
  cp ./src/.env.prod "$project_root/functions/.env.prod"
61
61
  fi
62
62
 
63
- if [ ! -f "$project_root/.env.development" ]; then
64
- cp ./src/.env.development "$project_root/.env.development"
63
+ if [ ! -f "$project_root/.env.dev" ]; then
64
+ cp ./src/.env.development "$project_root/.env.dev"
65
65
  fi
66
66
 
67
- if [ ! -f "$project_root/.env.production" ]; then
68
- cp ./src/.env.production "$project_root/.env.production"
67
+ if [ ! -f "$project_root/.env" ]; then
68
+ cp ./src/.env.production "$project_root/.env"
69
69
  fi
70
70
 
71
71
  if [ ! -f "$project_root/functions/package.json" ]; then
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
  }