@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 +76 -34
- package/package.json +1 -1
- package/src/.env.dev +1 -2
- package/src/.env.development +2 -1
- package/src/.env.prod +1 -2
- package/src/.env.production +2 -1
- package/src/edgeFirebase.js +22 -137
- package/src/postinstall.sh +4 -4
- package/src/storage.rules +37 -10
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
|
|
|
@@ -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
|
|
2126
|
-
|
|
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
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/src/.env.dev
CHANGED
package/src/.env.development
CHANGED
package/src/.env.prod
CHANGED
package/src/.env.production
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/postinstall.sh
CHANGED
|
@@ -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.
|
|
64
|
-
cp ./src/.env.development "$project_root/.env.
|
|
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
|
|
68
|
-
cp ./src/.env.production "$project_root/.env
|
|
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
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
}
|