chore: initial commit of external-app SSO consumer
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>
This commit is contained in:
172
app/DashboardPage.tsx
Normal file
172
app/DashboardPage.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user