diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index bc1f626..82fee22 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -1,45 +1,81 @@ -import { fetchAllUsers } from "@/lib/actions"; -import { sha256sum } from "@/lib/util"; -import { Avatar, Button, Flex, Heading, Table, Text } from "@radix-ui/themes"; +"use client"; -export default async function Users() { - const data = await fetchAllUsers(); +import { fetchAllUsers } from "@/lib/actions"; +import GhostMessage from "@/lib/components/ui/GhostMessage"; +import LoadingSpinner from "@/lib/components/ui/LoadingSpinner"; +import CreateUserButton from "@/lib/components/ui/admin/CreateUserButton"; +import { sha256sum } from "@/lib/util"; +import { Avatar, Button, Card, Flex, Heading, Table, Text, TextField } from "@radix-ui/themes"; +import { SearchIcon, UserRoundXIcon } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; + +export default function Users() { + const [data, setData] = useState(null); + const [filter, setFilter] = useState(""); + const filtered = useMemo(() => filter.length ? data?.filter((email) => email.includes(filter)) : data, [data, filter]); + + useEffect(() => { + fetchAllUsers().then(setData); + }, []); return ( <> - Users - - - - - - Email - Actions - - - - - {data.map((email) => ( - - - - - {email} - - - - - - - ))} - - + + Users + + + + + + setFilter(e.currentTarget.value)} + placeholder="Filter table..." + /> + + setData([...(data ?? []), user])} /> + + + + { + filtered + ? filtered.length + ? + + + + Email + Actions + + + + + {filtered.map((email) => ( + + + + + {email} + + + + + + + ))} + + + + : } header="No users" message="Try adjusting your search query" /> + : } header="Loading" /> + } + ); } \ No newline at end of file diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 8c445eb..9111aca 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,12 +1,13 @@ "use server"; import { getServerSession } from "next-auth"; -import { AliasEntry, approveAliasEntry, createAlias, database, deleteAliasEntry, getAlias, getAllAliases, getUserAliases, setUserPassword } from "./db"; +import { AliasEntry, approveAliasEntry, createAlias, createUserEntry, database, deleteAliasEntry, getAlias, getAllAliases, getUserAliases, setUserPassword } from "./db"; import { aliasesNeedApproval, isAdmin } from "./util"; export async function fetchAllUsers(): Promise { - return new Promise((resolve, reject) => { - if (!isAdmin) return reject("Unauthenticated"); + return new Promise(async (resolve, reject) => { + const session = await getServerSession(); + if (!isAdmin(session)) return reject("Unauthenticated"); const db = database('credentials'); db.all("SELECT key FROM passwords", (err, res: any) => { @@ -93,3 +94,19 @@ export async function approveAlias(alias: string) { await approveAliasEntry(alias); } + +export async function createUser(email: string, password: string) { + const session = await getServerSession(); + if (!session?.user?.email) throw new Error("Unauthenticated"); + if (!isAdmin(session)) throw new Error("Unauthorized"); + + if (!await aliasAvailable(email.toLowerCase())) { + throw new Error("Alias unavailable"); + } + + if (password.length < 8 || !email.includes("@")) { + throw new Error("Validation failed"); + } + + await createUserEntry(email, password); +} diff --git a/src/lib/components/ui/admin/CreateUserButton.tsx b/src/lib/components/ui/admin/CreateUserButton.tsx new file mode 100644 index 0000000..ab57229 --- /dev/null +++ b/src/lib/components/ui/admin/CreateUserButton.tsx @@ -0,0 +1,134 @@ +import { Button, Callout, Dialog, Flex, IconButton, TextArea, TextField } from "@radix-ui/themes"; +import crypto from "crypto"; +import { AsteriskIcon, AtSignIcon, EyeIcon, EyeOffIcon, UserIcon } from "lucide-react"; +import { useState } from "react"; +import GhostMessage from "../GhostMessage"; +import LoadingSpinner from "../LoadingSpinner"; +import { createUser } from "@/lib/actions"; + +export default function CreateUserButton({ onCreate }: { onCreate?: (email: string) => any }) { + const [passwordHidden, setPasswordHidden] = useState(true); + const [password, setPassword] = useState(""); + const [email, setEmail] = useState(""); + const [open, setOpen] = useState(false); + const [state, setState] = useState<"input" | "loading" | "created">("input"); + const textAreaString = `Email: ${email}\nPassword: ${password}\nLogin: https://mail.amogus.cloud`; + + return ( + { + setOpen(open); + if (!open) { + setState("input"); + setPasswordHidden(true); + setPassword(""); + setEmail(""); + } + }}> + + + + + + Create user + { + state == "input" + ? <> + Provide the user's primary email address and a password. The user can change their password later. + + + + + + + Make sure that the provided email address uses a configured domain! + + + + + + + + + setEmail(e.currentTarget.value)} + /> + + + + + + + setPassword(e.currentTarget.value)} + /> + + setPasswordHidden(!passwordHidden)} size="1" variant="ghost"> + { + passwordHidden + ? + : + } + + + + + + + + + + + + + + + : state == "loading" + ? } header="Creating user..." /> + : <> + + Provide the user with the following text block to allow them to log in: + + +