Files
leaudit-platform-frontend/app/api/login/auth.server.ts
T
LiangShiyong d09d5b709d Merge branch 'PingChuan' into shiy-login
# Conflicts:
#	app/config/api-config.ts
fix: 1. 修复无法加载数据的问题:没有从入口页中进来会缺少数据。
2. 加强后端接口关于token的校验错误和权限校验错误的管理。

feat: 1. 对接后端的数据看板的接口。
2. 将系统设置单独抽出来作为管理员的固定一个入口。
2025-11-22 15:57:22 +08:00

777 lines
27 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 { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
import axios from 'axios';
/**
* 用户角色类型定义
*
* @typedef {string} UserRole
* @property {'common'} common - 普通用户,有基本的系统访问权限
* @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限
*/
export type UserRole = 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
user_id?: string | number;
user_role?: string
}
/**
* 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 * 8, // 8小时,确保大于等于JWT token最大有效期(通常为6小时)
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;
const accessToken = session.get("accessToken");
const refreshToken = session.get("refreshToken");
const tokenIssuedAt = session.get("tokenIssuedAt");
let tokenExpiresIn = session.get("tokenExpiresIn");
const userInfo = session.get("userInfo");
const frontendJWT = session.get("frontendJWT");
// 🔑 检查是否是公共路径(不需要认证的路径)
const url = new URL(request.url);
const pathname = url.pathname;
const publicPaths = ['/login', '/favicon.ico', '/callback'];
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
let refreshedSession = null;
// let shouldRegenerateJWT = false;
// 🔑 新的统一过期检查逻辑
// 不区分 admin 和 OAuth 用户,所有用户都使用同样的过期检查
// 如果没有设置 tokenExpiresIn,给一个默认值(24小时)
if (!tokenExpiresIn) {
tokenExpiresIn = 86400; // 24小时(与后端登录接口返回的一致)
session.set("tokenExpiresIn", tokenExpiresIn);
refreshedSession = session;
}
// 🔑 简化:不再自动重新生成 JWT
// JWT 即将过期时,直接让用户重新登录
// 🔑 关键:验证 session 完整性
// 如果 isAuthenticated 为 true,但缺少关键数据(userInfo),则认为 session 无效
let finalIsAuthenticated = isAuthenticated;
if (finalIsAuthenticated) {
// 检查是否有用户信息
if (!userInfo) {
console.warn("⚠️ [getUserSession] Session 不完整:缺少 userInfo");
finalIsAuthenticated = false;
}
// 检查是否有前端 JWT
if (!frontendJWT) {
console.warn("⚠️ [getUserSession] Session 不完整:缺少 frontendJWT");
finalIsAuthenticated = false;
}
// 🔑 统一的 token 过期检查(所有用户类型)
// 使用签发时间 + 有效期 - 5分钟缓冲来判断是否需要重新登录
if (tokenIssuedAt && tokenExpiresIn) {
const now = Date.now(); // 当前时间(毫秒)
const expiresAt = tokenIssuedAt + (tokenExpiresIn * 1000); // 过期时间(毫秒)
const bufferTime = 5 * 60 * 1000; // 5分钟缓冲时间(毫秒)
// 如果当前时间 >= 过期时间 - 5分钟,则认为 token 即将过期或已过期
if (now >= expiresAt - bufferTime) {
const remainingSeconds = Math.floor((expiresAt - now) / 1000);
console.error(`❌ [getUserSession] Token 即将过期或已过期 (剩余: ${remainingSeconds} 秒)`);
finalIsAuthenticated = false;
}
} else if (!tokenIssuedAt) {
// 如果没有签发时间,认为 session 无效(可能是旧版本数据)
console.warn("⚠️ [getUserSession] Session 缺少 tokenIssuedAt,认为已过期");
finalIsAuthenticated = false;
}
}
// 🚨 统一的认证检查和重定向逻辑
if (!finalIsAuthenticated) {
// 如果是公共路径,不重定向,直接返回未认证状态
if (isPublicPath) {
// console.log("️ [getUserSession] 公共路径,允许未认证访问:", pathname);
return {
isAuthenticated: false,
userRole,
accessToken,
refreshToken,
userInfo,
refreshedSession,
frontendJWT
};
}
// 非公共路径且未认证,重定向到登录页
if (isAuthenticated) {
// Session 存在但已失效(token 过期或数据不完整)
console.error("❌ [getUserSession] Session 已失效,清除 session 并重定向到登录页");
const { redirect } = await import("@remix-run/node");
const destroyedSession = await sessionStorage.destroySession(session);
// 重定向到登录页,添加 expired=true 参数
throw redirect("/login?expired=true", {
headers: {
"Set-Cookie": destroyedSession
}
});
} else {
// Session 不存在(首次访问或已登出)
console.warn("⚠️ [getUserSession] 未登录,重定向到登录页");
const { redirect } = await import("@remix-run/node");
// 保存当前路径,登录后可以跳转回来
const redirectTo = pathname !== '/login' ? pathname : '/';
throw redirect(`/login?redirect=${encodeURIComponent(redirectTo)}`);
}
}
return {
isAuthenticated: finalIsAuthenticated,
userRole,
accessToken,
refreshToken,
userInfo,
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;
tokenIssuedAt?: 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);
}
if (params.refreshToken) {
session.set("refreshToken", params.refreshToken);
}
// 🔑 Token 时间信息(与 accessToken 独立,支持管理员登录)
// 只要有 tokenExpiresIn 或 tokenIssuedAt,就设置这些字段
if (params.tokenExpiresIn) {
session.set("tokenExpiresIn", params.tokenExpiresIn);
}
if (params.tokenIssuedAt !== undefined) {
// 优先使用传递的 tokenIssuedAt
session.set("tokenIssuedAt", params.tokenIssuedAt);
} else if (params.accessToken || params.frontendJWT) {
// 如果没有传递 tokenIssuedAt,但有 tokenaccessToken 或 frontendJWT),则使用当前时间
session.set("tokenIssuedAt", Date.now());
}
// 用户信息和JWT
if (params.userInfo) {
session.set("userInfo", params.userInfo);
}
if (params.frontendJWT) {
session.set("frontendJWT", params.frontendJWT);
}
// 🔑 根据 tokenExpiresIn 动态设置 Cookie 的 maxAge
// 如果有 tokenExpiresIn,使用它作为 Cookie 有效期;否则使用默认值(8小时)
const cookieMaxAge = params.tokenExpiresIn || (60 * 60 * 8); // 默认8小时
// console.log("🍪 [createUserSession] Cookie maxAge:", cookieMaxAge, "秒 (", (cookieMaxAge / 3600).toFixed(2), "小时)");
const cookie = await sessionStorage.commitSession(session, {
maxAge: cookieMaxAge // 🔑 动态设置 Cookie 有效期
});
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';
console.log("🚪 [Logout] 开始登出流程...");
console.log("🔑 [Logout] accessToken 存在:", !!accessToken);
console.log("📱 [Logout] appId:", appId);
// 如果存在访问令牌,调用IDaaS单点登出(仅 OAuth 登录用户)
if (accessToken && appId) {
console.log("🌐 [Logout] OAuth 用户,准备调用 IDaaS 单点登出...");
try {
await callIDaaSLogout(accessToken, appId);
console.log("✅ [Logout] IDaaS单点登出成功");
} catch (error) {
console.error("❌ [Logout] IDaaS单点登出失败:");
console.error(" 错误详情:", error);
if (error instanceof Error) {
console.error(" 错误消息:", error.message);
console.error(" 错误堆栈:", error.stack);
}
// 即使IDaaS登出失败,也继续清除本地会话
}
} else {
console.log("️ [Logout] 管理员登录用户,无需调用 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}`;
console.log("📡 [callIDaaSLogout] 准备发送登出请求:");
console.log(" 登出URL:", logoutUrl);
console.log(" 重定向URL:", redirectUri);
console.log(" accessToken:", accessToken ? `${accessToken.substring(0, 20)}...` : 'null');
const formData = new URLSearchParams();
formData.append('access_token', accessToken);
formData.append('redirect_url', encodeURIComponent(redirectUri));
try {
const response = await axios.post(logoutUrl, formData.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
console.log("✅ [callIDaaSLogout] IDaaS单点登出请求成功");
console.log(" 响应状态:", response.status);
console.log(" 响应数据:", response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败:");
console.error(" HTTP状态:", error.response?.status);
console.error(" 状态文本:", error.response?.statusText);
console.error(" 响应数据:", error.response?.data);
console.error(" 请求配置:", error.config?.url, error.config?.method);
throw new Error(`IDaaS登出失败: ${error.response?.status} ${error.response?.statusText}`);
}
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败(非HTTP错误):", 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)}`
};
}
}