From c6d0070dd3a215d9d75a38cdbeda138607d23c03 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 18 Apr 2023 15:50:48 +0200 Subject: [PATCH] moved firebase admin into its own module not initialising if ther eis no service account this allows the backend server to be ran without firebase (all though it will throw errors) closes #4190 --- backend/src/api/controllers/user.ts | 35 ++++++++++--------- backend/src/init/firebase-admin.ts | 52 +++++++++++++++++++++++++++++ backend/src/server.ts | 11 ++---- backend/src/utils/auth.ts | 8 ++--- 4 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 backend/src/init/firebase-admin.ts diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 259df4221..b0c3d7eb8 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -20,6 +20,7 @@ import { v4 as uuidv4 } from "uuid"; import { ObjectId } from "mongodb"; import * as ReportDAL from "../../dal/report"; import emailQueue from "../../queues/email-queue"; +import FirebaseAdmin from "../../init/firebase-admin"; async function verifyCaptcha(captcha: string): Promise { if (!(await verify(captcha))) { @@ -37,7 +38,7 @@ export async function createNewUser( await verifyCaptcha(captcha); } catch (e) { try { - await admin.auth().deleteUser(uid); + await FirebaseAdmin().auth().deleteUser(uid); } catch (e) { // user might be deleted on the frontend } @@ -84,12 +85,14 @@ export async function sendVerificationEmail( let link = ""; try { - link = await admin.auth().generateEmailVerificationLink(email, { - url: - process.env.MODE === "dev" - ? "http://localhost:3000" - : "https://monkeytype.com", - }); + link = await FirebaseAdmin() + .auth() + .generateEmailVerificationLink(email, { + url: + process.env.MODE === "dev" + ? "http://localhost:3000" + : "https://monkeytype.com", + }); } catch (e) { if ( e.code === "auth/internal-error" && @@ -121,7 +124,7 @@ export async function sendForgotPasswordEmail( let auth; try { - auth = await admin.auth().getUserByEmail(email); + auth = await FirebaseAdmin().auth().getUserByEmail(email); } catch (e) { if (e.code === "auth/user-not-found") { throw new MonkeyError(404, "User not found"); @@ -134,12 +137,14 @@ export async function sendForgotPasswordEmail( "request forgot password email" ); - const link = await admin.auth().generatePasswordResetLink(email, { - url: - process.env.MODE === "dev" - ? "http://localhost:3000" - : "https://monkeytype.com", - }); + const link = await FirebaseAdmin() + .auth() + .generatePasswordResetLink(email, { + url: + process.env.MODE === "dev" + ? "http://localhost:3000" + : "https://monkeytype.com", + }); await emailQueue.sendForgotPasswordEmail(email, userInfo.name, link); return new MonkeyResponse("Email sent if user was found"); @@ -292,7 +297,7 @@ export async function getUser( if (e.status === 404) { let user; try { - user = await admin.auth().getUser(uid); + user = await FirebaseAdmin().auth().getUser(uid); //exists, recreate in db await UserDAL.addUser(user.displayName, user.email, uid); userInfo = await UserDAL.getUser(uid, "get user (recreated)"); diff --git a/backend/src/init/firebase-admin.ts b/backend/src/init/firebase-admin.ts new file mode 100644 index 000000000..af79b31f8 --- /dev/null +++ b/backend/src/init/firebase-admin.ts @@ -0,0 +1,52 @@ +import admin, { ServiceAccount } from "firebase-admin"; +import Logger from "../utils/logger"; +import { readFileSync, existsSync } from "fs"; +import MonkeyError from "../utils/error"; +import path from "path"; + +const SERVICE_ACCOUNT_PATH = path.join( + __dirname, + "../credentials/serviceAccountKey.json" +); + +export function init(): void { + if (!existsSync(SERVICE_ACCOUNT_PATH)) { + if (process.env.MODE === "dev") { + Logger.warning( + "Firebase service account key not found! Continuing in dev mode, but authentication will throw errors." + ); + } else { + throw new MonkeyError( + 500, + "Firebase service account key not found! Make sure generate a service account key and place it in credentials/serviceAccountKey.json.", + "init() firebase-admin.ts" + ); + } + } else { + const serviceAccount = JSON.parse( + readFileSync(SERVICE_ACCOUNT_PATH, { + encoding: "utf8", + flag: "r", + }) + ); + admin.initializeApp({ + credential: admin.credential.cert( + serviceAccount as unknown as ServiceAccount + ), + }); + Logger.success("Firebase app initialized"); + } +} + +function get(): typeof admin { + if (admin.apps.length === 0) { + throw new MonkeyError( + 500, + "Firebase app not initialized! Make sure generate a service account key and place it in credentials/serviceAccountKey.json.", + "get() firebase-admin.ts" + ); + } + return admin; +} + +export default get; diff --git a/backend/src/server.ts b/backend/src/server.ts index 3e5e380a8..df202fe7e 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,7 +1,4 @@ import "dotenv/config"; -import admin, { ServiceAccount } from "firebase-admin"; -// @ts-ignore -import serviceAccount from "./credentials/serviceAccountKey.json"; // eslint-disable-line require-path-exists/exists import * as db from "./init/db"; import jobs from "./jobs"; import { getLiveConfiguration } from "./init/configuration"; @@ -15,6 +12,7 @@ import queues from "./queues"; import workers from "./workers"; import Logger from "./utils/logger"; import * as EmailClient from "./init/email-client"; +import { init as initFirebaseAdmin } from "./init/firebase-admin"; async function bootServer(port: number): Promise { try { @@ -25,12 +23,7 @@ async function bootServer(port: number): Promise { Logger.success("Connected to database"); Logger.info("Initializing Firebase app instance..."); - admin.initializeApp({ - credential: admin.credential.cert( - serviceAccount as unknown as ServiceAccount - ), - }); - Logger.success("Firebase app initialized"); + initFirebaseAdmin(); Logger.info("Fetching live configuration..."); const liveConfiguration = await getLiveConfiguration(); diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index a6222f706..f48bbbed0 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,4 +1,4 @@ -import admin from "firebase-admin"; +import FirebaseAdmin from "./../init/firebase-admin"; import { UserRecord } from "firebase-admin/lib/auth/user-record"; import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier"; import LRUCache from "lru-cache"; @@ -22,7 +22,7 @@ export async function verifyIdToken( noCache = false ): Promise { if (noCache) { - return await admin.auth().verifyIdToken(idToken, true); + return await FirebaseAdmin().auth().verifyIdToken(idToken, true); } setTokenCacheLength(tokenCache.size); @@ -44,7 +44,7 @@ export async function verifyIdToken( recordTokenCacheAccess("miss"); } - const decoded = await admin.auth().verifyIdToken(idToken, true); + const decoded = await FirebaseAdmin().auth().verifyIdToken(idToken, true); tokenCache.set(idToken, decoded); return decoded; } @@ -53,7 +53,7 @@ export async function updateUserEmail( uid: string, email: string ): Promise { - return await admin.auth().updateUser(uid, { + return await FirebaseAdmin().auth().updateUser(uid, { email, emailVerified: false, });