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>
This commit is contained in:
2026-04-28 15:06:13 +08:00
parent 9d80eb3b85
commit 94724a5081
9 changed files with 219 additions and 21 deletions

View File

@@ -1,4 +1,5 @@
import {
Alert,
Avatar,
Badge,
Button,
@@ -10,7 +11,7 @@ import {
Title,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { TbLogout, TbUser } from 'react-icons/tb'
import { TbClock, TbLogout, TbUser } from 'react-icons/tb'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
export const Route = createFileRoute('/profile')({
@@ -30,6 +31,7 @@ export const Route = createFileRoute('/profile')({
})
const roleBadgeColor: Record<string, string> = {
USER: 'gray',
ADMIN: 'violet',
DEVELOPER: 'red',
}
@@ -55,9 +57,26 @@ function ProfilePage() {
</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 color="blue" radius="xl" size={80}>
<Avatar
src={user?.image ?? undefined}
color="blue"
radius="xl"
size={80}
>
{user?.name?.charAt(0).toUpperCase()}
</Avatar>
<div style={{ textAlign: 'center' }}>