Next.js sample application that integrates with the mock SSO service: redirects to sso-mock for login, receives the one-time sso_code on its /callback route, and exchanges it for user info to establish a session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
173 lines
5.5 KiB
TypeScript
173 lines
5.5 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect } from 'react'
|
||
import {
|
||
AppShell,
|
||
Group,
|
||
Text,
|
||
Button,
|
||
Avatar,
|
||
Paper,
|
||
Stack,
|
||
Badge,
|
||
Code,
|
||
Divider,
|
||
SimpleGrid,
|
||
Menu,
|
||
} from '@mantine/core'
|
||
import {
|
||
IconLogout,
|
||
IconUser,
|
||
IconShield,
|
||
IconServer,
|
||
IconKey,
|
||
} from '@tabler/icons-react'
|
||
|
||
interface UserInfo {
|
||
id: string
|
||
account: string
|
||
name: string
|
||
phone: string
|
||
roles: string[]
|
||
}
|
||
|
||
export default function DashboardPage({ user }: { user: UserInfo | null }) {
|
||
// 未登录 -> 跳转 SSO
|
||
useEffect(() => {
|
||
if (!user) {
|
||
const appId = 'app_demo_001'
|
||
const callbackUrl = encodeURIComponent('http://localhost:4000/callback')
|
||
window.location.href = `http://localhost:5501/login?app_id=${appId}&app_url=${callbackUrl}`
|
||
}
|
||
}, [user])
|
||
|
||
if (!user) {
|
||
return (
|
||
<Stack align="center" justify="center" h="100vh">
|
||
<Text size="lg" c="dimmed">正在跳转到 SSO 登录页面...</Text>
|
||
</Stack>
|
||
)
|
||
}
|
||
|
||
const handleLogout = async () => {
|
||
await fetch('/api/logout', { method: 'POST' })
|
||
// 跳到 SSO 登录页(带上 app_id 让 SSO 知道是哪个应用)
|
||
const appId = 'app_demo_001'
|
||
const callbackUrl = encodeURIComponent('http://localhost:4000/callback')
|
||
window.location.href = `http://localhost:5501/login?app_id=${appId}&app_url=${callbackUrl}`
|
||
}
|
||
|
||
return (
|
||
<AppShell header={{ height: 60 }} padding="md">
|
||
<AppShell.Header>
|
||
<Group h="100%" px="md" justify="space-between">
|
||
<Group gap="sm">
|
||
<IconServer size={24} color="#0080FF" />
|
||
<Text fw={700} size="lg">外部应用 Demo</Text>
|
||
<Badge color="green" variant="light">已登录</Badge>
|
||
</Group>
|
||
<Menu shadow="md" width={220}>
|
||
<Menu.Target>
|
||
<Group gap="sm" style={{ cursor: 'pointer' }}>
|
||
<Avatar color="brand" radius="xl" size="sm">
|
||
{user.name?.[0] || 'U'}
|
||
</Avatar>
|
||
<Text size="sm" fw={500}>{user.name}</Text>
|
||
</Group>
|
||
</Menu.Target>
|
||
<Menu.Dropdown>
|
||
<Menu.Item leftSection={<IconUser size={14} />}>{user.account}</Menu.Item>
|
||
<Menu.Divider />
|
||
<Menu.Item leftSection={<IconLogout size={14} />} color="red" onClick={handleLogout}>
|
||
退出登录
|
||
</Menu.Item>
|
||
</Menu.Dropdown>
|
||
</Menu>
|
||
</Group>
|
||
</AppShell.Header>
|
||
|
||
<AppShell.Main>
|
||
<Stack maw={800} mx="auto" pt="xl">
|
||
<Text size="xl" fw={700}>登录成功!</Text>
|
||
<Text c="dimmed">您已通过 Mock SSO 认证登录到此应用。</Text>
|
||
|
||
<SimpleGrid cols={{ base: 1, sm: 2 }} mt="md">
|
||
<Paper withBorder p="lg" radius="md">
|
||
<Group gap="sm" mb="md">
|
||
<IconUser size={20} color="#0080FF" />
|
||
<Text fw={600}>用户信息</Text>
|
||
</Group>
|
||
<Stack gap="xs">
|
||
<Group>
|
||
<Text size="sm" c="dimmed" w={80}>ID</Text>
|
||
<Code>{user.id}</Code>
|
||
</Group>
|
||
<Group>
|
||
<Text size="sm" c="dimmed" w={80}>账号</Text>
|
||
<Text size="sm">{user.account}</Text>
|
||
</Group>
|
||
<Group>
|
||
<Text size="sm" c="dimmed" w={80}>姓名</Text>
|
||
<Text size="sm">{user.name}</Text>
|
||
</Group>
|
||
<Group>
|
||
<Text size="sm" c="dimmed" w={80}>手机</Text>
|
||
<Text size="sm">{user.phone}</Text>
|
||
</Group>
|
||
</Stack>
|
||
</Paper>
|
||
|
||
<Paper withBorder p="lg" radius="md">
|
||
<Group gap="sm" mb="md">
|
||
<IconShield size={20} color="#20C997" />
|
||
<Text fw={600}>角色权限</Text>
|
||
</Group>
|
||
<Group gap="xs">
|
||
{user.roles?.map((role: string) => (
|
||
<Badge key={role} variant="light">{role}</Badge>
|
||
))}
|
||
</Group>
|
||
</Paper>
|
||
</SimpleGrid>
|
||
|
||
<Divider my="md" />
|
||
|
||
<Paper withBorder p="lg" radius="md">
|
||
<Group gap="sm" mb="md">
|
||
<IconKey size={20} color="#F59F00" />
|
||
<Text fw={600}>SSO 流程说明</Text>
|
||
</Group>
|
||
<Stack gap="sm">
|
||
<Text size="sm">
|
||
<strong>1.</strong> 您打开此应用(<Code>http://localhost:4000</Code>)
|
||
</Text>
|
||
<Text size="sm">
|
||
<strong>2.</strong> 检测到未登录,自动跳转到 Mock SSO(<Code>http://localhost:5501/login</Code>)
|
||
</Text>
|
||
<Text size="sm">
|
||
<strong>3.</strong> 在 SSO 页面完成登录,SSO 返回 <Code>sso_code</Code>
|
||
</Text>
|
||
<Text size="sm">
|
||
<strong>4.</strong> 此应用用 <Code>sso_code</Code> 调用 <Code>POST /api/auth/verify</Code> 换取用户信息
|
||
</Text>
|
||
<Text size="sm">
|
||
<strong>5.</strong> 用户信息存入 Cookie,显示此页面
|
||
</Text>
|
||
</Stack>
|
||
</Paper>
|
||
|
||
<Group justify="center" mt="xl">
|
||
<Button
|
||
color="red"
|
||
variant="light"
|
||
leftSection={<IconLogout size={16} />}
|
||
onClick={handleLogout}>
|
||
退出登录(跳转回 SSO)
|
||
</Button>
|
||
</Group>
|
||
</Stack>
|
||
</AppShell.Main>
|
||
</AppShell>
|
||
)
|
||
}
|