From 7856e76b15c1b859cfb2e3f9cf12dd8a5f4b5352 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 22 Apr 2024 18:00:59 -0400 Subject: [PATCH 1/5] basic user listing --- .env.sample | 1 + lib/api/controllers/users/getUsers.ts | 22 ++++++++ pages/admin.tsx | 77 +++++++++++++++++++++++++++ pages/api/v1/users/index.ts | 8 +++ types/enviornment.d.ts | 3 +- 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 lib/api/controllers/users/getUsers.ts create mode 100644 pages/admin.tsx diff --git a/.env.sample b/.env.sample index 0e692b0..a579635 100644 --- a/.env.sample +++ b/.env.sample @@ -22,6 +22,7 @@ BROWSER_TIMEOUT= IGNORE_UNAUTHORIZED_CA= IGNORE_HTTPS_ERRORS= IGNORE_URL_SIZE_LIMIT= +ADMINISTRATOR= # AWS S3 Settings SPACES_KEY= diff --git a/lib/api/controllers/users/getUsers.ts b/lib/api/controllers/users/getUsers.ts new file mode 100644 index 0000000..dd2b168 --- /dev/null +++ b/lib/api/controllers/users/getUsers.ts @@ -0,0 +1,22 @@ +import { prisma } from "@/lib/api/db"; + +export default async function getUsers() { + // Get all users + const users = await prisma.user.findMany({ + select: { + id: true, + username: true, + email: true, + emailVerified: true, + subscriptions: { + select: { + active: true, + }, + }, + createdAt: true, + updatedAt: true, + }, + }); + + return { response: users, status: 200 }; +} diff --git a/pages/admin.tsx b/pages/admin.tsx new file mode 100644 index 0000000..6624fe3 --- /dev/null +++ b/pages/admin.tsx @@ -0,0 +1,77 @@ +import { User as U } from "@prisma/client"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +interface User extends U { + subscriptions: { + active: boolean; + }; +} + +export default function Admin() { + const [users, setUsers] = useState(); + + useEffect(() => { + // fetch users + fetch("/api/v1/users") + .then((res) => res.json()) + .then((data) => setUsers(data.response)); + }, []); + + return ( +
+
+ + + +

+ User Administration +

+
+ +
+ + {users && users.length > 0 ? ( +
+ + + + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( + + )} + {process.env.NEXT_PUBLIC_STRIPE === "true" && ( + + )} + + + + + + {users.map((user, index) => ( + + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( + + )} + {process.env.NEXT_PUBLIC_STRIPE === "true" && ( + + )} + + + + ))} + +
UsernameEmailSubscribedCreated AtUpdated At
{index + 1}{user.username}{user.email}{user.subscriptions.active ? "Yes" : "No"}{new Date(user.createdAt).toLocaleString()}{new Date(user.updatedAt).toLocaleString()}
+
+ ) : ( +

No users found.

+ )} +
+ ); +} diff --git a/pages/api/v1/users/index.ts b/pages/api/v1/users/index.ts index 3af7bf8..cce6b4e 100644 --- a/pages/api/v1/users/index.ts +++ b/pages/api/v1/users/index.ts @@ -1,9 +1,17 @@ import type { NextApiRequest, NextApiResponse } from "next"; import postUser from "@/lib/api/controllers/users/postUser"; +import getUsers from "@/lib/api/controllers/users/getUsers"; +import verifyUser from "@/lib/api/verifyUser"; export default async function users(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") { const response = await postUser(req, res); return response; + } else if (req.method === "GET") { + const user = await verifyUser({ req, res }); + if (!user || process.env.ADMINISTRATOR !== user.username) return; + + const response = await getUsers(); + return res.status(response.status).json({ response: response.response }); } } diff --git a/types/enviornment.d.ts b/types/enviornment.d.ts index e8c5a7c..5a94472 100644 --- a/types/enviornment.d.ts +++ b/types/enviornment.d.ts @@ -14,6 +14,7 @@ declare global { ARCHIVE_TAKE_COUNT?: string; IGNORE_UNAUTHORIZED_CA?: string; IGNORE_URL_SIZE_LIMIT?: string; + ADMINISTRATOR?: string; SPACES_KEY?: string; SPACES_SECRET?: string; @@ -418,4 +419,4 @@ declare global { } } -export { }; +export {}; From 154d0d5fb6ea84f724ea222a1988b8853e2a8638 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 24 Apr 2024 09:16:34 -0400 Subject: [PATCH 2/5] add search to user admin --- lib/api/controllers/users/getUsers.ts | 1 - pages/admin.tsx | 145 ++++++++++++++++++-------- 2 files changed, 99 insertions(+), 47 deletions(-) diff --git a/lib/api/controllers/users/getUsers.ts b/lib/api/controllers/users/getUsers.ts index dd2b168..496efcf 100644 --- a/lib/api/controllers/users/getUsers.ts +++ b/lib/api/controllers/users/getUsers.ts @@ -14,7 +14,6 @@ export default async function getUsers() { }, }, createdAt: true, - updatedAt: true, }, }); diff --git a/pages/admin.tsx b/pages/admin.tsx index 6624fe3..be634f8 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -11,6 +11,9 @@ interface User extends U { export default function Admin() { const [users, setUsers] = useState(); + const [searchQuery, setSearchQuery] = useState(""); + const [filteredUsers, setFilteredUsers] = useState(); + useEffect(() => { // fetch users fetch("/api/v1/users") @@ -19,59 +22,109 @@ export default function Admin() { }, []); return ( -
-
- - - -

- User Administration -

+
+
+
+ + + +

+ User Administration +

+
+ +
+
+ + + { + setSearchQuery(e.target.value); + + if (users) { + setFilteredUsers( + users.filter((user) => + JSON.stringify(user) + .toLowerCase() + .includes(e.target.value.toLowerCase()) + ) + ); + } + }} + className="border border-neutral-content bg-base-200 focus:border-primary py-1 rounded-md pl-9 pr-2 w-full max-w-[15rem] md:w-[15rem] md:max-w-full duration-200 outline-none" + /> +
+ +
+ +
+
- {users && users.length > 0 ? ( -
- - - - - - {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( - - )} - {process.env.NEXT_PUBLIC_STRIPE === "true" && ( - - )} - - - - - - {users.map((user, index) => ( - - - - {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( - - )} - {process.env.NEXT_PUBLIC_STRIPE === "true" && ( - - )} - - - - ))} - -
UsernameEmailSubscribedCreated AtUpdated At
{index + 1}{user.username}{user.email}{user.subscriptions.active ? "Yes" : "No"}{new Date(user.createdAt).toLocaleString()}{new Date(user.updatedAt).toLocaleString()}
-
+ {filteredUsers && filteredUsers.length > 0 && searchQuery !== "" ? ( + UserLising(filteredUsers) + ) : searchQuery !== "" ? ( +

No users found with the given search query.

+ ) : users && users.length > 0 ? ( + UserLising(users) ) : (

No users found.

)}
); } + +const UserLising = (users: User[]) => { + return ( +
+ + + + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( + + )} + {process.env.NEXT_PUBLIC_STRIPE === "true" && } + + + + + + {users.map((user, index) => ( + + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( + + )} + {process.env.NEXT_PUBLIC_STRIPE === "true" && ( + + )} + + + + ))} + +
UsernameEmailSubscribedCreated At
{index + 1}{user.username}{user.email}{JSON.stringify(user.subscriptions.active)}{new Date(user.createdAt).toLocaleString()} + +
+
+ ); +}; From 08c2ff278fec1ed8af6ec524dc17072526c91d64 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 2 May 2024 09:17:56 -0400 Subject: [PATCH 3/5] 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; From 915d08a315f26b0959481bee9c4b84d4cbe80aea Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Fri, 3 May 2024 10:22:45 -0400 Subject: [PATCH 4/5] finalized administration panel --- components/ModalContent/NewUserModal.tsx | 133 ++++++++++++++++++ lib/api/controllers/users/postUser.ts | 89 +++++++++--- .../users/userId/deleteUserById.ts | 3 +- pages/admin.tsx | 40 ++++-- pages/api/v1/users/index.ts | 3 +- store/admin/users.ts | 11 +- 6 files changed, 245 insertions(+), 34 deletions(-) create mode 100644 components/ModalContent/NewUserModal.tsx diff --git a/components/ModalContent/NewUserModal.tsx b/components/ModalContent/NewUserModal.tsx new file mode 100644 index 0000000..27e1a15 --- /dev/null +++ b/components/ModalContent/NewUserModal.tsx @@ -0,0 +1,133 @@ +import toast from "react-hot-toast"; +import Modal from "../Modal"; +import useUserStore from "@/store/admin/users"; +import TextInput from "../TextInput"; +import { FormEvent, useState } from "react"; + +type Props = { + onClose: Function; +}; + +type FormData = { + name: string; + username?: string; + email?: string; + password: string; +}; + +const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"; + +export default function NewUserModal({ onClose }: Props) { + const { addUser } = useUserStore(); + + const [form, setForm] = useState({ + name: "", + username: "", + email: emailEnabled ? "" : undefined, + password: "", + }); + + const [submitLoader, setSubmitLoader] = useState(false); + + async function submit(event: FormEvent) { + event.preventDefault(); + + if (!submitLoader) { + const checkFields = () => { + if (emailEnabled) { + return form.name !== "" && form.email !== "" && form.password !== ""; + } else { + return ( + form.name !== "" && form.username !== "" && form.password !== "" + ); + } + }; + + if (checkFields()) { + if (form.password.length < 8) + return toast.error("Passwords must be at least 8 characters."); + + setSubmitLoader(true); + + const load = toast.loading("Creating Account..."); + + const response = await addUser(form); + + toast.dismiss(load); + setSubmitLoader(false); + + if (response.ok) { + toast.success("User Created!"); + onClose(); + } else { + toast.error(response.data as string); + } + } else { + toast.error("Please fill out all the fields."); + } + } + } + + return ( + +

Create New User

+ +
+ +
+
+
+

Display Name

+ setForm({ ...form, name: e.target.value })} + value={form.name} + /> +
+ + {emailEnabled ? ( +
+

Username

+ setForm({ ...form, username: e.target.value })} + value={form.username} + /> +
+ ) : undefined} + +
+

Email

+ setForm({ ...form, email: e.target.value })} + value={form.email} + /> +
+ +
+

Password

+ setForm({ ...form, password: e.target.value })} + value={form.password} + /> +
+
+ +
+ +
+
+
+ ); +} diff --git a/lib/api/controllers/users/postUser.ts b/lib/api/controllers/users/postUser.ts index f24738b..84ab2e0 100644 --- a/lib/api/controllers/users/postUser.ts +++ b/lib/api/controllers/users/postUser.ts @@ -1,9 +1,11 @@ import { prisma } from "@/lib/api/db"; import type { NextApiRequest, NextApiResponse } from "next"; import bcrypt from "bcrypt"; +import verifyUser from "../../verifyUser"; const emailEnabled = process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; +const stripeEnabled = process.env.STRIPE_SECRET_KEY ? true : false; interface Data { response: string | object; @@ -20,7 +22,15 @@ export default async function postUser( req: NextApiRequest, res: NextApiResponse ) { - if (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true") { + let isServerAdmin = false; + + const user = await verifyUser({ req, res }); + if (process.env.ADMINISTRATOR === user?.username) isServerAdmin = true; + + if ( + process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" && + !isServerAdmin + ) { return res.status(400).json({ response: "Registration is disabled." }); } @@ -57,13 +67,16 @@ export default async function postUser( }); const checkIfUserExists = await prisma.user.findFirst({ - where: emailEnabled - ? { + where: { + OR: [ + { email: body.email?.toLowerCase().trim(), - } - : { + }, + { username: (body.username as string).toLowerCase().trim(), }, + ], + }, }); if (!checkIfUserExists) { @@ -71,21 +84,63 @@ export default async function postUser( const hashedPassword = bcrypt.hashSync(body.password, saltRounds); - await prisma.user.create({ - data: { - name: body.name, - username: emailEnabled - ? undefined - : (body.username as string).toLowerCase().trim(), - email: emailEnabled ? body.email?.toLowerCase().trim() : undefined, - password: hashedPassword, - }, - }); + // Subscription dates + const currentPeriodStart = new Date(); + const currentPeriodEnd = new Date(); + currentPeriodEnd.setFullYear(currentPeriodEnd.getFullYear() + 1000); // end date is in 1000 years... - return res.status(201).json({ response: "User successfully created." }); + if (isServerAdmin) { + const user = await prisma.user.create({ + data: { + name: body.name, + username: (body.username as string).toLowerCase().trim(), + email: emailEnabled ? body.email?.toLowerCase().trim() : undefined, + password: hashedPassword, + emailVerified: new Date(), + subscriptions: stripeEnabled + ? { + create: { + stripeSubscriptionId: + "fake_sub_" + Math.round(Math.random() * 10000000000000), + active: true, + currentPeriodStart, + currentPeriodEnd, + }, + } + : undefined, + }, + select: { + id: true, + username: true, + email: true, + emailVerified: true, + subscriptions: { + select: { + active: true, + }, + }, + createdAt: true, + }, + }); + + return res.status(201).json({ response: user }); + } else { + await prisma.user.create({ + data: { + name: body.name, + username: emailEnabled + ? undefined + : (body.username as string).toLowerCase().trim(), + email: emailEnabled ? body.email?.toLowerCase().trim() : undefined, + password: hashedPassword, + }, + }); + + return res.status(201).json({ response: "User successfully created." }); + } } else if (checkIfUserExists) { return res.status(400).json({ - response: `${emailEnabled ? "Email" : "Username"} already exists.`, + response: `Email or Username already exists.`, }); } } diff --git a/lib/api/controllers/users/userId/deleteUserById.ts b/lib/api/controllers/users/userId/deleteUserById.ts index 7e33344..9c01dcd 100644 --- a/lib/api/controllers/users/userId/deleteUserById.ts +++ b/lib/api/controllers/users/userId/deleteUserById.ts @@ -11,7 +11,7 @@ const authentikEnabled = process.env.AUTHENTIK_CLIENT_SECRET; export default async function deleteUserById( userId: number, body: DeleteUserBody, - isServerAdmin: boolean + isServerAdmin?: boolean ) { // First, we retrieve the user from the database const user = await prisma.user.findUnique({ @@ -93,6 +93,7 @@ export default async function deleteUserById( await prisma.subscription.delete({ where: { userId }, }); + // .catch((err) => console.log(err)); await prisma.usersAndCollections.deleteMany({ where: { diff --git a/pages/admin.tsx b/pages/admin.tsx index 3d5a0a3..d7b9fda 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -1,4 +1,5 @@ import DeleteUserModal from "@/components/ModalContent/DeleteUserModal"; +import NewUserModal from "@/components/ModalContent/NewUserModal"; import useUserStore from "@/store/admin/users"; import { User as U } from "@prisma/client"; import Link from "next/link"; @@ -26,11 +27,10 @@ export default function Admin() { userId: null, }); + const [newUserModal, setNewUserModal] = useState(false); + useEffect(() => { - // fetch users - fetch("/api/v1/users") - .then((res) => res.json()) - .then((data) => setUsers(data.response)); + setUsers(); }, []); return ( @@ -79,7 +79,10 @@ export default function Admin() { />
-
+
setNewUserModal(true)} + className="flex items-center btn btn-accent dark:border-violet-400 text-white btn-sm px-2 aspect-square relative" + >
@@ -96,6 +99,10 @@ export default function Admin() { ) : (

No users found.

)} + + {newUserModal ? ( + setNewUserModal(false)} /> + ) : null} ); } @@ -107,7 +114,7 @@ const UserListing = ( ) => { return (
- +
@@ -122,19 +129,28 @@ const UserListing = ( {users.map((user, index) => ( - - - + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( )} {process.env.NEXT_PUBLIC_STRIPE === "true" && ( - + )} -
{index + 1}{user.username}
{index + 1}{user.username ? user.username : N/A}{user.email}{JSON.stringify(user.subscriptions.active)} + {user.subscriptions?.active ? ( + JSON.stringify(user.subscriptions?.active) + ) : ( + N/A + )} + {new Date(user.createdAt).toLocaleString()} +