@devtion/backend 0.0.0-7e983e3
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 +151 -0
- package/dist/src/functions/index.js +2644 -0
- package/dist/src/functions/index.mjs +2596 -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 +7 -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/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 +75 -0
- package/dist/types/lib/errors.d.ts.map +1 -0
- package/dist/types/lib/services.d.ts +9 -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 +130 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +89 -0
- package/src/functions/ceremony.ts +333 -0
- package/src/functions/circuit.ts +1092 -0
- package/src/functions/index.ts +36 -0
- package/src/functions/participant.ts +526 -0
- package/src/functions/storage.ts +548 -0
- package/src/functions/timeout.ts +294 -0
- package/src/functions/user.ts +142 -0
- package/src/lib/errors.ts +237 -0
- package/src/lib/services.ts +28 -0
- package/src/lib/utils.ts +472 -0
- package/src/types/enums.ts +12 -0
- package/src/types/index.ts +140 -0
- package/test/index.test.ts +62 -0
|
@@ -0,0 +1,294 @@
|
|
|
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 "@p0tion/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: "512MB"
|
|
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 { 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
|
+
)
|
|
149
|
+
timeoutType = TimeoutType.BLOCKING_CONTRIBUTION
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
timeoutExpirationDateInMsForVerificationCloudFunction > 0 &&
|
|
153
|
+
timeoutExpirationDateInMsForVerificationCloudFunction < currentServerTimestamp &&
|
|
154
|
+
contributionStep === ParticipantContributionStep.VERIFYING
|
|
155
|
+
)
|
|
156
|
+
timeoutType = TimeoutType.BLOCKING_CLOUD_FUNCTION
|
|
157
|
+
|
|
158
|
+
printLog(
|
|
159
|
+
`${timeoutType} detected for circuit ${circuit.id} - ceremony ${ceremony.id}`,
|
|
160
|
+
LogLevel.DEBUG
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if (!timeoutType)
|
|
164
|
+
// Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
|
|
165
|
+
printLog(
|
|
166
|
+
`No timeout for circuit ${circuit.id} - ceremony ${ceremony.id}`,
|
|
167
|
+
LogLevel.WARN
|
|
168
|
+
)
|
|
169
|
+
else {
|
|
170
|
+
// Case (E).
|
|
171
|
+
let nextCurrentContributorId = ""
|
|
172
|
+
|
|
173
|
+
// Prepare Firestore batch of txs.
|
|
174
|
+
const batch = firestore.batch()
|
|
175
|
+
|
|
176
|
+
// Remove current contributor from waiting queue.
|
|
177
|
+
contributors.shift(1)
|
|
178
|
+
|
|
179
|
+
// Check if someone else is ready to start the contribution.
|
|
180
|
+
if (contributors.length > 0) {
|
|
181
|
+
// Step (E.1).
|
|
182
|
+
|
|
183
|
+
// Take the next participant to be current contributor.
|
|
184
|
+
nextCurrentContributorId = contributors.at(0)
|
|
185
|
+
|
|
186
|
+
// Get the document of the next current contributor.
|
|
187
|
+
const nextCurrentContributor = await getDocumentById(
|
|
188
|
+
getParticipantsCollectionPath(ceremony.id),
|
|
189
|
+
nextCurrentContributorId
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// Prepare next current contributor.
|
|
193
|
+
batch.update(nextCurrentContributor.ref, {
|
|
194
|
+
status: ParticipantStatus.READY,
|
|
195
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Step (E.2).
|
|
200
|
+
// Update accordingly the waiting queue.
|
|
201
|
+
batch.update(circuit.ref, {
|
|
202
|
+
waitingQueue: {
|
|
203
|
+
...waitingQueue,
|
|
204
|
+
contributors,
|
|
205
|
+
currentContributor: nextCurrentContributorId,
|
|
206
|
+
failedContributions: failedContributions + 1
|
|
207
|
+
},
|
|
208
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Step (E.3).
|
|
212
|
+
batch.update(participant.ref, {
|
|
213
|
+
status: ParticipantStatus.TIMEDOUT,
|
|
214
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Compute the timeout duration (penalty) in milliseconds.
|
|
218
|
+
const timeoutPenaltyInMs = Number(penalty) * 60000 // 60000 = amount of ms x minute.
|
|
219
|
+
|
|
220
|
+
// Prepare an empty doc for timeout (w/ auto-gen uid).
|
|
221
|
+
const timeout = await firestore
|
|
222
|
+
.collection(getTimeoutsCollectionPath(ceremony.id, participant.id))
|
|
223
|
+
.doc()
|
|
224
|
+
.get()
|
|
225
|
+
|
|
226
|
+
// Prepare tx to store info about the timeout.
|
|
227
|
+
batch.create(timeout.ref, {
|
|
228
|
+
type: timeoutType,
|
|
229
|
+
startDate: currentServerTimestamp,
|
|
230
|
+
endDate: currentServerTimestamp + timeoutPenaltyInMs
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Send atomic update for Firestore.
|
|
234
|
+
await batch.commit()
|
|
235
|
+
|
|
236
|
+
printLog(
|
|
237
|
+
`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`,
|
|
238
|
+
LogLevel.DEBUG
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resume the contributor circuit contribution from scratch after the timeout expiration.
|
|
251
|
+
* @dev The participant can resume the contribution if and only if the last timeout in progress was verified as expired (status == EXHUMED).
|
|
252
|
+
*/
|
|
253
|
+
export const resumeContributionAfterTimeoutExpiration = functions
|
|
254
|
+
.region("europe-west1")
|
|
255
|
+
.runWith({
|
|
256
|
+
memory: "512MB"
|
|
257
|
+
})
|
|
258
|
+
.https.onCall(async (data: { ceremonyId: string }, context: functions.https.CallableContext): Promise<void> => {
|
|
259
|
+
if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
|
|
260
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
|
|
261
|
+
|
|
262
|
+
if (!data.ceremonyId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
263
|
+
|
|
264
|
+
// Get data.
|
|
265
|
+
const { ceremonyId } = data
|
|
266
|
+
const userId = context.auth?.uid
|
|
267
|
+
|
|
268
|
+
// Look for the ceremony document.
|
|
269
|
+
const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
|
|
270
|
+
const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
|
|
271
|
+
|
|
272
|
+
// Prepare documents data.
|
|
273
|
+
const participantData = participantDoc.data()
|
|
274
|
+
|
|
275
|
+
if (!ceremonyDoc.data() || !participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
276
|
+
|
|
277
|
+
// Extract data.
|
|
278
|
+
const { contributionProgress, status } = participantData!
|
|
279
|
+
|
|
280
|
+
// Check pre-condition for resumable contribution after timeout expiration.
|
|
281
|
+
if (status === ParticipantStatus.EXHUMED)
|
|
282
|
+
await participantDoc.ref.update({
|
|
283
|
+
status: ParticipantStatus.READY,
|
|
284
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
285
|
+
})
|
|
286
|
+
else logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT)
|
|
287
|
+
|
|
288
|
+
printLog(
|
|
289
|
+
`Contributor ${userId} can retry the contribution for the circuit in position ${
|
|
290
|
+
contributionProgress + 1
|
|
291
|
+
} after timeout expiration`,
|
|
292
|
+
LogLevel.DEBUG
|
|
293
|
+
)
|
|
294
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
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 "@p0tion/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: "512MB"
|
|
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
|
|
44
|
+
const encodedDisplayName = encode(displayName)
|
|
45
|
+
// we only do reputation check if the user is not a coordinator
|
|
46
|
+
if (
|
|
47
|
+
!(
|
|
48
|
+
email?.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
|
|
49
|
+
email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
const auth = admin.auth()
|
|
53
|
+
// if provider == github.com let's use our functions to check the user's reputation
|
|
54
|
+
if (user.providerData[0].providerId === "github.com") {
|
|
55
|
+
const vars = getGitHubVariables()
|
|
56
|
+
|
|
57
|
+
// this return true or false
|
|
58
|
+
try {
|
|
59
|
+
const res = await githubReputation(
|
|
60
|
+
user.providerData[0].uid,
|
|
61
|
+
vars.minimumFollowing,
|
|
62
|
+
vars.minimumFollowers,
|
|
63
|
+
vars.minimumPublicRepos
|
|
64
|
+
)
|
|
65
|
+
if (!res) {
|
|
66
|
+
// Delete user
|
|
67
|
+
await auth.deleteUser(user.uid)
|
|
68
|
+
// Throw error
|
|
69
|
+
logAndThrowError(
|
|
70
|
+
makeError(
|
|
71
|
+
"permission-denied",
|
|
72
|
+
"The user is not allowed to sign up because their Github reputation is not high enough.",
|
|
73
|
+
`The user ${user.displayName} 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.`
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
printLog(`Github reputation check passed for user ${user.displayName}`, LogLevel.DEBUG)
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
// Delete user
|
|
80
|
+
await auth.deleteUser(user.uid)
|
|
81
|
+
logAndThrowError(
|
|
82
|
+
makeError(
|
|
83
|
+
"permission-denied",
|
|
84
|
+
"There was an error while checking the user's Github reputation.",
|
|
85
|
+
`${error}`
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Set document (nb. we refer to providerData[0] because we use Github OAuth provider only).
|
|
92
|
+
await userRef.set({
|
|
93
|
+
name: encodedDisplayName,
|
|
94
|
+
encodedDisplayName,
|
|
95
|
+
// Metadata.
|
|
96
|
+
creationTime,
|
|
97
|
+
lastSignInTime,
|
|
98
|
+
// Optional.
|
|
99
|
+
email: email || "",
|
|
100
|
+
emailVerified: emailVerified || false,
|
|
101
|
+
photoURL: photoURL || "",
|
|
102
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
103
|
+
})
|
|
104
|
+
printLog(`Authenticated user document with identifier ${uid} has been correctly stored`, LogLevel.DEBUG)
|
|
105
|
+
})
|
|
106
|
+
/**
|
|
107
|
+
* Set custom claims for role-based access control on the newly created user.
|
|
108
|
+
* @notice this method is automatically triggered upon user authentication in the Firebase app
|
|
109
|
+
* which uses the Firebase Authentication service.
|
|
110
|
+
*/
|
|
111
|
+
export const processSignUpWithCustomClaims = functions
|
|
112
|
+
.region("europe-west1")
|
|
113
|
+
.runWith({
|
|
114
|
+
memory: "512MB"
|
|
115
|
+
})
|
|
116
|
+
.auth.user()
|
|
117
|
+
.onCreate(async (user: UserRecord) => {
|
|
118
|
+
// Get user information.
|
|
119
|
+
if (!user.uid) logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
|
|
120
|
+
// Prepare state.
|
|
121
|
+
let customClaims: any
|
|
122
|
+
// Check if user meets role criteria to be a coordinator.
|
|
123
|
+
if (
|
|
124
|
+
user.email &&
|
|
125
|
+
(user.email.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
|
|
126
|
+
user.email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN)
|
|
127
|
+
) {
|
|
128
|
+
customClaims = { coordinator: true }
|
|
129
|
+
printLog(`Authenticated user ${user.uid} has been identified as coordinator`, LogLevel.DEBUG)
|
|
130
|
+
} else {
|
|
131
|
+
customClaims = { participant: true }
|
|
132
|
+
printLog(`Authenticated user ${user.uid} has been identified as participant`, LogLevel.DEBUG)
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// Set custom user claims on this newly created user.
|
|
136
|
+
await admin.auth().setCustomUserClaims(user.uid, customClaims)
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
const specificError = SPECIFIC_ERRORS.SE_AUTH_SET_CUSTOM_USER_CLAIMS_FAIL
|
|
139
|
+
const additionalDetails = error.toString()
|
|
140
|
+
logAndThrowError(makeError(specificError.code, specificError.message, additionalDetails))
|
|
141
|
+
}
|
|
142
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as functions from "firebase-functions"
|
|
2
|
+
import { FunctionsErrorCode, HttpsError } from "firebase-functions/v1/https"
|
|
3
|
+
import { LogLevel } from "../types/enums"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a new custom HTTPs error for cloud functions.
|
|
7
|
+
* @notice the set of Firebase Functions status codes. The codes are the same at the
|
|
8
|
+
* ones exposed by {@link https://github.com/grpc/grpc/blob/master/doc/statuscodes.md | gRPC}.
|
|
9
|
+
* @param errorCode <FunctionsErrorCode> - the set of possible error codes.
|
|
10
|
+
* @param message <string> - the error messge.
|
|
11
|
+
* @param [details] <string> - the details of the error (optional).
|
|
12
|
+
* @returns <HttpsError>
|
|
13
|
+
*/
|
|
14
|
+
export const makeError = (errorCode: FunctionsErrorCode, message: string, details?: string): HttpsError =>
|
|
15
|
+
new functions.https.HttpsError(errorCode, message, details)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Log a custom message on console using a specific level.
|
|
19
|
+
* @param message <string> - the message to be shown.
|
|
20
|
+
* @param logLevel <LogLevel> - the level of the log to be used to show the message (e.g., debug, error).
|
|
21
|
+
*/
|
|
22
|
+
export const printLog = (message: string, logLevel: LogLevel) => {
|
|
23
|
+
switch (logLevel) {
|
|
24
|
+
case LogLevel.INFO:
|
|
25
|
+
functions.logger.info(`[${logLevel}] ${message}`)
|
|
26
|
+
break
|
|
27
|
+
case LogLevel.DEBUG:
|
|
28
|
+
functions.logger.debug(`[${logLevel}] ${message}`)
|
|
29
|
+
break
|
|
30
|
+
case LogLevel.WARN:
|
|
31
|
+
functions.logger.warn(`[${logLevel}] ${message}`)
|
|
32
|
+
break
|
|
33
|
+
case LogLevel.ERROR:
|
|
34
|
+
functions.logger.error(`[${logLevel}] ${message}`)
|
|
35
|
+
break
|
|
36
|
+
case LogLevel.LOG:
|
|
37
|
+
functions.logger.log(`[${logLevel}] ${message}`)
|
|
38
|
+
break
|
|
39
|
+
default:
|
|
40
|
+
console.log(`[${logLevel}] ${message}`)
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Log and throw an HTTPs error.
|
|
47
|
+
* @param error <HttpsError> - the error to be logged and thrown.
|
|
48
|
+
*/
|
|
49
|
+
export const logAndThrowError = (error: HttpsError) => {
|
|
50
|
+
printLog(`${error.code}: ${error.message} ${!error.details ? "" : `\ndetails: ${error.details}`}`, LogLevel.ERROR)
|
|
51
|
+
throw error
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A set of Cloud Function specific errors.
|
|
56
|
+
* @notice these are errors that happen only on specific cloud functions.
|
|
57
|
+
*/
|
|
58
|
+
export const SPECIFIC_ERRORS = {
|
|
59
|
+
SE_AUTH_NO_CURRENT_AUTH_USER: makeError(
|
|
60
|
+
"failed-precondition",
|
|
61
|
+
"Unable to retrieve the authenticated user.",
|
|
62
|
+
"Authenticated user information could not be retrieved. No document will be created in the relevant collection."
|
|
63
|
+
),
|
|
64
|
+
SE_AUTH_SET_CUSTOM_USER_CLAIMS_FAIL: makeError(
|
|
65
|
+
"invalid-argument",
|
|
66
|
+
"Unable to set custom claims for authenticated user."
|
|
67
|
+
),
|
|
68
|
+
SE_AUTH_USER_NOT_REPUTABLE: makeError(
|
|
69
|
+
"permission-denied",
|
|
70
|
+
"The authenticated user is not reputable.",
|
|
71
|
+
"The authenticated user is not reputable. No document will be created in the relevant collection."
|
|
72
|
+
),
|
|
73
|
+
SE_STORAGE_INVALID_BUCKET_NAME: makeError(
|
|
74
|
+
"already-exists",
|
|
75
|
+
"Unable to create the AWS S3 bucket for the ceremony since the provided name is already in use. Please, provide a different bucket name for the ceremony.",
|
|
76
|
+
"More info about the error could be found at the following link https://docs.aws.amazon.com/simspaceweaver/latest/userguide/troubleshooting_bucket-name-too-long.html"
|
|
77
|
+
),
|
|
78
|
+
SE_STORAGE_TOO_MANY_BUCKETS: makeError(
|
|
79
|
+
"resource-exhausted",
|
|
80
|
+
"Unable to create the AWS S3 bucket for the ceremony since the are too many buckets already in use. Please, delete 2 or more existing Amazon S3 buckets that you don't need or increase your limits.",
|
|
81
|
+
"More info about the error could be found at the following link https://docs.aws.amazon.com/simspaceweaver/latest/userguide/troubeshooting_too-many-buckets.html"
|
|
82
|
+
),
|
|
83
|
+
SE_STORAGE_MISSING_PERMISSIONS: makeError(
|
|
84
|
+
"permission-denied",
|
|
85
|
+
"You do not have privileges to perform this operation.",
|
|
86
|
+
"Authenticated user does not have proper permissions on AWS S3."
|
|
87
|
+
),
|
|
88
|
+
SE_STORAGE_BUCKET_NOT_CONNECTED_TO_CEREMONY: makeError(
|
|
89
|
+
"not-found",
|
|
90
|
+
"Unable to generate a pre-signed url for the given object in the provided bucket.",
|
|
91
|
+
"The bucket is not associated with any valid ceremony document on the Firestore database."
|
|
92
|
+
),
|
|
93
|
+
SE_STORAGE_WRONG_OBJECT_KEY: makeError(
|
|
94
|
+
"failed-precondition",
|
|
95
|
+
"Unable to interact with a multi-part upload (start, create pre-signed urls or complete).",
|
|
96
|
+
"The object key provided does not match the expected one."
|
|
97
|
+
),
|
|
98
|
+
SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD: makeError(
|
|
99
|
+
"failed-precondition",
|
|
100
|
+
"Unable to interact with a multi-part upload (start, create pre-signed urls or complete).",
|
|
101
|
+
"Authenticated user is not a current contributor which is currently in the uploading step."
|
|
102
|
+
),
|
|
103
|
+
SE_STORAGE_DOWNLOAD_FAILED: makeError(
|
|
104
|
+
"failed-precondition",
|
|
105
|
+
"Unable to download the AWS S3 object from the provided ceremony bucket.",
|
|
106
|
+
"This could happen if the file reference stored in the database or bucket turns out to be wrong or if the pre-signed url was not generated correctly."
|
|
107
|
+
),
|
|
108
|
+
SE_STORAGE_UPLOAD_FAILED: makeError(
|
|
109
|
+
"failed-precondition",
|
|
110
|
+
"Unable to upload the file to the AWS S3 ceremony bucket.",
|
|
111
|
+
"This could happen if the local file or bucket do not exist or if the pre-signed url was not generated correctly."
|
|
112
|
+
),
|
|
113
|
+
SE_STORAGE_DELETE_FAILED: makeError(
|
|
114
|
+
"failed-precondition",
|
|
115
|
+
"Unable to delete the AWS S3 object from the provided ceremony bucket.",
|
|
116
|
+
"This could happen if the local file or the bucket do not exist."
|
|
117
|
+
),
|
|
118
|
+
SE_CONTRIBUTE_NO_CEREMONY_CIRCUITS: makeError(
|
|
119
|
+
"not-found",
|
|
120
|
+
"There is no circuit associated with the ceremony.",
|
|
121
|
+
"No documents in the circuits subcollection were found for the selected ceremony."
|
|
122
|
+
),
|
|
123
|
+
SE_CONTRIBUTE_NO_OPENED_CEREMONIES: makeError("not-found", "There are no ceremonies open to contributions."),
|
|
124
|
+
SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT: makeError(
|
|
125
|
+
"failed-precondition",
|
|
126
|
+
"Unable to progress to next circuit for contribution",
|
|
127
|
+
"In order to progress for the contribution the participant must have just been registered for the ceremony or have just finished a contribution."
|
|
128
|
+
),
|
|
129
|
+
SE_PARTICIPANT_CEREMONY_NOT_OPENED: makeError(
|
|
130
|
+
"failed-precondition",
|
|
131
|
+
"Unable to progress to next contribution step.",
|
|
132
|
+
"The ceremony does not appear to be opened"
|
|
133
|
+
),
|
|
134
|
+
SE_PARTICIPANT_NOT_CONTRIBUTING: makeError(
|
|
135
|
+
"failed-precondition",
|
|
136
|
+
"Unable to progress to next contribution step.",
|
|
137
|
+
"This may happen due wrong contribution step from participant."
|
|
138
|
+
),
|
|
139
|
+
SE_PARTICIPANT_CANNOT_STORE_PERMANENT_DATA: makeError(
|
|
140
|
+
"failed-precondition",
|
|
141
|
+
"Unable to store contribution hash and computing time.",
|
|
142
|
+
"This may happen due wrong contribution step from participant or missing coordinator permission (only when finalizing)."
|
|
143
|
+
),
|
|
144
|
+
SE_PARTICIPANT_CANNOT_STORE_TEMPORARY_DATA: makeError(
|
|
145
|
+
"failed-precondition",
|
|
146
|
+
"Unable to store temporary data to resume a multi-part upload.",
|
|
147
|
+
"This may happen due wrong contribution step from participant."
|
|
148
|
+
),
|
|
149
|
+
SE_VERIFICATION_NO_PARTICIPANT_CONTRIBUTION_DATA: makeError(
|
|
150
|
+
"not-found",
|
|
151
|
+
`Unable to retrieve current contribution data from participant document.`
|
|
152
|
+
),
|
|
153
|
+
SE_CEREMONY_CANNOT_FINALIZE_CEREMONY: makeError(
|
|
154
|
+
"failed-precondition",
|
|
155
|
+
`Unable to finalize the ceremony.`,
|
|
156
|
+
`Please, verify to have successfully completed the finalization of each circuit in the ceremony.`
|
|
157
|
+
),
|
|
158
|
+
SE_FINALIZE_NO_CEREMONY_CONTRIBUTIONS: makeError(
|
|
159
|
+
"not-found",
|
|
160
|
+
"There are no contributions associated with the ceremony circuit.",
|
|
161
|
+
"No documents in the contributions subcollection were found for the selected ceremony circuit."
|
|
162
|
+
),
|
|
163
|
+
SE_FINALIZE_NO_FINAL_CONTRIBUTION: makeError(
|
|
164
|
+
"not-found",
|
|
165
|
+
"There is no final contribution associated with the ceremony circuit."
|
|
166
|
+
),
|
|
167
|
+
SE_VM_NOT_RUNNING: makeError("failed-precondition", "The EC2 VM is not running yet"),
|
|
168
|
+
SE_VM_FAILED_COMMAND_EXECUTION: makeError(
|
|
169
|
+
"failed-precondition",
|
|
170
|
+
"VM command execution failed",
|
|
171
|
+
"Please, contact the coordinator if this error persists."
|
|
172
|
+
),
|
|
173
|
+
SE_VM_TIMEDOUT_COMMAND_EXECUTION: makeError(
|
|
174
|
+
"deadline-exceeded",
|
|
175
|
+
"VM command execution took too long and has been timed-out",
|
|
176
|
+
"Please, contact the coordinator if this error persists."
|
|
177
|
+
),
|
|
178
|
+
SE_VM_CANCELLED_COMMAND_EXECUTION: makeError(
|
|
179
|
+
"cancelled",
|
|
180
|
+
"VM command execution has been cancelled",
|
|
181
|
+
"Please, contact the coordinator if this error persists."
|
|
182
|
+
),
|
|
183
|
+
SE_VM_DELAYED_COMMAND_EXECUTION: makeError(
|
|
184
|
+
"unavailable",
|
|
185
|
+
"VM command execution has been delayed since there were no available instance at the moment",
|
|
186
|
+
"Please, contact the coordinator if this error persists."
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A set of common errors.
|
|
192
|
+
* @notice these are errors that happen on multiple cloud functions (e.g., auth, missing data).
|
|
193
|
+
*/
|
|
194
|
+
export const COMMON_ERRORS = {
|
|
195
|
+
CM_NOT_COORDINATOR_ROLE: makeError(
|
|
196
|
+
"permission-denied",
|
|
197
|
+
"You do not have privileges to perform this operation.",
|
|
198
|
+
"Authenticated user does not have the coordinator role (missing custom claims)."
|
|
199
|
+
),
|
|
200
|
+
CM_MISSING_OR_WRONG_INPUT_DATA: makeError(
|
|
201
|
+
"invalid-argument",
|
|
202
|
+
"Unable to perform the operation due to incomplete or incorrect data."
|
|
203
|
+
),
|
|
204
|
+
CM_WRONG_CONFIGURATION: makeError(
|
|
205
|
+
"failed-precondition",
|
|
206
|
+
"Missing or incorrect configuration.",
|
|
207
|
+
"This may happen due wrong environment configuration for the backend services."
|
|
208
|
+
),
|
|
209
|
+
CM_NOT_AUTHENTICATED: makeError(
|
|
210
|
+
"failed-precondition",
|
|
211
|
+
"You are not authorized to perform this operation.",
|
|
212
|
+
"You could not perform the requested operation because you are not authenticated on the Firebase Application."
|
|
213
|
+
),
|
|
214
|
+
CM_INEXISTENT_DOCUMENT: makeError(
|
|
215
|
+
"not-found",
|
|
216
|
+
"Unable to find a document with the given identifier for the provided collection path."
|
|
217
|
+
),
|
|
218
|
+
CM_INEXISTENT_DOCUMENT_DATA: makeError(
|
|
219
|
+
"not-found",
|
|
220
|
+
"The provided document with the given identifier has no data associated with it.",
|
|
221
|
+
"This problem may occur if the document has not yet been written in the database."
|
|
222
|
+
),
|
|
223
|
+
CM_INVALID_CEREMONY_FOR_PARTICIPANT: makeError(
|
|
224
|
+
"not-found",
|
|
225
|
+
"The participant does not seem to be related to a ceremony."
|
|
226
|
+
),
|
|
227
|
+
CM_NO_CIRCUIT_FOR_GIVEN_SEQUENCE_POSITION: makeError(
|
|
228
|
+
"not-found",
|
|
229
|
+
"Unable to find the circuit having the provided sequence position for the given ceremony"
|
|
230
|
+
),
|
|
231
|
+
CM_INVALID_REQUEST: makeError("unknown", "Failed request."),
|
|
232
|
+
CM_INVALID_COMMAND_EXECUTION: makeError(
|
|
233
|
+
"unknown",
|
|
234
|
+
"There was an error while executing the command on the VM",
|
|
235
|
+
"Please, contact the coordinator if the error persists."
|
|
236
|
+
)
|
|
237
|
+
}
|