Files
easyai-ai-gateway/apps/web/src/components/AuthPanel.tsx
T
chengcheng c0296dbf06 fix(identity): 支持平台用户显式登录 AI Gateway
修复多租户身份配置将所有人类登录都强制解释为租户上下文的问题。Web 现在提供平台与租户两个受控入口,API 严格绑定 context_type、tid、issuer、application 和 subject,并为平台用户建立独立本地投影与可刷新会话。\n\n风险:新增会话身份列保持旧会话可读,新会话一律使用严格约束;未改变租户数据隔离和 API Key 行为。\n\n验证:Go 全量测试与 go vet 通过;PostgreSQL 平台投影、会话和安全事件集成测试通过;前端 lint、141 项测试与生产 build 通过;OpenAPI 生成和迁移安全测试通过。
2026-07-31 14:24:40 +08:00

165 lines
6.1 KiB
TypeScript

import type { FormEvent } from 'react';
import { LogIn, UserPlus } from 'lucide-react';
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, PasswordInput, Tabs } from './ui';
import type { AuthMode, LoadState, LoginForm, RegisterForm } from '../types';
import type { OIDCContextType } from '../lib/oidc';
const tabs = [
{ value: 'login', label: '账号登录' },
{ value: 'register', label: '注册账号' },
{ value: 'external', label: '外部 Token' },
] satisfies Array<{ value: AuthMode; label: string }>;
export function AuthPanel(props: {
authMode: AuthMode;
externalToken: string;
loginForm: LoginForm;
registerForm: RegisterForm;
state: LoadState;
onAuthModeChange: (value: AuthMode) => void;
onExternalTokenChange: (value: string) => void;
onLoginChange: (value: LoginForm) => void;
onRegisterChange: (value: RegisterForm) => void;
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
onSubmitLogin: (event: FormEvent<HTMLFormElement>) => void;
onSubmitRegister: (event: FormEvent<HTMLFormElement>) => void;
oidcEnabled: boolean;
oidcContextTypes: readonly OIDCContextType[];
onOIDCLogin: (contextType: OIDCContextType) => void;
}) {
return (
<section className="authShell" aria-label="登录">
<Card className="authCard">
<CardHeader>
<div>
<p className="eyebrow">Gateway Identity</p>
<CardTitle>登录 AI Gateway</CardTitle>
</div>
</CardHeader>
<CardContent className="authContent">
{props.oidcEnabled ? (
<div className="formGrid">
{props.oidcContextTypes.includes('platform') && (
<Button
type="button"
disabled={props.state === 'loading'}
onClick={() => props.onOIDCLogin('platform')}
>
<LogIn size={15} />
使用平台账号登录
</Button>
)}
{props.oidcContextTypes.includes('tenant') && (
<Button
type="button"
disabled={props.state === 'loading'}
onClick={() => props.onOIDCLogin('tenant')}
>
<LogIn size={15} />
使用租户账号登录
</Button>
)}
</div>
) : (
<>
<Tabs value={props.authMode} tabs={tabs} onValueChange={props.onAuthModeChange} />
{props.authMode === 'login' && <LocalLoginForm {...props} />}
{props.authMode === 'register' && <RegisterFormView {...props} />}
{props.authMode === 'external' && <ExternalTokenForm {...props} />}
</>
)}
</CardContent>
</Card>
</section>
);
}
export function LocalLoginForm(props: {
loginForm: LoginForm;
state: LoadState;
onLoginChange: (value: LoginForm) => void;
onSubmitLogin: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="formGrid" onSubmit={props.onSubmitLogin}>
<Label>
账号
<Input
autoComplete="username"
value={props.loginForm.account}
onChange={(event) => props.onLoginChange({ ...props.loginForm, account: event.target.value })}
placeholder="用户名或邮箱"
/>
</Label>
<Label>
密码
<PasswordInput
autoComplete="current-password"
value={props.loginForm.password}
onChange={(event) => props.onLoginChange({ ...props.loginForm, password: event.target.value })}
placeholder="至少 8 位"
/>
</Label>
<Button type="submit" disabled={props.state === 'loading'}>
<LogIn size={15} />
{props.state === 'loading' ? '登录中' : '登录'}
</Button>
</form>
);
}
function RegisterFormView(props: {
registerForm: RegisterForm;
state: LoadState;
onRegisterChange: (value: RegisterForm) => void;
onSubmitRegister: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="formGrid two" onSubmit={props.onSubmitRegister}>
<Label>
用户名
<Input autoComplete="username" value={props.registerForm.username} onChange={(event) => props.onRegisterChange({ ...props.registerForm, username: event.target.value })} />
</Label>
<Label>
邮箱
<Input autoComplete="email" type="email" value={props.registerForm.email} onChange={(event) => props.onRegisterChange({ ...props.registerForm, email: event.target.value })} />
</Label>
<Label>
显示名
<Input value={props.registerForm.displayName} onChange={(event) => props.onRegisterChange({ ...props.registerForm, displayName: event.target.value })} />
</Label>
<Label>
密码
<Input autoComplete="new-password" type="password" value={props.registerForm.password} onChange={(event) => props.onRegisterChange({ ...props.registerForm, password: event.target.value })} />
</Label>
<Label>
邀请码
<Input value={props.registerForm.invitationCode} onChange={(event) => props.onRegisterChange({ ...props.registerForm, invitationCode: event.target.value })} placeholder="可选" />
</Label>
<Button type="submit" disabled={props.state === 'loading'} className="spanTwo">
<UserPlus size={15} />
{props.state === 'loading' ? '注册中' : '注册并登录'}
</Button>
</form>
);
}
function ExternalTokenForm(props: {
externalToken: string;
state: LoadState;
onExternalTokenChange: (value: string) => void;
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="formGrid" onSubmit={props.onSubmitExternalToken}>
<Label>
Access Token
<Input value={props.externalToken} onChange={(event) => props.onExternalTokenChange(event.target.value)} placeholder="粘贴 server-main access token" />
</Label>
<Button type="submit" disabled={props.state === 'loading'}>
{props.state === 'loading' ? '验证中' : '进入控制台'}
</Button>
</form>
);
}