@edgedev/create-edge-app 1.0.43 → 1.0.44
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/firestore.rules +41 -22
- package/firestore.rules.backup +30 -1
- package/functions/config.js +87 -0
- package/functions/edgeFirebase.js +424 -0
- package/package.json +2 -1
- package/plugins/fileUpload.ts +6 -0
- package/storage.rules +53 -0
- package/storage.rules.backup +8 -0
package/firestore.rules
CHANGED
|
@@ -31,6 +31,14 @@ service cloud.firestore {
|
|
|
31
31
|
allow update: if false;
|
|
32
32
|
allow delete: if false;
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
match /databases/{database}/documents/system/{event} {
|
|
37
|
+
allow read: if false;
|
|
38
|
+
allow create: if false;
|
|
39
|
+
allow update: if false;
|
|
40
|
+
allow delete: if false;
|
|
41
|
+
}
|
|
34
42
|
|
|
35
43
|
match /databases/{database}/documents/rule-helpers/{helper} {
|
|
36
44
|
allow read: if false;
|
|
@@ -56,11 +64,14 @@ service cloud.firestore {
|
|
|
56
64
|
|
|
57
65
|
match /databases/{database}/documents/collection-data/{collectionPath} {
|
|
58
66
|
// TODO: these rules need tested.
|
|
59
|
-
function getRolePermission(role,
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
function getRolePermission(role, permissionCheck) {
|
|
68
|
+
let permissions = {
|
|
69
|
+
'admin': {'assign': true, 'delete': true, 'read': true, 'write': true},
|
|
70
|
+
'editor': {'assign': false, 'delete': true, 'read': true, 'write': true},
|
|
71
|
+
'user': {'assign': false, 'delete': false, 'read': true, 'write': false},
|
|
72
|
+
'writer': {'assign': false, 'delete': false, 'read': true, 'write': true}
|
|
73
|
+
};
|
|
74
|
+
return permissions[role][permissionCheck];
|
|
64
75
|
}
|
|
65
76
|
function canAssign() {
|
|
66
77
|
let user = get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
|
@@ -76,7 +87,7 @@ service cloud.firestore {
|
|
|
76
87
|
"roles" in user &&
|
|
77
88
|
ruleHelper[collectionPath].permissionCheckPath in user.roles &&
|
|
78
89
|
"role" in user.roles[ruleHelper[collectionPath].permissionCheckPath] &&
|
|
79
|
-
getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role,
|
|
90
|
+
getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role, "assign")
|
|
80
91
|
);
|
|
81
92
|
}
|
|
82
93
|
allow read: if request.auth != null; // All signed in users can read collection-data
|
|
@@ -141,7 +152,7 @@ service cloud.firestore {
|
|
|
141
152
|
(
|
|
142
153
|
(
|
|
143
154
|
"roles" in user &&
|
|
144
|
-
getRolePermission(user.roles[permissionCheckPath].role,
|
|
155
|
+
getRolePermission(user.roles[permissionCheckPath].role, "assign")
|
|
145
156
|
) ||
|
|
146
157
|
(
|
|
147
158
|
"specialPermissions" in user &&
|
|
@@ -160,7 +171,7 @@ service cloud.firestore {
|
|
|
160
171
|
(
|
|
161
172
|
"roles" in user &&
|
|
162
173
|
ruleHelper["edge-assignment-helper"].permissionCheckPath in user.roles &&
|
|
163
|
-
getRolePermission(user.roles[ruleHelper["edge-assignment-helper"].permissionCheckPath].role,
|
|
174
|
+
getRolePermission(user.roles[ruleHelper["edge-assignment-helper"].permissionCheckPath].role, 'assign')
|
|
164
175
|
) ||
|
|
165
176
|
(
|
|
166
177
|
"specialPermissions" in user &&
|
|
@@ -188,7 +199,7 @@ service cloud.firestore {
|
|
|
188
199
|
(
|
|
189
200
|
"roles" in user &&
|
|
190
201
|
permissionCheckPath in user.roles &&
|
|
191
|
-
getRolePermission(user.roles[permissionCheckPath].role,
|
|
202
|
+
getRolePermission(user.roles[permissionCheckPath].role, "assign")
|
|
192
203
|
) ||
|
|
193
204
|
(
|
|
194
205
|
"specialPermissions" in user &&
|
|
@@ -216,12 +227,15 @@ service cloud.firestore {
|
|
|
216
227
|
return request.resource.data.roles.size() == 0 && request.resource.data.specialPermissions.size() == 0;
|
|
217
228
|
}
|
|
218
229
|
|
|
219
|
-
function getRolePermission(role,
|
|
220
|
-
let
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
230
|
+
function getRolePermission(role, permissionCheck) {
|
|
231
|
+
let permissions = {
|
|
232
|
+
'admin': {'assign': true, 'delete': true, 'read': true, 'write': true},
|
|
233
|
+
'editor': {'assign': false, 'delete': true, 'read': true, 'write': true},
|
|
234
|
+
'user': {'assign': false, 'delete': false, 'read': true, 'write': false},
|
|
235
|
+
'writer': {'assign': false, 'delete': false, 'read': true, 'write': true}
|
|
236
|
+
};
|
|
237
|
+
return permissions[role][permissionCheck];
|
|
238
|
+
}
|
|
225
239
|
|
|
226
240
|
function canGet () {
|
|
227
241
|
return resource == null ||
|
|
@@ -237,11 +251,14 @@ service cloud.firestore {
|
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
match /databases/{database}/documents/{seg1} {
|
|
240
|
-
function getRolePermission(role,
|
|
241
|
-
let
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
function getRolePermission(role, permissionCheck) {
|
|
255
|
+
let permissions = {
|
|
256
|
+
'admin': {'assign': true, 'delete': true, 'read': true, 'write': true},
|
|
257
|
+
'editor': {'assign': false, 'delete': true, 'read': true, 'write': true},
|
|
258
|
+
'user': {'assign': false, 'delete': false, 'read': true, 'write': false},
|
|
259
|
+
'writer': {'assign': false, 'delete': false, 'read': true, 'write': true}
|
|
260
|
+
};
|
|
261
|
+
return permissions[role][permissionCheck];
|
|
245
262
|
}
|
|
246
263
|
function checkPermission(collectionPath, permissionCheck) {
|
|
247
264
|
let user = get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
|
@@ -251,7 +268,9 @@ service cloud.firestore {
|
|
|
251
268
|
!(permissionCheck == "write" &&
|
|
252
269
|
(
|
|
253
270
|
("stripeCustomerId" in request.resource.data && (!("stripeCustomerId" in resource.data) || resource.data.stripeCustomerId != request.resource.data.stripeCustomerId)) ||
|
|
254
|
-
("stripeSubscription" in request.resource.data && (!("stripeSubscription" in resource.data) || resource.data.stripeSubscription != request.resource.data.stripeSubscription))
|
|
271
|
+
("stripeSubscription" in request.resource.data && (!("stripeSubscription" in resource.data) || resource.data.stripeSubscription != request.resource.data.stripeSubscription)) ||
|
|
272
|
+
("stripeProductId" in request.resource.data && (!("stripeProductId" in resource.data) || resource.data.stripeProductId != request.resource.data.stripeProductId)) ||
|
|
273
|
+
("stripePriceId" in request.resource.data && (!("stripePriceId" in resource.data) || resource.data.stripePriceId != request.resource.data.stripePriceId))
|
|
255
274
|
)
|
|
256
275
|
) &&
|
|
257
276
|
request.auth != null &&
|
|
@@ -265,7 +284,7 @@ service cloud.firestore {
|
|
|
265
284
|
(
|
|
266
285
|
"roles" in user &&
|
|
267
286
|
ruleHelper[collectionPath].permissionCheckPath in user.roles &&
|
|
268
|
-
getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role,
|
|
287
|
+
getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role, permissionCheck)
|
|
269
288
|
) ||
|
|
270
289
|
(
|
|
271
290
|
"specialPermissions" in user &&
|
package/firestore.rules.backup
CHANGED
|
@@ -2,6 +2,29 @@ rules_version = '2';
|
|
|
2
2
|
// #EDGE FIREBASE RULES START
|
|
3
3
|
service cloud.firestore {
|
|
4
4
|
|
|
5
|
+
match /databases/{database}/documents/phone-auth/{phone} {
|
|
6
|
+
allow read: if false;
|
|
7
|
+
allow create: if false;
|
|
8
|
+
allow update: if false;
|
|
9
|
+
allow delete: if false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
match /databases/{database}/documents/topic-queue/{topic} {
|
|
13
|
+
allow read: if false;
|
|
14
|
+
allow create: if false;
|
|
15
|
+
allow update: if false;
|
|
16
|
+
allow delete: if false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
match /databases/{database}/documents/public-users/{user} {
|
|
21
|
+
allow read: if request.auth != null;
|
|
22
|
+
allow list: if request.auth != null;
|
|
23
|
+
allow create: if false;
|
|
24
|
+
allow update: if false;
|
|
25
|
+
allow delete: if false;
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
match /databases/{database}/documents/events/{event} {
|
|
6
29
|
allow read: if false;
|
|
7
30
|
allow create: if false;
|
|
@@ -222,9 +245,15 @@ service cloud.firestore {
|
|
|
222
245
|
}
|
|
223
246
|
function checkPermission(collectionPath, permissionCheck) {
|
|
224
247
|
let user = get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
|
225
|
-
let skipPaths = ["collection-data", "users", "staged-users", "events", "rule-helpers"];
|
|
248
|
+
let skipPaths = ["collection-data", "users", "staged-users", "events", "rule-helpers", "phone-auth", "public-users", "topic-queue"];
|
|
226
249
|
let ruleHelper = get(/databases/$(database)/documents/rule-helpers/$(request.auth.uid)).data;
|
|
227
250
|
return !(collectionPath in skipPaths) &&
|
|
251
|
+
!(permissionCheck == "write" &&
|
|
252
|
+
(
|
|
253
|
+
("stripeCustomerId" in request.resource.data && (!("stripeCustomerId" in resource.data) || resource.data.stripeCustomerId != request.resource.data.stripeCustomerId)) ||
|
|
254
|
+
("stripeSubscription" in request.resource.data && (!("stripeSubscription" in resource.data) || resource.data.stripeSubscription != request.resource.data.stripeSubscription))
|
|
255
|
+
)
|
|
256
|
+
) &&
|
|
228
257
|
request.auth != null &&
|
|
229
258
|
collectionPath in ruleHelper &&
|
|
230
259
|
"permissionCheckPath" in ruleHelper[collectionPath] &&
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
const functions = require('firebase-functions')
|
|
4
|
+
const { PubSub } = require('@google-cloud/pubsub')
|
|
5
|
+
const admin = require('firebase-admin')
|
|
6
|
+
|
|
7
|
+
const pubsub = new PubSub()
|
|
8
|
+
|
|
9
|
+
admin.initializeApp()
|
|
10
|
+
|
|
11
|
+
const { onMessagePublished } = require('firebase-functions/v2/pubsub')
|
|
12
|
+
|
|
13
|
+
const { onCall, HttpsError, onRequest } = require('firebase-functions/v2/https')
|
|
14
|
+
const { onSchedule } = require('firebase-functions/v2/scheduler')
|
|
15
|
+
const { Storage } = require('@google-cloud/storage')
|
|
16
|
+
const {
|
|
17
|
+
onDocumentWritten,
|
|
18
|
+
onDocumentCreated,
|
|
19
|
+
onDocumentUpdated,
|
|
20
|
+
onDocumentDeleted,
|
|
21
|
+
Change,
|
|
22
|
+
FirestoreEvent,
|
|
23
|
+
} = require('firebase-functions/v2/firestore')
|
|
24
|
+
const { logger } = require('firebase-functions/v2')
|
|
25
|
+
const { getFirestore } = require('firebase-admin/firestore')
|
|
26
|
+
const twilio = require('twilio')
|
|
27
|
+
const db = getFirestore()
|
|
28
|
+
|
|
29
|
+
// The permissionCheck function
|
|
30
|
+
|
|
31
|
+
const permissions = {
|
|
32
|
+
'admin': {'assign': true, 'delete': true, 'read': true, 'write': true},
|
|
33
|
+
'editor': {'assign': false, 'delete': true, 'read': true, 'write': true},
|
|
34
|
+
'user': {'assign': false, 'delete': false, 'read': true, 'write': false},
|
|
35
|
+
'writer': {'assign': false, 'delete': false, 'read': true, 'write': true}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const permissionCheck = async (userId, action, originalFilePath) => {
|
|
39
|
+
// Fetch user document
|
|
40
|
+
const collectionPath = originalFilePath.replace(/\//g, '-')
|
|
41
|
+
const userDoc = await db.collection('users').doc(userId).get()
|
|
42
|
+
if (!userDoc.exists) {
|
|
43
|
+
console.log('No such user!')
|
|
44
|
+
return false // Or handle as needed
|
|
45
|
+
}
|
|
46
|
+
const userData = userDoc.data()
|
|
47
|
+
|
|
48
|
+
// Fetch roles from user data
|
|
49
|
+
const roles = Object.values(userData.roles || {})
|
|
50
|
+
|
|
51
|
+
for (const role of roles) {
|
|
52
|
+
// Check if the role's collectionPath is a prefix of the collectionPath
|
|
53
|
+
if (collectionPath.startsWith(role.collectionPath)) {
|
|
54
|
+
// Use permissions object instead of fetching collection data
|
|
55
|
+
const rolePermissions = permissions[role.role];
|
|
56
|
+
if (rolePermissions && rolePermissions[action]) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
pubsub,
|
|
66
|
+
onMessagePublished,
|
|
67
|
+
onRequest,
|
|
68
|
+
onSchedule,
|
|
69
|
+
onDocumentWritten,
|
|
70
|
+
onDocumentCreated,
|
|
71
|
+
onDocumentUpdated,
|
|
72
|
+
onDocumentDeleted,
|
|
73
|
+
Change,
|
|
74
|
+
FirestoreEvent,
|
|
75
|
+
onCall,
|
|
76
|
+
HttpsError,
|
|
77
|
+
logger,
|
|
78
|
+
getFirestore,
|
|
79
|
+
functions,
|
|
80
|
+
admin,
|
|
81
|
+
twilio,
|
|
82
|
+
db,
|
|
83
|
+
Storage,
|
|
84
|
+
permissionCheck,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
const { onCall, HttpsError, logger, getFirestore, functions, admin, twilio, db, onSchedule, onDocumentUpdated, pubsub, Storage, permissionCheck } = require('./config.js')
|
|
4
|
+
|
|
5
|
+
const authToken = process.env.TWILIO_AUTH_TOKEN
|
|
6
|
+
const accountSid = process.env.TWILIO_SID
|
|
7
|
+
const systemNumber = process.env.TWILIO_SYSTEM_NUMBER
|
|
8
|
+
|
|
9
|
+
function formatPhoneNumber(phone) {
|
|
10
|
+
// Remove non-numeric characters from the phone number
|
|
11
|
+
const numericPhone = phone.replace(/\D/g, '')
|
|
12
|
+
// Return the formatted number
|
|
13
|
+
return `+1${numericPhone}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
exports.topicQueue = onSchedule({ schedule: 'every 1 minutes', timeoutSeconds: 180 }, async (event) => {
|
|
17
|
+
const queuedTopicsRef = db.collection('topic-queue')
|
|
18
|
+
const snapshot = await queuedTopicsRef.get()
|
|
19
|
+
|
|
20
|
+
for (const doc of snapshot.docs) {
|
|
21
|
+
await db.runTransaction(async (transaction) => {
|
|
22
|
+
const docSnapshot = await transaction.get(doc.ref)
|
|
23
|
+
if (!docSnapshot.exists) {
|
|
24
|
+
throw new Error('Document does not exist!')
|
|
25
|
+
}
|
|
26
|
+
const docData = docSnapshot.data()
|
|
27
|
+
const emailTimestamp = docData.timestamp ? docData.timestamp.toMillis() : 0
|
|
28
|
+
const delayTimestamp = docData.minuteDelay ? emailTimestamp + docData.minuteDelay * 60 * 1000 : 0
|
|
29
|
+
const currentTimestamp = Date.now()
|
|
30
|
+
// Check if current time is beyond the timestamp + minuteDelay, or if timestamp or minuteDelay is not set
|
|
31
|
+
if (emailTimestamp > currentTimestamp || currentTimestamp >= delayTimestamp || !docData.timestamp || !docData.minuteDelay) {
|
|
32
|
+
// Check if topic and payload exist and are not empty
|
|
33
|
+
if (docData.topic && docData.payload && typeof docData.payload === 'object' && docData.topic.trim() !== '') {
|
|
34
|
+
try {
|
|
35
|
+
await pubsub.topic(docData.topic).publishMessage({ data: Buffer.from(JSON.stringify(docData.payload)) })
|
|
36
|
+
// Delete the document after successfully publishing the message
|
|
37
|
+
transaction.delete(doc.ref)
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error(`Error publishing message to topic ${docData.topic}:`, error)
|
|
41
|
+
// Increment retry count and set new delay
|
|
42
|
+
const retryCount = docData.retry ? docData.retry + 1 : 1
|
|
43
|
+
if (retryCount <= 3) {
|
|
44
|
+
const minuteDelay = retryCount === 1 ? 1 : retryCount === 2 ? 10 : 30
|
|
45
|
+
transaction.update(doc.ref, { retry: retryCount, minuteDelay })
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Delete the document if there was an error publishing the topic after 3 retries
|
|
49
|
+
transaction.delete(doc.ref)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Delete the document if topic or payload does not exist or is empty
|
|
54
|
+
else {
|
|
55
|
+
transaction.delete(doc.ref)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
exports.sendVerificationCode = onCall(async (request) => {
|
|
63
|
+
const data = request.data
|
|
64
|
+
let code = (Math.floor(Math.random() * 1000000) + 1000000).toString().substring(1)
|
|
65
|
+
const phone = formatPhoneNumber(data.phone)
|
|
66
|
+
|
|
67
|
+
if (phone === '+19999999999') {
|
|
68
|
+
code = '123456'
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
try {
|
|
72
|
+
const client = twilio(accountSid, authToken)
|
|
73
|
+
await client.messages.create({
|
|
74
|
+
body: `Your verification code is: ${code}`,
|
|
75
|
+
to: phone, // the user's phone number
|
|
76
|
+
from: systemNumber, // your Twilio phone number from the configuration
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.log(error)
|
|
81
|
+
return { success: false, error: 'Invalid Phone #' }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Use the formatted phone number as the document ID for Firestore
|
|
87
|
+
await db.collection('phone-auth').doc(phone).set({
|
|
88
|
+
phone,
|
|
89
|
+
code,
|
|
90
|
+
})
|
|
91
|
+
return phone
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return { success: false, error }
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
exports.verifyPhoneNumber = onCall(async (request) => {
|
|
99
|
+
const data = request.data
|
|
100
|
+
const phone = data.phone
|
|
101
|
+
const code = data.code
|
|
102
|
+
|
|
103
|
+
// Get the phone-auth document with the given phone number
|
|
104
|
+
const phoneDoc = await db.collection('phone-auth').doc(phone).get()
|
|
105
|
+
|
|
106
|
+
if (!phoneDoc.exists) {
|
|
107
|
+
return { success: false, error: 'Phone number not found.' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const storedCode = phoneDoc.data().code
|
|
111
|
+
|
|
112
|
+
if (storedCode !== code) {
|
|
113
|
+
return { success: false, error: 'Invalid verification code.' }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If the code matches, authenticate the user with Firebase Custom Auth
|
|
117
|
+
try {
|
|
118
|
+
// You would typically generate a UID based on the phone number or another system
|
|
119
|
+
const uid = phone
|
|
120
|
+
|
|
121
|
+
// Create a custom token (this can be used on the client to sign in)
|
|
122
|
+
const customToken = await admin.auth().createCustomToken(uid)
|
|
123
|
+
|
|
124
|
+
return { success: true, token: customToken }
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('Error creating custom token:', error)
|
|
128
|
+
return { success: false, error: 'Failed to authenticate.' }
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
exports.initFirestore = onCall(async (request) => {
|
|
133
|
+
// checks to see of the collections 'staged-users' exist if not will seed them with data
|
|
134
|
+
const stagedUsers = await db.collection('staged-users').get()
|
|
135
|
+
if (stagedUsers.empty) {
|
|
136
|
+
const templateUser = {
|
|
137
|
+
docId: 'organization-registration-template',
|
|
138
|
+
isTemplate: true,
|
|
139
|
+
meta: {
|
|
140
|
+
name: 'Organization Registration Template',
|
|
141
|
+
},
|
|
142
|
+
subCreate: {
|
|
143
|
+
documentStructure: {
|
|
144
|
+
name: '',
|
|
145
|
+
},
|
|
146
|
+
dynamicDocumentField: 'name',
|
|
147
|
+
role: 'admin',
|
|
148
|
+
rootPath: 'organizations',
|
|
149
|
+
},
|
|
150
|
+
userId: '',
|
|
151
|
+
}
|
|
152
|
+
await db.collection('staged-users').doc('organization-registration-template').set(templateUser)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
exports.removeNonRegisteredUser = onCall(async (request) => {
|
|
157
|
+
const data = request.data
|
|
158
|
+
const auth = request.auth
|
|
159
|
+
if (data.uid === auth.uid) {
|
|
160
|
+
const stagedUser = await db.collection('staged-users').doc(data.docId).get()
|
|
161
|
+
if (stagedUser.exists) {
|
|
162
|
+
const stagedUserData = stagedUser.data()
|
|
163
|
+
|
|
164
|
+
const rolesExist = stagedUserData.roles && Object.keys(stagedUserData.roles).length !== 0
|
|
165
|
+
const specialPermissionsExist = stagedUserData.specialPermissions && Object.keys(stagedUserData.specialPermissions).length !== 0
|
|
166
|
+
const userIdExistsAndNotBlank = stagedUserData.userId && stagedUserData.userId !== ''
|
|
167
|
+
|
|
168
|
+
if (!rolesExist && !specialPermissionsExist && !userIdExistsAndNotBlank) {
|
|
169
|
+
await db.collection('staged-users').doc(data.docId).delete()
|
|
170
|
+
return { success: true, message: '' }
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
let message = ''
|
|
174
|
+
if (rolesExist && specialPermissionsExist) {
|
|
175
|
+
message = 'Cannot delete because the non-registered user still has roles and special permissions assigned.'
|
|
176
|
+
}
|
|
177
|
+
else if (rolesExist) {
|
|
178
|
+
message = 'Cannot delete because the non-registered user still has roles assigned.'
|
|
179
|
+
}
|
|
180
|
+
else if (specialPermissionsExist) {
|
|
181
|
+
message = 'Cannot delete because the non-registered user still has special permissions assigned.'
|
|
182
|
+
}
|
|
183
|
+
else if (userIdExistsAndNotBlank) {
|
|
184
|
+
message = 'Cannot delete because the user is registered.'
|
|
185
|
+
}
|
|
186
|
+
return { success: false, message }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { success: false, message: 'Non-registered user not found.' }
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
exports.currentUserRegister = onCall(async (request) => {
|
|
194
|
+
const data = request.data
|
|
195
|
+
const auth = request.auth
|
|
196
|
+
if (data.uid === auth.uid) {
|
|
197
|
+
const stagedUser = await db.collection('staged-users').doc(data.registrationCode).get()
|
|
198
|
+
if (!stagedUser.exists) {
|
|
199
|
+
return { success: false, message: 'Registration code not found.' }
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const stagedUserData = await stagedUser.data()
|
|
203
|
+
let process = false
|
|
204
|
+
if (stagedUserData.isTemplate) {
|
|
205
|
+
process = true
|
|
206
|
+
}
|
|
207
|
+
if (!stagedUserData.isTemplate && stagedUserData.userId === '') {
|
|
208
|
+
process = true
|
|
209
|
+
}
|
|
210
|
+
if (!process) {
|
|
211
|
+
return { success: false, message: 'Registration code not valid.' }
|
|
212
|
+
}
|
|
213
|
+
const newRoles = stagedUserData.roles || {}
|
|
214
|
+
const currentUser = await db.collection('users').doc(data.uid).get()
|
|
215
|
+
const currentUserData = await currentUser.data()
|
|
216
|
+
const currentRoles = currentUserData.roles || {}
|
|
217
|
+
const currentUserCollectionPaths = currentUserData.collectionPaths || []
|
|
218
|
+
let newRole = {}
|
|
219
|
+
if (stagedUserData.subCreate && Object.keys(stagedUserData.subCreate).length !== 0 && stagedUserData.isTemplate) {
|
|
220
|
+
if (!data.dynamicDocumentFieldValue) {
|
|
221
|
+
return { success: false, message: 'Dynamic document field value is required.' }
|
|
222
|
+
}
|
|
223
|
+
const rootPath = stagedUserData.subCreate.rootPath
|
|
224
|
+
const newDoc = stagedUserData.subCreate.documentStructure
|
|
225
|
+
newDoc[stagedUserData.subCreate.dynamicDocumentField] = data.dynamicDocumentFieldValue
|
|
226
|
+
const addedDoc = await db.collection(rootPath).add(newDoc)
|
|
227
|
+
await db.collection(rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
228
|
+
newRole = { [`${rootPath}-${addedDoc.id}`]: { collectionPath: `${rootPath}-${addedDoc.id}`, role: stagedUserData.subCreate.role } }
|
|
229
|
+
}
|
|
230
|
+
const combinedRoles = { ...currentRoles, ...newRoles, ...newRole }
|
|
231
|
+
Object.values(combinedRoles).forEach((role) => {
|
|
232
|
+
if (!currentUserCollectionPaths.includes(role.collectionPath)) {
|
|
233
|
+
currentUserCollectionPaths.push(role.collectionPath)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
await db.collection('staged-users').doc(currentUserData.stagedDocId).update({ roles: combinedRoles, collectionPaths: currentUserCollectionPaths })
|
|
237
|
+
if (!stagedUserData.isTemplate) {
|
|
238
|
+
await db.collection('staged-users').doc(data.registrationCode).delete()
|
|
239
|
+
}
|
|
240
|
+
return { success: true, message: '' }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
exports.checkOrgIdExists = onCall(async (request) => {
|
|
246
|
+
const data = request.data
|
|
247
|
+
const orgId = data.orgId.toLowerCase()
|
|
248
|
+
const orgDoc = await db.collection('organizations').doc(orgId).get()
|
|
249
|
+
return { exists: orgDoc.exists }
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
exports.deleteSelf = onCall(async (request) => {
|
|
253
|
+
if (request.data.uid === request.auth.uid) {
|
|
254
|
+
try {
|
|
255
|
+
const userDoc = await db.collection('staged-users').doc(request.auth.uid).get()
|
|
256
|
+
const userData = userDoc.data()
|
|
257
|
+
const userCollectionPaths = userData.collectionPaths || []
|
|
258
|
+
|
|
259
|
+
for (const path of userCollectionPaths) {
|
|
260
|
+
const usersWithSamePath = await db.collection('staged-users').where('collectionPaths', 'array-contains', path).get()
|
|
261
|
+
|
|
262
|
+
// If no other users have the same collection path, delete the path and all documents and collections under it
|
|
263
|
+
if (usersWithSamePath.size <= 1) {
|
|
264
|
+
const adjustedPath = path.replace(/-/g, '/')
|
|
265
|
+
const docRef = db.doc(adjustedPath)
|
|
266
|
+
const doc = await docRef.get()
|
|
267
|
+
|
|
268
|
+
if (doc.exists) {
|
|
269
|
+
// If the path is a document, delete it directly
|
|
270
|
+
await docRef.delete()
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// If the path is a collection, delete all documents under it
|
|
274
|
+
const docsToDelete = await db.collection(adjustedPath).get()
|
|
275
|
+
const batch = db.batch()
|
|
276
|
+
docsToDelete.docs.forEach((doc) => {
|
|
277
|
+
batch.delete(doc.ref)
|
|
278
|
+
})
|
|
279
|
+
await batch.commit()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Delete from 'staged-users' collection
|
|
285
|
+
await db.collection('staged-users').doc(request.data.uid).delete()
|
|
286
|
+
|
|
287
|
+
// Delete from 'users' collection
|
|
288
|
+
await db.collection('users').doc(request.data.uid).delete()
|
|
289
|
+
|
|
290
|
+
// Delete the user from Firebase
|
|
291
|
+
await admin.auth().deleteUser(request.data.uid)
|
|
292
|
+
|
|
293
|
+
return { success: true }
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
console.error('Error deleting user:', error)
|
|
297
|
+
return { success: false, error }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
exports.updateUser = onDocumentUpdated({ document: 'staged-users/{docId}', timeoutSeconds: 180 }, async (event) => {
|
|
303
|
+
const change = event.data
|
|
304
|
+
const eventId = event.id
|
|
305
|
+
const eventRef = db.collection('events').doc(eventId)
|
|
306
|
+
const stagedDocId = event.params.docId
|
|
307
|
+
let newData = change.after.data()
|
|
308
|
+
const oldData = change.before.data()
|
|
309
|
+
|
|
310
|
+
const shouldProcess = await eventRef.get().then((eventDoc) => {
|
|
311
|
+
return !eventDoc.exists || !eventDoc.data().processed
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
if (!shouldProcess) {
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Note: we can trust on newData.uid because we are checking in rules that it matches the auth.uid
|
|
319
|
+
if (newData.userId) {
|
|
320
|
+
const userRef = db.collection('users').doc(newData.userId)
|
|
321
|
+
await setUser(userRef, newData, oldData, stagedDocId)
|
|
322
|
+
await markProcessed(eventRef)
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
if (newData.templateUserId !== oldData.templateUserId) {
|
|
326
|
+
// Check if templateUserId already exists in the staged-users collection
|
|
327
|
+
const stagedUserRef = db.collection('staged-users').doc(newData.templateUserId)
|
|
328
|
+
const doc = await stagedUserRef.get()
|
|
329
|
+
|
|
330
|
+
// If it exists, skip the creation process
|
|
331
|
+
if (doc.exists) {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
newData.isTemplate = false
|
|
336
|
+
const templateUserId = newData.templateUserId
|
|
337
|
+
newData.meta = newData.templateMeta
|
|
338
|
+
delete newData.templateMeta
|
|
339
|
+
delete newData.templateUserId
|
|
340
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'subCreate') && Object.values(newData.subCreate).length > 0) {
|
|
341
|
+
const subCreate = newData.subCreate
|
|
342
|
+
delete newData.subCreate
|
|
343
|
+
let newDocId = ''
|
|
344
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'requestedOrgId')) {
|
|
345
|
+
newDocId = newData.requestedOrgId.toLowerCase()
|
|
346
|
+
delete newData.requestedOrgId
|
|
347
|
+
}
|
|
348
|
+
let addedDoc
|
|
349
|
+
if (newDocId) {
|
|
350
|
+
const docRef = db.collection(subCreate.rootPath).doc(newDocId)
|
|
351
|
+
const doc = await docRef.get()
|
|
352
|
+
if (!doc.exists) {
|
|
353
|
+
await docRef.set({ [subCreate.dynamicDocumentField]: newData.dynamicDocumentFieldValue })
|
|
354
|
+
addedDoc = docRef
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
addedDoc = await db.collection(subCreate.rootPath).add({ [subCreate.dynamicDocumentField]: newData.dynamicDocumentFieldValue })
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
addedDoc = await db.collection(subCreate.rootPath).add({ [subCreate.dynamicDocumentField]: newData.dynamicDocumentFieldValue })
|
|
362
|
+
}
|
|
363
|
+
await db.collection(subCreate.rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
364
|
+
delete newData.dynamicDocumentFieldValue
|
|
365
|
+
const newRole = { [`${subCreate.rootPath}-${addedDoc.id}`]: { collectionPath: `${subCreate.rootPath}-${addedDoc.id}`, role: subCreate.role } }
|
|
366
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'collectionPaths')) {
|
|
367
|
+
newData.collectionPaths.push(`${subCreate.rootPath}-${addedDoc.id}`)
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
newData.collectionPaths = [`${subCreate.rootPath}-${addedDoc.id}`]
|
|
371
|
+
}
|
|
372
|
+
const newRoles = { ...newData.roles, ...newRole }
|
|
373
|
+
newData = { ...newData, roles: newRoles }
|
|
374
|
+
const stagedUserRef = db.collection('staged-users').doc(templateUserId)
|
|
375
|
+
await stagedUserRef.set({ ...newData, userId: templateUserId })
|
|
376
|
+
const userRef = db.collection('users').doc(templateUserId)
|
|
377
|
+
await setUser(userRef, newData, oldData, templateUserId)
|
|
378
|
+
await markProcessed(eventRef)
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const stagedUserRef = db.collection('staged-users').doc(templateUserId)
|
|
382
|
+
await stagedUserRef.set({ ...newData, userId: templateUserId })
|
|
383
|
+
const userRef = db.collection('users').doc(templateUserId)
|
|
384
|
+
await setUser(userRef, newData, oldData, templateUserId)
|
|
385
|
+
await markProcessed(eventRef)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await markProcessed(eventRef)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
async function setUser(userRef, newData, oldData, stagedDocId) {
|
|
393
|
+
const user = await userRef.get()
|
|
394
|
+
let userUpdate = { meta: newData.meta, stagedDocId }
|
|
395
|
+
|
|
396
|
+
if (newData.meta && newData.meta.name) {
|
|
397
|
+
const publicUserRef = db.collection('public-users').doc(stagedDocId)
|
|
398
|
+
const publicMeta = { name: newData.meta.name }
|
|
399
|
+
publicUserRef.set({ uid: newData.uid, meta: publicMeta, collectionPaths: newData.collectionPaths, userId: stagedDocId })
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'roles')) {
|
|
403
|
+
userUpdate = { ...userUpdate, roles: newData.roles }
|
|
404
|
+
}
|
|
405
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'specialPermissions')) {
|
|
406
|
+
userUpdate = { ...userUpdate, specialPermissions: newData.specialPermissions }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!oldData.userId) {
|
|
410
|
+
userUpdate = { ...userUpdate, userId: newData.uid }
|
|
411
|
+
}
|
|
412
|
+
if (!user.exists) {
|
|
413
|
+
return userRef.set(userUpdate)
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
return userRef.update(userUpdate)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function markProcessed(eventRef) {
|
|
421
|
+
return eventRef.set({ processed: true }).then(() => {
|
|
422
|
+
return null
|
|
423
|
+
})
|
|
424
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgedev/create-edge-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"description": "Create Edge Starter App",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-edge-app": "./bin/cli.js"
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"vaul-vue": "^0.1.2",
|
|
40
40
|
"vee-validate": "^4.13.1",
|
|
41
41
|
"vue-sonner": "^1.1.2",
|
|
42
|
+
"vue-upload-component": "^3.1.17",
|
|
42
43
|
"zod": "^3.23.8"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
package/storage.rules
CHANGED
|
@@ -6,3 +6,56 @@ service firebase.storage {
|
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
// #EDGE FIREBASE RULES START
|
|
10
|
+
service firebase.storage {
|
|
11
|
+
match /b/{bucket}/o {
|
|
12
|
+
// Match the file path structure you're using, simulating the Firestore document path structure for permissions.
|
|
13
|
+
function getRolePermission(role, permissionCheck) {
|
|
14
|
+
let permissions = {
|
|
15
|
+
'admin': {'assign': true, 'delete': true, 'read': true, 'write': true},
|
|
16
|
+
'editor': {'assign': false, 'delete': true, 'read': true, 'write': true},
|
|
17
|
+
'user': {'assign': false, 'delete': false, 'read': true, 'write': false},
|
|
18
|
+
'writer': {'assign': false, 'delete': false, 'read': true, 'write': true}
|
|
19
|
+
};
|
|
20
|
+
return permissions[role][permissionCheck];
|
|
21
|
+
}
|
|
22
|
+
function checkPermission(permissionCheck, collectionPath) {
|
|
23
|
+
let user = firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data;
|
|
24
|
+
let ruleHelper = firestore.get(/databases/(default)/documents/rule-helpers/$(request.auth.uid)).data;
|
|
25
|
+
return request.auth != null &&
|
|
26
|
+
collectionPath in ruleHelper &&
|
|
27
|
+
"permissionCheckPath" in ruleHelper[collectionPath] &&
|
|
28
|
+
(
|
|
29
|
+
ruleHelper[collectionPath].permissionCheckPath == "-" ||
|
|
30
|
+
collectionPath.matches("^" + ruleHelper[collectionPath].permissionCheckPath + ".*$")
|
|
31
|
+
) &&
|
|
32
|
+
(
|
|
33
|
+
(
|
|
34
|
+
"roles" in user &&
|
|
35
|
+
ruleHelper[collectionPath].permissionCheckPath in user.roles &&
|
|
36
|
+
getRolePermission(user.roles[ruleHelper[collectionPath].permissionCheckPath].role, permissionCheck)
|
|
37
|
+
) ||
|
|
38
|
+
(
|
|
39
|
+
"specialPermissions" in user &&
|
|
40
|
+
ruleHelper[collectionPath].permissionCheckPath in user.specialPermissions &&
|
|
41
|
+
permissionCheck in user.specialPermissions[ruleHelper[collectionPath].permissionCheckPath] &&
|
|
42
|
+
user.specialPermissions[ruleHelper[collectionPath].permissionCheckPath][permissionCheck]
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
match /public/{fileId} {
|
|
47
|
+
// Public directory with special rules
|
|
48
|
+
allow read: if true; // Unrestricted read access
|
|
49
|
+
allow write, delete: if request.auth != null; // Write and delete require authentication
|
|
50
|
+
}
|
|
51
|
+
match /{dir}/{fileId} {
|
|
52
|
+
// General read permission check based on Firestore data
|
|
53
|
+
allow read: if checkPermission("read", dir);
|
|
54
|
+
// General write permission check, including creating and updating files
|
|
55
|
+
allow write: if checkPermission("write", dir);
|
|
56
|
+
// General delete permission check
|
|
57
|
+
allow delete: if checkPermission("write", dir);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// #EDGE FIREBASE RULES END
|