Elysia.js API with session-based auth (email/password + Google OAuth), role system (USER/ADMIN/SUPER_ADMIN), Prisma + PostgreSQL, React 19 with Mantine UI, TanStack Router, dark theme, and comprehensive test suite (unit, integration, E2E with Lightpanda). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
import {
|
|
Avatar,
|
|
Badge,
|
|
Button,
|
|
Container,
|
|
Group,
|
|
Paper,
|
|
Stack,
|
|
Text,
|
|
Title,
|
|
} from '@mantine/core'
|
|
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
import { TbLogout, TbUser } from 'react-icons/tb'
|
|
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
|
|
|
|
export const Route = createFileRoute('/profile')({
|
|
beforeLoad: async ({ context }) => {
|
|
try {
|
|
const data = await context.queryClient.ensureQueryData({
|
|
queryKey: ['auth', 'session'],
|
|
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
|
|
})
|
|
if (!data?.user) throw redirect({ to: '/login' })
|
|
} catch (e) {
|
|
if (e instanceof Error) throw redirect({ to: '/login' })
|
|
throw e
|
|
}
|
|
},
|
|
component: ProfilePage,
|
|
})
|
|
|
|
const roleBadgeColor: Record<string, string> = {
|
|
USER: 'blue',
|
|
ADMIN: 'violet',
|
|
SUPER_ADMIN: 'red',
|
|
}
|
|
|
|
function ProfilePage() {
|
|
const { data } = useSession()
|
|
const logout = useLogout()
|
|
const user = data?.user
|
|
|
|
return (
|
|
<Container size="sm" py="xl">
|
|
<Stack gap="xl">
|
|
<Group justify="space-between">
|
|
<Title order={2}>Profile</Title>
|
|
<Button
|
|
variant="light"
|
|
color="red"
|
|
leftSection={<TbLogout size={16} />}
|
|
onClick={() => logout.mutate()}
|
|
loading={logout.isPending}
|
|
>
|
|
Logout
|
|
</Button>
|
|
</Group>
|
|
|
|
<Paper withBorder p="xl" radius="md">
|
|
<Stack align="center" gap="md">
|
|
<Avatar color="blue" radius="xl" size={80}>
|
|
{user?.name?.charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<Text fw={600} size="lg">{user?.name}</Text>
|
|
<Text c="dimmed" size="sm">{user?.email}</Text>
|
|
</div>
|
|
<Badge color={roleBadgeColor[user?.role ?? 'USER']} variant="light" size="lg">
|
|
{user?.role}
|
|
</Badge>
|
|
</Stack>
|
|
</Paper>
|
|
|
|
<Paper withBorder p="lg" radius="md">
|
|
<Stack gap="sm">
|
|
<Group gap="xs">
|
|
<TbUser size={16} />
|
|
<Text fw={500} size="sm">Account Info</Text>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Name</Text>
|
|
<Text size="sm">{user?.name}</Text>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Email</Text>
|
|
<Text size="sm">{user?.email}</Text>
|
|
</Group>
|
|
<Group justify="space-between">
|
|
<Text size="sm" c="dimmed">Role</Text>
|
|
<Text size="sm">{user?.role}</Text>
|
|
</Group>
|
|
</Stack>
|
|
</Paper>
|
|
</Stack>
|
|
</Container>
|
|
)
|
|
}
|