Files
leaudit-platform-frontend/app/api/login/auth.server.ts
T
2025-11-18 11:06:24 +08:00

907 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 认证服务核心实现
*
* 这个文件包含了用户认证、会话管理、Token 刷新等核心功能的具体实现。
*
* 主要功能:
* - 基于 Cookie 的会话管理
* - OAuth2.0 Token 自动刷新
* - 用户登录状态检查
* - 会话创建和销毁
* - 用户信息保存到数据库
*
* 技术栈:
* - Remix Session Storage (Cookie-based)
* - OAuth2.0 Token Management
* - TypeScript 类型安全
*
*/
import { createCookieSessionStorage } from "@remix-run/node";
import { tokenManager } from "./token-manager.server";
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
/**
* 用户角色类型定义
*
* @typedef {string} UserRole
* @property {'common'} common - 普通用户,有基本的系统访问权限
* @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限
*/
export type UserRole = 'common' | 'admin' | 'deptLeader' | 'groupLeader' | string;
/**
* 用户信息接口,对应 sso_users 表结构
*/
export interface UserInfo {
sub: string; // IDaaS用户唯一标识
username?: string; // 显示用户名称/工号
nick_name?: string; // 用户真实姓名
nickname?: string; // OAuth返回的昵称字段
name?: string; // 用户姓名(通常映射到 nick_name)
phone_number?: string; // 手机号
email?: string; // 邮箱地址
ou_id?: string; // 所属组织单位ID
ou_name?: string; // 所属部门名称
status?: number; // 账户状态: 0=正常, 1=禁用
is_leader?: boolean; // 是否为部门负责人
area?: string; // 用户所属地区
id?: string | number; // 临时的用户id
}
/**
* sso_users 表记录接口
*/
export interface SsoUser {
id?: string;
sub: string;
username: string;
nick_name: string;
phone_number?: string;
email?: string;
ou_id: string;
ou_name: string;
status: number;
is_leader: boolean;
area?: string;
created_at?: string;
updated_at?: string;
deleted_at?: string;
}
/**
* 会话存储配置
*
* 使用 Remix 的 Cookie Session Storage 来管理用户会话。
* Cookie 存储方式的优势:
* - 服务器端控制,安全性高
* - 自动处理过期时间
* - 支持 HttpOnly,防止 XSS 攻击
*
* 配置说明:
* - name: Cookie 名称,用于标识会话
* - httpOnly: 防止客户端 JavaScript 访问,提高安全性
* - sameSite: CSRF 保护
* - secrets: 用于签名和加密 Cookie 内容
* - maxAge: 会话有效期,与 OAuth Token 过期时间保持一致
*/
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__lgsession", // 登录会话 Cookie 名称
httpOnly: true, // 仅服务器端可访问,防止 XSS
path: "/", // Cookie 作用域为整个应用
sameSite: "lax", // CSRF 保护,允许顶级导航
secrets: ["s3cr3t"], // TODO: 应该从环境变量读取
maxAge: 60 * 60 * 2, // 2小时,与 OAuth Token 同步
secure: false, // 开发环境中禁用 HTTPS 要求
},
});
/**
* 获取会话对象
*
* 从请求中提取 Cookie 并获取对应的会话对象。
* 这是一个底层函数,通常不直接使用,而是通过 getUserSession 使用。
*
* @param request - Remix Request 对象,包含 HTTP 请求信息
* @returns 会话对象,可以用来读取和设置会话数据
*/
export async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return sessionStorage.getSession(cookie);
}
/**
* 获取用户登录状态(带自动Token刷新)
*
* 这是认证系统的核心函数,负责:
* 1. 检查用户是否已登录
* 2. 验证 OAuth Token 是否有效
* 3. 自动刷新即将过期的 Token
* 4. 返回用户信息和认证状态
*
* Token 刷新机制:
* - 检查 access_token 是否即将过期(距离过期时间 < 5 分钟)
* - 如果即将过期,使用 refresh_token 自动获取新的 access_token
* - 如果刷新失败,则认为用户未登录
*
* @param request - Remix Request 对象
* @returns 包含以下字段的对象:
* - isAuthenticated: 是否已认证
* - userRole: 用户角色 ('common' | 'developer')
* - accessToken: OAuth 访问令牌
* - refreshToken: OAuth 刷新令牌
* - userInfo: 用户详细信息
* - isTokenExpired: Token 是否已过期
* - refreshedSession: 如果刷新了 Token,返回更新后的会话对象
*/
/**
* 生成前端JWT
* @param userInfo 用户信息
* @param expiresIn OAuth token过期时间(秒)
* @returns JWT字符串
*/
async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, expiresIn: number): Promise<string> {
const jwtUserInfo: UserInfoForJWT = {
sub: userInfo.sub,
user_id: savedUserData.id!,
username: savedUserData.username,
nick_name: savedUserData.nick_name,
email: savedUserData.email,
phone_number: savedUserData.phone_number,
ou_id: savedUserData.ou_id,
ou_name: savedUserData.ou_name,
is_leader: savedUserData.is_leader,
user_role: userRole
};
return JWTUtils.generateJWT(jwtUserInfo, expiresIn);
}
/**
* 创建包含JWT的用户信息对象
* @param userInfo OAuth用户信息
* @param savedUserData 数据库中保存的用户数据
* @param userRole 用户角色
* @param frontendJWT 前端JWT
* @returns 完整的用户信息对象
*/
function createUserInfoWithJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, frontendJWT: string) {
return {
// 保持与callback.tsx中enhancedUserInfo相同的数据结构
sub: userInfo.sub,
username: savedUserData.username,
nick_name: savedUserData.nick_name,
phone_number: savedUserData.phone_number,
email: savedUserData.email,
ou_id: savedUserData.ou_id,
ou_name: savedUserData.ou_name,
status: savedUserData.status,
is_leader: savedUserData.is_leader,
// 增强字段,与OAuth登录保持一致
user_id: savedUserData.id,
user_role: userRole,
frontend_jwt: frontendJWT
};
}
export async function getUserSession(request: Request) {
const session = await getSession(request);
const isAuthenticated = session.get("isAuthenticated") === true;
const userRole = session.get("userRole") as UserRole;
let accessToken = session.get("accessToken");
const refreshToken = session.get("refreshToken");
let tokenIssuedAt = session.get("tokenIssuedAt");
let tokenExpiresIn = session.get("tokenExpiresIn");
const userInfo = session.get("userInfo");
let frontendJWT = session.get("frontendJWT");
let isTokenExpired = false;
let refreshedSession = null;
let shouldRegenerateJWT = false;
// 🔑 admin 用户不需要刷新 OAuth token,只需要维护 JWT
const isAdmin = userRole === 'admin';
// 如果有token信息,检查是否需要刷新(admin用户跳过OAuth token刷新)
if (!isAdmin && accessToken && refreshToken && tokenIssuedAt && tokenExpiresIn) {
try {
const tokenInfo = {
accessToken,
refreshToken,
tokenIssuedAt,
tokenExpiresIn
};
// 检查并自动刷新token
const refreshResult = await tokenManager.checkAndRefreshToken(tokenInfo);
if (refreshResult.success && refreshResult.newTokenInfo) {
const newToken = refreshResult.newTokenInfo;
// 如果token被刷新了,更新session
if (newToken.accessToken !== accessToken) {
console.log("Token已刷新,更新session");
session.set("accessToken", newToken.accessToken);
session.set("refreshToken", newToken.refreshToken);
session.set("tokenIssuedAt", newToken.tokenIssuedAt);
session.set("tokenExpiresIn", newToken.tokenExpiresIn);
// 更新本地变量
accessToken = newToken.accessToken;
tokenIssuedAt = newToken.tokenIssuedAt;
tokenExpiresIn = newToken.tokenExpiresIn;
// 标记需要重新生成JWT
shouldRegenerateJWT = true;
refreshedSession = session;
}
isTokenExpired = false;
} else {
console.error("Token刷新失败:", refreshResult.error);
isTokenExpired = true;
}
} catch (error) {
console.error("Token验证过程中出错:", error);
isTokenExpired = true;
}
} else if (isAdmin) {
// admin 用户:不检查 OAuth token 过期,始终保持登录状态
// console.log("admin 用户登录,跳过 OAuth token 刷新");
isTokenExpired = false;
// admin 用户需要有一个合理的 tokenExpiresIn 用于 JWT 生成
// 如果没有设置,使用一个默认值(如2小时)
if (!tokenExpiresIn) {
tokenExpiresIn = 7200; // 2小时
session.set("tokenExpiresIn", tokenExpiresIn);
refreshedSession = session;
}
}
// 检查前端JWT状态
if (isAuthenticated && !isTokenExpired && userInfo) {
let needsJWTRefresh = false;
// 检查是否有前端JWT
if (!frontendJWT) {
needsJWTRefresh = true;
console.log("缺少前端JWT,需要生成");
} else {
// 检查JWT是否即将过期
if (JWTUtils.isJWTExpiringSoon(frontendJWT)) {
needsJWTRefresh = true;
console.log("前端JWT即将过期,需要重新生成");
}
}
// 如果OAuth token被刷新了,也需要重新生成JWT
if (shouldRegenerateJWT) {
needsJWTRefresh = true;
console.log("OAuth token已刷新,需要重新生成JWT");
}
// 重新生成JWT
if (needsJWTRefresh && tokenExpiresIn) {
try {
// 从userInfo中获取用户数据
if (userInfo.user_id && userInfo.sub) {
const mockSavedUserData: SsoUser = {
id: userInfo.user_id,
sub: userInfo.sub,
username: userInfo.username || userInfo.sub,
nick_name: userInfo.nick_name || "未知用户",
phone_number: userInfo.phone_number,
email: userInfo.email,
ou_id: userInfo.ou_id || "default",
ou_name: userInfo.ou_name || "未知部门",
status: 0,
is_leader: userInfo.is_leader || false
};
const newJWT = await generateFrontendJWT(userInfo, mockSavedUserData, userRole, tokenExpiresIn);
// 打印JWT重新生成信息
console.log("=== Token刷新时重新生成JWT ===");
// console.log("原始userInfo:", userInfo);
// console.log("重构的用户数据:", mockSavedUserData);
// console.log("用户角色:", userRole);
// console.log("新生成的JWT:", newJWT);
// console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
// 更新session中的JWT
if (!refreshedSession) {
refreshedSession = session;
}
refreshedSession.set("frontendJWT", newJWT);
// 更新userInfo以包含新的JWT
const updatedUserInfo = createUserInfoWithJWT(userInfo, mockSavedUserData, userRole, newJWT);
refreshedSession.set("userInfo", updatedUserInfo);
console.log("更新后的userInfo:", updatedUserInfo);
console.log("=== JWT重新生成完成 ===");
frontendJWT = newJWT;
}
} catch (error) {
console.error("生成前端JWT失败:", error);
}
}
}
return {
isAuthenticated: isAuthenticated && !isTokenExpired,
userRole,
accessToken,
refreshToken,
userInfo,
isTokenExpired,
refreshedSession, // 如果刷新了token,返回更新后的session
frontendJWT // 返回前端JWT
};
}
/**
* 创建用户登录会话(完整版)
*
* 在用户成功登录后调用此函数来创建完整的会话并设置 Cookie。
* 这个函数支持完整的OAuth2.0登录流程,包括token管理和用户信息存储。
*
* 处理流程:
* 1. 创建新的会话对象
* 2. 设置认证状态、用户角色、token信息
* 3. 保存用户信息和前端JWT
* 4. 生成签名的 Cookie
* 5. 返回重定向响应并设置 Cookie
*
* @param params - 会话创建参数
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
*/
export async function createUserSession(params: {
isAuthenticated: boolean;
userRole: string;
redirectTo: string;
accessToken?: string;
refreshToken?: string;
tokenExpiresIn?: number;
userInfo?: UserInfo;
frontendJWT?: string;
}) {
const session = await sessionStorage.getSession();
// 基础认证信息
session.set("isAuthenticated", params.isAuthenticated);
session.set("userRole", params.userRole);
// OAuth token信息
if (params.accessToken) {
session.set("accessToken", params.accessToken);
session.set("tokenIssuedAt", Date.now());
}
if (params.refreshToken) {
session.set("refreshToken", params.refreshToken);
}
if (params.tokenExpiresIn) {
session.set("tokenExpiresIn", params.tokenExpiresIn);
}
// 用户信息和JWT
if (params.userInfo) {
session.set("userInfo", params.userInfo);
}
if (params.frontendJWT) {
session.set("frontendJWT", params.frontendJWT);
}
const cookie = await sessionStorage.commitSession(session);
// console.log("创建完整会话 - 设置Cookie:", !!cookie);
// console.log("创建完整会话 - 用户角色:", params.userRole);
// console.log("创建完整会话 - 重定向到:", params.redirectTo);
return new Response(null, {
status: 302, // HTTP 重定向状态码
headers: {
Location: params.redirectTo, // 重定向目标 URL
"Set-Cookie": cookie, // 设置会话 Cookie
},
});
}
/**
* 创建用户登录会话(简化版)
*
* 在用户成功登录后调用此函数来创建会话并设置 Cookie。
* 这个函数通常在以下场景中使用:
* - 临时管理员登录
* - 测试用户登录
* - 其他简单认证方式成功后
*
* 处理流程:
* 1. 创建新的会话对象
* 2. 设置认证状态和用户角色
* 3. 生成签名的 Cookie
* 4. 返回重定向响应并设置 Cookie
*
* @param isAuthenticated - 是否已认证,通常为 true
* @param userRole - 用户角色,决定用户的权限级别
* @param redirectTo - 登录成功后重定向的 URL,默认为首页
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
*/
export async function createSimpleUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
const session = await sessionStorage.getSession();
session.set("isAuthenticated", isAuthenticated);
session.set("userRole", userRole);
const cookie = await sessionStorage.commitSession(session);
console.log("创建简化会话 - 设置Cookie:", !!cookie);
console.log("创建简化会话 - 用户角色:", userRole);
console.log("创建简化会话 - 重定向到:", redirectTo);
return new Response(null, {
status: 302, // HTTP 重定向状态码
headers: {
Location: redirectTo, // 重定向目标 URL
"Set-Cookie": cookie, // 设置会话 Cookie
},
});
}
/**
* 销毁会话(用户登出)
*
* 当用户主动登出或会话失效时调用此函数。
*
* 处理流程:
* 1. 获取当前用户的会话和访问令牌
* 2. 调用 IDaaS 单点登出接口
* 3. 销毁本地会话数据(清除所有存储的信息)
* 4. 清除客户端的会话 Cookie
* 5. 重定向到登录页面
*
* 注意事项:
* - 这个函数会同时处理本地会话和 IDaaS 的单点登出
* - 即使 IDaaS 登出失败,也会清除本地会话
* - 销毁会话后,用户需要重新登录才能访问受保护的页面
*
* @param request - Remix Request 对象,用于获取当前会话
* @returns HTTP 302 重定向响应,清除 Cookie 并跳转到登录页
*/
export async function logout(request: Request) {
const session = await getSession(request);
// 获取访问令牌和应用ID,用于调用IDaaS单点登出
const accessToken = session.get("accessToken");
const appId = OAUTH_CONFIG.appId || 'idaasoauth2';
// 如果存在访问令牌,调用IDaaS单点登出
if (accessToken && appId) {
try {
await callIDaaSLogout(accessToken, appId);
console.log("IDaaS单点登出成功");
} catch (error) {
console.error("IDaaS单点登出失败:", error);
// 即使IDaaS登出失败,也继续清除本地会话
}
}
return new Response(null, {
status: 302, // HTTP 重定向状态码
headers: {
Location: "/login", // 重定向到登录页面
"Set-Cookie": await sessionStorage.destroySession(session), // 清除会话 Cookie
},
});
}
/**
* 调用IDaaS单点登出接口
*
* @param accessToken - 用户的访问令牌
* @param appId - 应用ID
* @returns Promise<void>
*/
async function callIDaaSLogout(accessToken: string, appId: string): Promise<void> {
const serverUrl = OAUTH_CONFIG.serverUrl || 'http://10.79.112.85';
const redirectUri = OAUTH_CONFIG.redirectUri || 'http://10.79.97.17/';
const logoutUrl = `${serverUrl}/public/sp/slo/${appId}`;
const formData = new URLSearchParams();
formData.append('access_token', accessToken);
formData.append('redirect_url', encodeURIComponent(redirectUri));
try {
const response = await fetch(logoutUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData.toString(),
});
if (!response.ok) {
throw new Error(`IDaaS登出失败: ${response.status} ${response.statusText}`);
}
console.log("IDaaS单点登出请求成功");
} catch (error) {
console.error("调用IDaaS登出接口失败:", error);
throw error;
}
}
/**
* 保存用户信息到数据库
*
* 此函数实现以下逻辑:
* 1. 内部生成临时 JWTuser_id 为 'login',仅用于数据库操作)
* 2. 根据 userInfo.sub 查询 sso_users 表中是否已存在该用户
* 3. 如果存在,则更新用户信息(如果用户已有 area 值则不更新)
* 4. 如果不存在,则插入新的用户记录
* 5. 返回保存的用户数据和临时 JWT
*
* @param userInfo - 从 IDaaS 获取的用户信息
* @param userRole - 用户角色
* @param tokenExpiresIn - Token过期时间(秒)
* @param area - 用户所属地区,根据端口号确定
* @returns Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}>
*/
export async function saveUserInfo(
userInfo: UserInfo,
userRole: UserRole,
tokenExpiresIn: number,
area?: string
): Promise<{success: boolean, data?: SsoUser, tempToken?: string, error?: string}> {
try {
console.log("开始保存用户信息", userInfo);
// 验证必要字段
if (!userInfo.sub) {
return { success: false, error: "用户唯一标识 sub 不能为空" };
}
// 🔒 安全:在服务端生成临时 JWT,user_id 使用占位符 'login'
// 这样客户端无法看到真实的 user_id
const tempUserInfo: UserInfoForJWT = {
sub: userInfo.sub,
user_id: 'login', // 使用占位符,避免在客户端暴露真实ID
username: 'login',
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户",
email: userInfo.email,
phone_number: userInfo.phone_number,
ou_id: userInfo.ou_id || "default",
ou_name: userInfo.ou_name || "未知部门",
is_leader: userInfo.is_leader || false,
user_role: userRole
};
const tempToken = JWTUtils.generateJWT(tempUserInfo, tokenExpiresIn);
// 1. 根据 sub 查询是否已存在该用户
const existingUserResult = await postgrestGet<SsoUser[]>("sso_users", {
filter: {
"sub": `eq.${userInfo.sub}`,
"deleted_at": "is.null" // 只查询未删除的记录
},
token: tempToken
});
if (existingUserResult.error) {
console.error("查询用户失败:", existingUserResult.error);
return { success: false, error: `查询用户失败: ${existingUserResult.error}` };
}
const existingUsers = existingUserResult.data || [];
const existingUser = existingUsers.length > 0 ? existingUsers[0] : null;
// 准备要保存的用户数据
// 注意:OAuth返回的字段是nickname,而不是nick_name
const userData: Partial<SsoUser> = {
sub: userInfo.sub,
username: userInfo.username || userInfo.name || userInfo.sub,
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name || "未知用户",
phone_number: userInfo.phone_number || undefined,
email: userInfo.email || undefined,
ou_id: userInfo.ou_id || "default",
ou_name: userInfo.ou_name || "未知部门",
status: userInfo.status !== undefined ? userInfo.status : 0,
is_leader: userInfo.is_leader || false,
};
if (existingUser) {
// 2. 用户已存在,执行更新操作
console.log("用户已存在,执行更新操作", existingUser.id);
// 只有在现有用户没有 area 或 area 为空时,才更新 area
if (area && !existingUser.area) {
userData.area = area;
console.log("用户原本无地区信息,更新地区为:", area);
} else if (existingUser.area) {
console.log("用户已有地区信息:", existingUser.area, "不更新");
}
const updateResult = await postgrestPut<SsoUser[], Partial<SsoUser>>(
"sso_users",
userData,
{ id: existingUser.id! },
tempToken
);
if (updateResult.error) {
console.error("更新用户失败:", updateResult.error);
return { success: false, error: `更新用户失败: ${updateResult.error}` };
}
console.log("用户信息更新成功");
return {
success: true,
data: Array.isArray(updateResult.data) ? updateResult.data[0] : updateResult.data as unknown as SsoUser,
tempToken // 返回临时 JWT
};
} else {
// 3. 用户不存在,执行插入操作,设置地区信息
console.log("用户不存在,执行插入操作");
// 新用户直接设置 area
if (area) {
userData.area = area;
console.log("新用户,设置地区为:", area);
}
const insertResult = await postgrestPost<SsoUser[], SsoUser>("sso_users", userData as SsoUser, tempToken);
if (insertResult.error) {
console.error("插入用户失败:", insertResult.error);
return { success: false, error: `插入用户失败: ${insertResult.error}` };
}
console.log("用户信息插入成功");
// 4. 给这个用户默认添加一个角色,角色为common
const userData_with_id = Array.isArray(insertResult.data) ? insertResult.data[0] : insertResult.data as unknown as SsoUser;
if (userData_with_id?.id) {
await addDefaultRole(userData_with_id.id, 2, tempToken);
}
return {
success: true,
data: userData_with_id,
tempToken // 返回临时 JWT
};
}
} catch (error) {
console.error("保存用户信息时发生错误:", error);
return {
success: false,
error: `保存用户信息失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 为用户添加默认角色
*
* @param userId - 用户ID
* @param roleId - 角色ID,默认为2common角色)
* @param token - JWT令牌,用于调用postgrest服务
* @returns 添加结果
*/
export async function addDefaultRole(userId: string, roleId: number = 2, token?: string) {
try {
console.log(`为用户 ${userId} 添加默认角色 ${roleId}`);
// 检查用户是否已经有此角色
const existingRoleResult = await postgrestGet<Array<{id: number, user_id: string, role_id: number}>>("user_role", {
filter: {
user_id: `eq.${userId}`,
role_id: `eq.${roleId}`
},
token
});
if (existingRoleResult.error) {
console.error("查询用户角色失败:", existingRoleResult.error);
return { success: false, error: `查询用户角色失败: ${existingRoleResult.error}` };
}
const existingRoles = existingRoleResult.data || [];
if (existingRoles.length > 0) {
console.log("用户已经拥有此角色,跳过添加");
return { success: true, data: existingRoles[0] };
}
// 添加角色
const addRoleResult = await postgrestPost<Array<{id: number, user_id: string, role_id: number}>, {user_id: string, role_id: number}>("user_role", {
user_id: userId,
role_id: roleId
}, token);
if (addRoleResult.error) {
console.error("添加用户角色失败:", addRoleResult.error);
return { success: false, error: `添加用户角色失败: ${addRoleResult.error}` };
}
console.log("用户角色添加成功");
return {
success: true,
data: Array.isArray(addRoleResult.data) ? addRoleResult.data[0] : addRoleResult.data
};
} catch (error) {
console.error("添加用户角色时发生错误:", error);
return {
success: false,
error: `添加用户角色失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 通过用户sub获取用户信息
*
* @param sub - 用户的唯一标识
* @returns 用户信息
*/
export async function getUserBySub(sub: string) {
try {
// console.log(`查询用户: ${sub}`);
const userResult = await postgrestGet<SsoUser[]>("sso_users", {
filter: {
sub: `eq.${sub}`
}
});
if (userResult.error) {
console.error("查询用户失败:", userResult.error);
return { success: false, error: `查询用户失败: ${userResult.error}` };
}
const users = userResult.data || [];
const user = users.length > 0 ? users[0] : null;
if (!user) {
return { success: false, error: "用户不存在" };
}
return { success: true, data: user };
} catch (error) {
console.error("查询用户时发生错误:", error);
return {
success: false,
error: `查询用户失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* 账号密码登录接口
*
* @param username - 用户名
* @param password - 密码
* @param redirectTo - 登录成功后重定向的URL
* @returns HTTP重定向响应或错误响应
*/
export async function simpleRootLogin(
username: string,
password: string,
redirectTo: string
) {
try {
// 输入验证
if (!username?.trim() || !password?.trim()) {
return new Response(JSON.stringify({
success: false,
error: "用户名和密码不能为空"
}), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
// 调用登录接口
const loginResponse = await fetch(`${API_BASE_URL}/password_login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sub: username.trim(),
password: password.trim()
})
});
const loginResult = await loginResponse.json();
console.log('登录接口返回', loginResult);
// 检查重试次数
const retryCount = loginResult.retryCount || loginResult.retry_count || 0;
console.log('登录重试次数:', retryCount);
if (loginResult.code === 0 && loginResult.data) {
// 登录成功,构建用户信息
const userData = loginResult.data;
// console.log('管理员登录userData', userData);
const userRole = userData.role; // 默认角色
// 生成模拟的OAuth token信息
const mockTokenExpiresIn = 7200; // 2小时
const mockAccessToken = `mock_access_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const mockRefreshToken = `mock_refresh_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 生成前端JWT
const jwtUserInfo: UserInfoForJWT = {
sub: userData.sub,
user_id: userData.user_id,
username: userData.username,
nick_name: userData.nick_name,
email: userData.email,
phone_number: userData.phone_number,
ou_id: userData.ou_id,
ou_name: userData.ou_name,
is_leader: userData.is_leader,
user_role: userRole
};
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, mockTokenExpiresIn);
// 构建增强的用户信息对象
const enhancedUserInfo = {
...userData,
user_id: userData.user_id,
user_role: userRole,
frontend_jwt: frontendJWT
};
// 使用统一的session创建函数
return createUserSession({
isAuthenticated: true,
userRole: userRole,
redirectTo,
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
tokenExpiresIn: mockTokenExpiresIn,
userInfo: enhancedUserInfo,
frontendJWT
});
} else {
// 登录失败,检查账户是否被锁定
let errorMsg = loginResult.msg || "登录失败,请检查用户名和密码";
let isLocked = false;
// 检查是否因重试次数过多被锁定
if (retryCount >= 5) {
errorMsg = "账户已被锁定,密码错误次数过多,请联系管理员";
isLocked = true;
} else if (retryCount > 0) {
// 显示剩余尝试次数
const remainingAttempts = 5 - retryCount;
errorMsg = `${loginResult.msg || "用户名或密码错误"},还有 ${remainingAttempts} 次尝试机会`;
}
return new Response(JSON.stringify({
success: false,
error: errorMsg,
retryCount: retryCount,
isLocked: isLocked,
remainingAttempts: isLocked ? 0 : (5 - retryCount)
}), {
status: isLocked ? 403 : 401, // 403 表示禁止访问(账户被锁)
headers: { "Content-Type": "application/json" }
});
}
} catch (error) {
console.error("登录请求失败:", error);
return new Response(JSON.stringify({
success: false,
error: "登录请求失败,请稍后重试"
}), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
}