Files
ln-bi/src/auth/AuthProvider.tsx
T
kkfluousandClaude Opus 4.7 26f7d7ab3f
ci/woodpecker/push/woodpecker Pipeline was successful
feat(auth): 能源管理模块需要 BI-LEADER-ENERGY 角色
- 新增 ENERGY_ACCESS_ROLES 与 canAccessEnergy(roles) 守卫(全量权限角色亦可访问)
- 后端 /api/energy/* 加模块级守卫:无角色返回 403
- 前端 App.tsx 按角色动态注入 EnergyModule,无权限时主导航不显示
- dev mock 用户(前端 + 后端)追加 BI-LEADER-ENERGY 便于本地调试

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:55:29 +08:00

128 lines
4.0 KiB
TypeScript

import { useState, useEffect, useRef, type ReactNode } from 'react';
import { AuthContext, type AuthState } from './useAuth';
import { setTokenGetter } from './api-client';
const AUTH_API = '/api/auth';
export default function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
isLoading: true,
isAuthenticated: false,
user: null,
error: null,
});
const tokenRef = useRef<string | null>(null);
const authStarted = useRef(false);
useEffect(() => {
// 设置全局 token getter
setTokenGetter(() => tokenRef.current);
// 防止 StrictMode 双重调用(jumpToken 一次性使用)
if (authStarted.current) return;
authStarted.current = true;
// 监听 401 事件
const onUnauthorized = () => {
tokenRef.current = null;
sessionStorage.removeItem('bi_jwt');
setState({ isLoading: false, isAuthenticated: false, user: null, error: '会话已过期' });
};
window.addEventListener('auth:unauthorized', onUnauthorized);
authenticate();
return () => window.removeEventListener('auth:unauthorized', onUnauthorized);
}, []);
async function authenticate() {
// 本地开发免登录开关:.env 里设 VITE_DEV_BYPASS_AUTH=1 启用,仅 dev 生效
if (import.meta.env.DEV && import.meta.env.VITE_DEV_BYPASS_AUTH === '1') {
setState({
isLoading: false,
isAuthenticated: true,
user: {
userId: 'dev-local',
userName: '本地开发',
permissionLevel: 'full',
depName: '',
roles: ['所有权限', 'BI-SCHEDULE-OPT', 'BI-ADMIN-FEEDBACK', 'BI-LEADER-ENERGY'],
},
error: null,
});
return;
}
// 1. 检查 sessionStorage 中是否有 JWT
const savedToken = sessionStorage.getItem('bi_jwt');
if (savedToken) {
tokenRef.current = savedToken;
// 验证 token 是否仍然有效(尝试请求 health)
try {
const res = await fetch('/api/health', {
headers: { Authorization: `Bearer ${savedToken}` },
});
if (res.ok) {
const savedUser = sessionStorage.getItem('bi_user');
setState({
isLoading: false,
isAuthenticated: true,
user: savedUser ? JSON.parse(savedUser) : null,
error: null,
});
return;
}
} catch { /* token 无效,继续流程 */ }
sessionStorage.removeItem('bi_jwt');
sessionStorage.removeItem('bi_user');
}
// 2. 从 URL 提取 jumpToken
const params = new URLSearchParams(window.location.search);
const jumpToken = params.get('jumpToken');
if (!jumpToken) {
setState({ isLoading: false, isAuthenticated: false, user: null, error: '请从业务系统跳转访问' });
return;
}
try {
// 3. 一步完成:jumpToken → 用户信息 + JWT
const res = await fetch(`${AUTH_API}/exchange?jumpToken=${encodeURIComponent(jumpToken)}`);
const data = await res.json();
if (!res.ok || !data.token) {
setState({ isLoading: false, isAuthenticated: false, user: null, error: data.message || '跳转令牌无效或已过期' });
return;
}
// 4. 存储 JWT
tokenRef.current = data.token;
sessionStorage.setItem('bi_jwt', data.token);
sessionStorage.setItem('bi_user', JSON.stringify(data.user));
// 6. 清除 URL 中的 jumpToken
params.delete('jumpToken');
const cleanUrl = params.toString()
? `${window.location.pathname}?${params.toString()}${window.location.hash}`
: `${window.location.pathname}${window.location.hash}`;
window.history.replaceState({}, '', cleanUrl);
setState({
isLoading: false,
isAuthenticated: true,
user: data.user,
error: null,
});
} catch (e) {
setState({ isLoading: false, isAuthenticated: false, user: null, error: '认证过程出错' });
}
}
return (
<AuthContext.Provider value={state}>
{children}
</AuthContext.Provider>
);
}