import { ERROR } from './constants' import { getCloudbaseContext, parseContext } from './context' import { validateUid } from './utils' import type { ICreateTicketOpts, IGetUserInfoResult, IGetEndUserInfoResult, IUserInfoQuery, ITemplateNotifyReq, } from './types' import { ICloudbaseConfig } from '@cloudbase/types' // 延迟加载,避免非 Node 环境打包时引入此包 // eslint-disable-next-line @typescript-eslint/no-explicit-any let jwtSign: ((...args: any[]) => any) | null = null async function getJwtSign() { if (!jwtSign) { try { // @ts-ignore — 该包仅在 Node 运行时存在,开发环境可能未安装 const mod = await import('jsonwebtoken') jwtSign = mod.default?.sign || mod.sign } catch (e) { console.error('缺少依赖 jsonwebtoken,请执行以下命令安装:\n\n' + ' npm install jsonwebtoken\n\n' + '该依赖用于 Node 环境下的自定义登录票据生成。') } } return jwtSign! } /** * 从云函数运行时上下文中获取当前请求的用户信息 * 数据来源为环境变量(微信 openId、TCB uuid 等) */ function getDefaultUserInfo(): IGetUserInfoResult { const { WX_OPENID, WX_APPID, TCB_UUID, TCB_CUSTOM_USER_ID, TCB_ISANONYMOUS_USER } = getCloudbaseContext() return { openId: WX_OPENID || '', appId: WX_APPID || '', uid: TCB_UUID || '', customUserId: TCB_CUSTOM_USER_ID || '', isAnonymous: TCB_ISANONYMOUS_USER === 'true', } } /** * 调用后端管理接口查询用户信息 * 统一封装 auth.getUserInfoForAdmin 的请求逻辑 */ function sendUserInfoRequest(app: any, params: Record): Promise { return app?.request?.send?.('auth.getUserInfoForAdmin', params, { pathname: 'web', endPointMode: 'CLOUD_API', }) } /** * 初始化 Node 端工具方法,挂载到 js-sdk app 实例上 * 包含:auth 相关方法、模板消息推送、context 解析 * * @param app - js-sdk cloudbase 实例 * @param config - 配置信息,包含认证凭证和环境 ID */ export const nodeTool = (app: any, config: ICloudbaseConfig) => { // 仅当 app 已初始化 auth 模块时,才注入 auth 相关方法 if (app.auth) { const auth = { /** 获取当前请求的用户信息(从环境变量读取,同步) */ getUserInfo(): IGetUserInfoResult { return getDefaultUserInfo() }, /** * 获取终端用户信息 * 不传 uid 时返回当前请求用户信息,传 uid 时查询指定用户 */ async getEndUserInfo(uid?: string): Promise { const defaultUserInfo = getDefaultUserInfo() if (uid === undefined) { return { userInfo: defaultUserInfo } } validateUid(uid) return sendUserInfoRequest(app, { uuid: uid, envName: config.env, }).then((result: any) => { if (result.code) { return result } return { userInfo: { ...defaultUserInfo, ...result.data }, requestId: result.requestId, } }) }, /** * 创建自定义登录 Ticket * 使用 RSA 私钥签发 JWT,客户端凭此 Ticket 换取登录态 * * @param uid - 自定义用户 ID(4~32 位) * @param options - 刷新间隔和过期时间配置 * @returns 格式为 "{private_key_id}/@@/{jwt_token}" 的 Ticket 字符串 */ async createTicket(uid: string, options: ICreateTicketOpts = {}): Promise { validateUid(uid) const timestamp = new Date().getTime() const { credentials } = config.auth || {} const { env } = config if (!env) { throw { ...ERROR.INVALID_PARAM, message: 'no env in config' } } if (!credentials?.env_id) { throw { ...ERROR.INVALID_PARAM, message: '当前私钥未包含env_id 信息, 请前往腾讯云云开发控制台,获取自定义登录最新私钥', } } if (credentials.env_id !== env) { throw { ...ERROR.INVALID_PARAM, message: '当前私钥所属环境与 init 指定环境不一致!', } } const { refresh = 3600 * 1000, // 默认 1 小时刷新 expire = timestamp + 7 * 24 * 60 * 60 * 1000, // 默认 7 天过期 } = options const sign = await getJwtSign() const token = sign( { alg: 'RS256', env, iat: timestamp, exp: timestamp + 10 * 60 * 1000, // Ticket 本身 10 分钟有效 uid, refresh, expire, }, credentials.private_key, { allowInsecureKeySizes: true, algorithm: 'RS256', }, ) return `${credentials.private_key_id}/@@/${token}` }, /** * 按条件查询用户信息(管理端接口) * 支持按 uid、platform、platformId 查询 */ async queryUserInfo(query: IUserInfoQuery): Promise { const { uid, platform, platformId } = query return sendUserInfoRequest(app, { uuid: uid, platform, platformId, envName: config.env, }).then((result: any) => { if (result.code) { return result } return { userInfo: { ...result.data }, requestId: result.requestId, } }) }, /** 获取客户端 IP 地址 */ getClientIP(): string { const { TCB_SOURCE_IP } = getCloudbaseContext() return TCB_SOURCE_IP || '' }, } // 将 auth 方法逐一挂载到 app.auth 上 Object.keys(auth).forEach((key) => { app.auth[key] = (auth as Record)[key] }) } /** * 发送模板消息通知 * 通过调用 lowcode-datasource 云函数间接调用微搭 API * * @param params - 通知参数(策略 ID、模板变量、跳转链接) * @param opts - 可选配置,如超时时间 */ app.sendTemplateNotification = async (params: ITemplateNotifyReq, opts?: { timeout?: number }) => await app?.callFunction?.( { name: 'lowcode-datasource', data: { methodName: 'callWedaApi', params: { action: 'PushNotifyMsg', data: { NotifyId: params.notifyId, Data: JSON.stringify(params.data), NotifyUsers: undefined, Url: params.url, }, }, mode: 'c', }, }, undefined, opts, ) /** 挂载 context 解析工具,方便用户在云函数中使用 */ app.parseContext = parseContext }