diff --git a/package-lock.json b/package-lock.json index 6503e655..6fab7055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "express-handlebars": "^7.1.2", "firebase": "9.19.1", "firebase-admin": "^11.10.1", + "firebase-scrypt": "^2.2.0", "formidable": "^3.5.0", "formidable-serverless": "^1.1.1", "framer-motion": "^9.0.2", @@ -4056,6 +4057,20 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4619,6 +4634,13 @@ "node": ">= 0.6" } }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6224,6 +6246,17 @@ "@google-cloud/storage": "^6.9.5" } }, + "node_modules/firebase-scrypt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/firebase-scrypt/-/firebase-scrypt-2.2.0.tgz", + "integrity": "sha512-36vJZVPFepErsNw+nBjb9cpM9wYPtcxk1bKN//vLdVkNPhaw1cogzwxtMs0s+dYg1gvBDakg2Q4ch8zAWAvnxA==", + "dependencies": { + "babel-runtime": "^6.26.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/firebase/node_modules/@firebase/util": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", @@ -14697,6 +14730,22 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -15083,6 +15132,11 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -16352,6 +16406,14 @@ "uuid": "^9.0.0" } }, + "firebase-scrypt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/firebase-scrypt/-/firebase-scrypt-2.2.0.tgz", + "integrity": "sha512-36vJZVPFepErsNw+nBjb9cpM9wYPtcxk1bKN//vLdVkNPhaw1cogzwxtMs0s+dYg1gvBDakg2Q4ch8zAWAvnxA==", + "requires": { + "babel-runtime": "^6.26.0" + } + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", diff --git a/package.json b/package.json index c351d119..90f3d697 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "express-handlebars": "^7.1.2", "firebase": "9.19.1", "firebase-admin": "^11.10.1", + "firebase-scrypt": "^2.2.0", "formidable": "^3.5.0", "formidable-serverless": "^1.1.1", "framer-motion": "^9.0.2", diff --git a/src/firebase/index.ts b/src/firebase/index.ts index 95b1a515..c62c9d6e 100644 --- a/src/firebase/index.ts +++ b/src/firebase/index.ts @@ -1,6 +1,7 @@ import {initializeApp} from "firebase/app"; import * as admin from "firebase-admin/app"; import {getStorage} from "firebase/storage"; +import { base64 } from "@firebase/util"; const stagingServiceAccount = require("@/constants/staging.json"); const platformServiceAccount = require("@/constants/platform.json"); @@ -22,3 +23,10 @@ export const adminApp = admin.initializeApp( Math.random().toString(), ); export const storage = getStorage(app); + +export const firebaseAuthScryptParams = { + memCost: Number(process.env.FIREBASE_SCRYPT_MEM_COST), + rounds: Number(process.env.FIREBASE_SCRYPT_ROUNDS), + saltSeparator: process.env.FIREBASE_SCRYPT_B64_SALT_SEPARATOR!, + signerKey: process.env.FIREBASE_SCRYPT_B64_SIGNER_KEY!, +} diff --git a/src/pages/(admin)/BatchCreateUser.tsx b/src/pages/(admin)/BatchCreateUser.tsx index 03417eba..5f31c7c3 100644 --- a/src/pages/(admin)/BatchCreateUser.tsx +++ b/src/pages/(admin)/BatchCreateUser.tsx @@ -125,7 +125,7 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr demographicInformation: { country: countryItem?.countryCode, passport_id: passport_id?.toString().trim() || undefined, - phone, + phone: phone.toString(), }, } : undefined; @@ -161,7 +161,7 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr setIsLoading(true); try { - for (const newUser of newUsers) await axios.post("/api/make_user", {...newUser, type, expiryDate}); + await axios.post("/api/batch_users", { users: newUsers.map(user => ({...user, type, expiryDate})) }); toast.success(`Successfully added ${newUsers.length} user(s)!`); onFinish(); } catch { diff --git a/src/pages/api/batch_users.ts b/src/pages/api/batch_users.ts new file mode 100644 index 00000000..f54c0d49 --- /dev/null +++ b/src/pages/api/batch_users.ts @@ -0,0 +1,61 @@ +import type {NextApiRequest, NextApiResponse} from "next"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import { FirebaseScrypt } from 'firebase-scrypt'; +import { firebaseAuthScryptParams } from "@/firebase"; +import crypto from 'crypto'; +import axios from "axios"; + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "POST") return post(req, res); + + return res.status(404).json({ok: false}); +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + const maker = req.session.user; + if (!maker) { + return res.status(401).json({ok: false, reason: "You must be logged in to make user!"}); + } + + const scrypt = new FirebaseScrypt(firebaseAuthScryptParams) + + const users = req.body.users as { + email: string; + name: string; + type: string; + passport_id: string; + groupName?: string; + corporate?: string; + studentID?: string; + expiryDate?: string; + demographicInformation: { + country?: string; + passport_id?: string; + phone: string; + }; + passwordHash: string | undefined; + passwordSalt: string | undefined; + }[]; + + const usersWithPasswordHashes = await Promise.all(users.map(async (user) => { + const currentUser = { ...user }; + const salt = crypto.randomBytes(16).toString('base64'); + const hash = await scrypt.hash(user.passport_id, salt); + + currentUser.email = currentUser.email.toLowerCase(); + currentUser.passwordHash = hash; + currentUser.passwordSalt = salt; + return currentUser; + })); + + const backendRequest = await axios.post(`${process.env.BACKEND_URL}/batch_users`, { makerID: maker.id, users: usersWithPasswordHashes }, { + headers: { + Authorization: `Bearer ${process.env.BACKEND_JWT}`, + }, + }); + + return res.status(backendRequest.status).json(backendRequest.data) +} diff --git a/yarn.lock b/yarn.lock index 16085d68..ebda6715 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2362,6 +2362,14 @@ babel-plugin-syntax-jsx@^6.18.0: resolved "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz" integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz" + integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -2753,6 +2761,11 @@ cookie@^0.5.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +core-js@^2.4.0: + version "2.6.12" + resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" @@ -3685,6 +3698,13 @@ firebase-admin@^11.10.1: "@google-cloud/firestore" "^6.8.0" "@google-cloud/storage" "^6.9.5" +firebase-scrypt@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/firebase-scrypt/-/firebase-scrypt-2.2.0.tgz" + integrity sha512-36vJZVPFepErsNw+nBjb9cpM9wYPtcxk1bKN//vLdVkNPhaw1cogzwxtMs0s+dYg1gvBDakg2Q4ch8zAWAvnxA== + dependencies: + babel-runtime "^6.26.0" + firebase@9.19.1: version "9.19.1" resolved "https://registry.npmjs.org/firebase/-/firebase-9.19.1.tgz" @@ -6093,6 +6113,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + regenerator-runtime@^0.13.11: version "0.13.11" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"