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:
zhangheng
2026-07-06 18:11:23 +08:00
commit f737603b0c
14 changed files with 1639 additions and 0 deletions

172
app/DashboardPage.tsx Normal file
View 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>
)
}