From 08c2ff278fec1ed8af6ec524dc17072526c91d64 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 2 May 2024 09:17:56 -0400 Subject: [PATCH] delete user functionality --- components/ModalContent/DeleteUserModal.tsx | 51 ++++++++++++++++ .../users/userId/deleteUserById.ts | 12 +++- pages/admin.tsx | 44 ++++++++++--- pages/api/v1/users/[id].ts | 14 ++++- store/admin/users.ts | 61 +++++++++++++++++++ 5 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 components/ModalContent/DeleteUserModal.tsx create mode 100644 store/admin/users.ts diff --git a/components/ModalContent/DeleteUserModal.tsx b/components/ModalContent/DeleteUserModal.tsx new file mode 100644 index 0000000..97c1cff --- /dev/null +++ b/components/ModalContent/DeleteUserModal.tsx @@ -0,0 +1,51 @@ +import toast from "react-hot-toast"; +import Modal from "../Modal"; +import useUserStore from "@/store/admin/users"; + +type Props = { + onClose: Function; + userId: number; +}; + +export default function DeleteUserModal({ onClose, userId }: Props) { + const { removeUser } = useUserStore(); + + const deleteUser = async () => { + const load = toast.loading("Deleting..."); + + const response = await removeUser(userId); + + toast.dismiss(load); + + response.ok && toast.success(`User Deleted.`); + + onClose(); + }; + + return ( + +

Delete User

+ +
+ +
+

Are you sure you want to remove this user?

+ +
+ + + Warning: This action is irreversible! + +
+ + +
+
+ ); +} diff --git a/lib/api/controllers/users/userId/deleteUserById.ts b/lib/api/controllers/users/userId/deleteUserById.ts index 976bd71..7e33344 100644 --- a/lib/api/controllers/users/userId/deleteUserById.ts +++ b/lib/api/controllers/users/userId/deleteUserById.ts @@ -10,7 +10,8 @@ const authentikEnabled = process.env.AUTHENTIK_CLIENT_SECRET; export default async function deleteUserById( userId: number, - body: DeleteUserBody + body: DeleteUserBody, + isServerAdmin: boolean ) { // First, we retrieve the user from the database const user = await prisma.user.findUnique({ @@ -25,13 +26,13 @@ export default async function deleteUserById( } // Then, we check if the provided password matches the one stored in the database (disabled in Keycloak integration) - if (!keycloakEnabled && !authentikEnabled) { + if (!keycloakEnabled && !authentikEnabled && !isServerAdmin) { const isPasswordValid = bcrypt.compareSync( body.password, user.password as string ); - if (!isPasswordValid) { + if (!isPasswordValid && !isServerAdmin) { return { response: "Invalid credentials.", status: 401, // Unauthorized @@ -43,6 +44,11 @@ export default async function deleteUserById( await prisma .$transaction( async (prisma) => { + // Delete Access Tokens + await prisma.accessToken.deleteMany({ + where: { userId }, + }); + // Delete whitelisted users await prisma.whitelistedUser.deleteMany({ where: { userId }, diff --git a/pages/admin.tsx b/pages/admin.tsx index be634f8..3d5a0a3 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -1,6 +1,8 @@ +import DeleteUserModal from "@/components/ModalContent/DeleteUserModal"; +import useUserStore from "@/store/admin/users"; import { User as U } from "@prisma/client"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; interface User extends U { subscriptions: { @@ -8,12 +10,22 @@ interface User extends U { }; } +type UserModal = { + isOpen: boolean; + userId: number | null; +}; + export default function Admin() { - const [users, setUsers] = useState(); + const { users, setUsers } = useUserStore(); const [searchQuery, setSearchQuery] = useState(""); const [filteredUsers, setFilteredUsers] = useState(); + const [deleteUserModal, setDeleteUserModal] = useState({ + isOpen: false, + userId: null, + }); + useEffect(() => { // fetch users fetch("/api/v1/users") @@ -31,7 +43,7 @@ export default function Admin() { > -

+

User Administration

@@ -76,11 +88,11 @@ export default function Admin() {
{filteredUsers && filteredUsers.length > 0 && searchQuery !== "" ? ( - UserLising(filteredUsers) + UserListing(filteredUsers, deleteUserModal, setDeleteUserModal) ) : searchQuery !== "" ? (

No users found with the given search query.

) : users && users.length > 0 ? ( - UserLising(users) + UserListing(users, deleteUserModal, setDeleteUserModal) ) : (

No users found.

)} @@ -88,7 +100,11 @@ export default function Admin() { ); } -const UserLising = (users: User[]) => { +const UserListing = ( + users: User[], + deleteUserModal: UserModal, + setDeleteUserModal: Function +) => { return (
@@ -106,7 +122,7 @@ const UserLising = (users: User[]) => { {users.map((user, index) => ( - + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( @@ -117,7 +133,12 @@ const UserLising = (users: User[]) => { )} @@ -125,6 +146,13 @@ const UserLising = (users: User[]) => { ))}
{index + 1} {user.username}{new Date(user.createdAt).toLocaleString()} -
+ + {deleteUserModal.isOpen && deleteUserModal.userId ? ( + setDeleteUserModal({ isOpen: false, userId: null })} + userId={deleteUserModal.userId} + /> + ) : null}
); }; diff --git a/pages/api/v1/users/[id].ts b/pages/api/v1/users/[id].ts index cf75e16..128b426 100644 --- a/pages/api/v1/users/[id].ts +++ b/pages/api/v1/users/[id].ts @@ -16,9 +16,17 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) { return null; } - const userId = token?.id; + const user = await prisma.user.findUnique({ + where: { + id: token?.id, + }, + }); - if (userId !== Number(req.query.id)) + const isServerAdmin = process.env.ADMINISTRATOR === user?.username; + + const userId = isServerAdmin ? Number(req.query.id) : token.id; + + if (userId !== Number(req.query.id) && !isServerAdmin) return res.status(401).json({ response: "Permission denied." }); if (req.method === "GET") { @@ -53,7 +61,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) { const updated = await updateUserById(userId, req.body); return res.status(updated.status).json({ response: updated.response }); } else if (req.method === "DELETE") { - const updated = await deleteUserById(userId, req.body); + const updated = await deleteUserById(userId, req.body, isServerAdmin); return res.status(updated.status).json({ response: updated.response }); } } diff --git a/store/admin/users.ts b/store/admin/users.ts new file mode 100644 index 0000000..c5cb5af --- /dev/null +++ b/store/admin/users.ts @@ -0,0 +1,61 @@ +import { User as U } from "@prisma/client"; +import { create } from "zustand"; + +interface User extends U { + subscriptions: { + active: boolean; + }; +} + +type ResponseObject = { + ok: boolean; + data: object | string; +}; + +type UserStore = { + users: User[]; + setUsers: (users: User[]) => void; + addUser: () => Promise; + removeUser: (userId: number) => Promise; +}; + +const useUserStore = create((set) => ({ + users: [], + setUsers: async () => { + const response = await fetch("/api/v1/users"); + + const data = await response.json(); + + if (response.ok) set({ users: data.response }); + }, + addUser: async () => { + const response = await fetch("/api/v1/users", { + method: "POST", + }); + + const data = await response.json(); + + if (response.ok) + set((state) => ({ + users: [...state.users, data.response], + })); + + return { ok: response.ok, data: data.response }; + }, + removeUser: async (userId) => { + const response = await fetch(`/api/v1/users/${userId}`, { + method: "DELETE", + }); + + const data = await response.json(); + + if (response.ok) + set((state) => ({ + users: state.users.filter((user) => user.id !== userId), + })); + + return { ok: response.ok, data: data.response }; + }, +})); + +export default useUserStore;