Files
monitoring-app/src/frontend/routes/profile.tsx
amaliadwiy 94724a5081 feat: add Google OAuth login with USER role and pending approval flow
- Add GET /api/auth/google and GET /api/auth/callback/google routes with CSRF state protection and account linking via googleId
- Add getPublicOrigin() for dynamic redirect_uri (supports reverse proxy via X-Forwarded-Proto)
- Add USER role to schema (default for new Google sign-ins), make password optional, add googleId and image fields
- Role-based redirect after login: USER → /profile, ADMIN/DEVELOPER → /dashboard
- Profile page shows pending approval alert for USER role
- Dashboard redirects USER role back to profile
- Login page shows specific error messages per OAuth error code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:06:13 +08:00

116 lines
3.3 KiB
TypeScript

import {
Alert,
Avatar,
Badge,
Button,
Container,
Group,
Paper,
Stack,
Text,
Title,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { TbClock, 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: 'gray',
ADMIN: 'violet',
DEVELOPER: '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>
{user?.role === 'USER' && (
<Alert
icon={<TbClock size={18} />}
title="Akun Menunggu Persetujuan"
color="yellow"
variant="light"
radius="md"
>
Akun kamu sedang menunggu persetujuan admin. Hubungi admin atau developer untuk mendapatkan akses ke fitur dashboard.
</Alert>
)}
<Paper withBorder p="xl" radius="md">
<Stack align="center" gap="md">
<Avatar
src={user?.image ?? undefined}
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>
)
}