Compare commits
2 commits
dc0cc4b541
...
c9421cebb4
Author | SHA1 | Date | |
---|---|---|---|
|
c9421cebb4 | ||
|
54917bfe70 |
BIN
public/mindblown.png
Normal file
BIN
public/mindblown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
137
src/app/admin/aliases/page.tsx
Normal file
137
src/app/admin/aliases/page.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { approveAlias, deleteAlias, fetchAllAliases } from "@/lib/actions";
|
||||||
|
import GhostMessage from "@/lib/components/ui/GhostMessage";
|
||||||
|
import LoadingSpinner from "@/lib/components/ui/LoadingSpinner";
|
||||||
|
import { AliasEntry } from "@/lib/db";
|
||||||
|
import { Button, Card, Code, Dialog, Flex, Heading, Table } from "@radix-ui/themes";
|
||||||
|
import { ListChecksIcon } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function Aliases() {
|
||||||
|
const [aliases, setAliases] = useState<AliasEntry[] | null>();
|
||||||
|
const pending = aliases?.filter((alias) => alias.pending);
|
||||||
|
const active = aliases?.filter((alias) => !alias.pending);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllAliases().then(setAliases);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading className="pb-4">Aliases</Heading>
|
||||||
|
|
||||||
|
<Flex gap="3" direction="column">
|
||||||
|
<Card>
|
||||||
|
<Heading size="3" className="pb-2">Pending</Heading>
|
||||||
|
|
||||||
|
{pending
|
||||||
|
? pending.length
|
||||||
|
? <Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell justify='start'>User</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell justify='start'>Alias</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell justify='end'>Actions</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{pending.map((alias) => (
|
||||||
|
<Table.Row key={alias.id}>
|
||||||
|
<Table.Cell justify='start'>{alias.address}</Table.Cell>
|
||||||
|
<Table.Cell justify='start'>{alias.alias}</Table.Cell>
|
||||||
|
<Table.Cell justify='end'>
|
||||||
|
<Flex gap='3' justify="end">
|
||||||
|
<Button
|
||||||
|
size="1"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => approveAlias(alias.alias)
|
||||||
|
.then(() => { setAliases(aliases?.map((a) => a.id == alias.id ? { ...a, pending: false } : a)); })}
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="1"
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => deleteAlias(alias.alias)
|
||||||
|
.then(() => setAliases(aliases?.filter(a => a.id != alias.id)))}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
: <GhostMessage icon={<ListChecksIcon />} header="All caught up" message="There are no pending alias requests" />
|
||||||
|
: <GhostMessage icon={<LoadingSpinner />} header="Loading" />
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Heading size="3" className="pb-2">Active aliases</Heading>
|
||||||
|
|
||||||
|
{active
|
||||||
|
? active.length
|
||||||
|
? <Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell justify='start'>User</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell justify='start'>Alias</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell justify='end'>Actions</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{active.map((alias) => (
|
||||||
|
<Table.Row key={alias.id}>
|
||||||
|
<Table.Cell justify='start'>{alias.address}</Table.Cell>
|
||||||
|
<Table.Cell justify='start'>{alias.alias}</Table.Cell>
|
||||||
|
<Table.Cell justify='end'>
|
||||||
|
<Flex gap='3' justify="end">
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger>
|
||||||
|
<Button
|
||||||
|
size="1"
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Title>Delete alias</Dialog.Title>
|
||||||
|
<Dialog.Description>Are you sure you want to delete <Code>{alias.alias}</Code>?</Dialog.Description>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => deleteAlias(alias.alias)
|
||||||
|
.then(() => setAliases(aliases?.filter(a => a.id != alias.id)))}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Flex>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
: <GhostMessage icon={<Image src="/mindblown.png" alt="Mind blown" width="40" height="40" />} header="No aliases" message="Hmm, that's weird..." />
|
||||||
|
: <GhostMessage icon={<LoadingSpinner />} header="Loading" />
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { AliasEntry, createAlias, database, deleteAliasEntry, getAlias, getUserAliases, setUserPassword } from "./db";
|
import { AliasEntry, approveAliasEntry, createAlias, database, deleteAliasEntry, getAlias, getAllAliases, getUserAliases, setUserPassword } from "./db";
|
||||||
import { aliasesNeedApproval, isAdmin } from "./util";
|
import { aliasesNeedApproval, isAdmin } from "./util";
|
||||||
|
|
||||||
export async function fetchAllUsers(): Promise<string[]> {
|
export async function fetchAllUsers(): Promise<string[]> {
|
||||||
|
@ -30,6 +30,14 @@ export async function fetchOwnAliases() {
|
||||||
return await getUserAliases(session.user.email);
|
return await getUserAliases(session.user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAllAliases() {
|
||||||
|
const session = await getServerSession();
|
||||||
|
if (!session?.user?.email) throw new Error("Unauthenticated");
|
||||||
|
if (!isAdmin(session)) throw new Error("Unauthorized");
|
||||||
|
|
||||||
|
return await getAllAliases();
|
||||||
|
}
|
||||||
|
|
||||||
export async function aliasAvailable(email: string) {
|
export async function aliasAvailable(email: string) {
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
if (!session?.user) throw new Error("Unauthenticated");
|
if (!session?.user) throw new Error("Unauthenticated");
|
||||||
|
@ -77,3 +85,11 @@ export async function deleteAlias(alias: string) {
|
||||||
|
|
||||||
await deleteAliasEntry(alias);
|
await deleteAliasEntry(alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function approveAlias(alias: string) {
|
||||||
|
const session = await getServerSession();
|
||||||
|
if (!session?.user?.email) throw new Error("Unauthenticated");
|
||||||
|
if (!isAdmin(session)) throw new Error("Unauthorized");
|
||||||
|
|
||||||
|
await approveAliasEntry(alias);
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { isAdmin } from "@/lib/util";
|
import { isAdmin } from "@/lib/util";
|
||||||
import { Avatar, Box, Button, Card, Flex, IconButton, Popover, ScrollArea, Text } from "@radix-ui/themes";
|
import { Avatar, Button, Card, Flex, IconButton, Popover, ScrollArea, Text, Tooltip } from "@radix-ui/themes";
|
||||||
import { BookUserIcon, HomeIcon } from "lucide-react";
|
import { BookUserIcon, HomeIcon, Users2Icon } from "lucide-react";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
@ -12,6 +12,7 @@ dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export default function NavigationPanel({ mobileUi }: { mobileUi?: boolean }) {
|
export default function NavigationPanel({ mobileUi }: { mobileUi?: boolean }) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const tooltipSide = mobileUi ? "bottom" : "right";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={mobileUi ? "w-full" : "w-fit"}>
|
<Card className={mobileUi ? "w-full" : "w-fit"}>
|
||||||
|
@ -58,18 +59,31 @@ export default function NavigationPanel({ mobileUi }: { mobileUi?: boolean }) {
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href="/">
|
<Tooltip content="Home" side={tooltipSide}>
|
||||||
<IconButton variant="outline" size='4'>
|
<Link href="/">
|
||||||
<HomeIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{isAdmin(session.data) && (
|
|
||||||
<Link href="/admin/users">
|
|
||||||
<IconButton variant="outline" size='4'>
|
<IconButton variant="outline" size='4'>
|
||||||
<BookUserIcon />
|
<HomeIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Link>
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{isAdmin(session.data) && (
|
||||||
|
<>
|
||||||
|
<Tooltip content="Users" side={tooltipSide}>
|
||||||
|
<Link href="/admin/users">
|
||||||
|
<IconButton variant="outline" size='4'>
|
||||||
|
<BookUserIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Aliases" side={tooltipSide}>
|
||||||
|
<Link href="/admin/aliases">
|
||||||
|
<IconButton variant="outline" size='4'>
|
||||||
|
<Users2Icon />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
|
@ -62,6 +62,20 @@ export function setUserPassword(email: string, newPass: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AliasEntry = { id: number, address: string, alias: string, pending: boolean };
|
export type AliasEntry = { id: number, address: string, alias: string, pending: boolean };
|
||||||
|
export function getAllAliases() {
|
||||||
|
return new Promise<AliasEntry[]>(async (resolve, reject) => {
|
||||||
|
const db = database('aliases');
|
||||||
|
|
||||||
|
db.all("SELECT id, address, alias, pending FROM aliases", (err, res: any[]) => {
|
||||||
|
db.close();
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(res.map((data) => ({
|
||||||
|
...data,
|
||||||
|
pending: !!data.pending,
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
export function getUserAliases(email: string) {
|
export function getUserAliases(email: string) {
|
||||||
return new Promise<AliasEntry[]>(async (resolve, reject) => {
|
return new Promise<AliasEntry[]>(async (resolve, reject) => {
|
||||||
const db = database('aliases');
|
const db = database('aliases');
|
||||||
|
@ -81,7 +95,7 @@ export function getAlias(alias: string) {
|
||||||
return new Promise<AliasEntry | undefined>(async (resolve, reject) => {
|
return new Promise<AliasEntry | undefined>(async (resolve, reject) => {
|
||||||
const db = database('aliases');
|
const db = database('aliases');
|
||||||
|
|
||||||
db.get("SELECT id, address, alias, pending FROM aliases WHERE address = ?", alias, (err, res: AliasEntry) => {
|
db.get("SELECT id, address, alias, pending FROM aliases WHERE alias = ?", alias, (err, res: AliasEntry) => {
|
||||||
db.close();
|
db.close();
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (!res) return resolve(undefined);
|
if (!res) return resolve(undefined);
|
||||||
|
@ -112,6 +126,21 @@ export function createAlias(user: string, alias: string, pending: boolean) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function approveAliasEntry(alias: string) {
|
||||||
|
return new Promise<void>(async (resolve, reject) => {
|
||||||
|
const db = database('aliases');
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
"UPDATE aliases SET pending = 0 WHERE alias = ?",
|
||||||
|
alias,
|
||||||
|
function(err: any) {
|
||||||
|
db.close();
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteAliasEntry(alias: string) {
|
export function deleteAliasEntry(alias: string) {
|
||||||
return new Promise<void>(async (resolve, reject) => {
|
return new Promise<void>(async (resolve, reject) => {
|
||||||
const db = database('aliases');
|
const db = database('aliases');
|
||||||
|
|
Loading…
Reference in a new issue