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