From 36f518afca3a1714406cb4d41b01f5a6cd133593 Mon Sep 17 00:00:00 2001 From: mzerone Date: Thu, 1 Aug 2024 00:41:35 +0200 Subject: [PATCH] add create user in settings. --- src/pages/(admin)/BatchCreateUser.tsx | 236 ++++++++++++++++++++++++++ src/pages/api/make_user.ts | 110 ++++++++++++ src/pages/settings.tsx | 6 +- src/utils/permissions.ts | 2 +- 4 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 src/pages/(admin)/BatchCreateUser.tsx create mode 100644 src/pages/api/make_user.ts diff --git a/src/pages/(admin)/BatchCreateUser.tsx b/src/pages/(admin)/BatchCreateUser.tsx new file mode 100644 index 00000000..d470c2e8 --- /dev/null +++ b/src/pages/(admin)/BatchCreateUser.tsx @@ -0,0 +1,236 @@ +import Button from "@/components/Low/Button"; +import useUsers from "@/hooks/useUsers"; +import { Type as UserType, User } from "@/interfaces/user"; +import axios from "axios"; +import { uniqBy } from "lodash"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { useFilePicker } from "use-file-picker"; +import readXlsxFile from "read-excel-file"; +import Modal from "@/components/Modal"; +import { BsQuestionCircleFill } from "react-icons/bs"; +import { PermissionType } from "@/interfaces/permissions"; +const EMAIL_REGEX = new RegExp( + /^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/ +); + +type Type = Exclude + +const USER_TYPE_LABELS: {[key in Type]: string} = { + student: "Student", + teacher: "Teacher", + corporate: "Corporate", +}; + +const USER_TYPE_PERMISSIONS: { + [key in Type]: { perm: PermissionType | undefined; list: Type[] }; +} = { + student: { + perm: "createCodeStudent", + list: [], + }, + teacher: { + perm: "createCodeTeacher", + list: [], + }, + corporate: { + perm: "createCodeCorporate", + list: ["student", "teacher"], + }, +}; + +export default function BatchCreateUser({ user }: { user: User }) { + const [infos, setInfos] = useState< + { email: string; name: string; passport_id:string, type: Type, demographicInformation: { + country: string, + passport_id:string, + phone: string + } }[] + >([]); + const [isLoading, setIsLoading] = useState(false); + const [type, setType] = useState("student"); + const [showHelp, setShowHelp] = useState(false); + + const { users } = useUsers(); + + const { openFilePicker, filesContent, clear } = useFilePicker({ + accept: ".xlsx", + multiple: false, + readAs: "ArrayBuffer", + }); + + + useEffect(() => { + if (filesContent.length > 0) { + const file = filesContent[0]; + readXlsxFile(file.content).then((rows) => { + try { + const information = uniqBy( + rows + .map((row) => { + const [ + firstName, + lastName, + country, + passport_id, + email, + phone + ] = row as string[]; + return EMAIL_REGEX.test(email.toString().trim()) + ? { + email: email.toString().trim().toLowerCase(), + name: `${firstName ?? ""} ${lastName ?? ""}`.trim().toLowerCase(), + type: type, + passport_id: passport_id?.toString().trim() || undefined, + demographicInformation: { + country: country, + passport_id: passport_id?.toString().trim() || undefined, + phone, + } + } + : undefined; + }) + .filter((x) => !!x) as typeof infos, + (x) => x.email + ); + + if (information.length === 0) { + toast.error( + "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!" + ); + return clear(); + } + + setInfos(information); + } catch { + toast.error( + "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!" + ); + return clear(); + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filesContent]); + + const makeUsers = async () => { + const newUsers = infos.filter( + (x) => !users.map((u) => u.email).includes(x.email) + ); + const confirmed = confirm( + `You are about to add ${newUsers.length}, are you sure you want to continue?` + ) + if (!confirmed) + return; + + if (newUsers.length > 0) + { + setIsLoading(true); + Promise.all(newUsers.map((user) => { + return axios.post("/api/make_user", user) + })).finally(() => { + return clear(); + }) + } + setIsLoading(false); + setInfos([]); + }; + + + return ( + <> + setShowHelp(false)} + title="Excel File Format" + > +
+ Please upload an Excel file with the following format: + + + + + + + + + + + +
+ First Name + + Last Name + Country + Passport/National ID + E-mail + Phone Number +
+ + Notes: +
    +
  • - All incorrect e-mails will be ignored;
  • +
  • - All already registered e-mails will be ignored;
  • +
  • + - You may have a header row with the format above, however, it + is not necessary; +
  • +
  • + - All of the e-mails in the file will receive an e-mail to join + EnCoach with the role selected below. +
  • +
+
+
+
+
+
+ +
setShowHelp(true)} + > + +
+
+ + + {user && ( + + )} + +
+ + ); +} diff --git a/src/pages/api/make_user.ts b/src/pages/api/make_user.ts new file mode 100644 index 00000000..2abd5c90 --- /dev/null +++ b/src/pages/api/make_user.ts @@ -0,0 +1,110 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + setDoc, + doc, + query, + collection, + where, + getDocs, + getDoc, + deleteDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import {v4} from "uuid"; +import {Group} from "@/interfaces/user"; +import {createUserWithEmailAndPassword, getAuth} from "firebase/auth"; + +const DEFAULT_DESIRED_LEVELS = { + reading: 9, + listening: 9, + writing: 9, + speaking: 9, +}; + +const DEFAULT_LEVELS = { + reading: 0, + listening: 0, + writing: 0, + speaking: 0, +}; + +const auth = getAuth(app); +const db = getFirestore(app); + +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) { + if (!req.session.user) { + return res + .status(401) + .json({ ok: false, reason: "You must be logged in to make user!" }); + } + + const { email, passport_id, type } = req.body as { + email: string; + passport_id: string; + type: string + }; + createUserWithEmailAndPassword(auth, email.toLowerCase(), passport_id) + .then(async (userCredentials) => { + const userId = userCredentials.user.uid; + + const user = { + ...req.body, + bio: "", + type: type, + focus: "academic", + status: "paymentDue", + + desiredLevels: DEFAULT_DESIRED_LEVELS, + levels: DEFAULT_LEVELS, + isFirstLogin: false, + }; + await setDoc(doc(db, "users", userId), user); + if (type === "corporate") { + const defaultTeachersGroup: Group = { + admin: userId, + id: v4(), + name: "Teachers", + participants: [], + disableEditing: true, + }; + + const defaultStudentsGroup: Group = { + admin: userId, + id: v4(), + name: "Students", + participants: [], + disableEditing: true, + }; + + const defaultCorporateGroup: Group = { + admin: userId, + id: v4(), + name: "Corporate", + participants: [], + disableEditing: true, + }; + + + await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup); + await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup); + await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup); + } + res.status(200).json({ ok: true }); + }) + .catch((error) => { + console.log(error); + res.status(401).json({error}); + }); +} diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 3886d13e..e3488cd9 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -13,6 +13,7 @@ import Lists from "./(admin)/Lists"; import BatchCodeGenerator from "./(admin)/BatchCodeGenerator"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import ExamGenerator from "./(admin)/ExamGenerator"; +import BatchCreateUser from "./(admin)/BatchCreateUser"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -59,10 +60,11 @@ export default function Admin() {
+ {user.type !== "teacher" && ( <> - - + + )}
diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index daa309a3..65d7d17c 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -42,4 +42,4 @@ export function getTypesOfUser(types: Type[]) { return userTypes.filter((userType) => { return !types.includes(userType); }) -} \ No newline at end of file +}