Files
external-app/app/DashboardPage.tsx
zhangheng f737603b0c 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>
2026-07-06 18:11:23 +08:00

173 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
)
}