From 1a7d35317b479ff9f47f34acf3df299a279d6d63 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 11 Dec 2024 22:00:43 +0000 Subject: [PATCH] ENCOA-263 --- src/components/UserCard.tsx | 2 - src/hooks/useCodes.tsx | 12 +- src/pages/(admin)/BatchCodeGenerator.tsx | 68 +++++---- src/pages/(admin)/CodeGenerator.tsx | 56 ++++--- src/pages/(admin)/Lists/CodeList.tsx | 16 +- src/pages/(admin)/Lists/DiscountList.tsx | 27 ++-- src/pages/(admin)/Lists/index.tsx | 2 +- src/pages/api/code/index.ts | 177 +++++++++++------------ src/pages/api/register.ts | 53 ++++--- src/pages/settings.tsx | 15 +- 10 files changed, 234 insertions(+), 194 deletions(-) diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index dd90e5b1..a1e2dc7b 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -19,7 +19,6 @@ import Select from "react-select"; import useUsers from "@/hooks/useUsers"; import { USER_TYPE_LABELS } from "@/resources/user"; import { CURRENCIES } from "@/resources/paypal"; -import useCodes from "@/hooks/useCodes"; import { checkAccess, getTypesOfUser } from "@/utils/permissions"; import { PERMISSIONS } from "@/constants/userPermissions"; import { PermissionType } from "@/interfaces/permissions"; @@ -119,7 +118,6 @@ const UserCard = ({ ); const { data: stats } = useFilterRecordsByUser(user.id); const { users } = useUsers(); - const { codes } = useCodes(user.id); const { permissions } = usePermissions(loggedInUser.id); useEffect(() => { diff --git a/src/hooks/useCodes.tsx b/src/hooks/useCodes.tsx index b88416a1..6c586532 100644 --- a/src/hooks/useCodes.tsx +++ b/src/hooks/useCodes.tsx @@ -1,8 +1,8 @@ -import {Code, Group, User} from "@/interfaces/user"; +import { Code, Group, User } from "@/interfaces/user"; import axios from "axios"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; -export default function useCodes(creator?: string) { +export default function useCodes(entity?: string) { const [codes, setCodes] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); @@ -10,12 +10,12 @@ export default function useCodes(creator?: string) { const getData = () => { setIsLoading(true); axios - .get(`/api/code${creator ? `?creator=${creator}` : ""}`) + .get(`/api/code${entity ? `?entity=${entity}` : ""}`) .then((response) => setCodes(response.data)) .finally(() => setIsLoading(false)); }; - useEffect(getData, [creator]); + useEffect(getData, [entity]); - return {codes, isLoading, isError, reload: getData}; + return { codes, isLoading, isError, reload: getData }; } diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index f10dc2c1..166103c1 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -1,29 +1,31 @@ import Button from "@/components/Low/Button"; import Checkbox from "@/components/Low/Checkbox"; -import {PERMISSIONS} from "@/constants/userPermissions"; +import { PERMISSIONS } from "@/constants/userPermissions"; import useUsers from "@/hooks/useUsers"; -import {Type, User} from "@/interfaces/user"; -import {USER_TYPE_LABELS} from "@/resources/user"; +import { Type, User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; import axios from "axios"; import clsx from "clsx"; -import {capitalize, uniqBy} from "lodash"; +import { capitalize, uniqBy } from "lodash"; import moment from "moment"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {toast} from "react-toastify"; +import { toast } from "react-toastify"; import ShortUniqueId from "short-unique-id"; -import {useFilePicker} from "use-file-picker"; +import { useFilePicker } from "use-file-picker"; import readXlsxFile from "read-excel-file"; import Modal from "@/components/Modal"; -import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs"; -import {checkAccess, getTypesOfUser} from "@/utils/permissions"; -import {PermissionType} from "@/interfaces/permissions"; +import { BsFileEarmarkEaselFill, BsQuestionCircleFill } from "react-icons/bs"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import { PermissionType } from "@/interfaces/permissions"; import usePermissions from "@/hooks/usePermissions"; +import { EntityWithRoles } from "@/interfaces/entity"; +import Select from "@/components/Low/Select"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); const USER_TYPE_PERMISSIONS: { - [key in Type]: {perm: PermissionType | undefined; list: Type[]}; + [key in Type]: { perm: PermissionType | undefined; list: Type[] }; } = { student: { perm: "createCodeStudent", @@ -59,11 +61,12 @@ interface Props { user: User; users: User[]; permissions: PermissionType[]; + entities: EntityWithRoles[] onFinish: () => void; } -export default function BatchCodeGenerator({user, users, permissions, onFinish}: Props) { - const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]); +export default function BatchCodeGenerator({ user, users, entities = [], permissions, onFinish }: Props) { + const [infos, setInfos] = useState<{ email: string; name: string; passport_id: string }[]>([]); const [isLoading, setIsLoading] = useState(false); const [expiryDate, setExpiryDate] = useState( user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null, @@ -71,8 +74,9 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}: const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); const [type, setType] = useState("student"); const [showHelp, setShowHelp] = useState(false); + const [entity, setEntity] = useState((entities || [])[0]?.id || undefined) - const {openFilePicker, filesContent, clear} = useFilePicker({ + const { openFilePicker, filesContent, clear } = useFilePicker({ accept: ".xlsx", multiple: false, readAs: "ArrayBuffer", @@ -93,10 +97,10 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}: 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(), - passport_id: passport_id?.toString().trim() || undefined, - } + email: email.toString().trim().toLowerCase(), + name: `${firstName ?? ""} ${lastName ?? ""}`.trim(), + passport_id: passport_id?.toString().trim() || undefined, + } : undefined; }) .filter((x) => !!x) as typeof infos, @@ -139,7 +143,7 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}: return; setIsLoading(true); - Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, {to: u.id, from: user.id}))) + Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, { to: u.id, from: user.id }))) .then(() => toast.success(`Successfully invited ${existingUsers.length} registered student(s)!`)) .finally(() => { if (newUsers.length === 0) setIsLoading(false); @@ -155,19 +159,20 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}: setIsLoading(true); axios - .post<{ok: boolean; valid?: number; reason?: string}>("/api/code", { + .post<{ ok: boolean; valid?: number; reason?: string }>("/api/code", { type, codes, - infos: informations, + infos: informations.map((info, index) => ({ ...info, code: codes[index] })), expiryDate, + entity }) - .then(({data, status}) => { + .then(({ data, status }) => { if (data.ok) { toast.success( `Successfully generated${data.valid ? ` ${data.valid}/${informations.length}` : ""} ${capitalize( type, )} codes and they have been notified by e-mail!`, - {toastId: "success"}, + { toastId: "success" }, ); onFinish(); @@ -175,12 +180,12 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}: } if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); + toast.error(data.reason, { toastId: "forbidden" }); } }) - .catch(({response: {status, data}}) => { + .catch(({ response: { status, data } }) => { if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); + toast.error(data.reason, { toastId: "forbidden" }); return; } @@ -258,6 +263,15 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}: )} )} +
+ + {Object.keys(USER_TYPE_LABELS) .filter((x) => { - const {list, perm} = USER_TYPE_PERMISSIONS[x as Type]; + const { list, perm } = USER_TYPE_PERMISSIONS[x as Type]; return checkAccess(user, getTypesOfUser(list), permissions, perm); }) .map((type) => ( diff --git a/src/pages/(admin)/CodeGenerator.tsx b/src/pages/(admin)/CodeGenerator.tsx index c1799783..ce65d719 100644 --- a/src/pages/(admin)/CodeGenerator.tsx +++ b/src/pages/(admin)/CodeGenerator.tsx @@ -1,22 +1,25 @@ import Button from "@/components/Low/Button"; import Checkbox from "@/components/Low/Checkbox"; -import {PERMISSIONS} from "@/constants/userPermissions"; -import {Type, User} from "@/interfaces/user"; -import {USER_TYPE_LABELS} from "@/resources/user"; +import { PERMISSIONS } from "@/constants/userPermissions"; +import { Type, User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; +import { capitalize } from "lodash"; import moment from "moment"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {toast} from "react-toastify"; +import { toast } from "react-toastify"; import ShortUniqueId from "short-unique-id"; -import {checkAccess, getTypesOfUser} from "@/utils/permissions"; -import {PermissionType} from "@/interfaces/permissions"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import { PermissionType } from "@/interfaces/permissions"; import usePermissions from "@/hooks/usePermissions"; +import { EntityWithRoles } from "@/interfaces/entity"; +import Select from "@/components/Low/Select"; +import { useAllowedEntities } from "@/hooks/useEntityPermissions"; const USER_TYPE_PERMISSIONS: { - [key in Type]: {perm: PermissionType | undefined; list: Type[]}; + [key in Type]: { perm: PermissionType | undefined; list: Type[] }; } = { student: { perm: "createCodeStudent", @@ -51,16 +54,19 @@ const USER_TYPE_PERMISSIONS: { interface Props { user: User; permissions: PermissionType[]; + entities: EntityWithRoles[] onFinish: () => void; } -export default function CodeGenerator({user, permissions, onFinish}: Props) { +export default function CodeGenerator({ user, entities = [], permissions, onFinish }: Props) { const [generatedCode, setGeneratedCode] = useState(); + const [expiryDate, setExpiryDate] = useState( user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null, ); const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); const [type, setType] = useState("student"); + const [entity, setEntity] = useState((entities || [])[0]?.id || undefined) useEffect(() => { if (!isExpiryDateEnabled) setExpiryDate(null); @@ -71,8 +77,8 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) { const code = uid.randomUUID(6); axios - .post("/api/code", {type, codes: [code], expiryDate}) - .then(({data, status}) => { + .post("/api/code", { type, codes: [code], expiryDate, entity }) + .then(({ data, status }) => { if (data.ok) { toast.success(`Successfully generated a ${capitalize(type)} code!`, { toastId: "success", @@ -82,12 +88,12 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) { } if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); + toast.error(data.reason, { toastId: "forbidden" }); } }) - .catch(({response: {status, data}}) => { + .catch(({ response: { status, data } }) => { if (status === 403) { - toast.error(data.reason, {toastId: "forbidden"}); + toast.error(data.reason, { toastId: "forbidden" }); return; } @@ -100,14 +106,25 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) { return (
- {user && ( +
+ + setType(e.target.value as typeof user.type)} className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white"> {Object.keys(USER_TYPE_LABELS) .filter((x) => { - const {list, perm} = USER_TYPE_PERMISSIONS[x as Type]; + const { list, perm } = USER_TYPE_PERMISSIONS[x as Type]; return checkAccess(user, getTypesOfUser(list), permissions, perm); }) .map((type) => ( @@ -116,8 +133,9 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) { ))} - )} - {user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && ( +
+ + {checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && ( <>
diff --git a/src/pages/(admin)/Lists/CodeList.tsx b/src/pages/(admin)/Lists/CodeList.tsx index 0d2ab5b6..3d71b533 100644 --- a/src/pages/(admin)/Lists/CodeList.tsx +++ b/src/pages/(admin)/Lists/CodeList.tsx @@ -16,6 +16,9 @@ import ReactDatePicker from "react-datepicker"; import clsx from "clsx"; import { checkAccess } from "@/utils/permissions"; import usePermissions from "@/hooks/usePermissions"; +import { EntityWithRoles } from "@/interfaces/entity"; +import { isAdmin } from "@/utils/users"; +import { findBy } from "@/utils"; const columnHelper = createColumnHelper(); @@ -34,16 +37,15 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => { ); }; -export default function CodeList({ user, canDeleteCodes }: { user: User, canDeleteCodes?: boolean }) { +export default function CodeList({ user, entities, canDeleteCodes } + : { user: User, entities: EntityWithRoles[], canDeleteCodes?: boolean }) { const [selectedCodes, setSelectedCodes] = useState([]); const [filteredCorporate, setFilteredCorporate] = useState(user?.type === "corporate" ? user : undefined); const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">(); - const { permissions } = usePermissions(user?.id || ""); - const { users } = useUsers(); - const { codes, reload } = useCodes(user?.type === "corporate" ? user?.id : undefined); + const { codes, reload } = useCodes(); const [startDate, setStartDate] = useState(moment("01/01/2023").toDate()); const [endDate, setEndDate] = useState(moment().endOf("day").toDate()); @@ -158,13 +160,17 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"), }), columnHelper.accessor("email", { - header: "Invited E-mail", + header: "E-mail", cell: (info) => info.getValue() || "N/A", }), columnHelper.accessor("creator", { header: "Creator", cell: (info) => , }), + columnHelper.accessor("entity", { + header: "Entity", + cell: (info) => findBy(entities, 'id', info.getValue())?.label || "N/A", + }), columnHelper.accessor("userId", { header: "Availability", cell: (info) => diff --git a/src/pages/(admin)/Lists/DiscountList.tsx b/src/pages/(admin)/Lists/DiscountList.tsx index f0478e7f..b3db37f1 100644 --- a/src/pages/(admin)/Lists/DiscountList.tsx +++ b/src/pages/(admin)/Lists/DiscountList.tsx @@ -3,31 +3,30 @@ import Checkbox from "@/components/Low/Checkbox"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import Modal from "@/components/Modal"; -import useCodes from "@/hooks/useCodes"; import useDiscounts from "@/hooks/useDiscounts"; import useUser from "@/hooks/useUser"; import useUsers from "@/hooks/useUsers"; -import {Discount} from "@/interfaces/paypal"; -import {Code, User} from "@/interfaces/user"; -import {USER_TYPE_LABELS} from "@/resources/user"; -import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import { Discount } from "@/interfaces/paypal"; +import { Code, User } from "@/interfaces/user"; +import { USER_TYPE_LABELS } from "@/resources/user"; +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; import moment from "moment"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {BsPencil, BsTrash} from "react-icons/bs"; -import {toast} from "react-toastify"; +import { BsPencil, BsTrash } from "react-icons/bs"; +import { toast } from "react-toastify"; const columnHelper = createColumnHelper(); -const DiscountCreator = ({discount, onClose}: {discount?: Discount; onClose: () => void}) => { +const DiscountCreator = ({ discount, onClose }: { discount?: Discount; onClose: () => void }) => { const [percentage, setPercentage] = useState(discount?.percentage); const [domain, setDomain] = useState(discount?.domain); const [validUntil, setValidUntil] = useState(discount?.validUntil); const submit = async () => { - const body = {percentage, domain, validUntil: validUntil?.toISOString() || undefined}; + const body = { percentage, domain, validUntil: validUntil?.toISOString() || undefined }; if (discount) { return axios @@ -112,7 +111,7 @@ const DiscountCreator = ({discount, onClose}: {discount?: Discount; onClose: () ); }; -export default function DiscountList({user}: {user: User}) { +export default function DiscountList({ user }: { user: User }) { const [selectedDiscounts, setSelectedDiscounts] = useState([]); const [isCreating, setIsCreating] = useState(false); @@ -120,8 +119,8 @@ export default function DiscountList({user}: {user: User}) { const [filteredDiscounts, setFilteredDiscounts] = useState([]); - const {users} = useUsers(); - const {discounts, reload} = useDiscounts(); + const { users } = useUsers(); + const { discounts, reload } = useDiscounts(); useEffect(() => { setFilteredDiscounts(discounts); @@ -220,7 +219,7 @@ export default function DiscountList({user}: {user: User}) { { header: "", id: "actions", - cell: ({row}: {row: {original: Discount}}) => { + cell: ({ row }: { row: { original: Discount } }) => { return (
0 && ( - 0} /> + 0} /> )} {checkAccess(user, ["developer", "admin"]) && ( diff --git a/src/pages/api/code/index.ts b/src/pages/api/code/index.ts index f3d7410f..d2eaf8e8 100644 --- a/src/pages/api/code/index.ts +++ b/src/pages/api/code/index.ts @@ -6,6 +6,12 @@ import { sessionOptions } from "@/lib/session"; import { Code, Group, Type } from "@/interfaces/user"; import { PERMISSIONS } from "@/constants/userPermissions"; import { prepareMailer, prepareMailOptions } from "@/email"; +import { isAdmin } from "@/utils/users"; +import { requestUser } from "@/utils/api"; +import { doesEntityAllow } from "@/utils/permissions"; +import { getEntity, getEntityWithRoles } from "@/utils/entities.be"; +import { findBy } from "@/utils"; +import { EntityWithRoles } from "@/interfaces/entity"; const db = client.db(process.env.MONGODB_DB); @@ -25,117 +31,98 @@ async function get(req: NextApiRequest, res: NextApiResponse) { return; } - const { creator } = req.query as { creator?: string }; - const snapshot = await db.collection("codes").find(creator ? { creator: creator } : {}).toArray(); + const { entity } = req.query as { entity?: string }; + const snapshot = await db.collection("codes").find(entity ? { entity } : {}).toArray(); res.status(200).json(snapshot); } -async function post(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ ok: false, reason: "You must be logged in to generate a code!" }); - return; +const generateAndSendCode = async ( + code: string, + type: Type, + expiryDate: null | Date, + entity?: string, + info?: { + email: string; name: string; passport_id?: string + }) => { + if (!info) { + await db.collection("codes").insertOne({ + code, type, expiryDate, entity + }) + return true } - const { type, codes, infos, expiryDate } = req.body as { + const previousCode = await db.collection("codes").findOne({ email: info.email, entity }) + + const transport = prepareMailer(); + const mailOptions = prepareMailOptions( + { + type, + code: previousCode ? previousCode.code : code, + environment: process.env.ENVIRONMENT, + }, + [info.email.toLowerCase().trim()], + "EnCoach Registration", + "main", + ); + + try { + await transport.sendMail(mailOptions); + if (!previousCode) { + await db.collection("codes").insertOne({ + code, type, expiryDate, entity, name: info.name.trim(), email: info.email.trim().toLowerCase(), + ...(info.passport_id ? { passport_id: info.passport_id.trim() } : {}) + }) + } + + return true; + } catch (e) { + return false; + } +} + +const countAvailableCodes = async (entity: EntityWithRoles) => { + const usedUp = await db.collection("codes").countDocuments({ entity: entity.id }) + const total = entity.licenses + + return total - usedUp +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + const user = await requestUser(req, res) + if (!user) return res.status(401).json({ ok: false, reason: "You must be logged in to generate a code!" }); + + const { type, codes, infos, expiryDate, entity } = req.body as { type: Type; codes: string[]; - infos?: { email: string; name: string; passport_id?: string }[]; + infos?: { email: string; name: string; passport_id?: string, code: string }[]; expiryDate: null | Date; + entity?: string }; - const permission = PERMISSIONS.generateCode[type]; - if (!permission.includes(req.session.user.type)) { - res.status(403).json({ - ok: false, - reason: "Your account type does not have permissions to generate a code for that type of user!", - }); - return; - } + if (!entity && !isAdmin(user)) + return res.status(403).json({ ok: false, reason: "You must be an admin to generate a code without an entity!" }); - const userCodes = await db.collection("codes").find({ creator: req.session.user.id }).toArray() - const creatorGroupsSnapshot = await db.collection("groups").find({ admin: req.session.user.id }).toArray() + const entityObj = entity ? await getEntityWithRoles(entity) : undefined + const isAllowed = entityObj ? doesEntityAllow(user, entityObj, 'create_code') : true + if (!isAllowed) return res.status(403).json({ ok: false, reason: "You do not have permissions to generate a code!" }); - const creatorGroups = creatorGroupsSnapshot.filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate"); - const usersInGroups = creatorGroups.flatMap((x) => x.participants); - - - if (req.session.user.type === "corporate") { - const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length; - const allowedCodes = 0; - - if (totalCodes > allowedCodes) { - res.status(403).json({ + if (entityObj) { + const availableCodes = await countAvailableCodes(entityObj) + if (availableCodes < codes.length) + return res.status(400).json({ ok: false, - reason: `You have or would have exceeded your amount of allowed codes, you currently are allowed to generate ${allowedCodes - userCodes.length - } codes.`, - }); - return; - } + reason: `You only have ${availableCodes} codes available, while trying to create ${codes.length} codes` + }) + } + const valid = [] + for (const code of codes) { + const info = findBy(infos || [], 'code', code) + const isValid = await generateAndSendCode(code, type, expiryDate, entity, info) + valid.push(isValid) } - const codePromises = codes.map(async (code, index) => { - const codeRef = await db.collection("codes").findOne({ id: code }); - let codeInformation = { - type, - code, - creator: req.session.user!.id, - creationDate: new Date().toISOString(), - expiryDate, - }; - - if (infos && infos.length > index) { - const { email, name, passport_id } = infos[index]; - const previousCode = userCodes.find((x) => x.email === email) as Code; - - const transport = prepareMailer(); - const mailOptions = prepareMailOptions( - { - type, - code: previousCode ? previousCode.code : code, - environment: process.env.ENVIRONMENT, - }, - [email.toLowerCase().trim()], - "EnCoach Registration", - "main", - ); - - try { - await transport.sendMail(mailOptions); - - if (!previousCode && codeRef) { - await db.collection("codes").updateOne( - { id: codeRef.id }, - { - $set: { - id: codeRef.id, - ...codeInformation, - email: email.trim().toLowerCase(), - name: name.trim(), - ...(passport_id ? { passport_id: passport_id.trim() } : {}), - } - }, - { upsert: true } - ); - } - - return true; - } catch (e) { - return false; - } - } else { - // upsert: true -> if it doesnt exist insert - await db.collection("codes").updateOne( - { id: code }, - { $set: { id: code, ...codeInformation } }, - { upsert: true } - ); - } - }); - - Promise.all(codePromises).then((results) => { - res.status(200).json({ ok: true, valid: results.filter((x) => x).length }); - }); + return res.status(200).json({ ok: true, valid: valid.length }); } async function del(req: NextApiRequest, res: NextApiResponse) { diff --git a/src/pages/api/register.ts b/src/pages/api/register.ts index e0f0b591..b5dc4d1f 100644 --- a/src/pages/api/register.ts +++ b/src/pages/api/register.ts @@ -1,13 +1,15 @@ -import {NextApiRequest, NextApiResponse} from "next"; -import {createUserWithEmailAndPassword, getAuth} from "firebase/auth"; -import {app} from "@/firebase"; -import {sessionOptions} from "@/lib/session"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {Code, CorporateInformation, DemographicInformation, Group, Type} from "@/interfaces/user"; -import {addUserToGroupOnCreation} from "@/utils/registration"; +import { NextApiRequest, NextApiResponse } from "next"; +import { createUserWithEmailAndPassword, getAuth } from "firebase/auth"; +import { app } from "@/firebase"; +import { sessionOptions } from "@/lib/session"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { Code, CorporateInformation, DemographicInformation, Group, Type } from "@/interfaces/user"; +import { addUserToGroupOnCreation } from "@/utils/registration"; import moment from "moment"; -import {v4} from "uuid"; +import { v4 } from "uuid"; import client from "@/lib/mongodb"; +import { addUserToEntity, getEntityWithRoles } from "@/utils/entities.be"; +import { findBy } from "@/utils"; const auth = getAuth(app); const db = client.db(process.env.MONGODB_DB); @@ -29,7 +31,7 @@ const DEFAULT_LEVELS = { }; async function register(req: NextApiRequest, res: NextApiResponse) { - const {type} = req.body as { + const { type } = req.body as { type: "individual" | "corporate"; }; @@ -38,19 +40,18 @@ async function register(req: NextApiRequest, res: NextApiResponse) { } async function registerIndividual(req: NextApiRequest, res: NextApiResponse) { - const {email, passport_id, password, code} = req.body as { + const { email, passport_id, password, code } = req.body as { email: string; passport_id?: string; password: string; code?: string; }; - const codeDoc = await db.collection("codes").findOne({code}); + const codeDoc = await db.collection("codes").findOne({ code }); + + if (code && code.length > 0 && !codeDoc) + return res.status(400).json({ error: "Invalid Code!" }); - if (code && code.length > 0 && !!codeDoc) { - res.status(400).json({error: "Invalid Code!"}); - return; - } createUserWithEmailAndPassword(auth, email.toLowerCase(), password) .then(async (userCredentials) => { @@ -69,7 +70,7 @@ async function registerIndividual(req: NextApiRequest, res: NextApiResponse) { focus: "academic", type: email.endsWith("@ecrop.dev") ? "developer" : codeDoc ? codeDoc.type : "student", subscriptionExpirationDate: codeDoc ? codeDoc.expiryDate : moment().subtract(1, "days").toISOString(), - ...(passport_id ? {demographicInformation: {passport_id}} : {}), + ...(passport_id ? { demographicInformation: { passport_id } } : {}), registrationDate: new Date().toISOString(), status: code ? "active" : "paymentDue", // apparently there's an issue with the verification email system @@ -80,23 +81,29 @@ async function registerIndividual(req: NextApiRequest, res: NextApiResponse) { await db.collection("users").insertOne(user); if (!!codeDoc) { - await db.collection("codes").updateOne({code: codeDoc.code}, {$set: {userId}}); - if (codeDoc.creator) await addUserToGroupOnCreation(userId, codeDoc.type, codeDoc.creator); + await db.collection("codes").updateOne({ code: codeDoc.code }, { $set: { userId } }); + if (codeDoc.entity) { + const inviteEntity = await getEntityWithRoles(codeDoc.entity) + if (inviteEntity) { + const defaultRole = findBy(inviteEntity.roles, 'isDefault', true)! + await addUserToEntity(userId, codeDoc.entity, defaultRole.id) + } + } } req.session.user = user; await req.session.save(); - res.status(200).json({user}); + res.status(200).json({ user }); }) .catch((error) => { console.log(error); - res.status(401).json({error}); + res.status(401).json({ error }); }); } async function registerCorporate(req: NextApiRequest, res: NextApiResponse) { - const {email, password} = req.body as { + const { email, password } = req.body as { email: string; password: string; corporateInformation: CorporateInformation; @@ -155,10 +162,10 @@ async function registerCorporate(req: NextApiRequest, res: NextApiResponse) { req.session.user = user; await req.session.save(); - res.status(200).json({user}); + res.status(200).json({ user }); }) .catch((error) => { console.log(error); - res.status(401).json({error}); + res.status(401).json({ error }); }); } diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 5dd67d47..e3688205 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -94,10 +94,21 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG /> setModalOpen(undefined)}> - setModalOpen(undefined)} /> + setModalOpen(undefined)} + /> setModalOpen(undefined)}> - setModalOpen(undefined)} /> + setModalOpen(undefined)} + /> setModalOpen(undefined)}>