meowmeowmeowmeowmeowmeow

This commit is contained in:
Lea 2024-01-17 00:45:48 +01:00
parent eb0cfdad71
commit 8cab872b7d
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
11 changed files with 177 additions and 43 deletions

View file

@ -12,6 +12,7 @@
"@radix-ui/themes": "^2.0.3",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
"lucide-react": "^0.311.0",
"next": "14.0.4",
"next-auth": "^4.24.5",
"react": "^18",

View file

@ -14,6 +14,9 @@ dependencies:
bcryptjs:
specifier: ^2.4.3
version: 2.4.3
lucide-react:
specifier: ^0.311.0
version: 0.311.0(react@18.2.0)
next:
specifier: 14.0.4
version: 14.0.4(react-dom@18.2.0)(react@18.2.0)
@ -3307,6 +3310,14 @@ packages:
dependencies:
yallist: 4.0.0
/lucide-react@0.311.0(react@18.2.0):
resolution: {integrity: sha512-kyMc6YyVepMVnmZT2X1sl7iyf7w6l8YdhEVKmcBLiT0lMlXqcPGsr2/drYP1/VaGf7DmBJ4/CjQAmOpDd8R8uw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/make-fetch-happen@9.1.0:
resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
engines: {node: '>= 10'}

View file

@ -0,0 +1,45 @@
import { fetchAllUsers } from "@/lib/actions";
import { sha256sum } from "@/lib/util";
import { Avatar, Button, Flex, Heading, Table, Text } from "@radix-ui/themes";
export default async function Users() {
const data = await fetchAllUsers();
return (
<>
<Heading>Users</Heading>
<Flex direction='column' className="flex-1">
<Table.Root className="w-full" variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell justify='start'>Email</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell justify='end'>Actions</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{data.map((email) => (
<Table.Row key={email}>
<Table.Cell justify='start'>
<Flex direction='row' gap='4' align='center'>
<Avatar
size="2"
src={`https://gravatar.com/avatar/${sha256sum(email)}?d=identicon`}
radius='full'
fallback={email.slice(0, 1) || "@"}
/>
<Text size='2'>{email}</Text>
</Flex>
</Table.Cell>
<Table.Cell justify='end'>
<Button variant="soft">Manage</Button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Flex>
</>
);
}

View file

@ -4,6 +4,8 @@ import ThemeWrapper from '@/lib/components/ThemeWrapper';
import './globals.css';
import AuthWrapper from '@/lib/components/AuthWrapper';
import { getServerSession } from 'next-auth';
import NavigationPanel from '@/lib/components/ui/NavigationPanel';
import { Flex } from '@radix-ui/themes';
const inter = Inter({ subsets: ['latin'] });
@ -20,11 +22,14 @@ export default async function RootLayout({
return (
<html lang="en">
<body className={`${inter.className} p-4`}>
<AuthWrapper session={await getServerSession()}>
<ThemeWrapper>
{children}
</ThemeWrapper>
</AuthWrapper>
<ThemeWrapper>
<AuthWrapper session={await getServerSession()}>
<Flex direction='row' gap='4'>
<div><NavigationPanel /></div>
<div className='w-full'>{children}</div>
</Flex>
</AuthWrapper>
</ThemeWrapper>
</body>
</html>
);

View file

@ -1,39 +1,11 @@
"use client";
import { Avatar, Card, Flex, Heading, Text, Box, Button } from '@radix-ui/themes';
import { useSession } from 'next-auth/react';
import Link from 'next/link';
import { Heading } from '@radix-ui/themes';
export default function Home() {
const session = useSession().data;
return (
<>
<Heading size="9">Welcome back.</Heading>
{session?.user && (
<Card className='w-fit'>
<Flex gap="3" align="center">
<Avatar
size="3"
src={session.user.image ?? "/favicon.ico"}
radius='full'
fallback={session.user.name?.slice(0, 1) || "@"}
/>
<Box>
<Text as="div" size="2" weight="bold">
{session.user.email}
</Text>
</Box>
</Flex>
</Card>
)}
<Link href={session?.user ? "/api/auth/signout" : "/api/auth/signin"}>
<Button variant='soft'>
Sign {session?.user ? "out" : "in"}
</Button>
</Link>
</>
);
}

17
src/lib/actions.ts Normal file
View file

@ -0,0 +1,17 @@
"use server";
import { database } from "./db";
import { isAdmin } from "./util";
export async function fetchAllUsers(): Promise<string[]> {
return new Promise((resolve, reject) => {
if (!isAdmin) return reject("Unauthenticated");
const db = database();
db.all("SELECT key FROM passwords", (err, res: any) => {
if (err) return reject(err);
resolve(res.map((row: any) => row.key));
});
});
}

View file

@ -1,7 +1,8 @@
"use client";
import { Button, Flex, Heading } from "@radix-ui/themes";
import { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { SessionProvider, signIn } from "next-auth/react";
export default function AuthWrapper({
children,
@ -10,9 +11,18 @@ export default function AuthWrapper({
children: React.ReactNode,
session: Session | null,
}) {
return (
<SessionProvider session={session}>
{children}
</SessionProvider>
);
if (session?.user) {
return (
<SessionProvider session={session}>
{children}
</SessionProvider>
);
} else {
return (
<Flex direction='column' gap='3' align='center'>
<Heading size='8'>Unauthenticated</Heading>
<Button variant="outline" size='3' onClick={() => signIn()}>Sign in</Button>
</Flex>
);
}
}

View file

@ -13,6 +13,7 @@ export default function ThemeWrapper({
// Automatically respond to theme changes
useEffect(() => {
console.log("meow");
setDark(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
const onThemeChange = (event: MediaQueryListEvent) => {

View file

@ -0,0 +1,67 @@
"use client";
import { isAdmin } from "@/lib/util";
import { Avatar, Box, Button, Card, Flex, IconButton, Popover, Text } from "@radix-ui/themes";
import { BookUserIcon, HomeIcon } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
export default function NavigationPanel() {
const session = useSession();
return (
<Card className="w-fit">
<Flex direction='column' gap='2'>
{session.data?.user
&& (
<Popover.Root>
<Popover.Trigger>
<IconButton variant="outline" size='4' color="gray">
<Avatar
size="2"
src={session.data.user.image ?? "/favicon.ico"}
fallback={session.data.user.name?.slice(0, 1) || "@"}
/>
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Flex gap='3' direction='column'>
<Card>
<Flex gap='2' align='center'>
<Avatar
size="3"
src={session.data.user.image ?? "/favicon.ico"}
radius="full"
fallback={session.data.user.name?.slice(0, 1) || "@"}
/>
<Text size='4'>{session.data.user.email}</Text>
</Flex>
</Card>
<Flex gap='2' direction='row' justify='end'>
<Button variant="soft">Account</Button>
<Button variant="surface" onClick={() => signOut()}>Log out</Button>°
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
)}
<Link href="/">
<IconButton variant="outline" size='4'>
<HomeIcon />
</IconButton>
</Link>
{isAdmin(session.data) && (
<Link href="/admin/users">
<IconButton variant="outline" size='4'>
<BookUserIcon />
</IconButton>
</Link>
)}
</Flex>
</Card>
);
}

View file

@ -6,7 +6,7 @@ if (!CREDENTIALS_DB_PATH) {
throw "$CREDENTIALS_DB_PATH not provided; unable to connect to database";
}
const database = () => new sqlite.Database(CREDENTIALS_DB_PATH!);
export const database = () => new sqlite.Database(CREDENTIALS_DB_PATH!);
export function validateCredentials(email: string, password: string) {
const db = database();

View file

@ -1,7 +1,12 @@
import crypto from 'node:crypto';
import { Session } from 'next-auth';
import crypto from 'crypto';
export function sha256sum(input: any) {
const hash = crypto.createHash('sha256');
hash.update(input);
return hash.digest('hex');
}
}
export function isAdmin(session: Session | null) {
return session?.user?.email == "lea@amogus.cloud"; // todo
}