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,20 +1,25 @@
import { APP_CONFIGS } from '@/frontend/config/appMenus'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
import React from 'react'
import {
ActionIcon,
Alert,
AppShell,
Avatar,
Box,
Burger,
Button,
Center,
Group,
Loader,
LoadingOverlay,
Menu,
NavLink,
Select,
Stack,
Text,
ThemeIcon,
Title,
useComputedColorScheme,
useMantineColorScheme
} from '@mantine/core'
@@ -26,6 +31,7 @@ import {
TbApps,
TbArrowLeft,
TbChevronRight,
TbClock,
TbDashboard,
TbDeviceMobile,
TbHistory,
@@ -54,10 +60,17 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const currentPath = matches[matches.length - 1]?.pathname
// ─── Connect to auth system ──────────────────────────
const { data: sessionData } = useSession()
const { data: sessionData, isLoading: sessionLoading } = useSession()
const user = sessionData?.user
const logout = useLogout()
// Redirect USER role to profile (pending approval)
React.useEffect(() => {
if (!sessionLoading && user?.role === 'USER') {
navigate({ to: '/profile' })
}
}, [user?.role, sessionLoading, navigate])
// ─── Fetch registered apps from database ─────────────
const { data: appsData } = useQuery({
queryKey: ['apps'],
@@ -99,6 +112,15 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
logout.mutate()
}
// Prevent dashboard flash for USER role while redirect is happening
if (sessionLoading || user?.role === 'USER') {
return (
<Center mih="100vh">
<LoadingOverlay visible />
</Center>
)
}
return (
<AppShell
header={{ height: 70 }}