@devtion/backend 0.0.0-004e6ad
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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/src/functions/index.js +2884 -0
- package/dist/src/functions/index.mjs +2834 -0
- package/dist/types/functions/bandada.d.ts +4 -0
- package/dist/types/functions/bandada.d.ts.map +1 -0
- package/dist/types/functions/ceremony.d.ts +33 -0
- package/dist/types/functions/ceremony.d.ts.map +1 -0
- package/dist/types/functions/circuit.d.ts +63 -0
- package/dist/types/functions/circuit.d.ts.map +1 -0
- package/dist/types/functions/index.d.ts +9 -0
- package/dist/types/functions/index.d.ts.map +1 -0
- package/dist/types/functions/participant.d.ts +58 -0
- package/dist/types/functions/participant.d.ts.map +1 -0
- package/dist/types/functions/siwe.d.ts +4 -0
- package/dist/types/functions/siwe.d.ts.map +1 -0
- package/dist/types/functions/storage.d.ts +37 -0
- package/dist/types/functions/storage.d.ts.map +1 -0
- package/dist/types/functions/timeout.d.ts +26 -0
- package/dist/types/functions/timeout.d.ts.map +1 -0
- package/dist/types/functions/user.d.ts +15 -0
- package/dist/types/functions/user.d.ts.map +1 -0
- package/dist/types/lib/errors.d.ts +76 -0
- package/dist/types/lib/errors.d.ts.map +1 -0
- package/dist/types/lib/services.d.ts +16 -0
- package/dist/types/lib/services.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +141 -0
- package/dist/types/lib/utils.d.ts.map +1 -0
- package/dist/types/types/enums.d.ts +13 -0
- package/dist/types/types/enums.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +186 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +90 -0
- package/src/functions/bandada.ts +155 -0
- package/src/functions/ceremony.ts +338 -0
- package/src/functions/circuit.ts +1044 -0
- package/src/functions/index.ts +38 -0
- package/src/functions/participant.ts +526 -0
- package/src/functions/siwe.ts +77 -0
- package/src/functions/storage.ts +551 -0
- package/src/functions/timeout.ts +296 -0
- package/src/functions/user.ts +167 -0
- package/src/lib/errors.ts +242 -0
- package/src/lib/services.ts +64 -0
- package/src/lib/utils.ts +474 -0
- package/src/types/declarations.d.ts +1 -0
- package/src/types/enums.ts +12 -0
- package/src/types/index.ts +200 -0
- package/test/index.test.ts +62 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import * as functions from "firebase-functions"
|
|
2
|
+
import admin from "firebase-admin"
|
|
3
|
+
import dotenv from "dotenv"
|
|
4
|
+
import {
|
|
5
|
+
CeremonyTimeoutType,
|
|
6
|
+
getParticipantsCollectionPath,
|
|
7
|
+
ParticipantContributionStep,
|
|
8
|
+
TimeoutType,
|
|
9
|
+
ParticipantStatus,
|
|
10
|
+
getTimeoutsCollectionPath,
|
|
11
|
+
commonTerms
|
|
12
|
+
} from "@devtion/actions"
|
|
13
|
+
import {
|
|
14
|
+
getCeremonyCircuits,
|
|
15
|
+
getCurrentServerTimestampInMillis,
|
|
16
|
+
getDocumentById,
|
|
17
|
+
queryOpenedCeremonies
|
|
18
|
+
} from "../lib/utils"
|
|
19
|
+
import { COMMON_ERRORS, logAndThrowError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
|
|
20
|
+
import { LogLevel } from "../types/enums"
|
|
21
|
+
|
|
22
|
+
dotenv.config()
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check and remove the current contributor if it doesn't complete the contribution on the specified amount of time.
|
|
26
|
+
* @dev since this cloud function is executed every minute, delay problems may occur. See issue #192 (https://github.com/quadratic-funding/mpc-phase2-suite/issues/192).
|
|
27
|
+
* @notice the reasons why a contributor may be considered blocking are many.
|
|
28
|
+
* for example due to network latency, disk availability issues, un/intentional crashes, limited hardware capabilities.
|
|
29
|
+
* the timeout mechanism (fixed/dynamic) could also influence this decision.
|
|
30
|
+
* this cloud function should check each circuit and:
|
|
31
|
+
* A) avoid timeout if there's no current contributor for the circuit.
|
|
32
|
+
* B) avoid timeout if the current contributor is the first for the circuit
|
|
33
|
+
* and timeout mechanism type is dynamic (suggestion: coordinator should be the first contributor).
|
|
34
|
+
* C) check if the current contributor is a potential blocking contributor for the circuit.
|
|
35
|
+
* D) discriminate between blocking contributor (= when downloading, computing, uploading contribution steps)
|
|
36
|
+
* or verification (= verifying contribution step) timeout types.
|
|
37
|
+
* E) execute timeout.
|
|
38
|
+
* E.1) prepare next contributor (if any).
|
|
39
|
+
* E.2) update circuit contributors waiting queue removing the current contributor.
|
|
40
|
+
* E.3) assign timeout to blocking contributor (participant doc update + timeout doc).
|
|
41
|
+
*/
|
|
42
|
+
export const checkAndRemoveBlockingContributor = functions
|
|
43
|
+
.region("europe-west1")
|
|
44
|
+
.runWith({
|
|
45
|
+
memory: "1GB"
|
|
46
|
+
})
|
|
47
|
+
.pubsub.schedule("every 1 minutes")
|
|
48
|
+
.onRun(async () => {
|
|
49
|
+
// Prepare Firestore DB.
|
|
50
|
+
const firestore = admin.firestore()
|
|
51
|
+
// Get current server timestamp in milliseconds.
|
|
52
|
+
const currentServerTimestamp = getCurrentServerTimestampInMillis()
|
|
53
|
+
|
|
54
|
+
// Get opened ceremonies.
|
|
55
|
+
const ceremonies = await queryOpenedCeremonies()
|
|
56
|
+
|
|
57
|
+
// For each ceremony.
|
|
58
|
+
for (const ceremony of ceremonies) {
|
|
59
|
+
if (!ceremony.data())
|
|
60
|
+
// Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
|
|
61
|
+
printLog(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA.message, LogLevel.WARN)
|
|
62
|
+
else {
|
|
63
|
+
// Get ceremony circuits.
|
|
64
|
+
const circuits = await getCeremonyCircuits(ceremony.id)
|
|
65
|
+
|
|
66
|
+
// Extract ceremony data.
|
|
67
|
+
const { timeoutType: timeoutMechanismType, penalty } = ceremony.data()!
|
|
68
|
+
|
|
69
|
+
for (const circuit of circuits) {
|
|
70
|
+
if (!circuit.data())
|
|
71
|
+
// Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
|
|
72
|
+
printLog(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA.message, LogLevel.WARN)
|
|
73
|
+
else {
|
|
74
|
+
// Extract circuit data.
|
|
75
|
+
const { waitingQueue, avgTimings, dynamicThreshold, fixedTimeWindow } = circuit.data()
|
|
76
|
+
const { contributors, currentContributor, failedContributions, completedContributions } =
|
|
77
|
+
waitingQueue
|
|
78
|
+
const {
|
|
79
|
+
fullContribution: avgFullContribution,
|
|
80
|
+
contributionComputation: avgContributionComputation,
|
|
81
|
+
verifyCloudFunction: avgVerifyCloudFunction
|
|
82
|
+
} = avgTimings
|
|
83
|
+
|
|
84
|
+
// Case (A).
|
|
85
|
+
if (!currentContributor)
|
|
86
|
+
// Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
|
|
87
|
+
printLog(
|
|
88
|
+
`No current contributor for circuit ${circuit.id} - ceremony ${ceremony.id}`,
|
|
89
|
+
LogLevel.WARN
|
|
90
|
+
)
|
|
91
|
+
else if (
|
|
92
|
+
avgFullContribution === 0 &&
|
|
93
|
+
avgContributionComputation === 0 &&
|
|
94
|
+
avgVerifyCloudFunction === 0 &&
|
|
95
|
+
completedContributions === 0 &&
|
|
96
|
+
timeoutMechanismType === CeremonyTimeoutType.DYNAMIC
|
|
97
|
+
)
|
|
98
|
+
printLog(
|
|
99
|
+
`No timeout will be executed for the first contributor to the circuit ${circuit.id} - ceremony ${ceremony.id}`,
|
|
100
|
+
LogLevel.WARN
|
|
101
|
+
)
|
|
102
|
+
else {
|
|
103
|
+
// Get current contributor document.
|
|
104
|
+
const participant = await getDocumentById(
|
|
105
|
+
getParticipantsCollectionPath(ceremony.id),
|
|
106
|
+
currentContributor
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (!participant.data())
|
|
110
|
+
// Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
|
|
111
|
+
printLog(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA.message, LogLevel.WARN)
|
|
112
|
+
else {
|
|
113
|
+
// Extract participant data.
|
|
114
|
+
const { contributionStartedAt, verificationStartedAt, contributionStep } =
|
|
115
|
+
participant.data()!
|
|
116
|
+
|
|
117
|
+
// Case (C).
|
|
118
|
+
|
|
119
|
+
// Compute dynamic timeout threshold.
|
|
120
|
+
const timeoutDynamicThreshold =
|
|
121
|
+
timeoutMechanismType === CeremonyTimeoutType.DYNAMIC
|
|
122
|
+
? (avgFullContribution / 100) * Number(dynamicThreshold)
|
|
123
|
+
: 0
|
|
124
|
+
|
|
125
|
+
// Compute the timeout expiration date (in ms).
|
|
126
|
+
const timeoutExpirationDateInMsForBlockingContributor =
|
|
127
|
+
timeoutMechanismType === CeremonyTimeoutType.DYNAMIC
|
|
128
|
+
? Number(contributionStartedAt) +
|
|
129
|
+
Number(avgFullContribution) +
|
|
130
|
+
Number(timeoutDynamicThreshold)
|
|
131
|
+
: Number(contributionStartedAt) + Number(fixedTimeWindow) * 60000 // * 60000 = convert minutes to millis.
|
|
132
|
+
|
|
133
|
+
// Case (D).
|
|
134
|
+
const timeoutExpirationDateInMsForVerificationCloudFunction =
|
|
135
|
+
contributionStep === ParticipantContributionStep.VERIFYING &&
|
|
136
|
+
!!verificationStartedAt
|
|
137
|
+
? Number(verificationStartedAt) + 3540000 // 3540000 = 59 minutes in ms.
|
|
138
|
+
: 0
|
|
139
|
+
|
|
140
|
+
// Assign the timeout type.
|
|
141
|
+
let timeoutType: string = ""
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
timeoutExpirationDateInMsForBlockingContributor < currentServerTimestamp &&
|
|
145
|
+
(contributionStep === ParticipantContributionStep.DOWNLOADING ||
|
|
146
|
+
contributionStep === ParticipantContributionStep.COMPUTING ||
|
|
147
|
+
contributionStep === ParticipantContributionStep.UPLOADING ||
|
|
148
|
+
contributionStep === ParticipantContributionStep.COMPLETED)
|
|
149
|
+
)
|
|
150
|
+
timeoutType = TimeoutType.BLOCKING_CONTRIBUTION
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
timeoutExpirationDateInMsForVerificationCloudFunction > 0 &&
|
|
154
|
+
timeoutExpirationDateInMsForVerificationCloudFunction < currentServerTimestamp &&
|
|
155
|
+
contributionStep === ParticipantContributionStep.VERIFYING
|
|
156
|
+
)
|
|
157
|
+
timeoutType = TimeoutType.BLOCKING_CLOUD_FUNCTION
|
|
158
|
+
|
|
159
|
+
printLog(
|
|
160
|
+
`${timeoutType} detected for circuit ${circuit.id} - ceremony ${ceremony.id}`,
|
|
161
|
+
LogLevel.DEBUG
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if (!timeoutType)
|
|
165
|
+
// Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
|
|
166
|
+
printLog(
|
|
167
|
+
`No timeout for circuit ${circuit.id} - ceremony ${ceremony.id}`,
|
|
168
|
+
LogLevel.WARN
|
|
169
|
+
)
|
|
170
|
+
else {
|
|
171
|
+
// Case (E).
|
|
172
|
+
let nextCurrentContributorId = ""
|
|
173
|
+
|
|
174
|
+
// Prepare Firestore batch of txs.
|
|
175
|
+
const batch = firestore.batch()
|
|
176
|
+
|
|
177
|
+
// Remove current contributor from waiting queue.
|
|
178
|
+
contributors.shift()
|
|
179
|
+
|
|
180
|
+
// Check if someone else is ready to start the contribution.
|
|
181
|
+
if (contributors.length > 0) {
|
|
182
|
+
// Step (E.1).
|
|
183
|
+
|
|
184
|
+
// Take the next participant to be current contributor.
|
|
185
|
+
nextCurrentContributorId = contributors.at(0)
|
|
186
|
+
|
|
187
|
+
// Get the document of the next current contributor.
|
|
188
|
+
const nextCurrentContributor = await getDocumentById(
|
|
189
|
+
getParticipantsCollectionPath(ceremony.id),
|
|
190
|
+
nextCurrentContributorId
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// Prepare next current contributor.
|
|
194
|
+
batch.update(nextCurrentContributor.ref, {
|
|
195
|
+
status: ParticipantStatus.READY,
|
|
196
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Step (E.2).
|
|
201
|
+
// Update accordingly the waiting queue.
|
|
202
|
+
batch.update(circuit.ref, {
|
|
203
|
+
waitingQueue: {
|
|
204
|
+
...waitingQueue,
|
|
205
|
+
contributors,
|
|
206
|
+
currentContributor: nextCurrentContributorId,
|
|
207
|
+
failedContributions: failedContributions + 1
|
|
208
|
+
},
|
|
209
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Step (E.3).
|
|
213
|
+
batch.update(participant.ref, {
|
|
214
|
+
status: ParticipantStatus.TIMEDOUT,
|
|
215
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Compute the timeout duration (penalty) in milliseconds.
|
|
219
|
+
const timeoutPenaltyInMs = Number(penalty) * 60000 // 60000 = amount of ms x minute.
|
|
220
|
+
|
|
221
|
+
// Prepare an empty doc for timeout (w/ auto-gen uid).
|
|
222
|
+
const timeout = await firestore
|
|
223
|
+
.collection(getTimeoutsCollectionPath(ceremony.id, participant.id))
|
|
224
|
+
.doc()
|
|
225
|
+
.get()
|
|
226
|
+
|
|
227
|
+
// Prepare tx to store info about the timeout.
|
|
228
|
+
batch.create(timeout.ref, {
|
|
229
|
+
type: timeoutType,
|
|
230
|
+
startDate: currentServerTimestamp,
|
|
231
|
+
endDate: currentServerTimestamp + timeoutPenaltyInMs
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Send atomic update for Firestore.
|
|
235
|
+
await batch.commit()
|
|
236
|
+
|
|
237
|
+
printLog(
|
|
238
|
+
`The contributor ${participant.id} has been identified as potential blocking contributor. A timeout of type ${timeoutType} has been triggered w/ a penalty of ${timeoutPenaltyInMs} ms`,
|
|
239
|
+
LogLevel.DEBUG
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Resume the contributor circuit contribution from scratch after the timeout expiration.
|
|
252
|
+
* @dev The participant can resume the contribution if and only if the last timeout in progress was verified as expired (status == EXHUMED).
|
|
253
|
+
*/
|
|
254
|
+
export const resumeContributionAfterTimeoutExpiration = functions
|
|
255
|
+
.region("europe-west1")
|
|
256
|
+
.runWith({
|
|
257
|
+
memory: "1GB"
|
|
258
|
+
})
|
|
259
|
+
.https.onCall(async (data: { ceremonyId: string }, context: functions.https.CallableContext): Promise<void> => {
|
|
260
|
+
if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
|
|
261
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
|
|
262
|
+
|
|
263
|
+
if (!data.ceremonyId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
264
|
+
|
|
265
|
+
// Get data.
|
|
266
|
+
const { ceremonyId } = data
|
|
267
|
+
const userId = context.auth?.uid
|
|
268
|
+
|
|
269
|
+
// Look for the ceremony document.
|
|
270
|
+
const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
|
|
271
|
+
const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
|
|
272
|
+
|
|
273
|
+
// Prepare documents data.
|
|
274
|
+
const participantData = participantDoc.data()
|
|
275
|
+
|
|
276
|
+
if (!ceremonyDoc.data() || !participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
277
|
+
|
|
278
|
+
// Extract data.
|
|
279
|
+
const { contributionProgress, status } = participantData!
|
|
280
|
+
|
|
281
|
+
// Check pre-condition for resumable contribution after timeout expiration.
|
|
282
|
+
if (status === ParticipantStatus.EXHUMED)
|
|
283
|
+
await participantDoc.ref.update({
|
|
284
|
+
status: ParticipantStatus.READY,
|
|
285
|
+
lastUpdated: getCurrentServerTimestampInMillis(),
|
|
286
|
+
tempContributionData: {}
|
|
287
|
+
})
|
|
288
|
+
else logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT)
|
|
289
|
+
|
|
290
|
+
printLog(
|
|
291
|
+
`Contributor ${userId} can retry the contribution for the circuit in position ${
|
|
292
|
+
contributionProgress + 1
|
|
293
|
+
} after timeout expiration`,
|
|
294
|
+
LogLevel.DEBUG
|
|
295
|
+
)
|
|
296
|
+
})
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as functions from "firebase-functions"
|
|
2
|
+
import { UserRecord } from "firebase-functions/v1/auth"
|
|
3
|
+
import admin from "firebase-admin"
|
|
4
|
+
import dotenv from "dotenv"
|
|
5
|
+
import { commonTerms, githubReputation } from "@devtion/actions"
|
|
6
|
+
import { encode } from "html-entities"
|
|
7
|
+
import { getGitHubVariables, getCurrentServerTimestampInMillis } from "../lib/utils"
|
|
8
|
+
import { logAndThrowError, makeError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
|
|
9
|
+
import { LogLevel } from "../types/enums"
|
|
10
|
+
|
|
11
|
+
dotenv.config()
|
|
12
|
+
/**
|
|
13
|
+
* Record the authenticated user information inside the Firestore DB upon authentication.
|
|
14
|
+
* @dev the data is recorded in a new document in the `users` collection.
|
|
15
|
+
* @notice this method is automatically triggered upon user authentication in the Firebase app
|
|
16
|
+
* which uses the Firebase Authentication service.
|
|
17
|
+
*/
|
|
18
|
+
export const registerAuthUser = functions
|
|
19
|
+
.region("europe-west1")
|
|
20
|
+
.runWith({
|
|
21
|
+
memory: "1GB"
|
|
22
|
+
})
|
|
23
|
+
.auth.user()
|
|
24
|
+
.onCreate(async (user: UserRecord) => {
|
|
25
|
+
// Get DB.
|
|
26
|
+
const firestore = admin.firestore()
|
|
27
|
+
// Get user information.
|
|
28
|
+
if (!user.uid) logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
|
|
29
|
+
// The user object has basic properties such as display name, email, etc.
|
|
30
|
+
const { displayName } = user
|
|
31
|
+
const { email } = user
|
|
32
|
+
const { photoURL } = user
|
|
33
|
+
const { emailVerified } = user
|
|
34
|
+
// Metadata.
|
|
35
|
+
const { creationTime } = user.metadata
|
|
36
|
+
const { lastSignInTime } = user.metadata
|
|
37
|
+
// The user's ID, unique to the Firebase project. Do NOT use
|
|
38
|
+
// this value to authenticate with your backend server, if
|
|
39
|
+
// you have one. Use User.getToken() instead.
|
|
40
|
+
const { uid } = user
|
|
41
|
+
// Reference to a document using uid.
|
|
42
|
+
const userRef = firestore.collection(commonTerms.collections.users.name).doc(uid)
|
|
43
|
+
// html encode the display name (or put the ID if the name is not displayed)
|
|
44
|
+
const encodedDisplayName =
|
|
45
|
+
user.displayName === "Null" || user.displayName === null ? user.uid : encode(displayName)
|
|
46
|
+
|
|
47
|
+
// store the avatar URL of a contributor
|
|
48
|
+
let avatarUrl: string = ""
|
|
49
|
+
// we only do reputation check if the user is not a coordinator
|
|
50
|
+
if (
|
|
51
|
+
!(
|
|
52
|
+
email?.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
|
|
53
|
+
email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN
|
|
54
|
+
)
|
|
55
|
+
) {
|
|
56
|
+
const auth = admin.auth()
|
|
57
|
+
// if provider == github.com let's use our functions to check the user's reputation
|
|
58
|
+
if (user.providerData.length > 0 && user.providerData[0].providerId === "github.com") {
|
|
59
|
+
const vars = getGitHubVariables()
|
|
60
|
+
|
|
61
|
+
// this return true or false
|
|
62
|
+
try {
|
|
63
|
+
const { reputable, avatarUrl: avatarURL } = await githubReputation(
|
|
64
|
+
user.providerData[0].uid,
|
|
65
|
+
vars.minimumFollowing,
|
|
66
|
+
vars.minimumFollowers,
|
|
67
|
+
vars.minimumPublicRepos,
|
|
68
|
+
vars.minimumAge
|
|
69
|
+
)
|
|
70
|
+
if (!reputable) {
|
|
71
|
+
// Delete user
|
|
72
|
+
await auth.deleteUser(user.uid)
|
|
73
|
+
// Throw error
|
|
74
|
+
logAndThrowError(
|
|
75
|
+
makeError(
|
|
76
|
+
"permission-denied",
|
|
77
|
+
"The user is not allowed to sign up because their Github reputation is not high enough.",
|
|
78
|
+
`The user ${
|
|
79
|
+
user.displayName === "Null" || user.displayName === null
|
|
80
|
+
? user.uid
|
|
81
|
+
: user.displayName
|
|
82
|
+
} is not allowed to sign up because their Github reputation is not high enough. Please contact the administrator if you think this is a mistake.`
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
// store locally
|
|
87
|
+
avatarUrl = avatarURL
|
|
88
|
+
printLog(
|
|
89
|
+
`Github reputation check passed for user ${
|
|
90
|
+
user.displayName === "Null" || user.displayName === null ? user.uid : user.displayName
|
|
91
|
+
}`,
|
|
92
|
+
LogLevel.DEBUG
|
|
93
|
+
)
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
// Delete user
|
|
96
|
+
await auth.deleteUser(user.uid)
|
|
97
|
+
logAndThrowError(
|
|
98
|
+
makeError(
|
|
99
|
+
"permission-denied",
|
|
100
|
+
"There was an error while checking the user's Github reputation.",
|
|
101
|
+
`${error}`
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Set document (nb. we refer to providerData[0] because we use Github OAuth provider only).
|
|
108
|
+
// In future releases we might want to loop through the providerData array as we support
|
|
109
|
+
// more providers.
|
|
110
|
+
await userRef.set({
|
|
111
|
+
name: encodedDisplayName,
|
|
112
|
+
encodedDisplayName,
|
|
113
|
+
// Metadata.
|
|
114
|
+
creationTime,
|
|
115
|
+
lastSignInTime: lastSignInTime || creationTime,
|
|
116
|
+
// Optional.
|
|
117
|
+
email: email || "",
|
|
118
|
+
emailVerified: emailVerified || false,
|
|
119
|
+
photoURL: photoURL || "",
|
|
120
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// we want to create a new collection for the users to store the avatars
|
|
124
|
+
const avatarRef = firestore.collection(commonTerms.collections.avatars.name).doc(uid)
|
|
125
|
+
await avatarRef.set({
|
|
126
|
+
avatarUrl: avatarUrl || ""
|
|
127
|
+
})
|
|
128
|
+
printLog(`Authenticated user document with identifier ${uid} has been correctly stored`, LogLevel.DEBUG)
|
|
129
|
+
printLog(`Authenticated user avatar with identifier ${uid} has been correctly stored`, LogLevel.DEBUG)
|
|
130
|
+
})
|
|
131
|
+
/**
|
|
132
|
+
* Set custom claims for role-based access control on the newly created user.
|
|
133
|
+
* @notice this method is automatically triggered upon user authentication in the Firebase app
|
|
134
|
+
* which uses the Firebase Authentication service.
|
|
135
|
+
*/
|
|
136
|
+
export const processSignUpWithCustomClaims = functions
|
|
137
|
+
.region("europe-west1")
|
|
138
|
+
.runWith({
|
|
139
|
+
memory: "1GB"
|
|
140
|
+
})
|
|
141
|
+
.auth.user()
|
|
142
|
+
.onCreate(async (user: UserRecord) => {
|
|
143
|
+
// Get user information.
|
|
144
|
+
if (!user.uid) logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
|
|
145
|
+
// Prepare state.
|
|
146
|
+
let customClaims: any
|
|
147
|
+
// Check if user meets role criteria to be a coordinator.
|
|
148
|
+
if (
|
|
149
|
+
user.email &&
|
|
150
|
+
(user.email.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
|
|
151
|
+
user.email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN)
|
|
152
|
+
) {
|
|
153
|
+
customClaims = { coordinator: true }
|
|
154
|
+
printLog(`Authenticated user ${user.uid} has been identified as coordinator`, LogLevel.DEBUG)
|
|
155
|
+
} else {
|
|
156
|
+
customClaims = { participant: true }
|
|
157
|
+
printLog(`Authenticated user ${user.uid} has been identified as participant`, LogLevel.DEBUG)
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
// Set custom user claims on this newly created user.
|
|
161
|
+
await admin.auth().setCustomUserClaims(user.uid, customClaims)
|
|
162
|
+
} catch (error: any) {
|
|
163
|
+
const specificError = SPECIFIC_ERRORS.SE_AUTH_SET_CUSTOM_USER_CLAIMS_FAIL
|
|
164
|
+
const additionalDetails = error.toString()
|
|
165
|
+
logAndThrowError(makeError(specificError.code, specificError.message, additionalDetails))
|
|
166
|
+
}
|
|
167
|
+
})
|