@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,548 @@
|
|
|
1
|
+
import * as functions from "firebase-functions"
|
|
2
|
+
import admin from "firebase-admin"
|
|
3
|
+
import {
|
|
4
|
+
GetObjectCommand,
|
|
5
|
+
CreateMultipartUploadCommand,
|
|
6
|
+
UploadPartCommand,
|
|
7
|
+
CompleteMultipartUploadCommand,
|
|
8
|
+
HeadObjectCommand,
|
|
9
|
+
CreateBucketCommand,
|
|
10
|
+
PutPublicAccessBlockCommand,
|
|
11
|
+
PutBucketCorsCommand,
|
|
12
|
+
HeadBucketCommand
|
|
13
|
+
} from "@aws-sdk/client-s3"
|
|
14
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
|
15
|
+
import dotenv from "dotenv"
|
|
16
|
+
import {
|
|
17
|
+
commonTerms,
|
|
18
|
+
getParticipantsCollectionPath,
|
|
19
|
+
ParticipantStatus,
|
|
20
|
+
ParticipantContributionStep,
|
|
21
|
+
formatZkeyIndex,
|
|
22
|
+
getZkeyStorageFilePath
|
|
23
|
+
} from "@p0tion/actions"
|
|
24
|
+
import { getCeremonyCircuits, getDocumentById } from "../lib/utils"
|
|
25
|
+
import { COMMON_ERRORS, logAndThrowError, makeError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
|
|
26
|
+
import { LogLevel } from "../types/enums"
|
|
27
|
+
import { getS3Client } from "../lib/services"
|
|
28
|
+
import {
|
|
29
|
+
BucketAndObjectKeyData,
|
|
30
|
+
CompleteMultiPartUploadData,
|
|
31
|
+
CreateBucketData,
|
|
32
|
+
GeneratePreSignedUrlsPartsData,
|
|
33
|
+
StartMultiPartUploadData
|
|
34
|
+
} from "../types/index"
|
|
35
|
+
|
|
36
|
+
dotenv.config()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the pre-condition for interacting w/ a multi-part upload for an identified current contributor is valid.
|
|
40
|
+
* @notice the precondition is be a current contributor (contributing status) in the uploading contribution step.
|
|
41
|
+
* @param contributorId <string> - the unique identifier of the contributor.
|
|
42
|
+
* @param ceremonyId <string> - the unique identifier of the ceremony.
|
|
43
|
+
*/
|
|
44
|
+
const checkPreConditionForCurrentContributorToInteractWithMultiPartUpload = async (
|
|
45
|
+
contributorId: string,
|
|
46
|
+
ceremonyId: string
|
|
47
|
+
) => {
|
|
48
|
+
// Get ceremony and participant documents.
|
|
49
|
+
const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
|
|
50
|
+
const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), contributorId!)
|
|
51
|
+
|
|
52
|
+
// Get data from docs.
|
|
53
|
+
const ceremonyData = ceremonyDoc.data()
|
|
54
|
+
const participantData = participantDoc.data()
|
|
55
|
+
|
|
56
|
+
if (!ceremonyData || !participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
57
|
+
|
|
58
|
+
// Check pre-condition to start multi-part upload for a current contributor.
|
|
59
|
+
const { status, contributionStep } = participantData!
|
|
60
|
+
|
|
61
|
+
if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING)
|
|
62
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Helper function to check whether a contributor is uploading a file related to its contribution.
|
|
67
|
+
* @param contributorId <string> - the unique identifier of the contributor.
|
|
68
|
+
* @param ceremonyId <string> - the unique identifier of the ceremony.
|
|
69
|
+
* @param objectKey <string> - the object key of the file being uploaded.
|
|
70
|
+
*/
|
|
71
|
+
const checkUploadingFileValidity = async (contributorId: string, ceremonyId: string, objectKey: string) => {
|
|
72
|
+
// Get the circuits for the ceremony
|
|
73
|
+
const circuits = await getCeremonyCircuits(ceremonyId)
|
|
74
|
+
|
|
75
|
+
// Get the participant document
|
|
76
|
+
const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), contributorId!)
|
|
77
|
+
const participantData = participantDoc.data()
|
|
78
|
+
|
|
79
|
+
if (!participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
80
|
+
|
|
81
|
+
// The index of the circuit will be the contribution progress - 1
|
|
82
|
+
const index = participantData?.contributionProgress
|
|
83
|
+
// If the index is zero the user is not the current contributor
|
|
84
|
+
if (index === 0) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD)
|
|
85
|
+
// We can safely use index - 1
|
|
86
|
+
const circuit = circuits.at(index - 1)
|
|
87
|
+
|
|
88
|
+
// If the circuit is undefined, throw an error
|
|
89
|
+
if (!circuit) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD)
|
|
90
|
+
// Extract the data we need
|
|
91
|
+
const { prefix, waitingQueue } = circuit!.data()
|
|
92
|
+
const { completedContributions, currentContributor } = waitingQueue
|
|
93
|
+
|
|
94
|
+
// If we are not a contributor to this circuit then we cannot upload files
|
|
95
|
+
if (currentContributor === contributorId) {
|
|
96
|
+
// Get the index of the zKey
|
|
97
|
+
const contributorZKeyIndex = formatZkeyIndex(completedContributions + 1)
|
|
98
|
+
// The uploaded file must be the expected one
|
|
99
|
+
const zkeyNameContributor = `${prefix}_${contributorZKeyIndex}.zkey`
|
|
100
|
+
const contributorZKeyStoragePath = getZkeyStorageFilePath(prefix, zkeyNameContributor)
|
|
101
|
+
|
|
102
|
+
// If the object key does not have the expected storage path, throw an error
|
|
103
|
+
if (objectKey !== contributorZKeyStoragePath) {
|
|
104
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_WRONG_OBJECT_KEY)
|
|
105
|
+
}
|
|
106
|
+
} else logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Helper function that confirms whether a bucket is used for a ceremony.
|
|
111
|
+
* @dev this helps to prevent unauthorized access to coordinator's buckets.
|
|
112
|
+
* @param bucketName
|
|
113
|
+
*/
|
|
114
|
+
const checkIfBucketIsDedicatedToCeremony = async (bucketName: string) => {
|
|
115
|
+
// Get Firestore DB.
|
|
116
|
+
const firestoreDatabase = admin.firestore()
|
|
117
|
+
|
|
118
|
+
// Extract ceremony prefix from bucket name.
|
|
119
|
+
const ceremonyPrefix = bucketName.replace(String(process.env.AWS_CEREMONY_BUCKET_POSTFIX), "")
|
|
120
|
+
|
|
121
|
+
// Query the collection.
|
|
122
|
+
const ceremonyCollection = await firestoreDatabase
|
|
123
|
+
.collection(commonTerms.collections.ceremonies.name)
|
|
124
|
+
.where(commonTerms.collections.ceremonies.fields.prefix, "==", ceremonyPrefix)
|
|
125
|
+
.get()
|
|
126
|
+
|
|
127
|
+
if (ceremonyCollection.empty) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_BUCKET_NOT_CONNECTED_TO_CEREMONY)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a new AWS S3 bucket for a particular ceremony.
|
|
132
|
+
* @notice the S3 bucket is used to store all the ceremony artifacts and contributions.
|
|
133
|
+
*/
|
|
134
|
+
export const createBucket = functions
|
|
135
|
+
.region("europe-west1")
|
|
136
|
+
.runWith({
|
|
137
|
+
memory: "512MB"
|
|
138
|
+
})
|
|
139
|
+
.https.onCall(async (data: CreateBucketData, context: functions.https.CallableContext) => {
|
|
140
|
+
// Check if the user has the coordinator claim.
|
|
141
|
+
if (!context.auth || !context.auth.token.coordinator) logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE)
|
|
142
|
+
|
|
143
|
+
if (!data.bucketName) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
144
|
+
|
|
145
|
+
// Connect to S3 client.
|
|
146
|
+
const S3 = await getS3Client()
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Try to get information about the bucket.
|
|
150
|
+
await S3.send(new HeadBucketCommand({ Bucket: data.bucketName }))
|
|
151
|
+
// If the command succeeded, the bucket exists, throw an error.
|
|
152
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_INVALID_BUCKET_NAME)
|
|
153
|
+
} catch (error: any) {
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
155
|
+
if (error.name === "NotFound") {
|
|
156
|
+
// Prepare S3 command.
|
|
157
|
+
const command = new CreateBucketCommand({
|
|
158
|
+
Bucket: data.bucketName,
|
|
159
|
+
// CreateBucketConfiguration: {
|
|
160
|
+
// LocationConstraint: String(process.env.AWS_REGION)
|
|
161
|
+
// },
|
|
162
|
+
ObjectOwnership: "BucketOwnerPreferred"
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// Execute S3 command.
|
|
167
|
+
const response = await S3.send(command)
|
|
168
|
+
|
|
169
|
+
// Check response.
|
|
170
|
+
if (response.$metadata.httpStatusCode === 200 && !!response.Location)
|
|
171
|
+
printLog(`The AWS S3 bucket ${data.bucketName} has been created successfully`, LogLevel.LOG)
|
|
172
|
+
|
|
173
|
+
const publicBlockCommand = new PutPublicAccessBlockCommand({
|
|
174
|
+
Bucket: data.bucketName,
|
|
175
|
+
PublicAccessBlockConfiguration: {
|
|
176
|
+
BlockPublicAcls: false,
|
|
177
|
+
BlockPublicPolicy: false
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Allow objects to be public
|
|
182
|
+
const publicBlockResponse = await S3.send(publicBlockCommand)
|
|
183
|
+
// Check response.
|
|
184
|
+
if (publicBlockResponse.$metadata.httpStatusCode === 204)
|
|
185
|
+
printLog(
|
|
186
|
+
`The AWS S3 bucket ${data.bucketName} has been set with the PublicAccessBlock disabled.`,
|
|
187
|
+
LogLevel.LOG
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Set CORS
|
|
191
|
+
const corsCommand = new PutBucketCorsCommand({
|
|
192
|
+
Bucket: data.bucketName,
|
|
193
|
+
CORSConfiguration: {
|
|
194
|
+
CORSRules: [
|
|
195
|
+
{
|
|
196
|
+
AllowedMethods: ["GET"],
|
|
197
|
+
AllowedOrigins: ["*"]
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
const corsResponse = await S3.send(corsCommand)
|
|
203
|
+
// Check response.
|
|
204
|
+
if (corsResponse.$metadata.httpStatusCode === 200)
|
|
205
|
+
printLog(
|
|
206
|
+
`The AWS S3 bucket ${data.bucketName} has been set with the CORS configuration.`,
|
|
207
|
+
LogLevel.LOG
|
|
208
|
+
)
|
|
209
|
+
} catch (error: any) {
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
211
|
+
/** * {@link https://docs.aws.amazon.com/simspaceweaver/latest/userguide/troubeshooting_too-many-buckets.html | TooManyBuckets} */
|
|
212
|
+
if (error.$metadata.httpStatusCode === 400 && error.Code === `TooManyBuckets`)
|
|
213
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_TOO_MANY_BUCKETS)
|
|
214
|
+
|
|
215
|
+
// @todo handle more errors here.
|
|
216
|
+
|
|
217
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
218
|
+
const additionalDetails = error.toString()
|
|
219
|
+
|
|
220
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// If there was a different error, re-throw it.
|
|
224
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
225
|
+
const additionalDetails = error.toString()
|
|
226
|
+
|
|
227
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if a specified object exist in a given AWS S3 bucket.
|
|
234
|
+
* @returns <Promise<boolean>> - true if the object exist in the given bucket; otherwise false.
|
|
235
|
+
*/
|
|
236
|
+
export const checkIfObjectExist = functions
|
|
237
|
+
.region("europe-west1")
|
|
238
|
+
.runWith({
|
|
239
|
+
memory: "512MB"
|
|
240
|
+
})
|
|
241
|
+
.https.onCall(async (data: BucketAndObjectKeyData, context: functions.https.CallableContext): Promise<boolean> => {
|
|
242
|
+
// Check if the user has the coordinator claim.
|
|
243
|
+
if (!context.auth || !context.auth.token.coordinator) logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE)
|
|
244
|
+
|
|
245
|
+
if (!data.bucketName || !data.objectKey) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
246
|
+
|
|
247
|
+
// Connect to S3 client.
|
|
248
|
+
const S3 = await getS3Client()
|
|
249
|
+
|
|
250
|
+
// Prepare S3 command.
|
|
251
|
+
const command = new HeadObjectCommand({ Bucket: data.bucketName, Key: data.objectKey })
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Execute S3 command.
|
|
255
|
+
const response = await S3.send(command)
|
|
256
|
+
|
|
257
|
+
// Check response.
|
|
258
|
+
if (response.$metadata.httpStatusCode === 200 && !!response.ETag) {
|
|
259
|
+
printLog(
|
|
260
|
+
`The object associated w/ ${data.objectKey} key has been found in the ${data.bucketName} bucket`,
|
|
261
|
+
LogLevel.LOG
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
268
|
+
if (error.$metadata.httpStatusCode === 403) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_MISSING_PERMISSIONS)
|
|
269
|
+
|
|
270
|
+
// @todo handle more specific errors here.
|
|
271
|
+
|
|
272
|
+
// nb. do not handle common errors! This method must return false if not found!
|
|
273
|
+
// const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
274
|
+
// const additionalDetails = error.toString()
|
|
275
|
+
|
|
276
|
+
// logAndThrowError(makeError(
|
|
277
|
+
// commonError.code,
|
|
278
|
+
// commonError.message,
|
|
279
|
+
// additionalDetails
|
|
280
|
+
// ))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return false
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Return a pre-signed url for a given object contained inside the provided AWS S3 bucket in order to perform a GET request.
|
|
288
|
+
* @notice the pre-signed url has a predefined expiration expressed in seconds inside the environment
|
|
289
|
+
* configuration of the `backend` package. The value should match the configuration of `phase2cli` package
|
|
290
|
+
* environment to avoid inconsistency between client request and CF.
|
|
291
|
+
*/
|
|
292
|
+
export const generateGetObjectPreSignedUrl = functions
|
|
293
|
+
.region("europe-west1")
|
|
294
|
+
.runWith({
|
|
295
|
+
memory: "512MB"
|
|
296
|
+
})
|
|
297
|
+
.https.onCall(async (data: BucketAndObjectKeyData, context: functions.https.CallableContext): Promise<any> => {
|
|
298
|
+
if (!context.auth) logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED)
|
|
299
|
+
|
|
300
|
+
if (!data.bucketName || !data.objectKey) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
301
|
+
|
|
302
|
+
// Prepare input data.
|
|
303
|
+
const { objectKey, bucketName } = data
|
|
304
|
+
|
|
305
|
+
// Check whether the bucket for which we are generating the pre-signed url is dedicated to a ceremony.
|
|
306
|
+
await checkIfBucketIsDedicatedToCeremony(bucketName)
|
|
307
|
+
|
|
308
|
+
// Connect to S3 client.
|
|
309
|
+
const S3 = await getS3Client()
|
|
310
|
+
|
|
311
|
+
// Prepare S3 command.
|
|
312
|
+
const command = new GetObjectCommand({ Bucket: bucketName, Key: objectKey })
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Execute S3 command.
|
|
316
|
+
const url = await getSignedUrl(S3, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION) })
|
|
317
|
+
|
|
318
|
+
if (url) {
|
|
319
|
+
printLog(`The generated pre-signed url is ${url}`, LogLevel.DEBUG)
|
|
320
|
+
|
|
321
|
+
return url
|
|
322
|
+
}
|
|
323
|
+
} catch (error: any) {
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
325
|
+
// @todo handle more errors here.
|
|
326
|
+
// if (error.$metadata.httpStatusCode !== 200) {
|
|
327
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
328
|
+
const additionalDetails = error.toString()
|
|
329
|
+
|
|
330
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
331
|
+
// }
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Start a new multi-part upload for a specific object in the given AWS S3 bucket.
|
|
337
|
+
* @notice this operation can be performed by either an authenticated participant or a coordinator.
|
|
338
|
+
*/
|
|
339
|
+
export const startMultiPartUpload = functions
|
|
340
|
+
.region("europe-west1")
|
|
341
|
+
.runWith({
|
|
342
|
+
memory: "512MB"
|
|
343
|
+
})
|
|
344
|
+
.https.onCall(async (data: StartMultiPartUploadData, context: functions.https.CallableContext): Promise<any> => {
|
|
345
|
+
if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
|
|
346
|
+
logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED)
|
|
347
|
+
|
|
348
|
+
if (!data.bucketName || !data.objectKey || (context.auth?.token.participant && !data.ceremonyId))
|
|
349
|
+
logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
350
|
+
|
|
351
|
+
// Prepare data.
|
|
352
|
+
const { bucketName, objectKey, ceremonyId } = data
|
|
353
|
+
const userId = context.auth?.uid
|
|
354
|
+
|
|
355
|
+
// Check if the user is a current contributor.
|
|
356
|
+
if (context.auth?.token.participant && !!ceremonyId) {
|
|
357
|
+
// Check pre-condition.
|
|
358
|
+
await checkPreConditionForCurrentContributorToInteractWithMultiPartUpload(userId!, ceremonyId)
|
|
359
|
+
|
|
360
|
+
// Check whether the bucket where the object for which we are generating the pre-signed url is dedicated to a ceremony.
|
|
361
|
+
await checkIfBucketIsDedicatedToCeremony(bucketName)
|
|
362
|
+
|
|
363
|
+
// Check the validity of the uploaded file.
|
|
364
|
+
await checkUploadingFileValidity(userId!, ceremonyId!, objectKey)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Connect to S3 client.
|
|
368
|
+
const S3 = await getS3Client()
|
|
369
|
+
|
|
370
|
+
// Prepare S3 command.
|
|
371
|
+
const command = new CreateMultipartUploadCommand({
|
|
372
|
+
Bucket: bucketName,
|
|
373
|
+
Key: objectKey,
|
|
374
|
+
ACL: context.auth?.token.participant ? "private" : "public-read"
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
// Execute S3 command.
|
|
379
|
+
const response = await S3.send(command)
|
|
380
|
+
if (response.$metadata.httpStatusCode === 200 && !!response.UploadId) {
|
|
381
|
+
printLog(
|
|
382
|
+
`The multi-part upload identifier is ${response.UploadId}. Requested by ${userId}`,
|
|
383
|
+
LogLevel.DEBUG
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return response.UploadId
|
|
387
|
+
}
|
|
388
|
+
} catch (error: any) {
|
|
389
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
390
|
+
// @todo handle more errors here.
|
|
391
|
+
if (error.$metadata.httpStatusCode !== 200) {
|
|
392
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
393
|
+
const additionalDetails = error.toString()
|
|
394
|
+
|
|
395
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Generate a new pre-signed url for each chunk related to a started multi-part upload.
|
|
402
|
+
* @notice this operation can be performed by either an authenticated participant or a coordinator.
|
|
403
|
+
* the pre-signed url has a predefined expiration expressed in seconds inside the environment
|
|
404
|
+
* configuration of the `backend` package. The value should match the configuration of `phase2cli` package
|
|
405
|
+
* environment to avoid inconsistency between client request and CF.
|
|
406
|
+
*/
|
|
407
|
+
export const generatePreSignedUrlsParts = functions
|
|
408
|
+
.region("europe-west1")
|
|
409
|
+
.runWith({
|
|
410
|
+
memory: "512MB"
|
|
411
|
+
})
|
|
412
|
+
.https.onCall(
|
|
413
|
+
async (
|
|
414
|
+
data: GeneratePreSignedUrlsPartsData,
|
|
415
|
+
context: functions.https.CallableContext
|
|
416
|
+
): Promise<Array<string>> => {
|
|
417
|
+
if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
|
|
418
|
+
logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED)
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
!data.bucketName ||
|
|
422
|
+
!data.objectKey ||
|
|
423
|
+
!data.uploadId ||
|
|
424
|
+
data.numberOfParts <= 0 ||
|
|
425
|
+
(context.auth?.token.participant && !data.ceremonyId)
|
|
426
|
+
)
|
|
427
|
+
logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
428
|
+
|
|
429
|
+
// Prepare data.
|
|
430
|
+
const { bucketName, objectKey, uploadId, numberOfParts, ceremonyId } = data
|
|
431
|
+
const userId = context.auth?.uid
|
|
432
|
+
|
|
433
|
+
// Check if the user is a current contributor.
|
|
434
|
+
if (context.auth?.token.participant && !!ceremonyId) {
|
|
435
|
+
// Check pre-condition.
|
|
436
|
+
await checkPreConditionForCurrentContributorToInteractWithMultiPartUpload(userId!, ceremonyId)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Connect to S3 client.
|
|
440
|
+
const S3 = await getS3Client()
|
|
441
|
+
|
|
442
|
+
// Prepare state.
|
|
443
|
+
const parts = []
|
|
444
|
+
|
|
445
|
+
for (let i = 0; i < numberOfParts; i += 1) {
|
|
446
|
+
// Prepare S3 command for each chunk.
|
|
447
|
+
const command = new UploadPartCommand({
|
|
448
|
+
Bucket: bucketName,
|
|
449
|
+
Key: objectKey,
|
|
450
|
+
PartNumber: i + 1,
|
|
451
|
+
UploadId: uploadId
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
// Get the pre-signed url for the specific chunk.
|
|
456
|
+
const url = await getSignedUrl(S3, command, {
|
|
457
|
+
expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
if (url) {
|
|
461
|
+
// Save.
|
|
462
|
+
parts.push(url)
|
|
463
|
+
}
|
|
464
|
+
} catch (error: any) {
|
|
465
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
466
|
+
// @todo handle more errors here.
|
|
467
|
+
// if (error.$metadata.httpStatusCode !== 200) {
|
|
468
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
469
|
+
const additionalDetails = error.toString()
|
|
470
|
+
|
|
471
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
472
|
+
// }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return parts
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Complete a multi-part upload for a specific object in the given AWS S3 bucket.
|
|
482
|
+
* @notice this operation can be performed by either an authenticated participant or a coordinator.
|
|
483
|
+
*/
|
|
484
|
+
export const completeMultiPartUpload = functions
|
|
485
|
+
.region("europe-west1")
|
|
486
|
+
.runWith({
|
|
487
|
+
memory: "512MB"
|
|
488
|
+
})
|
|
489
|
+
.https.onCall(async (data: CompleteMultiPartUploadData, context: functions.https.CallableContext): Promise<any> => {
|
|
490
|
+
if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
|
|
491
|
+
logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED)
|
|
492
|
+
|
|
493
|
+
if (
|
|
494
|
+
!data.bucketName ||
|
|
495
|
+
!data.objectKey ||
|
|
496
|
+
!data.uploadId ||
|
|
497
|
+
!data.parts ||
|
|
498
|
+
(context.auth?.token.participant && !data.ceremonyId)
|
|
499
|
+
)
|
|
500
|
+
logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
501
|
+
|
|
502
|
+
// Prepare data.
|
|
503
|
+
const { bucketName, objectKey, uploadId, parts, ceremonyId } = data
|
|
504
|
+
const userId = context.auth?.uid
|
|
505
|
+
|
|
506
|
+
// Check if the user is a current contributor.
|
|
507
|
+
if (context.auth?.token.participant && !!ceremonyId) {
|
|
508
|
+
// Check pre-condition.
|
|
509
|
+
await checkPreConditionForCurrentContributorToInteractWithMultiPartUpload(userId!, ceremonyId)
|
|
510
|
+
|
|
511
|
+
// Check if the bucket is dedicated to a ceremony.
|
|
512
|
+
await checkIfBucketIsDedicatedToCeremony(bucketName)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Connect to S3.
|
|
516
|
+
const S3 = await getS3Client()
|
|
517
|
+
|
|
518
|
+
// Prepare S3 command.
|
|
519
|
+
const command = new CompleteMultipartUploadCommand({
|
|
520
|
+
Bucket: bucketName,
|
|
521
|
+
Key: objectKey,
|
|
522
|
+
UploadId: uploadId,
|
|
523
|
+
MultipartUpload: { Parts: parts }
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// Execute S3 command.
|
|
528
|
+
const response = await S3.send(command)
|
|
529
|
+
|
|
530
|
+
if (response.$metadata.httpStatusCode === 200 && !!response.Location) {
|
|
531
|
+
printLog(
|
|
532
|
+
`Multi-part upload ${data.uploadId} completed. Object location: ${response.Location}`,
|
|
533
|
+
LogLevel.DEBUG
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return response.Location
|
|
537
|
+
}
|
|
538
|
+
} catch (error: any) {
|
|
539
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
540
|
+
// @todo handle more errors here.
|
|
541
|
+
if (error.$metadata.httpStatusCode !== 200) {
|
|
542
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
543
|
+
const additionalDetails = error.toString()
|
|
544
|
+
|
|
545
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
})
|