@edgedev/firebase 2.0.33 → 2.0.36
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 +72 -34
- package/package.json +1 -1
- package/src/.env.dev +1 -2
- package/src/.env.prod +1 -2
- package/src/edgeFirebase.js +22 -137
- package/src/storage.rules +41 -11
package/edgeFirebase.ts
CHANGED
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
} from "firebase/auth";
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
import { getStorage, ref
|
|
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,
|
|
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
|
|
2130
|
-
|
|
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
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/src/.env.dev
CHANGED
package/src/.env.prod
CHANGED
package/src/edgeFirebase.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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,18 +2,48 @@ rules_version = '2';
|
|
|
2
2
|
// #EDGE FIREBASE RULES START
|
|
3
3
|
service firebase.storage {
|
|
4
4
|
match /b/{bucket}/o {
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Match the file path structure you're using, simulating the Firestore document path structure for permissions.
|
|
6
|
+
function getRolePermission(role, permissionCheck) {
|
|
7
|
+
let permissions = {
|
|
8
|
+
'admin': {'assign': true, 'delete': true, 'read': true, 'write': true},
|
|
9
|
+
'editor': {'assign': false, 'delete': true, 'read': true, 'write': true},
|
|
10
|
+
'user': {'assign': false, 'delete': false, 'read': true, 'write': false},
|
|
11
|
+
'writer': {'assign': false, 'delete': false, 'read': true, 'write': true}
|
|
12
|
+
};
|
|
13
|
+
return permissions[role][permissionCheck];
|
|
8
14
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
function checkPermission(permissionCheck, collectionPath) {
|
|
16
|
+
let user = firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data;
|
|
17
|
+
let ruleHelper = firestore.get(/databases/(default)/documents/rule-helpers/$(request.auth.uid)).data;
|
|
18
|
+
return request.auth != null &&
|
|
19
|
+
collectionPath in ruleHelper &&
|
|
20
|
+
"permissionCheckPath" in ruleHelper[collectionPath] &&
|
|
21
|
+
(
|
|
22
|
+
ruleHelper[collectionPath].permissionCheckPath == "-" ||
|
|
23
|
+
collectionPath.matches("^" + ruleHelper[collectionPath].permissionCheckPath + ".*$")
|
|
24
|
+
) &&
|
|
25
|
+
(
|
|
26
|
+
(
|
|
27
|
+
"roles" in user &&
|
|
28
|
+
ruleHelper[collectionPath].permissionCheckPath in user.roles &&
|
|
29
|
+
getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role, permissionCheck)
|
|
30
|
+
) ||
|
|
31
|
+
(
|
|
32
|
+
"specialPermissions" in user &&
|
|
33
|
+
ruleHelper[collectionPath].permissionCheckPath in user.specialPermissions &&
|
|
34
|
+
permissionCheck in user.specialPermissions[ruleHelper[collectionPath].permissionCheckPath] &&
|
|
35
|
+
user.specialPermissions[ruleHelper[collectionPath].permissionCheckPath][permissionCheck]
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
match /{dir}/{fileId} {
|
|
40
|
+
// General read permission check based on Firestore data
|
|
41
|
+
allow read: if checkPermission("read", dir);
|
|
42
|
+
// General write permission check, including creating and updating files
|
|
43
|
+
allow write: if checkPermission("write", dir);
|
|
44
|
+
// General delete permission check
|
|
45
|
+
allow delete: if checkPermission("write", dir);
|
|
16
46
|
}
|
|
17
47
|
}
|
|
18
48
|
}
|
|
19
|
-
// #EDGE FIREBASE RULES END
|
|
49
|
+
// #EDGE FIREBASE RULES END
|