feat: add workflow studio and postgres support
This commit is contained in:
@@ -31,14 +31,20 @@ async function readErrorReason(res: Response): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
type AdminLoginMode = 'token' | 'password';
|
||||
|
||||
export default function AdminLoginPage({ t, onLogin }: AdminLoginPageProps) {
|
||||
const [mode, setMode] = useState<AdminLoginMode>('token');
|
||||
const [token, setToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loadingMode, setLoadingMode] = useState<AdminLoginMode | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleTokenLogin = async () => {
|
||||
const handleTokenLogin = async (event?: React.FormEvent) => {
|
||||
event?.preventDefault();
|
||||
if (!token.trim()) return;
|
||||
setLoading(true);
|
||||
setLoadingMode('token');
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('/api/settings/auth/info', {
|
||||
@@ -54,58 +60,198 @@ export default function AdminLoginPage({ t, onLogin }: AdminLoginPageProps) {
|
||||
} catch {
|
||||
setError(t(resolveLoginErrorMessage('network', '')));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingMode(null);
|
||||
}
|
||||
};
|
||||
|
||||
const canSubmit = !!token.trim();
|
||||
const handlePasswordLogin = async (event?: React.FormEvent) => {
|
||||
event?.preventDefault();
|
||||
if (!email.trim() || !password) return;
|
||||
setLoadingMode('password');
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email.trim(), password }),
|
||||
});
|
||||
const text = await res.text();
|
||||
let parsed: {
|
||||
token?: unknown;
|
||||
user?: { role?: unknown };
|
||||
data?: { token?: unknown; user?: { role?: unknown } };
|
||||
message?: unknown;
|
||||
error?: unknown;
|
||||
} = {};
|
||||
if (text) {
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
parsed = { message: text };
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
const reason =
|
||||
typeof parsed.message === 'string'
|
||||
? parsed.message
|
||||
: typeof parsed.error === 'string'
|
||||
? parsed.error
|
||||
: text;
|
||||
setError(t(resolveLoginErrorMessage(res.status, reason)));
|
||||
return;
|
||||
}
|
||||
|
||||
const jwt = typeof parsed.token === 'string' ? parsed.token : parsed.data?.token;
|
||||
const role =
|
||||
typeof parsed.user?.role === 'string' ? parsed.user.role : parsed.data?.user?.role;
|
||||
if (typeof jwt !== 'string' || !jwt) {
|
||||
setError(t('登录响应缺少访问令牌。'));
|
||||
return;
|
||||
}
|
||||
if (role !== 'admin') {
|
||||
setError(t('该账号不是管理员账号。'));
|
||||
return;
|
||||
}
|
||||
commitSession('admin', jwt);
|
||||
onLogin(jwt);
|
||||
} catch {
|
||||
setError(t(resolveLoginErrorMessage('network', '')));
|
||||
} finally {
|
||||
setLoadingMode(null);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = loadingMode !== null;
|
||||
const canTokenSubmit = !!token.trim();
|
||||
const canPasswordSubmit = !!email.trim() && !!password;
|
||||
|
||||
return (
|
||||
<LoginShell
|
||||
t={t}
|
||||
variant="admin"
|
||||
eyebrow={t('管理员入口')}
|
||||
title={t('登录')}
|
||||
description={t('请输入管理员令牌后继续。')}
|
||||
title={t('管理员登录')}
|
||||
description={t('使用管理员令牌,或管理员账号密码进入控制台。')}
|
||||
error={error}
|
||||
footer={<span>{t('管理员登录后继续。')}</span>}
|
||||
footer={<span>{t('管理员身份用于管理站点、模型路由、用户与计费。')}</span>}
|
||||
>
|
||||
<label className="login-auth-label" htmlFor="admin-token-input">
|
||||
{t('管理员令牌')}
|
||||
</label>
|
||||
<input
|
||||
id="admin-token-input"
|
||||
type="password"
|
||||
placeholder={t('管理员令牌')}
|
||||
value={token}
|
||||
onChange={(e) => {
|
||||
setToken(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleTokenLogin()}
|
||||
className="login-auth-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
onClick={handleTokenLogin}
|
||||
disabled={loading || !canSubmit}
|
||||
className="btn btn-primary login-auth-submit"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner spinner-sm"
|
||||
style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }}
|
||||
/>
|
||||
{t('验证中...')}
|
||||
</>
|
||||
) : (
|
||||
t('登录')
|
||||
)}
|
||||
</button>
|
||||
<div className="login-auth-note">
|
||||
{t('仅校验本地服务访问权限,不会把令牌发送到第三方。')}
|
||||
<div className="login-mode-tabs" role="tablist" aria-label={t('管理员登录方式')}>
|
||||
<button
|
||||
type="button"
|
||||
className={`login-mode-tab${mode === 'token' ? ' active' : ''}`}
|
||||
onClick={() => {
|
||||
setMode('token');
|
||||
setError('');
|
||||
}}
|
||||
aria-selected={mode === 'token'}
|
||||
>
|
||||
Auth Token
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`login-mode-tab${mode === 'password' ? ' active' : ''}`}
|
||||
onClick={() => {
|
||||
setMode('password');
|
||||
setError('');
|
||||
}}
|
||||
aria-selected={mode === 'password'}
|
||||
>
|
||||
{t('账号密码')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mode === 'token' ? (
|
||||
<form onSubmit={handleTokenLogin}>
|
||||
<label className="login-auth-label" htmlFor="admin-token-input">
|
||||
{t('管理员令牌')}
|
||||
</label>
|
||||
<input
|
||||
id="admin-token-input"
|
||||
type="password"
|
||||
placeholder={t('管理员令牌')}
|
||||
value={token}
|
||||
onChange={(e) => {
|
||||
setToken(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
className="login-auth-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !canTokenSubmit}
|
||||
className="btn btn-primary login-auth-submit"
|
||||
>
|
||||
{loadingMode === 'token' ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner spinner-sm"
|
||||
style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }}
|
||||
/>
|
||||
{t('验证中...')}
|
||||
</>
|
||||
) : (
|
||||
t('进入控制台')
|
||||
)}
|
||||
</button>
|
||||
<div className="login-auth-note">
|
||||
{t('仅校验本地服务访问权限,不会把令牌发送到第三方。')}
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={handlePasswordLogin}>
|
||||
<label className="login-auth-label" htmlFor="admin-email-input">
|
||||
{t('管理员邮箱')}
|
||||
</label>
|
||||
<input
|
||||
id="admin-email-input"
|
||||
type="email"
|
||||
placeholder={t('管理员邮箱')}
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
className="login-auth-input"
|
||||
autoComplete="username"
|
||||
/>
|
||||
<label className="login-auth-label" htmlFor="admin-password-input">
|
||||
{t('密码')}
|
||||
</label>
|
||||
<input
|
||||
id="admin-password-input"
|
||||
type="password"
|
||||
placeholder={t('密码')}
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
className="login-auth-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !canPasswordSubmit}
|
||||
className="btn btn-primary login-auth-submit"
|
||||
>
|
||||
{loadingMode === 'password' ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner spinner-sm"
|
||||
style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }}
|
||||
/>
|
||||
{t('验证中...')}
|
||||
</>
|
||||
) : (
|
||||
t('进入控制台')
|
||||
)}
|
||||
</button>
|
||||
<div className="login-auth-note">
|
||||
{t('只有 role=admin 的账号可以进入管理员控制台。')}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</LoginShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { SITE_DOCS_URL, SITE_GITHUB_URL } from '../../docsLink.js';
|
||||
|
||||
export type LoginShellVariant = 'admin' | 'user';
|
||||
|
||||
@@ -82,31 +81,6 @@ export default function LoginShell({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="login-brand-footer">
|
||||
<a
|
||||
href={SITE_GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="login-icon-link"
|
||||
aria-label="GitHub"
|
||||
title="GitHub"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" className="login-icon-link-svg">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2C6.48 2 2 6.59 2 12.25c0 4.53 2.87 8.38 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-1.04-.01-1.88-2.78.62-3.37-1.22-3.37-1.22-.45-1.2-1.11-1.52-1.11-1.52-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.35 1.12 2.92.85.09-.67.35-1.12.64-1.38-2.22-.26-4.55-1.15-4.55-5.13 0-1.13.39-2.05 1.03-2.77-.1-.26-.45-1.31.1-2.73 0 0 .84-.28 2.75 1.06A9.3 9.3 0 0 1 12 6.91c.85 0 1.71.12 2.51.35 1.91-1.34 2.75-1.06 2.75-1.06.55 1.42.2 2.47.1 2.73.64.72 1.03 1.64 1.03 2.77 0 3.99-2.34 4.86-4.57 5.12.36.33.68.97.68 1.96 0 1.42-.01 2.56-.01 2.91 0 .27.18.59.69.49A10.27 10.27 0 0 0 22 12.25C22 6.59 17.52 2 12 2Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href={SITE_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="login-doc-link"
|
||||
>
|
||||
{t('部署文档')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user