@devtion/backend
Version:
MPC Phase 2 backend for Firebase services management
168 lines (163 loc) • 7.81 kB
text/typescript
import * as functions from "firebase-functions"
import { UserRecord } from "firebase-functions/v1/auth"
import admin from "firebase-admin"
import dotenv from "dotenv"
import { commonTerms, githubReputation } from "@devtion/actions"
import { encode } from "html-entities"
import { getGitHubVariables, getCurrentServerTimestampInMillis } from "../lib/utils"
import { logAndThrowError, makeError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
import { LogLevel } from "../types/enums"
dotenv.config()
/**
* Record the authenticated user information inside the Firestore DB upon authentication.
* @dev the data is recorded in a new document in the `users` collection.
* @notice this method is automatically triggered upon user authentication in the Firebase app
* which uses the Firebase Authentication service.
*/
export const registerAuthUser = functions
.region("europe-west1")
.runWith({
memory: "1GB"
})
.auth.user()
.onCreate(async (user: UserRecord) => {
// Get DB.
const firestore = admin.firestore()
// Get user information.
if (!user.uid) logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
// The user object has basic properties such as display name, email, etc.
const { displayName } = user
const { email } = user
const { photoURL } = user
const { emailVerified } = user
// Metadata.
const { creationTime } = user.metadata
const { lastSignInTime } = user.metadata
// The user's ID, unique to the Firebase project. Do NOT use
// this value to authenticate with your backend server, if
// you have one. Use User.getToken() instead.
const { uid } = user
// Reference to a document using uid.
const userRef = firestore.collection(commonTerms.collections.users.name).doc(uid)
// html encode the display name (or put the ID if the name is not displayed)
const encodedDisplayName =
user.displayName === "Null" || user.displayName === null ? user.uid : encode(displayName)
// store the avatar URL of a contributor
let avatarUrl: string = ""
// we only do reputation check if the user is not a coordinator
if (
!(
email?.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN
)
) {
const auth = admin.auth()
// if provider == github.com let's use our functions to check the user's reputation
if (user.providerData.length > 0 && user.providerData[0].providerId === "github.com") {
const vars = getGitHubVariables()
// this return true or false
try {
const { reputable, avatarUrl: avatarURL } = await githubReputation(
user.providerData[0].uid,
vars.minimumFollowing,
vars.minimumFollowers,
vars.minimumPublicRepos,
vars.minimumAge
)
if (!reputable) {
// Delete user
await auth.deleteUser(user.uid)
// Throw error
logAndThrowError(
makeError(
"permission-denied",
"The user is not allowed to sign up because their Github reputation is not high enough.",
`The user ${
user.displayName === "Null" || user.displayName === null
? user.uid
: 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.`
)
)
}
// store locally
avatarUrl = avatarURL
printLog(
`Github reputation check passed for user ${
user.displayName === "Null" || user.displayName === null ? user.uid : user.displayName
}`,
LogLevel.DEBUG
)
} catch (error: any) {
// Delete user
await auth.deleteUser(user.uid)
logAndThrowError(
makeError(
"permission-denied",
"There was an error while checking the user's Github reputation.",
`${error}`
)
)
}
}
}
// Set document (nb. we refer to providerData[0] because we use Github OAuth provider only).
// In future releases we might want to loop through the providerData array as we support
// more providers.
await userRef.set({
name: encodedDisplayName,
encodedDisplayName,
// Metadata.
creationTime,
lastSignInTime: lastSignInTime || creationTime,
// Optional.
email: email || "",
emailVerified: emailVerified || false,
photoURL: photoURL || "",
lastUpdated: getCurrentServerTimestampInMillis()
})
// we want to create a new collection for the users to store the avatars
const avatarRef = firestore.collection(commonTerms.collections.avatars.name).doc(uid)
await avatarRef.set({
avatarUrl: avatarUrl || ""
})
printLog(`Authenticated user document with identifier ${uid} has been correctly stored`, LogLevel.DEBUG)
printLog(`Authenticated user avatar with identifier ${uid} has been correctly stored`, LogLevel.DEBUG)
})
/**
* Set custom claims for role-based access control on the newly created user.
* @notice this method is automatically triggered upon user authentication in the Firebase app
* which uses the Firebase Authentication service.
*/
export const processSignUpWithCustomClaims = functions
.region("europe-west1")
.runWith({
memory: "1GB"
})
.auth.user()
.onCreate(async (user: UserRecord) => {
// Get user information.
if (!user.uid) logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
// Prepare state.
let customClaims: any
// Check if user meets role criteria to be a coordinator.
if (
user.email &&
(user.email.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
user.email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN)
) {
customClaims = { coordinator: true }
printLog(`Authenticated user ${user.uid} has been identified as coordinator`, LogLevel.DEBUG)
} else {
customClaims = { participant: true }
printLog(`Authenticated user ${user.uid} has been identified as participant`, LogLevel.DEBUG)
}
try {
// Set custom user claims on this newly created user.
await admin.auth().setCustomUserClaims(user.uid, customClaims)
} catch (error: any) {
const specificError = SPECIFIC_ERRORS.SE_AUTH_SET_CUSTOM_USER_CLAIMS_FAIL
const additionalDetails = error.toString()
logAndThrowError(makeError(specificError.code, specificError.message, additionalDetails))
}
})