fix: bootstrap session after password login
This commit is contained in:
@@ -11,9 +11,11 @@ import { isAuthenticated } from '~/utils/auth-storage';
|
||||
|
||||
interface ClientAuthGuardProps {
|
||||
isPublicPath: boolean;
|
||||
frontendJWT?: string;
|
||||
userInfo?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
|
||||
export function ClientAuthGuard({ isPublicPath, frontendJWT, userInfo }: ClientAuthGuardProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -29,15 +31,17 @@ export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查客户端是否已认证(localStorage 中有 token)
|
||||
// 优先用服务端 session 回传的数据回填 localStorage,避免刚登录时客户端误判未登录
|
||||
const token = localStorage.getItem('access_token');
|
||||
const authenticated = isAuthenticated();
|
||||
if (!token && frontendJWT) {
|
||||
localStorage.setItem('access_token', frontendJWT);
|
||||
if (userInfo) {
|
||||
localStorage.setItem('user_info', JSON.stringify(userInfo));
|
||||
}
|
||||
console.log('✅ [Auth Guard] 已根据服务端 session 回填本地认证数据');
|
||||
}
|
||||
|
||||
// console.log('🔍 [Auth Guard] 认证检查', {
|
||||
// token: token ? `${token.substring(0, 20)}...` : null,
|
||||
// authenticated,
|
||||
// pathname: location.pathname
|
||||
// });
|
||||
const authenticated = isAuthenticated() || !!frontendJWT;
|
||||
|
||||
if (!authenticated) {
|
||||
console.log('🔒 [Auth Guard] 未认证,重定向到登录页');
|
||||
@@ -50,7 +54,7 @@ export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
|
||||
} else {
|
||||
// console.log('✅ [Auth Guard] 已认证,允许访问');
|
||||
}
|
||||
}, [isPublicPath, navigate, location.pathname]);
|
||||
}, [isPublicPath, navigate, location.pathname, frontendJWT, userInfo]);
|
||||
|
||||
// 这个组件不渲染任何内容
|
||||
return null;
|
||||
|
||||
+5
-2
@@ -205,6 +205,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
let userRole: UserRole = 'common'; // 默认为普通用户
|
||||
let userArea: string = '';
|
||||
let frontendJWT: string | null = null;
|
||||
let userInfo: any = null;
|
||||
let allowedPaths: string[] = []; // 用户允许访问的路由列表
|
||||
let permissionMap: Record<string, string[]> = {}; // ✅ 权限映射表
|
||||
|
||||
@@ -215,6 +216,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
userRole = session.userRole;
|
||||
userArea = session.userInfo?.area || '';
|
||||
frontendJWT = session.frontendJWT || null;
|
||||
userInfo = session.userInfo || null;
|
||||
|
||||
// 🔑 检查用户角色和JWT是否为空
|
||||
if (!userRole || userRole === '') {
|
||||
@@ -352,6 +354,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
userArea, // ✅ 返回用户所属地区
|
||||
pathname,
|
||||
frontendJWT,
|
||||
userInfo,
|
||||
isPublicPath, // 传递给客户端,用于判断是否需要认证
|
||||
isMobile, // 🔒 传递移动端标识
|
||||
permissionMap, // ✅ 传递权限映射表
|
||||
@@ -390,7 +393,7 @@ export function links() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { userRole, ENV, frontendJWT, isPublicPath, isMobile } = useLoaderData<typeof loader>();
|
||||
const { userRole, ENV, frontendJWT, userInfo, isPublicPath, isMobile } = useLoaderData<typeof loader>();
|
||||
|
||||
|
||||
return (
|
||||
@@ -424,7 +427,7 @@ export default function App() {
|
||||
<MessageModalProvider>
|
||||
<ToastProvider>
|
||||
{/* 客户端认证守卫 - 在客户端检查 localStorage 中的 token */}
|
||||
<ClientAuthGuard isPublicPath={isPublicPath} />
|
||||
<ClientAuthGuard isPublicPath={isPublicPath} frontendJWT={frontendJWT || undefined} userInfo={userInfo || undefined} />
|
||||
|
||||
{/* 🔑 公共路径(登录页、回调页)不显示 Layout,直接渲染内容 */}
|
||||
{isPublicPath ? (
|
||||
|
||||
+4
-29
@@ -152,38 +152,13 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
console.log("👤 [Login Action] 用户角色:", user_info.user_role); // 应该是 "admin"
|
||||
console.log("⏰ [Login Action] Token 有效期:", expires_in, "秒 (", expires_in / 3600, "小时)");
|
||||
|
||||
// 获取当前 URL 用于构建 callback URL
|
||||
const url = new URL(request.url);
|
||||
|
||||
// 🔑 重要:将 token 和用户信息作为 URL 参数传递给客户端
|
||||
// 复用 OAuth 登录的 callback 页面逻辑
|
||||
const callbackUrl = new URL('/callback', url.origin);
|
||||
callbackUrl.searchParams.set('token', access_token);
|
||||
callbackUrl.searchParams.set('userInfo', encodeURIComponent(JSON.stringify({
|
||||
user_id: user_info.user_id,
|
||||
username: user_info.username,
|
||||
nick_name: user_info.nick_name,
|
||||
email: user_info.email,
|
||||
phone_number: user_info.phone_number,
|
||||
ou_id: user_info.ou_id,
|
||||
ou_name: user_info.ou_name,
|
||||
is_leader: user_info.is_leader,
|
||||
user_role: user_info.user_role,
|
||||
area: user_info.area,
|
||||
sub: user_info.sub,
|
||||
// 🔑 包含后端返回的组织信息字段(可能为null)
|
||||
tenant_name: user_info.tenant_name,
|
||||
dep_name: user_info.dep_name,
|
||||
dep_short_name: user_info.dep_short_name,
|
||||
})));
|
||||
callbackUrl.searchParams.set('redirectTo', redirectTo);
|
||||
|
||||
// ✅ 使用统一的 session 创建函数(和 OAuth 登录一样)
|
||||
// ✅ 账密登录直接写入 Cookie Session 并跳首页
|
||||
// localStorage 由 root 中的客户端会话引导逻辑补写,避免 callback 跳转链路卡住
|
||||
return createUserSession({
|
||||
isAuthenticated: true,
|
||||
userRole: user_info.user_role,
|
||||
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token
|
||||
frontendJWT: access_token, // 保存到 Cookie Session
|
||||
redirectTo,
|
||||
frontendJWT: access_token,
|
||||
tokenExpiresIn: expires_in,
|
||||
tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间
|
||||
userInfo: {
|
||||
|
||||
Reference in New Issue
Block a user