meowmeowmeowmeowmeowmeow
This commit is contained in:
parent
eb0cfdad71
commit
8cab872b7d
|
@ -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",
|
||||
|
|
|
@ -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'}
|
||||
|
|
45
src/app/admin/users/page.tsx
Normal file
45
src/app/admin/users/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
17
src/lib/actions.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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) => {
|
||||
|
|
67
src/lib/components/ui/NavigationPanel.tsx
Normal file
67
src/lib/components/ui/NavigationPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue