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()} +