diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx index 79eb2d2a..38a971db 100644 --- a/src/components/High/Layout.tsx +++ b/src/components/High/Layout.tsx @@ -33,8 +33,7 @@ export default function Layout({user, children, className, navDisabled = false, focusMode={focusMode} onFocusLayerMouseEnter={onFocusLayerMouseEnter} className="-md:hidden" - userType={user.type} - userId={user.id} + user={user} />
void; - className?: string; - userType?: Type; - userId?: string; + path: string; + navDisabled?: boolean; + focusMode?: boolean; + onFocusLayerMouseEnter?: () => void; + className?: string; + user: User; } interface NavProps { - Icon: IconType; - label: string; - path: string; - keyPath: string; - disabled?: boolean; - isMinimized?: boolean; - badge?: number; + Icon: IconType; + label: string; + path: string; + keyPath: string; + disabled?: boolean; + isMinimized?: boolean; + badge?: number; } -const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, badge}: NavProps) => { - return ( - - - {!isMinimized && {label}} - {!!badge && badge > 0 && ( -
- {badge} -
- )} - - ); +const Nav = ({ + Icon, + label, + path, + keyPath, + disabled = false, + isMinimized = false, + badge, +}: NavProps) => { + return ( + + + {!isMinimized && {label}} + {!!badge && badge > 0 && ( +
+ {badge} +
+ )} + + ); }; -export default function Sidebar({path, navDisabled = false, focusMode = false, userType, onFocusLayerMouseEnter, className, userId}: Props) { - const router = useRouter(); +export default function Sidebar({ + path, + navDisabled = false, + focusMode = false, + user, + onFocusLayerMouseEnter, + className, +}: Props) { + const router = useRouter(); - const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]); + const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [ + state.isSidebarMinimized, + state.toggleSidebarMinimized, + ]); - const {totalAssignedTickets} = useTicketsListener(userId); + const { totalAssignedTickets } = useTicketsListener(user.id); - const logout = async () => { - axios.post("/api/logout").finally(() => { - setTimeout(() => router.reload(), 500); - }); - }; + const logout = async () => { + axios.post("/api/logout").finally(() => { + setTimeout(() => router.reload(), 500); + }); + }; - const disableNavigation = preventNavigation(navDisabled, focusMode); + const disableNavigation = preventNavigation(navDisabled, focusMode); - return ( -
-
-
-
-
+ return ( +
+
+
+
+
-
-
- {isMinimized ? : } - {!isMinimized && Minimize} -
-
{} : logout} - className={clsx( - "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", - isMinimized ? "w-fit" : "w-full min-w-[250px] px-8", - )}> - - {!isMinimized && Log Out} -
-
- {focusMode && } -
- ); +
+
+ {isMinimized ? ( + + ) : ( + + )} + {!isMinimized && ( + Minimize + )} +
+
{} : logout} + className={clsx( + "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", + isMinimized ? "w-fit" : "w-full min-w-[250px] px-8" + )} + > + + {!isMinimized && ( + Log Out + )} +
+
+ {focusMode && ( + + )} +
+ ); } diff --git a/src/interfaces/permissions.ts b/src/interfaces/permissions.ts new file mode 100644 index 00000000..2e448af5 --- /dev/null +++ b/src/interfaces/permissions.ts @@ -0,0 +1,49 @@ +export const markets = ["au", "br", "de"] as const; + +export const permissions = [ + // generate codes are basicly invites + "createCodeStudent", + "createCodeTeacher", + "createCodeCorporate", + "createCodeCountryManager", + "createCodeAdmin", + // exams + "createReadingExam", + "createListeningExam", + "createWritingExam", + "createSpeakingExam", + "createLevelExam", + // view pages + "viewExams", + "viewExercises", + "viewRecords", + "viewStats", + "viewTickets", + "viewPaymentRecords", + // view data + "viewStudent", + "viewTeacher", + "viewCorporate", + "viewCountryManager", + "viewAdmin", + // edit data + "editStudent", + "editTeacher", + "editCorporate", + "editCountryManager", + "editAdmin", + // delete data + "deleteStudent", + "deleteTeacher", + "deleteCorporate", + "deleteCountryManager", + "deleteAdmin", +] as const; + +export type PermissionType = (typeof permissions)[keyof typeof permissions]; + +export interface Permission { + id: string; + type: PermissionType; + users: string[]; +} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 542b36bf..ddda1ccb 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,5 +1,6 @@ import { Module } from "."; import { InstructorGender } from "./exam"; +import { PermissionType } from "./permissions"; export type User = | StudentUser @@ -26,6 +27,7 @@ export interface BasicUser { subscriptionExpirationDate?: null | Date; registrationDate?: Date; status: UserStatus; + permissions: PermissionType[], } export interface StudentUser extends BasicUser { diff --git a/src/pages/api/permissions/[id].ts b/src/pages/api/permissions/[id].ts new file mode 100644 index 00000000..7c379719 --- /dev/null +++ b/src/pages/api/permissions/[id].ts @@ -0,0 +1,30 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { getFirestore, doc, setDoc } from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "PATCH") return patch(req, res); +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + const { id } = req.query as { id: string }; + const { users } = req.body; + try { + await setDoc(doc(db, "permissions", id), { users }, { merge: true }); + return res.status(200).json({ ok: true }); + } catch (err) { + console.error(err); + return res.status(500).json({ ok: false }); + } +} diff --git a/src/pages/api/permissions/bootstrap.ts b/src/pages/api/permissions/bootstrap.ts new file mode 100644 index 00000000..cd36b52a --- /dev/null +++ b/src/pages/api/permissions/bootstrap.ts @@ -0,0 +1,43 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + collection, + getDocs, + query, + where, + doc, + setDoc, + addDoc, + getDoc, + deleteDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Permission } from "@/interfaces/permissions"; +import { bootstrap } from "@/utils/permissions.be"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + console.log("Boostrap"); + try { + await bootstrap(); + return res.status(200).json({ ok: true }); + } catch (err) { + console.error("Failed to update permissions", err); + return res.status(500).json({ ok: false }); + } +} diff --git a/src/pages/api/permissions/index.ts b/src/pages/api/permissions/index.ts new file mode 100644 index 00000000..3f9fbfac --- /dev/null +++ b/src/pages/api/permissions/index.ts @@ -0,0 +1,36 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + collection, + getDocs, + query, + where, + doc, + setDoc, + addDoc, + getDoc, + deleteDoc, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Permission } from "@/interfaces/permissions"; +import { getPermissions, getPermissionDocs } from "@/utils/permissions.be"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + const docs = await getPermissionDocs(); + res.status(200).json(docs); +} diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts index 921c0222..88925234 100644 --- a/src/pages/api/user.ts +++ b/src/pages/api/user.ts @@ -1,11 +1,22 @@ -import {PERMISSIONS} from "@/constants/userPermissions"; -import {app, adminApp} from "@/firebase"; -import {Group, User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import {collection, deleteDoc, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore"; -import {getAuth} from "firebase-admin/auth"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {NextApiRequest, NextApiResponse} from "next"; +import { PERMISSIONS } from "@/constants/userPermissions"; +import { app, adminApp } from "@/firebase"; +import { Group, User } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { + collection, + deleteDoc, + doc, + getDoc, + getDocs, + getFirestore, + query, + setDoc, + where, +} from "firebase/firestore"; +import { getAuth } from "firebase-admin/auth"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { NextApiRequest, NextApiResponse } from "next"; +import { getPermissions, getPermissionDocs } from "@/utils/permissions.be"; const db = getFirestore(app); const auth = getAuth(adminApp); @@ -13,89 +24,132 @@ const auth = getAuth(adminApp); export default withIronSessionApiRoute(user, sessionOptions); async function user(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "GET") return get(req, res); - if (req.method === "DELETE") return del(req, res); + if (req.method === "GET") return get(req, res); + if (req.method === "DELETE") return del(req, res); - res.status(404).json(undefined); + res.status(404).json(undefined); } async function del(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ok: false}); - return; - } + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } - const {id} = req.query as {id: string}; + const { id } = req.query as { id: string }; - const docUser = await getDoc(doc(db, "users", req.session.user.id)); - if (!docUser.exists()) { - res.status(401).json({ok: false}); - return; - } + const docUser = await getDoc(doc(db, "users", req.session.user.id)); + if (!docUser.exists()) { + res.status(401).json({ ok: false }); + return; + } - const user = docUser.data() as User; + const user = docUser.data() as User; - const docTargetUser = await getDoc(doc(db, "users", id)); - if (!docTargetUser.exists()) { - res.status(404).json({ok: false}); - return; - } + const docTargetUser = await getDoc(doc(db, "users", id)); + if (!docTargetUser.exists()) { + res.status(404).json({ ok: false }); + return; + } - const targetUser = {...docTargetUser.data(), id: docTargetUser.id} as User; + const targetUser = { ...docTargetUser.data(), id: docTargetUser.id } as User; - if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) { - res.json({ok: true}); + if ( + user.type === "corporate" && + (targetUser.type === "student" || targetUser.type === "teacher") + ) { + res.json({ ok: true }); - const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id))); - await Promise.all([ - ...userParticipantGroup.docs - .filter((x) => (x.data() as Group).admin === user.id) - .map(async (x) => await setDoc(x.ref, {participants: x.data().participants.filter((y: string) => y !== id)}, {merge: true})), - ]); + const userParticipantGroup = await getDocs( + query( + collection(db, "groups"), + where("participants", "array-contains", id) + ) + ); + await Promise.all([ + ...userParticipantGroup.docs + .filter((x) => (x.data() as Group).admin === user.id) + .map( + async (x) => + await setDoc( + x.ref, + { + participants: x + .data() + .participants.filter((y: string) => y !== id), + }, + { merge: true } + ) + ), + ]); - return; - } + return; + } - const permission = PERMISSIONS.deleteUser[targetUser.type]; - if (!permission.includes(user.type)) { - res.status(403).json({ok: false}); - return; - } + const permission = PERMISSIONS.deleteUser[targetUser.type]; + if (!permission.includes(user.type)) { + res.status(403).json({ ok: false }); + return; + } - res.json({ok: true}); + res.json({ ok: true }); - await auth.deleteUser(id); - await deleteDoc(doc(db, "users", id)); - const userCodeDocs = await getDocs(query(collection(db, "codes"), where("userId", "==", id))); - const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id))); - const userGroupAdminDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); - const userStatsDocs = await getDocs(query(collection(db, "stats"), where("user", "==", id))); + await auth.deleteUser(id); + await deleteDoc(doc(db, "users", id)); + const userCodeDocs = await getDocs( + query(collection(db, "codes"), where("userId", "==", id)) + ); + const userParticipantGroup = await getDocs( + query(collection(db, "groups"), where("participants", "array-contains", id)) + ); + const userGroupAdminDocs = await getDocs( + query(collection(db, "groups"), where("admin", "==", id)) + ); + const userStatsDocs = await getDocs( + query(collection(db, "stats"), where("user", "==", id)) + ); - await Promise.all([ - ...userCodeDocs.docs.map(async (x) => await deleteDoc(x.ref)), - ...userGroupAdminDocs.docs.map(async (x) => await deleteDoc(x.ref)), - ...userStatsDocs.docs.map(async (x) => await deleteDoc(x.ref)), - ...userParticipantGroup.docs.map( - async (x) => await setDoc(x.ref, {participants: x.data().participants.filter((y: string) => y !== id)}, {merge: true}), - ), - ]); + await Promise.all([ + ...userCodeDocs.docs.map(async (x) => await deleteDoc(x.ref)), + ...userGroupAdminDocs.docs.map(async (x) => await deleteDoc(x.ref)), + ...userStatsDocs.docs.map(async (x) => await deleteDoc(x.ref)), + ...userParticipantGroup.docs.map( + async (x) => + await setDoc( + x.ref, + { + participants: x.data().participants.filter((y: string) => y !== id), + }, + { merge: true } + ) + ), + ]); } async function get(req: NextApiRequest, res: NextApiResponse) { - if (req.session.user) { - const docUser = await getDoc(doc(db, "users", req.session.user.id)); - if (!docUser.exists()) { - res.status(401).json(undefined); - return; - } + if (req.session.user) { + const docUser = await getDoc(doc(db, "users", req.session.user.id)); + if (!docUser.exists()) { + res.status(401).json(undefined); + return; + } - const user = docUser.data() as User; + const user = docUser.data() as User; + + const permissionDocs = await getPermissionDocs(); - req.session.user = {...user, id: req.session.user.id}; - await req.session.save(); + const userWithPermissions = { + ...user, + permissions: getPermissions(req.session.user.id, permissionDocs), + }; + req.session.user = { + ...userWithPermissions, + id: req.session.user.id, + }; + await req.session.save(); - res.json({...user, id: req.session.user.id}); - } else { - res.status(401).json(undefined); - } + res.json({ ...userWithPermissions, id: req.session.user.id }); + } else { + res.status(401).json(undefined); + } } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 601d4838..0ef4fea7 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -8,7 +8,6 @@ import {useEffect, useState} from "react"; import useStats from "@/hooks/useStats"; import {averageScore, groupBySession, totalExams} from "@/utils/stats"; import useUser from "@/hooks/useUser"; -import Sidebar from "@/components/Sidebar"; import Diagnostic from "@/components/Diagnostic"; import {ToastContainer} from "react-toastify"; import {capitalize} from "lodash"; diff --git a/src/pages/permissions/[id].tsx b/src/pages/permissions/[id].tsx new file mode 100644 index 00000000..093c946b --- /dev/null +++ b/src/pages/permissions/[id].tsx @@ -0,0 +1,190 @@ +/* eslint-disable @next/next/no-img-element */ +import Head from "next/head"; +import { useState } from "react"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { Permission, PermissionType } from "@/interfaces/permissions"; +import { getPermissionDoc } from "@/utils/permissions.be"; +import { User } from "@/interfaces/user"; +import Layout from "@/components/High/Layout"; +import { getUsers } from "@/utils/users.be"; +import { BsTrash } from "react-icons/bs"; +import Select from "@/components/Low/Select"; +import Button from "@/components/Low/Button"; +import axios from "axios"; +import { toast, ToastContainer } from "react-toastify"; + +interface BasicUser { + id: string; + name: string; +} + +interface PermissionWithBasicUsers { + id: string; + type: PermissionType; + users: BasicUser[]; +} + +export const getServerSideProps = withIronSessionSsr(async (context) => { + const { req, params } = context; + const user = req.session.user; + + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + if (!params?.id) { + return { + redirect: { + destination: "/permissions", + permanent: false, + }, + }; + } + + // Fetch data from external API + const permission: Permission = await getPermissionDoc(params.id as string); + + const allUserData: User[] = await getUsers(); + const users = allUserData.map((u) => ({ + id: u.id, + name: u.name, + })) as BasicUser[]; + + // const res = await fetch("api/permissions"); + // const permissions: Permission[] = await res.json(); + // Pass data to the page via props + const usersData: BasicUser[] = permission.users.reduce( + (acc: BasicUser[], userId) => { + const user = users.find((u) => u.id === userId) as BasicUser; + if (user) { + acc.push(user); + } + return acc; + }, + [] + ); + + return { + props: { + // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), + permission: { + ...permission, + id: params.id, + users: usersData, + }, + user: req.session.user, + users, + }, + }; +}, sessionOptions); + +interface Props { + permission: PermissionWithBasicUsers; + user: User; + users: BasicUser[]; +} + +export default function Page(props: Props) { + console.log("Props", props); + + const { permission, user, users } = props; + + const [selectedUsers, setSelectedUsers] = useState(() => + permission.users.map((u) => u.id) + ); + + const onChange = (value: any) => { + console.log("value", value); + setSelectedUsers((prev) => { + if (value?.value) { + return [...prev, value?.value]; + } + return prev; + }); + }; + const removeUser = (id: string) => { + setSelectedUsers((prev) => prev.filter((u) => u !== id)); + }; + + const update = async () => { + console.log("update", selectedUsers); + try { + await axios.patch(`/api/permissions/${permission.id}`, { + users: selectedUsers, + }); + toast.success("Permission updated"); + } catch (err) { + toast.error("Failed to update permission"); + } + }; + + return ( + <> + + EnCoach + + + + + + +

+ Permission: {permission.type as string} +

+
+