@edgedev/firebase 1.9.23 → 2.0.3
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 +24 -7
- package/package.json +1 -1
- package/src/.env.dev +7 -0
- package/src/.env.prod +7 -0
- package/src/config.js +38 -0
- package/src/edgeFirebase.js +309 -0
- package/src/functions.js +2 -310
- package/src/postinstall.sh +13 -5
package/edgeFirebase.ts
CHANGED
|
@@ -110,7 +110,7 @@ interface UserDataObject {
|
|
|
110
110
|
firebaseUser: object;
|
|
111
111
|
oAuthCredential: { accessToken: string; idToken: string;}
|
|
112
112
|
loggedIn: boolean;
|
|
113
|
-
loggingIn: boolean;
|
|
113
|
+
loggingIn: boolean | null;
|
|
114
114
|
logInError: boolean;
|
|
115
115
|
logInErrorMessage: string;
|
|
116
116
|
meta: object;
|
|
@@ -144,6 +144,7 @@ interface userRegister {
|
|
|
144
144
|
meta: object;
|
|
145
145
|
registrationCode: string;
|
|
146
146
|
dynamicDocumentFieldValue?: string;
|
|
147
|
+
requestedOrgId?: string;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
|
|
@@ -423,6 +424,7 @@ export const EdgeFirebase = class {
|
|
|
423
424
|
|
|
424
425
|
private setOnAuthStateChanged = (): void => {
|
|
425
426
|
onAuthStateChanged(this.auth, (userAuth) => {
|
|
427
|
+
console.log('onAuthStateChanged')
|
|
426
428
|
const oldDiv = document.getElementById("recaptcha-container");
|
|
427
429
|
if (oldDiv) oldDiv.remove();
|
|
428
430
|
if (userAuth) {
|
|
@@ -473,7 +475,7 @@ export const EdgeFirebase = class {
|
|
|
473
475
|
|
|
474
476
|
public logInWithPhone = async (phoneNumber: string, phoneCode: string): Promise<void> => {
|
|
475
477
|
try {
|
|
476
|
-
const verifyCode: any = await this.runFunction("verifyPhoneNumber", {phone: phoneNumber, code: phoneCode});
|
|
478
|
+
const verifyCode: any = await this.runFunction("edgeFirebase-verifyPhoneNumber", {phone: phoneNumber, code: phoneCode});
|
|
477
479
|
if (verifyCode.data.success) {
|
|
478
480
|
const result = await signInWithCustomToken(this.auth, verifyCode.data.token);
|
|
479
481
|
if (!Object.prototype.hasOwnProperty.call(result, "user")) {
|
|
@@ -552,7 +554,7 @@ export const EdgeFirebase = class {
|
|
|
552
554
|
});
|
|
553
555
|
}
|
|
554
556
|
// userRegister.uid = this.user.uid;
|
|
555
|
-
const result = await this.runFunction("currentUserRegister", userRegister as unknown as Record<string, unknown>);
|
|
557
|
+
const result = await this.runFunction("edgeFirebase-currentUserRegister", userRegister as unknown as Record<string, unknown>);
|
|
556
558
|
const resultData = result.data as {success: boolean, message: string};
|
|
557
559
|
if (resultData.success) {
|
|
558
560
|
return this.sendResponse({
|
|
@@ -622,6 +624,18 @@ export const EdgeFirebase = class {
|
|
|
622
624
|
meta: {}
|
|
623
625
|
});
|
|
624
626
|
}
|
|
627
|
+
if (Object.prototype.hasOwnProperty.call(userRegister, 'requestedOrgId')) {
|
|
628
|
+
if (userRegister.requestedOrgId) {
|
|
629
|
+
const result: any = await this.runFunction("edgeFirebase-checkOrgIdExists", {orgId: userRegister.requestedOrgId.toLowerCase()});
|
|
630
|
+
if (result.data.exists) {
|
|
631
|
+
return this.sendResponse({
|
|
632
|
+
success: false,
|
|
633
|
+
message: "Organization ID already exists.",
|
|
634
|
+
meta: {}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
625
639
|
const userRef = doc(this.db, "staged-users", userRegister.registrationCode);
|
|
626
640
|
const userSnap = await getDoc(userRef);
|
|
627
641
|
if (userSnap.exists()) {
|
|
@@ -680,7 +694,7 @@ export const EdgeFirebase = class {
|
|
|
680
694
|
}
|
|
681
695
|
} else if (authProvider === "phone") {
|
|
682
696
|
try {
|
|
683
|
-
const verifyCode: any = await this.runFunction("verifyPhoneNumber", {phone: userRegister.phoneNumber, code: userRegister.phoneCode});
|
|
697
|
+
const verifyCode: any = await this.runFunction("edgeFirebase-verifyPhoneNumber", {phone: userRegister.phoneNumber, code: userRegister.phoneCode});
|
|
684
698
|
if (verifyCode.data.success) {
|
|
685
699
|
response = await signInWithCustomToken(this.auth, verifyCode.data.token);
|
|
686
700
|
await updateProfile(response.user, {
|
|
@@ -714,12 +728,15 @@ export const EdgeFirebase = class {
|
|
|
714
728
|
}else{
|
|
715
729
|
metaUpdate = user.meta;
|
|
716
730
|
}
|
|
717
|
-
let stagedUserUpdate: {userId?: string, templateUserId?: string, dynamicDocumentFieldValue?: string, uid: string, meta: unknown, templateMeta?: unknown} = {userId: response.user.uid, uid: response.user.uid, meta: metaUpdate}
|
|
731
|
+
let stagedUserUpdate: {userId?: string, templateUserId?: string, dynamicDocumentFieldValue?: string, uid: string, meta: unknown, templateMeta?: unknown, requestedOrgId?: unknown} = {userId: response.user.uid, uid: response.user.uid, meta: metaUpdate}
|
|
718
732
|
if (user.isTemplate) {
|
|
719
733
|
stagedUserUpdate = {templateUserId: response.user.uid, uid: response.user.uid, meta: user.meta, templateMeta: metaUpdate}
|
|
720
734
|
if (Object.prototype.hasOwnProperty.call(userRegister, 'dynamicDocumentFieldValue')) {
|
|
721
735
|
stagedUserUpdate = {templateUserId: response.user.uid, uid: response.user.uid, dynamicDocumentFieldValue: userRegister.dynamicDocumentFieldValue, meta: user.meta, templateMeta: metaUpdate}
|
|
722
736
|
}
|
|
737
|
+
if (Object.prototype.hasOwnProperty.call(userRegister, 'requestedOrgId')) {
|
|
738
|
+
stagedUserUpdate = {templateUserId: response.user.uid, uid: response.user.uid, dynamicDocumentFieldValue: userRegister.dynamicDocumentFieldValue, meta: user.meta, templateMeta: metaUpdate, requestedOrgId: userRegister.requestedOrgId.toLowerCase()}
|
|
739
|
+
}
|
|
723
740
|
}
|
|
724
741
|
const initRoleHelper = {uid: response.user.uid}
|
|
725
742
|
initRoleHelper["edge-assignment-helper"] = {permissionType: "roles"}
|
|
@@ -854,7 +871,7 @@ export const EdgeFirebase = class {
|
|
|
854
871
|
}
|
|
855
872
|
}
|
|
856
873
|
if (removedFrom.length > 0) {
|
|
857
|
-
const response = await this.runFunction("removeNonRegisteredUser", {uid: this.user.uid, docId});
|
|
874
|
+
const response = await this.runFunction("edgeFirebase-removeNonRegisteredUser", {uid: this.user.uid, docId});
|
|
858
875
|
return this.sendResponse({
|
|
859
876
|
success: true,
|
|
860
877
|
message: "line 704",
|
|
@@ -1206,7 +1223,7 @@ export const EdgeFirebase = class {
|
|
|
1206
1223
|
public user: UserDataObject = reactive({
|
|
1207
1224
|
uid: null,
|
|
1208
1225
|
email: "",
|
|
1209
|
-
loggingIn:
|
|
1226
|
+
loggingIn: null,
|
|
1210
1227
|
loggedIn: false,
|
|
1211
1228
|
logInError: false,
|
|
1212
1229
|
logInErrorMessage: "",
|
package/package.json
CHANGED
package/src/.env.dev
ADDED
package/src/.env.prod
ADDED
package/src/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const functions = require('firebase-functions')
|
|
2
|
+
const admin = require('firebase-admin')
|
|
3
|
+
|
|
4
|
+
admin.initializeApp()
|
|
5
|
+
|
|
6
|
+
const { onCall, HttpsError } = require('firebase-functions/v2/https')
|
|
7
|
+
const { onSchedule } = require('firebase-functions/v2/scheduler')
|
|
8
|
+
const {
|
|
9
|
+
onDocumentWritten,
|
|
10
|
+
onDocumentCreated,
|
|
11
|
+
onDocumentUpdated,
|
|
12
|
+
onDocumentDeleted,
|
|
13
|
+
Change,
|
|
14
|
+
FirestoreEvent,
|
|
15
|
+
} = require('firebase-functions/v2/firestore')
|
|
16
|
+
const { logger } = require('firebase-functions/v2')
|
|
17
|
+
const { getFirestore } = require('firebase-admin/firestore')
|
|
18
|
+
const twilio = require('twilio')
|
|
19
|
+
const db = getFirestore()
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
onSchedule,
|
|
23
|
+
onDocumentWritten,
|
|
24
|
+
onDocumentCreated,
|
|
25
|
+
onDocumentUpdated,
|
|
26
|
+
onDocumentDeleted,
|
|
27
|
+
Change,
|
|
28
|
+
FirestoreEvent,
|
|
29
|
+
onCall,
|
|
30
|
+
HttpsError,
|
|
31
|
+
logger,
|
|
32
|
+
getFirestore,
|
|
33
|
+
functions,
|
|
34
|
+
admin,
|
|
35
|
+
twilio,
|
|
36
|
+
db,
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const { onCall, HttpsError, logger, getFirestore, functions, admin, twilio, db } = require('./config.js')
|
|
2
|
+
|
|
3
|
+
const authToken = process.env.TWILIO_AUTH_TOKEN
|
|
4
|
+
const accountSid = process.env.TWILIO_SID
|
|
5
|
+
const systemNumber = process.env.TWILIO_SYSTEM_NUMBER
|
|
6
|
+
|
|
7
|
+
function formatPhoneNumber(phone) {
|
|
8
|
+
// Remove non-numeric characters from the phone number
|
|
9
|
+
const numericPhone = phone.replace(/\D/g, '')
|
|
10
|
+
// Return the formatted number
|
|
11
|
+
return `+1${numericPhone}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.sendVerificationCode = functions.https.onCall(async (data, context) => {
|
|
15
|
+
const code = (Math.floor(Math.random() * 1000000) + 1000000).toString().substring(1)
|
|
16
|
+
const phone = formatPhoneNumber(data.phone)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const client = twilio(accountSid, authToken)
|
|
20
|
+
await client.messages.create({
|
|
21
|
+
body: `Your verification code is: ${code}`,
|
|
22
|
+
to: phone, // the user's phone number
|
|
23
|
+
from: systemNumber, // your Twilio phone number from the configuration
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.log(error)
|
|
28
|
+
return { success: false, error: 'Invalid Phone #' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Use the formatted phone number as the document ID for Firestore
|
|
33
|
+
await db.collection('phone-auth').doc(phone).set({
|
|
34
|
+
phone,
|
|
35
|
+
code,
|
|
36
|
+
})
|
|
37
|
+
return phone
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return { success: false, error }
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
exports.verifyPhoneNumber = functions.https.onCall(async (data, context) => {
|
|
45
|
+
const phone = data.phone
|
|
46
|
+
const code = data.code
|
|
47
|
+
|
|
48
|
+
// Get the phone-auth document with the given phone number
|
|
49
|
+
const phoneDoc = await db.collection('phone-auth').doc(phone).get()
|
|
50
|
+
|
|
51
|
+
if (!phoneDoc.exists) {
|
|
52
|
+
return { success: false, error: 'Phone number not found.' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const storedCode = phoneDoc.data().code
|
|
56
|
+
|
|
57
|
+
if (storedCode !== code) {
|
|
58
|
+
return { success: false, error: 'Invalid verification code.' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If the code matches, authenticate the user with Firebase Custom Auth
|
|
62
|
+
try {
|
|
63
|
+
// You would typically generate a UID based on the phone number or another system
|
|
64
|
+
const uid = phone
|
|
65
|
+
|
|
66
|
+
// Create a custom token (this can be used on the client to sign in)
|
|
67
|
+
const customToken = await admin.auth().createCustomToken(uid)
|
|
68
|
+
|
|
69
|
+
return { success: true, token: customToken }
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Error creating custom token:', error)
|
|
73
|
+
return { success: false, error: 'Failed to authenticate.' }
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
exports.initFirestore = functions.https.onCall(async (data, context) => {
|
|
78
|
+
// checks to see of the collections 'collection-data' and 'staged-users' exist if not will seed them with data
|
|
79
|
+
const collectionData = await db.collection('collection-data').get()
|
|
80
|
+
const stagedUsers = await db.collection('staged-users').get()
|
|
81
|
+
if (collectionData.empty) {
|
|
82
|
+
// create a document with the id of '-' and one called '-default-':
|
|
83
|
+
const admin = { assign: true, delete: true, read: true, write: true }
|
|
84
|
+
const editor = { assign: false, delete: true, read: true, write: true }
|
|
85
|
+
const writer = { assign: false, delete: false, read: true, write: true }
|
|
86
|
+
const user = { assign: false, delete: false, read: true, write: false }
|
|
87
|
+
await db.collection('collection-data').doc('-').set({ admin, editor, writer, user })
|
|
88
|
+
await db.collection('collection-data').doc('-default-').set({ admin, editor, writer, user })
|
|
89
|
+
}
|
|
90
|
+
if (stagedUsers.empty) {
|
|
91
|
+
const templateUser = {
|
|
92
|
+
docId: 'organization-registration-template',
|
|
93
|
+
isTemplate: true,
|
|
94
|
+
meta: {
|
|
95
|
+
name: 'Organization Registration Template',
|
|
96
|
+
},
|
|
97
|
+
subCreate: {
|
|
98
|
+
documentStructure: {
|
|
99
|
+
name: '',
|
|
100
|
+
},
|
|
101
|
+
dynamicDocumentField: 'name',
|
|
102
|
+
role: 'admin',
|
|
103
|
+
rootPath: 'organizations',
|
|
104
|
+
},
|
|
105
|
+
userId: '',
|
|
106
|
+
}
|
|
107
|
+
await db.collection('staged-users').doc('organization-registration-template').set(templateUser)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
exports.removeNonRegisteredUser = functions.https.onCall(async (data, context) => {
|
|
112
|
+
if (data.uid === context.auth.uid) {
|
|
113
|
+
const stagedUser = await db.collection('staged-users').doc(data.docId).get()
|
|
114
|
+
if (stagedUser.exists) {
|
|
115
|
+
const stagedUserData = stagedUser.data()
|
|
116
|
+
|
|
117
|
+
const rolesExist = stagedUserData.roles && Object.keys(stagedUserData.roles).length !== 0
|
|
118
|
+
const specialPermissionsExist = stagedUserData.specialPermissions && Object.keys(stagedUserData.specialPermissions).length !== 0
|
|
119
|
+
const userIdExistsAndNotBlank = stagedUserData.userId && stagedUserData.userId !== ''
|
|
120
|
+
|
|
121
|
+
if (!rolesExist && !specialPermissionsExist && !userIdExistsAndNotBlank) {
|
|
122
|
+
await db.collection('staged-users').doc(data.docId).delete()
|
|
123
|
+
return { success: true, message: '' }
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
let message = ''
|
|
127
|
+
if (rolesExist && specialPermissionsExist) {
|
|
128
|
+
message = 'Cannot delete because the non-registered user still has roles and special permissions assigned.'
|
|
129
|
+
}
|
|
130
|
+
else if (rolesExist) {
|
|
131
|
+
message = 'Cannot delete because the non-registered user still has roles assigned.'
|
|
132
|
+
}
|
|
133
|
+
else if (specialPermissionsExist) {
|
|
134
|
+
message = 'Cannot delete because the non-registered user still has special permissions assigned.'
|
|
135
|
+
}
|
|
136
|
+
else if (userIdExistsAndNotBlank) {
|
|
137
|
+
message = 'Cannot delete because the user is registered.'
|
|
138
|
+
}
|
|
139
|
+
return { success: false, message }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { success: false, message: 'Non-registered user not found.' }
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
exports.currentUserRegister = functions.https.onCall(async (data, context) => {
|
|
147
|
+
if (data.uid === context.auth.uid) {
|
|
148
|
+
const stagedUser = await db.collection('staged-users').doc(data.registrationCode).get()
|
|
149
|
+
if (!stagedUser.exists) {
|
|
150
|
+
return { success: false, message: 'Registration code not found.' }
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const stagedUserData = await stagedUser.data()
|
|
154
|
+
let process = false
|
|
155
|
+
if (stagedUserData.isTemplate) {
|
|
156
|
+
process = true
|
|
157
|
+
}
|
|
158
|
+
if (!stagedUserData.isTemplate && stagedUserData.userId === '') {
|
|
159
|
+
process = true
|
|
160
|
+
}
|
|
161
|
+
if (!process) {
|
|
162
|
+
return { success: false, message: 'Registration code not valid.' }
|
|
163
|
+
}
|
|
164
|
+
const newRoles = stagedUserData.roles || {}
|
|
165
|
+
const currentUser = await db.collection('users').doc(data.uid).get()
|
|
166
|
+
const currentUserData = await currentUser.data()
|
|
167
|
+
const currentRoles = currentUserData.roles || {}
|
|
168
|
+
const currentUserCollectionPaths = currentUserData.collectionPaths || []
|
|
169
|
+
let newRole = {}
|
|
170
|
+
if (stagedUserData.subCreate && Object.keys(stagedUserData.subCreate).length !== 0 && stagedUserData.isTemplate) {
|
|
171
|
+
if (!data.dynamicDocumentFieldValue) {
|
|
172
|
+
return { success: false, message: 'Dynamic document field value is required.' }
|
|
173
|
+
}
|
|
174
|
+
const rootPath = stagedUserData.subCreate.rootPath
|
|
175
|
+
const newDoc = stagedUserData.subCreate.documentStructure
|
|
176
|
+
newDoc[stagedUserData.subCreate.dynamicDocumentField] = data.dynamicDocumentFieldValue
|
|
177
|
+
const addedDoc = await db.collection(rootPath).add(newDoc)
|
|
178
|
+
await db.collection(rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
179
|
+
newRole = { [`${rootPath}-${addedDoc.id}`]: { collectionPath: `${rootPath}-${addedDoc.id}`, role: stagedUserData.subCreate.role } }
|
|
180
|
+
}
|
|
181
|
+
const combinedRoles = { ...currentRoles, ...newRoles, ...newRole }
|
|
182
|
+
Object.values(combinedRoles).forEach((role) => {
|
|
183
|
+
if (!currentUserCollectionPaths.includes(role.collectionPath)) {
|
|
184
|
+
currentUserCollectionPaths.push(role.collectionPath)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
await db.collection('staged-users').doc(currentUserData.stagedDocId).update({ roles: combinedRoles, collectionPaths: currentUserCollectionPaths })
|
|
188
|
+
if (!stagedUserData.isTemplate) {
|
|
189
|
+
await db.collection('staged-users').doc(data.registrationCode).delete()
|
|
190
|
+
}
|
|
191
|
+
return { success: true, message: '' }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
exports.checkOrgIdExists = onCall(async (request) => {
|
|
197
|
+
const data = request.data
|
|
198
|
+
const orgId = data.orgId.toLowerCase()
|
|
199
|
+
const orgDoc = await db.collection('organizations').doc(orgId).get()
|
|
200
|
+
return { exists: orgDoc.exists }
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
exports.updateUser = functions.firestore.document('staged-users/{docId}').onUpdate(async (change, context) => {
|
|
204
|
+
const eventId = context.eventId
|
|
205
|
+
const eventRef = db.collection('events').doc(eventId)
|
|
206
|
+
const stagedDocId = context.params.docId
|
|
207
|
+
let newData = change.after.data()
|
|
208
|
+
const oldData = change.before.data()
|
|
209
|
+
|
|
210
|
+
const shouldProcess = await eventRef.get().then((eventDoc) => {
|
|
211
|
+
return !eventDoc.exists || !eventDoc.data().processed
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (!shouldProcess) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Note: we can trust on newData.uid because we are checking in rules that it matches the auth.uid
|
|
219
|
+
if (newData.userId) {
|
|
220
|
+
const userRef = db.collection('users').doc(newData.userId)
|
|
221
|
+
await setUser(userRef, newData, oldData, stagedDocId)
|
|
222
|
+
await markProcessed(eventRef)
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (newData.templateUserId !== oldData.templateUserId) {
|
|
226
|
+
// Check if templateUserId already exists in the staged-users collection
|
|
227
|
+
const stagedUserRef = db.collection('staged-users').doc(newData.templateUserId)
|
|
228
|
+
const doc = await stagedUserRef.get()
|
|
229
|
+
|
|
230
|
+
// If it exists, skip the creation process
|
|
231
|
+
if (doc.exists) {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
newData.isTemplate = false
|
|
236
|
+
const templateUserId = newData.templateUserId
|
|
237
|
+
newData.meta = newData.templateMeta
|
|
238
|
+
delete newData.templateMeta
|
|
239
|
+
delete newData.templateUserId
|
|
240
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'subCreate') && Object.values(newData.subCreate).length > 0) {
|
|
241
|
+
const subCreate = newData.subCreate
|
|
242
|
+
delete newData.subCreate
|
|
243
|
+
const addedDoc = await db.collection(subCreate.rootPath).add({ [subCreate.dynamicDocumentField]: newData.dynamicDocumentFieldValue })
|
|
244
|
+
await db.collection(subCreate.rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
245
|
+
delete newData.dynamicDocumentFieldValue
|
|
246
|
+
const newRole = { [`${subCreate.rootPath}-${addedDoc.id}`]: { collectionPath: `${subCreate.rootPath}-${addedDoc.id}`, role: subCreate.role } }
|
|
247
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'collectionPaths')) {
|
|
248
|
+
newData.collectionPaths.push(`${subCreate.rootPath}-${addedDoc.id}`)
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
newData.collectionPaths = [`${subCreate.rootPath}-${addedDoc.id}`]
|
|
252
|
+
}
|
|
253
|
+
const newRoles = { ...newData.roles, ...newRole }
|
|
254
|
+
newData = { ...newData, roles: newRoles }
|
|
255
|
+
const stagedUserRef = db.collection('staged-users').doc(templateUserId)
|
|
256
|
+
await stagedUserRef.set({ ...newData, userId: templateUserId })
|
|
257
|
+
const userRef = db.collection('users').doc(templateUserId)
|
|
258
|
+
await setUser(userRef, newData, oldData, templateUserId)
|
|
259
|
+
await markProcessed(eventRef)
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const stagedUserRef = db.collection('staged-users').doc(templateUserId)
|
|
263
|
+
await stagedUserRef.set({ ...newData, userId: templateUserId })
|
|
264
|
+
const userRef = db.collection('users').doc(templateUserId)
|
|
265
|
+
await setUser(userRef, newData, oldData, templateUserId)
|
|
266
|
+
await markProcessed(eventRef)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await markProcessed(eventRef)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
function setUser(userRef, newData, oldData, stagedDocId) {
|
|
274
|
+
// IT's OK If "users" doesn't match exactly matched "staged-users" because this is only preventing
|
|
275
|
+
// writing from outside the @edgdev/firebase functions, so discrepancies will be rare since
|
|
276
|
+
// the package will prevent before it gets this far.
|
|
277
|
+
return userRef.get().then((user) => {
|
|
278
|
+
let userUpdate = { meta: newData.meta, stagedDocId }
|
|
279
|
+
|
|
280
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'roles')) {
|
|
281
|
+
userUpdate = { ...userUpdate, roles: newData.roles }
|
|
282
|
+
}
|
|
283
|
+
if (Object.prototype.hasOwnProperty.call(newData, 'specialPermissions')) {
|
|
284
|
+
userUpdate = { ...userUpdate, specialPermissions: newData.specialPermissions }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!oldData.userId) {
|
|
288
|
+
userUpdate = { ...userUpdate, userId: newData.uid }
|
|
289
|
+
}
|
|
290
|
+
if (!user.exists) {
|
|
291
|
+
return userRef.set(userUpdate)
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
return userRef.update(userUpdate)
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function shouldProcess(eventRef) {
|
|
300
|
+
return eventRef.get().then((eventDoc) => {
|
|
301
|
+
return !eventDoc.exists || !eventDoc.data().processed
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function markProcessed(eventRef) {
|
|
306
|
+
return eventRef.set({ processed: true }).then(() => {
|
|
307
|
+
return null
|
|
308
|
+
})
|
|
309
|
+
}
|
package/src/functions.js
CHANGED
|
@@ -1,311 +1,3 @@
|
|
|
1
1
|
// START @edge/firebase functions
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const authToken = functions.config().twilio.auth_token
|
|
5
|
-
const accountSid = functions.config().twilio.sid
|
|
6
|
-
const systemNumber = functions.config().twilio.system_number
|
|
7
|
-
|
|
8
|
-
function formatPhoneNumber(phone) {
|
|
9
|
-
// Remove non-numeric characters from the phone number
|
|
10
|
-
const numericPhone = phone.replace(/\D/g, '')
|
|
11
|
-
// Return the formatted number
|
|
12
|
-
return `+1${numericPhone}`
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
exports.sendVerificationCode = functions.https.onCall(async (data, context) => {
|
|
16
|
-
const code = (Math.floor(Math.random() * 1000000) + 1000000).toString().substring(1)
|
|
17
|
-
const phone = formatPhoneNumber(data.phone)
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const client = twilio(accountSid, authToken)
|
|
21
|
-
await client.messages.create({
|
|
22
|
-
body: `Your verification code is: ${code}`,
|
|
23
|
-
to: phone, // the user's phone number
|
|
24
|
-
from: systemNumber, // your Twilio phone number from the configuration
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
console.log(error)
|
|
29
|
-
return { success: false, error: 'Invalid Phone #' }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Use the formatted phone number as the document ID for Firestore
|
|
34
|
-
await db.collection('phone-auth').doc(phone).set({
|
|
35
|
-
phone,
|
|
36
|
-
code,
|
|
37
|
-
})
|
|
38
|
-
return phone
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
return { success: false, error }
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
exports.verifyPhoneNumber = functions.https.onCall(async (data, context) => {
|
|
46
|
-
const phone = data.phone
|
|
47
|
-
const code = data.code
|
|
48
|
-
|
|
49
|
-
// Get the phone-auth document with the given phone number
|
|
50
|
-
const phoneDoc = await db.collection('phone-auth').doc(phone).get()
|
|
51
|
-
|
|
52
|
-
if (!phoneDoc.exists) {
|
|
53
|
-
return { success: false, error: 'Phone number not found.' }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const storedCode = phoneDoc.data().code
|
|
57
|
-
|
|
58
|
-
if (storedCode !== code) {
|
|
59
|
-
return { success: false, error: 'Invalid verification code.' }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// If the code matches, authenticate the user with Firebase Custom Auth
|
|
63
|
-
try {
|
|
64
|
-
// You would typically generate a UID based on the phone number or another system
|
|
65
|
-
const uid = phone
|
|
66
|
-
|
|
67
|
-
// Create a custom token (this can be used on the client to sign in)
|
|
68
|
-
const customToken = await admin.auth().createCustomToken(uid)
|
|
69
|
-
|
|
70
|
-
return { success: true, token: customToken }
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
console.error('Error creating custom token:', error)
|
|
74
|
-
return { success: false, error: 'Failed to authenticate.' }
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// // Generate custom token example:
|
|
79
|
-
// exports.generateCustomToken = functions.https.onCall(async (data, context) => {
|
|
80
|
-
// // You would want to have some sort of validation here
|
|
81
|
-
// const token = await admin.auth().createCustomToken(data.customUid)
|
|
82
|
-
// return { token }
|
|
83
|
-
// })
|
|
84
|
-
|
|
85
|
-
exports.initFirestore = functions.https.onCall(async (data, context) => {
|
|
86
|
-
// checks to see of the collections 'collection-data' and 'staged-users' exist if not will seed them with data
|
|
87
|
-
const collectionData = await db.collection('collection-data').get()
|
|
88
|
-
const stagedUsers = await db.collection('staged-users').get()
|
|
89
|
-
if (collectionData.empty) {
|
|
90
|
-
// create a document with the id of '-' and one called '-default-':
|
|
91
|
-
const admin = { assign: true, delete: true, read: true, write: true }
|
|
92
|
-
const editor = { assign: false, delete: true, read: true, write: true }
|
|
93
|
-
const writer = { assign: false, delete: false, read: true, write: true }
|
|
94
|
-
const user = { assign: false, delete: false, read: true, write: false }
|
|
95
|
-
await db.collection('collection-data').doc('-').set({ admin, editor, writer, user })
|
|
96
|
-
await db.collection('collection-data').doc('-default-').set({ admin, editor, writer, user })
|
|
97
|
-
}
|
|
98
|
-
if (stagedUsers.empty) {
|
|
99
|
-
const templateUser = {
|
|
100
|
-
docId: 'organization-registration-template',
|
|
101
|
-
isTemplate: true,
|
|
102
|
-
meta: {
|
|
103
|
-
name: 'Organization Registration Template',
|
|
104
|
-
},
|
|
105
|
-
subCreate: {
|
|
106
|
-
documentStructure: {
|
|
107
|
-
name: '',
|
|
108
|
-
},
|
|
109
|
-
dynamicDocumentField: 'name',
|
|
110
|
-
role: 'admin',
|
|
111
|
-
rootPath: 'organizations',
|
|
112
|
-
},
|
|
113
|
-
userId: '',
|
|
114
|
-
}
|
|
115
|
-
await db.collection('staged-users').doc('organization-registration-template').set(templateUser)
|
|
116
|
-
}
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
exports.removeNonRegisteredUser = functions.https.onCall(async (data, context) => {
|
|
120
|
-
if (data.uid === context.auth.uid) {
|
|
121
|
-
const stagedUser = await db.collection('staged-users').doc(data.docId).get()
|
|
122
|
-
if (stagedUser.exists) {
|
|
123
|
-
const stagedUserData = stagedUser.data()
|
|
124
|
-
|
|
125
|
-
const rolesExist = stagedUserData.roles && Object.keys(stagedUserData.roles).length !== 0
|
|
126
|
-
const specialPermissionsExist = stagedUserData.specialPermissions && Object.keys(stagedUserData.specialPermissions).length !== 0
|
|
127
|
-
const userIdExistsAndNotBlank = stagedUserData.userId && stagedUserData.userId !== ''
|
|
128
|
-
|
|
129
|
-
if (!rolesExist && !specialPermissionsExist && !userIdExistsAndNotBlank) {
|
|
130
|
-
await db.collection('staged-users').doc(data.docId).delete()
|
|
131
|
-
return { success: true, message: '' }
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
let message = ''
|
|
135
|
-
if (rolesExist && specialPermissionsExist) {
|
|
136
|
-
message = 'Cannot delete because the non-registered user still has roles and special permissions assigned.'
|
|
137
|
-
}
|
|
138
|
-
else if (rolesExist) {
|
|
139
|
-
message = 'Cannot delete because the non-registered user still has roles assigned.'
|
|
140
|
-
}
|
|
141
|
-
else if (specialPermissionsExist) {
|
|
142
|
-
message = 'Cannot delete because the non-registered user still has special permissions assigned.'
|
|
143
|
-
}
|
|
144
|
-
else if (userIdExistsAndNotBlank) {
|
|
145
|
-
message = 'Cannot delete because the user is registered.'
|
|
146
|
-
}
|
|
147
|
-
return { success: false, message }
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return { success: false, message: 'Non-registered user not found.' }
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
exports.currentUserRegister = functions.https.onCall(async (data, context) => {
|
|
155
|
-
if (data.uid === context.auth.uid) {
|
|
156
|
-
const stagedUser = await db.collection('staged-users').doc(data.registrationCode).get()
|
|
157
|
-
if (!stagedUser.exists) {
|
|
158
|
-
return { success: false, message: 'Registration code not found.' }
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
const stagedUserData = await stagedUser.data()
|
|
162
|
-
let process = false
|
|
163
|
-
if (stagedUserData.isTemplate) {
|
|
164
|
-
process = true
|
|
165
|
-
}
|
|
166
|
-
if (!stagedUserData.isTemplate && stagedUserData.userId === '') {
|
|
167
|
-
process = true
|
|
168
|
-
}
|
|
169
|
-
if (!process) {
|
|
170
|
-
return { success: false, message: 'Registration code not valid.' }
|
|
171
|
-
}
|
|
172
|
-
const newRoles = stagedUserData.roles || {}
|
|
173
|
-
const currentUser = await db.collection('users').doc(data.uid).get()
|
|
174
|
-
const currentUserData = await currentUser.data()
|
|
175
|
-
const currentRoles = currentUserData.roles || {}
|
|
176
|
-
const currentUserCollectionPaths = currentUserData.collectionPaths || []
|
|
177
|
-
let newRole = {}
|
|
178
|
-
if (stagedUserData.subCreate && Object.keys(stagedUserData.subCreate).length !== 0 && stagedUserData.isTemplate) {
|
|
179
|
-
if (!data.dynamicDocumentFieldValue) {
|
|
180
|
-
return { success: false, message: 'Dynamic document field value is required.' }
|
|
181
|
-
}
|
|
182
|
-
const rootPath = stagedUserData.subCreate.rootPath
|
|
183
|
-
const newDoc = stagedUserData.subCreate.documentStructure
|
|
184
|
-
newDoc[stagedUserData.subCreate.dynamicDocumentField] = data.dynamicDocumentFieldValue
|
|
185
|
-
const addedDoc = await db.collection(rootPath).add(newDoc)
|
|
186
|
-
await db.collection(rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
187
|
-
newRole = { [`${rootPath}-${addedDoc.id}`]: { collectionPath: `${rootPath}-${addedDoc.id}`, role: stagedUserData.subCreate.role } }
|
|
188
|
-
}
|
|
189
|
-
const combinedRoles = { ...currentRoles, ...newRoles, ...newRole }
|
|
190
|
-
Object.values(combinedRoles).forEach((role) => {
|
|
191
|
-
if (!currentUserCollectionPaths.includes(role.collectionPath)) {
|
|
192
|
-
currentUserCollectionPaths.push(role.collectionPath)
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
await db.collection('staged-users').doc(currentUserData.stagedDocId).update({ roles: combinedRoles, collectionPaths: currentUserCollectionPaths })
|
|
196
|
-
if (!stagedUserData.isTemplate) {
|
|
197
|
-
await db.collection('staged-users').doc(data.registrationCode).delete()
|
|
198
|
-
}
|
|
199
|
-
return { success: true, message: '' }
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
exports.updateUser = functions.firestore.document('staged-users/{docId}').onUpdate(async (change, context) => {
|
|
205
|
-
const eventId = context.eventId
|
|
206
|
-
const eventRef = db.collection('events').doc(eventId)
|
|
207
|
-
const stagedDocId = context.params.docId
|
|
208
|
-
let newData = change.after.data()
|
|
209
|
-
const oldData = change.before.data()
|
|
210
|
-
|
|
211
|
-
const shouldProcess = await eventRef.get().then((eventDoc) => {
|
|
212
|
-
return !eventDoc.exists || !eventDoc.data().processed
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
if (!shouldProcess) {
|
|
216
|
-
return null
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Note: we can trust on newData.uid because we are checking in rules that it matches the auth.uid
|
|
220
|
-
if (newData.userId) {
|
|
221
|
-
const userRef = db.collection('users').doc(newData.userId)
|
|
222
|
-
await setUser(userRef, newData, oldData, stagedDocId)
|
|
223
|
-
await markProcessed(eventRef)
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
if (newData.templateUserId !== oldData.templateUserId) {
|
|
227
|
-
// Check if templateUserId already exists in the staged-users collection
|
|
228
|
-
const stagedUserRef = db.collection('staged-users').doc(newData.templateUserId)
|
|
229
|
-
const doc = await stagedUserRef.get()
|
|
230
|
-
|
|
231
|
-
// If it exists, skip the creation process
|
|
232
|
-
if (doc.exists) {
|
|
233
|
-
return null
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
newData.isTemplate = false
|
|
237
|
-
const templateUserId = newData.templateUserId
|
|
238
|
-
newData.meta = newData.templateMeta
|
|
239
|
-
delete newData.templateMeta
|
|
240
|
-
delete newData.templateUserId
|
|
241
|
-
if (Object.prototype.hasOwnProperty.call(newData, 'subCreate') && Object.values(newData.subCreate).length > 0) {
|
|
242
|
-
const subCreate = newData.subCreate
|
|
243
|
-
delete newData.subCreate
|
|
244
|
-
const addedDoc = await db.collection(subCreate.rootPath).add({ [subCreate.dynamicDocumentField]: newData.dynamicDocumentFieldValue })
|
|
245
|
-
await db.collection(subCreate.rootPath).doc(addedDoc.id).update({ docId: addedDoc.id })
|
|
246
|
-
delete newData.dynamicDocumentFieldValue
|
|
247
|
-
const newRole = { [`${subCreate.rootPath}-${addedDoc.id}`]: { collectionPath: `${subCreate.rootPath}-${addedDoc.id}`, role: subCreate.role } }
|
|
248
|
-
if (Object.prototype.hasOwnProperty.call(newData, 'collectionPaths')) {
|
|
249
|
-
newData.collectionPaths.push(`${subCreate.rootPath}-${addedDoc.id}`)
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
newData.collectionPaths = [`${subCreate.rootPath}-${addedDoc.id}`]
|
|
253
|
-
}
|
|
254
|
-
const newRoles = { ...newData.roles, ...newRole }
|
|
255
|
-
newData = { ...newData, roles: newRoles }
|
|
256
|
-
const stagedUserRef = db.collection('staged-users').doc(templateUserId)
|
|
257
|
-
await stagedUserRef.set({ ...newData, userId: templateUserId })
|
|
258
|
-
const userRef = db.collection('users').doc(templateUserId)
|
|
259
|
-
await setUser(userRef, newData, oldData, templateUserId)
|
|
260
|
-
await markProcessed(eventRef)
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
const stagedUserRef = db.collection('staged-users').doc(templateUserId)
|
|
264
|
-
await stagedUserRef.set({ ...newData, userId: templateUserId })
|
|
265
|
-
const userRef = db.collection('users').doc(templateUserId)
|
|
266
|
-
await setUser(userRef, newData, oldData, templateUserId)
|
|
267
|
-
await markProcessed(eventRef)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
await markProcessed(eventRef)
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
function setUser(userRef, newData, oldData, stagedDocId) {
|
|
275
|
-
// IT's OK If "users" doesn't match exactly matched "staged-users" because this is only preventing
|
|
276
|
-
// writing from outside the @edgdev/firebase functions, so discrepancies will be rare since
|
|
277
|
-
// the package will prevent before it gets this far.
|
|
278
|
-
return userRef.get().then((user) => {
|
|
279
|
-
let userUpdate = { meta: newData.meta, stagedDocId }
|
|
280
|
-
|
|
281
|
-
if (Object.prototype.hasOwnProperty.call(newData, 'roles')) {
|
|
282
|
-
userUpdate = { ...userUpdate, roles: newData.roles }
|
|
283
|
-
}
|
|
284
|
-
if (Object.prototype.hasOwnProperty.call(newData, 'specialPermissions')) {
|
|
285
|
-
userUpdate = { ...userUpdate, specialPermissions: newData.specialPermissions }
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (!oldData.userId) {
|
|
289
|
-
userUpdate = { ...userUpdate, userId: newData.uid }
|
|
290
|
-
}
|
|
291
|
-
if (!user.exists) {
|
|
292
|
-
return userRef.set(userUpdate)
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
return userRef.update(userUpdate)
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function shouldProcess(eventRef) {
|
|
301
|
-
return eventRef.get().then((eventDoc) => {
|
|
302
|
-
return !eventDoc.exists || !eventDoc.data().processed
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function markProcessed(eventRef) {
|
|
307
|
-
return eventRef.set({ processed: true }).then(() => {
|
|
308
|
-
return null
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
// END @edge/firebase functions
|
|
2
|
+
exports.edgeFirebase = require('./edgeFirebase')
|
|
3
|
+
// END @edge/firebase functions
|
package/src/postinstall.sh
CHANGED
|
@@ -24,11 +24,19 @@ awk '/\/\/ #EDGE FIREBASE RULES START/,/\/\/ #EDGE FIREBASE RULES END/' ./src/fi
|
|
|
24
24
|
|
|
25
25
|
if [ ! -f "$project_root/functions/index.js" ]; then
|
|
26
26
|
mkdir -p "$project_root/functions"
|
|
27
|
-
echo "
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
echo "require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? '.env.prod' : '.env.dev' })" > "$project_root/functions/index.js";
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
cp ./src/edgeFirebase.js "$project_root/functions/edgeFirebase.js"
|
|
31
|
+
cp ./src/config.js "$project_root/functions/config.js"
|
|
32
|
+
|
|
33
|
+
if [ ! -f "$project_root/.env.dev" ]; then
|
|
34
|
+
cp ./src/.env.dev "$project_root/.env.dev"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ ! -f "$project_root/.env.prod" ]; then
|
|
38
|
+
cp ./src/.env.prod "$project_root/.env.prod"
|
|
39
|
+
fi
|
|
32
40
|
|
|
33
41
|
[ "$(tail -c1 $project_root/functions/index.js)" != "" ] && echo "" >> "$project_root/functions/index.js"
|
|
34
42
|
|