revert: restore v1.1.14 (37a9f303)
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
shishengliang
2026-09-16 09:29:30 +08:00
parent c49a02e3d9
commit 6ea1b3d5dd
210 changed files with 4448 additions and 36179 deletions
+8 -34
View File
@@ -5,7 +5,6 @@ import { setTokenGetter } from './api-client';
const AUTH_API = '/api/auth';
export default function AuthProvider({ children }: { children: ReactNode }) {
const [mode, setMode] = useState<'sso' | 'password'>('sso');
const [state, setState] = useState<AuthState>({
isLoading: true,
isAuthenticated: false,
@@ -20,6 +19,8 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
setTokenGetter(() => tokenRef.current);
// 防止 StrictMode 双重调用(jumpToken 一次性使用)
if (authStarted.current) return;
authStarted.current = true;
// 监听 401 事件
const onUnauthorized = () => {
@@ -29,25 +30,14 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
};
window.addEventListener('auth:unauthorized', onUnauthorized);
if (!authStarted.current) { authStarted.current = true; authenticate(); }
authenticate();
return () => window.removeEventListener('auth:unauthorized', onUnauthorized);
}, []);
async function authenticate() {
let currentMode: 'sso' | 'password';
try {
const response = await fetch(`${AUTH_API}/config`, { cache: 'no-store' });
const config = await response.json();
if (!response.ok || !['sso', 'password'].includes(config.mode)) throw new Error();
currentMode = config.mode;
setMode(currentMode);
} catch {
setState({ isLoading: false, isAuthenticated: false, user: null, error: '无法获取验证配置,请刷新重试' });
return;
}
// 本地开发免登录开关:.env 里设 VITE_DEV_BYPASS_AUTH=1 启用,仅 dev 生效
if (currentMode === 'sso' && import.meta.env.DEV && import.meta.env.VITE_DEV_BYPASS_AUTH === '1') {
if (import.meta.env.DEV && import.meta.env.VITE_DEV_BYPASS_AUTH === '1') {
setState({
isLoading: false,
isAuthenticated: true,
@@ -69,15 +59,15 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
tokenRef.current = savedToken;
// 验证 token 是否仍然有效(尝试请求 health)
try {
const res = await fetch(`${AUTH_API}/me`, {
const res = await fetch('/api/health', {
headers: { Authorization: `Bearer ${savedToken}` },
});
if (res.ok) {
const savedUser = await res.json();
const savedUser = sessionStorage.getItem('bi_user');
setState({
isLoading: false,
isAuthenticated: true,
user: savedUser,
user: savedUser ? JSON.parse(savedUser) : null,
error: null,
});
return;
@@ -85,12 +75,6 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
} catch { /* token 无效,继续流程 */ }
sessionStorage.removeItem('bi_jwt');
sessionStorage.removeItem('bi_user');
tokenRef.current = null;
}
if (currentMode === 'password') {
setState({ isLoading: false, isAuthenticated: false, user: null, error: null });
return;
}
// 2. 从 URL 提取 jumpToken
@@ -135,18 +119,8 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
}
}
async function loginWithPassword(password: string) {
const response = await fetch(`${AUTH_API}/password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) });
const data = await response.json();
if (!response.ok || !data.token) throw new Error(data.message || '登录失败');
tokenRef.current = data.token;
sessionStorage.setItem('bi_jwt', data.token);
sessionStorage.setItem('bi_user', JSON.stringify(data.user));
setState({ isLoading: false, isAuthenticated: true, user: data.user, error: null });
}
return (
<AuthContext.Provider value={{ ...state, mode, loginWithPassword }}>
<AuthContext.Provider value={state}>
{children}
</AuthContext.Provider>
);
-25
View File
@@ -1,25 +0,0 @@
import { useState } from 'react';
import { useAuth } from './useAuth';
export default function PasswordLogin() {
const { loginWithPassword, error: sessionError } = useAuth();
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
return <main className="min-h-screen flex items-center justify-center bg-slate-950 p-6">
<form className="w-full max-w-sm rounded-2xl bg-white p-7 shadow-xl" onSubmit={async event => {
event.preventDefault(); if (busy || !loginWithPassword) return;
setBusy(true); setError('');
try { await loginWithPassword(password); } catch (e) { setError(e instanceof Error ? e.message : '登录失败,请重试'); }
finally { setBusy(false); setPassword(''); }
}}>
<p className="text-xs font-semibold text-blue-600">羚牛 · 能源 BI</p>
<h1 className="mt-2 text-2xl font-bold text-slate-900">访问看板</h1>
<p className="mt-2 mb-6 text-sm text-slate-500">请输入管理员提供的访问密码。此入口仅提供只读访问。</p>
<label htmlFor="bi-password" className="text-sm font-medium text-slate-700">访问密码</label>
<input id="bi-password" type="password" autoComplete="current-password" required maxLength={1024} value={password} onChange={e => setPassword(e.target.value)} className="mt-2 w-full rounded-lg border border-slate-300 px-3 py-3 text-base focus:outline-blue-600" />
{error || sessionError ? <p role="alert" className="mt-3 text-sm text-red-600">{error || sessionError}</p> : null}
<button disabled={busy || !password} className="mt-5 min-h-11 w-full rounded-lg bg-blue-600 px-4 py-3 font-semibold text-white disabled:opacity-50">{busy ? '正在验证…' : '进入看板'}</button>
</form>
</main>;
}
-2
View File
@@ -1,8 +1,6 @@
import { createContext, useContext } from 'react';
export interface AuthState {
mode?: 'sso' | 'password';
loginWithPassword?: (password: string) => Promise<void>;
isLoading: boolean;
isAuthenticated: boolean;
user: {