feat: oauth相关aspect实现 && 给其他aspect定义加上了注释
This commit is contained in:
parent
a3a4837974
commit
35c6b01f65
|
|
@ -8,6 +8,12 @@ import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
|||
import { WechatPublicEventData, WechatMpEventData } from 'oak-external-sdk';
|
||||
|
||||
export type AspectDict<ED extends EntityDict> = {
|
||||
/**
|
||||
* 使用小程序 token 登录 Web 端
|
||||
* @param mpToken 小程序的 token
|
||||
* @param env Web 环境信息
|
||||
* @returns 返回 Web 端的 token
|
||||
*/
|
||||
loginWebByMpToken: (
|
||||
params: {
|
||||
mpToken: string;
|
||||
|
|
@ -15,18 +21,42 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 合并用户账号,将一个用户的数据迁移到另一个用户
|
||||
* @param from 源用户 ID
|
||||
* @param to 目标用户 ID
|
||||
*/
|
||||
mergeUser: (
|
||||
params: { from: string; to: string },
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 刷新微信公众号用户信息(昵称、头像、性别等)
|
||||
*/
|
||||
refreshWechatPublicUserInfo: (
|
||||
params: {},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取微信小程序用户的手机号
|
||||
* @param code 微信小程序获取手机号的 code
|
||||
* @param env 小程序环境信息
|
||||
* @returns 返回用户手机号
|
||||
*/
|
||||
getWechatMpUserPhoneNumber: (
|
||||
params: { code: string; env: WechatMpEnv },
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 通过手机号绑定当前登录用户
|
||||
* @param mobile 手机号
|
||||
* @param captcha 验证码
|
||||
* @param env 环境信息
|
||||
*/
|
||||
bindByMobile: (
|
||||
params: {
|
||||
mobile: string;
|
||||
|
|
@ -35,6 +65,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 通过邮箱绑定当前登录用户
|
||||
* @param email 邮箱地址
|
||||
* @param captcha 验证码
|
||||
* @param env 环境信息
|
||||
*/
|
||||
bindByEmail: (
|
||||
params: {
|
||||
email: string;
|
||||
|
|
@ -43,6 +80,15 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 通过手机号和验证码登录
|
||||
* @param mobile 手机号
|
||||
* @param captcha 验证码
|
||||
* @param disableRegister 是否禁止自动注册,true 时账号不存在会报错
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByMobile: (
|
||||
params: {
|
||||
mobile: string;
|
||||
|
|
@ -52,6 +98,12 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 验证用户密码是否正确
|
||||
* @param password 密码(明文或 SHA1 密文,根据系统配置)
|
||||
* @param env 环境信息
|
||||
*/
|
||||
verifyPassword: (
|
||||
params: {
|
||||
password: string;
|
||||
|
|
@ -59,6 +111,14 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 通过账号(手机号/邮箱/登录名)和密码登录
|
||||
* @param account 账号(可以是手机号、邮箱或登录名)
|
||||
* @param password 密码(明文或 SHA1 密文,根据系统配置)
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByAccount: (
|
||||
params: {
|
||||
account: string;
|
||||
|
|
@ -67,6 +127,15 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 通过邮箱和验证码登录
|
||||
* @param email 邮箱地址
|
||||
* @param captcha 验证码
|
||||
* @param disableRegister 是否禁止自动注册,true 时账号不存在会报错
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByEmail: (
|
||||
params: {
|
||||
email: string;
|
||||
|
|
@ -76,6 +145,14 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 微信公众号登录
|
||||
* @param code 微信授权 code
|
||||
* @param env Web 环境信息
|
||||
* @param wechatLoginId 可选的微信登录 ID(用于扫码登录场景)
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginWechat: (
|
||||
{
|
||||
code,
|
||||
|
|
@ -88,10 +165,22 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 用户登出,使指定 token 失效
|
||||
* @param tokenValue 要失效的 token
|
||||
*/
|
||||
logout: (
|
||||
params: { tokenValue: string },
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 微信小程序登录
|
||||
* @param code 微信授权 code
|
||||
* @param env 小程序环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginWechatMp: (
|
||||
{
|
||||
code,
|
||||
|
|
@ -102,6 +191,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 微信原生 APP 登录
|
||||
* @param code 微信授权 code
|
||||
* @param env 原生 APP 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginWechatNative: (
|
||||
{
|
||||
code,
|
||||
|
|
@ -112,6 +208,15 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 同步微信小程序用户信息(昵称、头像等)
|
||||
* @param nickname 昵称
|
||||
* @param avatarUrl 头像 URL
|
||||
* @param encryptedData 加密数据
|
||||
* @param iv 加密算法的初始向量
|
||||
* @param signature 签名
|
||||
*/
|
||||
syncUserInfoWechatMp: (
|
||||
{
|
||||
nickname,
|
||||
|
|
@ -128,6 +233,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 唤醒寄生用户(将 shadow 状态的用户激活)
|
||||
* @param id 用户 ID
|
||||
* @param env 环境信息
|
||||
* @returns 返回 token
|
||||
*/
|
||||
wakeupParasite: (
|
||||
params: {
|
||||
id: string;
|
||||
|
|
@ -135,6 +247,14 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 刷新 token,延长有效期
|
||||
* @param tokenValue 当前 token
|
||||
* @param env 环境信息
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回新的 token
|
||||
*/
|
||||
refreshToken: (
|
||||
params: {
|
||||
tokenValue: string;
|
||||
|
|
@ -143,6 +263,14 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 通过手机号发送验证码
|
||||
* @param mobile 手机号
|
||||
* @param env 环境信息
|
||||
* @param type 验证码类型:login-登录,changePassword-修改密码,confirm-确认操作
|
||||
* @returns 返回验证码 ID
|
||||
*/
|
||||
sendCaptchaByMobile: (
|
||||
params: {
|
||||
mobile: string;
|
||||
|
|
@ -151,6 +279,14 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 通过邮箱发送验证码
|
||||
* @param email 邮箱地址
|
||||
* @param env 环境信息
|
||||
* @param type 验证码类型:login-登录,changePassword-修改密码,confirm-确认操作
|
||||
* @returns 返回验证码 ID
|
||||
*/
|
||||
sendCaptchaByEmail: (
|
||||
params: {
|
||||
email: string;
|
||||
|
|
@ -159,6 +295,16 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 根据域名和应用类型获取应用信息,并检查版本兼容性
|
||||
* @param version 客户端版本号
|
||||
* @param type 应用类型(web/wechatMp/wechatPublic/native)
|
||||
* @param domain 域名
|
||||
* @param data 需要返回的应用数据字段
|
||||
* @param appId 可选的应用 ID
|
||||
* @returns 返回应用 ID
|
||||
*/
|
||||
getApplication: (
|
||||
params: {
|
||||
version: string;
|
||||
|
|
@ -169,6 +315,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 生成微信 JS-SDK 签名,用于调用微信 JS 接口
|
||||
* @param url 当前页面 URL
|
||||
* @param env Web 环境信息
|
||||
* @returns 返回签名信息(signature、noncestr、timestamp、appId)
|
||||
*/
|
||||
signatureJsSDK: (
|
||||
params: {
|
||||
url: string;
|
||||
|
|
@ -181,6 +334,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
timestamp: number;
|
||||
appId: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* 更新平台或系统的配置信息
|
||||
* @param entity 实体类型(platform 或 system)
|
||||
* @param entityId 实体 ID
|
||||
* @param config 配置对象
|
||||
*/
|
||||
updateConfig: (
|
||||
params: {
|
||||
entity: 'platform' | 'system';
|
||||
|
|
@ -189,6 +349,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 更新平台、系统或应用的样式配置
|
||||
* @param entity 实体类型(platform/system/application)
|
||||
* @param entityId 实体 ID
|
||||
* @param style 样式对象
|
||||
*/
|
||||
updateStyle: (
|
||||
params: {
|
||||
entity: 'platform' | 'system' | 'application';
|
||||
|
|
@ -197,6 +364,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 更新应用的配置信息
|
||||
* @param entity 实体类型(application)
|
||||
* @param entityId 应用 ID
|
||||
* @param config 应用配置对象
|
||||
*/
|
||||
updateApplicationConfig: (
|
||||
params: {
|
||||
entity: 'application';
|
||||
|
|
@ -205,16 +379,34 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 切换到指定用户(管理员扮演用户功能)
|
||||
* @param userId 目标用户 ID
|
||||
*/
|
||||
switchTo: (
|
||||
params: {
|
||||
userId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取小程序无限制二维码
|
||||
* @param wechatQrCodeId 微信二维码 ID
|
||||
* @returns 返回二维码图片数据(Base64 字符串)
|
||||
*/
|
||||
getMpUnlimitWxaCode: (
|
||||
wechatQrCodeId: string,
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 创建微信登录会话(用于扫码登录场景)
|
||||
* @param type 登录类型(login-登录,bind-绑定)
|
||||
* @param interval 会话有效期(毫秒)
|
||||
* @returns 返回登录会话 ID
|
||||
*/
|
||||
createWechatLogin: (
|
||||
params: {
|
||||
type: EntityDict['wechatLogin']['Schema']['type'];
|
||||
|
|
@ -222,6 +414,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 解绑微信用户
|
||||
* @param wechatUserId 微信用户 ID
|
||||
* @param captcha 可选的验证码
|
||||
* @param mobile 可选的手机号
|
||||
*/
|
||||
unbindingWechat: (
|
||||
params: {
|
||||
wechatUserId: string;
|
||||
|
|
@ -230,6 +429,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 通过微信登录会话 ID 完成登录(Web 端扫码登录确认)
|
||||
* @param wechatLoginId 微信登录会话 ID
|
||||
* @param env Web 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByWechat: (
|
||||
params: {
|
||||
wechatLoginId: string;
|
||||
|
|
@ -237,15 +443,37 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 从 URL 中提取网页信息(标题、发布时间、图片列表)
|
||||
* @param url 网页 URL
|
||||
* @returns 返回网页信息
|
||||
*/
|
||||
getInfoByUrl: (params: { url: string }) => Promise<{
|
||||
title: string;
|
||||
publishDate: number | undefined;
|
||||
imageList: string[];
|
||||
}>;
|
||||
|
||||
/**
|
||||
* 获取用户可用的修改密码方式
|
||||
* @param userId 用户 ID
|
||||
* @returns 返回可用的修改方式列表(mobile-手机号,password-原密码)
|
||||
*/
|
||||
getChangePasswordChannels: (
|
||||
params: { userId: string },
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string[]>;
|
||||
|
||||
/**
|
||||
* 修改用户密码
|
||||
* @param userId 用户 ID
|
||||
* @param prevPassword 原密码(使用原密码验证时提供)
|
||||
* @param mobile 手机号(使用手机号验证时提供)
|
||||
* @param captcha 验证码(使用手机号验证时提供)
|
||||
* @param newPassword 新密码
|
||||
* @returns 返回修改结果
|
||||
*/
|
||||
updateUserPassword: (
|
||||
params: {
|
||||
userId: string;
|
||||
|
|
@ -256,6 +484,15 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<{ result: string; times?: number }>;
|
||||
|
||||
/**
|
||||
* 创建或获取会话(用于客服系统等场景)
|
||||
* @param data 微信事件数据(可选)
|
||||
* @param type 应用类型
|
||||
* @param entity 关联实体类型(可选)
|
||||
* @param entityId 关联实体 ID(可选)
|
||||
* @returns 返回会话 ID
|
||||
*/
|
||||
createSession: (
|
||||
params: {
|
||||
data?: WechatPublicEventData | WechatMpEventData;
|
||||
|
|
@ -265,22 +502,47 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 上传素材到微信服务器
|
||||
* @param params 包含文件信息、应用 ID、素材类型等
|
||||
* @returns 返回微信 mediaId
|
||||
*/
|
||||
uploadWechatMedia: (
|
||||
params: any,
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<{ mediaId: string }>;
|
||||
|
||||
/**
|
||||
* 获取微信公众号当前使用的自定义菜单配置
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回当前菜单配置
|
||||
*/
|
||||
getCurrentMenu: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取微信公众号自定义菜单配置(包括默认和个性化菜单)
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回菜单配置
|
||||
*/
|
||||
getMenu: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 创建微信公众号自定义菜单
|
||||
* @param applicationId 应用 ID
|
||||
* @param menuConfig 菜单配置
|
||||
* @param id 菜单记录 ID
|
||||
*/
|
||||
createMenu: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -289,6 +551,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 创建微信公众号个性化菜单(针对特定用户群体)
|
||||
* @param applicationId 应用 ID
|
||||
* @param menuConfig 菜单配置
|
||||
* @param id 菜单记录 ID
|
||||
*/
|
||||
createConditionalMenu: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -297,6 +566,12 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 删除微信公众号个性化菜单
|
||||
* @param applicationId 应用 ID
|
||||
* @param menuId 微信菜单 ID
|
||||
*/
|
||||
deleteConditionalMenu: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -304,12 +579,26 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 删除微信公众号自定义菜单
|
||||
* @param applicationId 应用 ID
|
||||
*/
|
||||
deleteMenu: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 批量获取微信公众号图文消息素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param offset 起始位置(可选)
|
||||
* @param count 获取数量
|
||||
* @param noContent 是否不返回内容(0-返回,1-不返回)
|
||||
* @returns 返回图文消息列表
|
||||
*/
|
||||
batchGetArticle: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -319,6 +608,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取微信公众号单个图文消息素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param articleId 图文消息 ID
|
||||
* @returns 返回图文消息详情
|
||||
*/
|
||||
getArticle: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -326,6 +622,15 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 批量获取微信素材列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param type 素材类型(image-图片,voice-语音,video-视频,news-图文)
|
||||
* @param offset 起始位置(可选)
|
||||
* @param count 获取数量
|
||||
* @returns 返回素材列表
|
||||
*/
|
||||
batchGetMaterialList: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -335,6 +640,14 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取微信素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param mediaId 素材 ID
|
||||
* @param isPermanent 是否为永久素材(默认获取临时素材)
|
||||
* @returns 返回素材数据
|
||||
*/
|
||||
getMaterial: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -343,6 +656,12 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 删除微信永久素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param mediaId 素材 ID
|
||||
*/
|
||||
deleteMaterial: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -350,6 +669,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 创建微信公众号用户标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param name 标签名称
|
||||
* @returns 返回创建结果
|
||||
*/
|
||||
createTag: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -357,12 +683,25 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取微信公众号所有用户标签
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回标签列表
|
||||
*/
|
||||
getTags: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 编辑微信公众号用户标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param id 微信标签 ID
|
||||
* @param name 新标签名称
|
||||
*/
|
||||
editTag: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -371,6 +710,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 删除微信公众号用户标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param id 本地标签 ID
|
||||
* @param wechatId 微信标签 ID
|
||||
*/
|
||||
deleteTag: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -379,16 +725,33 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 同步微信公众号消息模板到本地
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回同步结果
|
||||
*/
|
||||
syncMessageTemplate: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取已注册的消息类型列表
|
||||
* @returns 返回消息类型数组
|
||||
*/
|
||||
getMessageType: (
|
||||
params: {},
|
||||
content: BackendRuntimeContext<ED>
|
||||
) => Promise<string[]>;
|
||||
|
||||
/**
|
||||
* 同步单个微信公众号用户标签到微信服务器
|
||||
* @param applicationId 应用 ID
|
||||
* @param id 本地标签 ID
|
||||
*/
|
||||
syncTag: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -396,12 +759,24 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 一键同步微信公众号用户标签(从微信服务器同步到本地)
|
||||
* @param applicationId 应用 ID
|
||||
*/
|
||||
oneKeySync: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取指定标签下的微信用户列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param tagId 微信标签 ID
|
||||
* @returns 返回用户列表
|
||||
*/
|
||||
getTagUsers: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -409,6 +784,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 批量为用户打标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param openIdList 微信用户 openId 列表
|
||||
* @param tagId 微信标签 ID
|
||||
*/
|
||||
batchtagging: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -417,6 +799,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 批量为用户取消标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param openIdList 微信用户 openId 列表
|
||||
* @param tagId 微信标签 ID
|
||||
*/
|
||||
batchuntagging: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -425,6 +814,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取用户身上的标签列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param openId 微信用户 openId
|
||||
* @returns 返回用户的标签 ID 列表
|
||||
*/
|
||||
getUserTags: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -432,6 +828,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取微信公众号用户列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param nextOpenId 下一个用户的 openId(用于分页)
|
||||
* @returns 返回用户列表
|
||||
*/
|
||||
getUsers: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -439,6 +842,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 为单个用户设置标签列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param openId 微信用户 openId
|
||||
* @param tagIdList 标签 ID 列表
|
||||
*/
|
||||
tagging: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -447,6 +857,12 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 从微信服务器同步用户标签到本地
|
||||
* @param applicationId 应用 ID
|
||||
* @param openId 微信用户 openId
|
||||
*/
|
||||
syncToLocale: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -454,6 +870,13 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 将本地用户标签同步到微信服务器
|
||||
* @param applicationId 应用 ID
|
||||
* @param id 本地用户标签关联 ID
|
||||
* @param openId 微信用户 openId
|
||||
*/
|
||||
syncToWechat: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
|
|
@ -462,6 +885,12 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 同步短信模板(从服务商同步到本地)
|
||||
* @param systemId 系统 ID
|
||||
* @param origin 短信服务商(如阿里云、腾讯云等)
|
||||
*/
|
||||
syncSmsTemplate: (
|
||||
params: {
|
||||
systemId: string;
|
||||
|
|
@ -469,18 +898,97 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取应用的登录方式配置列表
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回登录方式配置列表
|
||||
*/
|
||||
getApplicationPassports: (
|
||||
params: {
|
||||
applicationId: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<EntityDict['applicationPassport']['Schema'][]>;
|
||||
|
||||
/**
|
||||
* 根据登录方式 ID 列表删除应用的登录方式配置
|
||||
* @param passportIds 登录方式 ID 列表
|
||||
*/
|
||||
removeApplicationPassportsByPIds: (
|
||||
params: {
|
||||
passportIds: string[];
|
||||
},
|
||||
content: BackendRuntimeContext<ED>
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 通过 OAuth 2.0 第三方登录
|
||||
* @param code OAuth 授权码
|
||||
* @param state 状态码(用于验证请求)
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByOauth: (
|
||||
params: {
|
||||
code: string;
|
||||
state: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* 创建 OAuth 登录/绑定状态码
|
||||
* @param providerId OAuth 提供商 ID
|
||||
* @param userId 用户 ID(绑定时需要)
|
||||
* @param type 操作类型(bind-绑定,login-登录)
|
||||
* @returns 返回状态码
|
||||
*/
|
||||
createOAuthState: (
|
||||
params: {
|
||||
providerId: string;
|
||||
userId?: string;
|
||||
type: "bind" | "login";
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* OAuth 2.0 授权确认(用户同意或拒绝授权)
|
||||
* @param response_type 响应类型(固定为 "code")
|
||||
* @param client_id 客户端应用 ID
|
||||
* @param redirect_uri 回调地址
|
||||
* @param scope 授权范围
|
||||
* @param state 状态码
|
||||
* @param action 用户操作(grant-同意,deny-拒绝)
|
||||
* @returns 返回重定向 URL
|
||||
*/
|
||||
authorize: (
|
||||
params: {
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
scope: string;
|
||||
state: string;
|
||||
action: "grant" | "deny";
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<{
|
||||
redirectUri: string;
|
||||
}>
|
||||
|
||||
/**
|
||||
* 获取 OAuth 客户端应用信息
|
||||
* @param client_id 客户端应用 ID
|
||||
* @returns 返回客户端应用信息,不存在则返回 null
|
||||
*/
|
||||
getOAuthClientInfo: (
|
||||
params: {
|
||||
client_id: string;
|
||||
},
|
||||
context: BackendRuntimeContext<ED>
|
||||
) => Promise<EntityDict['oauthApplication']['Schema'] | null>;
|
||||
};
|
||||
|
||||
export default AspectDict;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ import {
|
|||
wechatMpJump,
|
||||
} from './wechatMpJump';
|
||||
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
|
||||
import { authorize, createOAuthState, getOAuthClientInfo, loginByOauth } from './oauth';
|
||||
|
||||
const aspectDict = {
|
||||
bindByEmail,
|
||||
|
|
@ -136,6 +137,11 @@ const aspectDict = {
|
|||
removeApplicationPassportsByPIds,
|
||||
verifyPassword,
|
||||
loginWebByMpToken,
|
||||
// oauth
|
||||
loginByOauth,
|
||||
getOAuthClientInfo,
|
||||
createOAuthState,
|
||||
authorize,
|
||||
};
|
||||
|
||||
export default aspectDict;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,483 @@
|
|||
import assert from "assert";
|
||||
import { BRC } from "../types/RuntimeCxt";
|
||||
import { EntityDict } from "../oak-app-domain";
|
||||
import { NativeEnv, OakUserException, WebEnv, WechatMpEnv } from "oak-domain/lib/types";
|
||||
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||
import { loadTokenInfo, setUpTokenAndUser } from "./token";
|
||||
import { randomUUID } from "crypto";
|
||||
import { processUserInfo } from "../utils/oauth";
|
||||
|
||||
export async function loginByOauth<ED extends EntityDict>(params: {
|
||||
code: string;
|
||||
state: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BRC<ED>) {
|
||||
const { code, state: stateCode, env } = params;
|
||||
|
||||
const closeRootMode = context.openRootMode();
|
||||
const currentUserId = context.getCurrentUserId(true);
|
||||
const applicationId = context.getApplicationId();
|
||||
const islogginedIn = !!currentUserId;
|
||||
|
||||
assert(applicationId, '无法获取当前应用ID');
|
||||
assert(code, 'code 参数缺失');
|
||||
assert(stateCode, 'state 参数缺失');
|
||||
|
||||
// 验证 state 并获取 OAuth 配置
|
||||
const [state] = await context.select("oauthState", {
|
||||
data: {
|
||||
provider: {
|
||||
type: 1,
|
||||
clientId: 1,
|
||||
redirectUri: 1,
|
||||
clientSecret: 1,
|
||||
tokenEndpoint: 1,
|
||||
userInfoEndpoint: 1,
|
||||
ableState: 1,
|
||||
autoRegister: 1,
|
||||
},
|
||||
usedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
state: stateCode,
|
||||
},
|
||||
}, {})
|
||||
|
||||
assert(state, '无效的 state 参数');
|
||||
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
|
||||
// 如果已经使用
|
||||
if (state.usedAt) {
|
||||
throw new OakUserException('该授权请求已被使用,请重新发起授权请求');
|
||||
}
|
||||
|
||||
// 更新为使用过
|
||||
await context.operate("oauthState", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
usedAt: Date.now(),
|
||||
},
|
||||
filter: {
|
||||
id: state.id,
|
||||
}
|
||||
}, {});
|
||||
|
||||
// 使用 code 换取 access_token 并获取用户信息
|
||||
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(
|
||||
code,
|
||||
state.provider,
|
||||
);
|
||||
|
||||
const [existingOAuthUser] = await context.select("oauthUser", {
|
||||
data: {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
providerUserId: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
refId: 1,
|
||||
ref: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
providerUserId: oauthUserInfo.providerUserId,
|
||||
providerConfigId: state.providerId!,
|
||||
}
|
||||
}, {})
|
||||
|
||||
// 已登录的情况
|
||||
if (islogginedIn) {
|
||||
|
||||
// 检查当前用户是否已绑定此提供商
|
||||
const [currentUserBinding] = await context.select("oauthUser", {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
userId: currentUserId,
|
||||
providerConfigId: state.providerId!,
|
||||
}
|
||||
}, {});
|
||||
|
||||
if (currentUserBinding) {
|
||||
throw new OakUserException('当前用户已绑定该 OAuth 平台账号');
|
||||
}
|
||||
|
||||
if (existingOAuthUser) {
|
||||
throw new OakUserException('该 OAuth 账号已被其他用户绑定');
|
||||
}
|
||||
|
||||
console.log("绑定 OAuth 账号到当前用户:", currentUserId, oauthUserInfo.providerUserId);
|
||||
|
||||
// 创建绑定关系
|
||||
await context.operate("oauthUser", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
userId: currentUserId,
|
||||
providerConfigId: state.providerId,
|
||||
providerUserId: oauthUserInfo.providerUserId,
|
||||
rawUserInfo: oauthUserInfo.rawData,
|
||||
accessToken,
|
||||
accessExpiresAt: accessTokenExp,
|
||||
refreshToken,
|
||||
refreshExpiresAt: refreshTokenExp,
|
||||
applicationId,
|
||||
stateId: state.id,
|
||||
}
|
||||
}, {});
|
||||
|
||||
// 返回当前 token
|
||||
const tokenValue = context.getTokenValue()!;
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
|
||||
// 未登录,OAuth账号已存在,直接登录
|
||||
} else if (existingOAuthUser) {
|
||||
|
||||
console.log("使用已绑定的 OAuth 账号登录:", existingOAuthUser.id);
|
||||
|
||||
const { user } = existingOAuthUser;
|
||||
const targetUser = user?.userState === 'merged' ? user.ref : user;
|
||||
|
||||
const tokenValue = await setUpTokenAndUser(
|
||||
env,
|
||||
context,
|
||||
'oauthUser',
|
||||
existingOAuthUser.id, // 使用已存在的 oauthUser ID
|
||||
undefined,
|
||||
targetUser // 关联的用户
|
||||
);
|
||||
|
||||
// 更新登录信息
|
||||
await context.operate("oauthUser", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
rawUserInfo: oauthUserInfo.rawData,
|
||||
accessToken,
|
||||
accessExpiresAt: accessTokenExp,
|
||||
refreshToken,
|
||||
refreshExpiresAt: refreshTokenExp,
|
||||
applicationId,
|
||||
stateId: state.id,
|
||||
},
|
||||
filter: {
|
||||
id: existingOAuthUser.id,
|
||||
}
|
||||
}, {});
|
||||
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
|
||||
}
|
||||
// 未登录,OAuth账号不存在,创建新用户
|
||||
else {
|
||||
|
||||
if (!state.provider.autoRegister) {
|
||||
throw new OakUserException('您还没有账号,请先注册一个账号');
|
||||
}
|
||||
|
||||
console.log("使用未绑定的 OAuth 账号登录:", oauthUserInfo.providerUserId);
|
||||
|
||||
const newUserId = await generateNewIdAsync();
|
||||
const oauthUserCreateData: EntityDict['oauthUser']['CreateOperationData'] = {
|
||||
id: newUserId,
|
||||
providerConfigId: state.providerId,
|
||||
providerUserId: oauthUserInfo.providerUserId,
|
||||
rawUserInfo: oauthUserInfo.rawData,
|
||||
accessToken,
|
||||
accessExpiresAt: accessTokenExp,
|
||||
refreshToken,
|
||||
refreshExpiresAt: refreshTokenExp,
|
||||
applicationId,
|
||||
stateId: state.id,
|
||||
loadState: 'unload'
|
||||
};
|
||||
|
||||
// 不传 user 参数,会自动创建新用户
|
||||
const tokenValue = await setUpTokenAndUser(
|
||||
env,
|
||||
context,
|
||||
'oauthUser',
|
||||
undefined,
|
||||
oauthUserCreateData, // 创建新的 oauthUser
|
||||
undefined // 不传 user,自动创建新用户
|
||||
);
|
||||
|
||||
await context.operate("oauthUser", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'loadUserInfo',
|
||||
data: {},
|
||||
filter: {
|
||||
id: newUserId,
|
||||
}
|
||||
}, {});
|
||||
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
}
|
||||
export async function getOAuthClientInfo<ED extends EntityDict>(params: {
|
||||
client_id: string;
|
||||
}, context: BRC<ED>) {
|
||||
const { client_id } = params;
|
||||
const closeRootMode = context.openRootMode();
|
||||
const systemId = context.getSystemId();
|
||||
const [oauthApp] = await context.select("oauthApplication", {
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
redirectUris: 1,
|
||||
description: 1,
|
||||
logo: 1,
|
||||
isConfidential: 1,
|
||||
},
|
||||
filter: {
|
||||
id: client_id,
|
||||
systemId: systemId,
|
||||
ableState: "enabled",
|
||||
}
|
||||
}, {})
|
||||
|
||||
if (!oauthApp) {
|
||||
throw new OakUserException('未经授权的客户端应用');
|
||||
}
|
||||
|
||||
closeRootMode();
|
||||
return oauthApp;
|
||||
}
|
||||
export async function createOAuthState<ED extends EntityDict>(params: {
|
||||
providerId: string;
|
||||
userId?: string;
|
||||
type: 'login' | 'bind';
|
||||
}, context: BRC<ED>) {
|
||||
const { providerId, userId, type } = params;
|
||||
const closeRootMode = context.openRootMode();
|
||||
const generateCode = () => {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
const state = generateCode();
|
||||
|
||||
await context.operate("oauthState", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
providerId,
|
||||
userId,
|
||||
type,
|
||||
state
|
||||
}
|
||||
}, {})
|
||||
|
||||
closeRootMode();
|
||||
return state;
|
||||
}
|
||||
export async function authorize<ED extends EntityDict>(params: {
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
scope?: string;
|
||||
state?: string;
|
||||
action: 'grant' | 'deny';
|
||||
}, context: BRC<ED>) {
|
||||
const { response_type, client_id, redirect_uri, scope, state, action } = params;
|
||||
|
||||
if (response_type !== 'code') {
|
||||
throw new OakUserException('不支持的 response_type 类型');
|
||||
}
|
||||
|
||||
const closeRootMode = context.openRootMode();
|
||||
const systemId = context.getSystemId();
|
||||
|
||||
const [oauthApp] = await context.select("oauthApplication", {
|
||||
data: {
|
||||
id: 1,
|
||||
redirectUris: 1,
|
||||
isConfidential: 1,
|
||||
},
|
||||
filter: {
|
||||
id: client_id,
|
||||
systemId: systemId,
|
||||
}
|
||||
}, {})
|
||||
|
||||
if (!oauthApp) {
|
||||
throw new OakUserException('未经授权的客户端应用');
|
||||
}
|
||||
|
||||
// 创建授权记录
|
||||
const recordId = await generateNewIdAsync();
|
||||
await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: recordId,
|
||||
userId: context.getCurrentUserId()!,
|
||||
applicationId: oauthApp.id,
|
||||
usageState: action === 'grant' ? 'granted' : 'denied',
|
||||
authorizedAt: Date.now(),
|
||||
}
|
||||
}, {})
|
||||
|
||||
if (action === 'deny') {
|
||||
const params = new URLSearchParams();
|
||||
params.set('error', 'access_denied');
|
||||
params.set('error_description', '用户拒绝了授权请求');
|
||||
|
||||
if (state) {
|
||||
params.set('state', state);
|
||||
}
|
||||
closeRootMode();
|
||||
return {
|
||||
redirectUri: `${redirect_uri}?${params.toString()}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'grant') {
|
||||
|
||||
// 检查redirectUri 是否在注册的列表中
|
||||
if (!oauthApp.redirectUris?.includes(redirect_uri)) {
|
||||
console.log('不合法的重定向 URI:', redirect_uri, oauthApp.redirectUris);
|
||||
throw new OakUserException('重定向 URI 不合法');
|
||||
}
|
||||
|
||||
const code = randomUUID();
|
||||
const codeId = await generateNewIdAsync();
|
||||
// 存储授权码
|
||||
await context.operate("oauthAuthorizationCode", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: codeId,
|
||||
code,
|
||||
redirectUri: redirect_uri,
|
||||
oauthAppId: oauthApp.id,
|
||||
applicationId: context.getApplicationId()!,
|
||||
userId: context.getCurrentUserId()!,
|
||||
scope: [scope || ""],
|
||||
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
|
||||
}
|
||||
}, {})
|
||||
|
||||
// 更新记录
|
||||
await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
codeId: codeId,
|
||||
},
|
||||
filter: {
|
||||
id: recordId,
|
||||
}
|
||||
}, {})
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('code', code);
|
||||
if (state) {
|
||||
params.set('state', state);
|
||||
}
|
||||
closeRootMode();
|
||||
return {
|
||||
redirectUri: `${redirect_uri}?${params.toString()}`,
|
||||
}
|
||||
}
|
||||
|
||||
closeRootMode();
|
||||
|
||||
throw new Error('unknown action');
|
||||
}
|
||||
|
||||
|
||||
const fetchOAuthUserInfo = async (
|
||||
code: string,
|
||||
providerConfig: EntityDict['oauthProvider']['Schema'],
|
||||
): Promise<{
|
||||
oauthUserInfo: {
|
||||
providerUserId: string;
|
||||
rawData: any;
|
||||
};
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
accessTokenExp?: number;
|
||||
refreshTokenExp?: number;
|
||||
}> => {
|
||||
// 1. 使用 code 换取 access_token
|
||||
const tokenResponse = await fetch(providerConfig.tokenEndpoint!, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: providerConfig.clientId!,
|
||||
client_secret: providerConfig.clientSecret!,
|
||||
redirect_uri: providerConfig.redirectUri!,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorjson = await tokenResponse.json();
|
||||
if (errorjson.error == "unauthorized_client") {
|
||||
throw new OakUserException(`授权校验已过期,请重新发起授权请求`);
|
||||
} else if (errorjson.error == "invalid_grant") {
|
||||
throw new OakUserException(`授权码无效或已过期,请重新发起授权请求`);
|
||||
} else if (errorjson.error) {
|
||||
throw new OakUserException(`获取访问令牌失败: ${errorjson.error_description || errorjson.error}`);
|
||||
}
|
||||
throw new OakUserException(`获取访问令牌失败: ${tokenResponse.statusText}`);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
const accessToken = tokenData.access_token;
|
||||
const refreshToken = tokenData.refresh_token;
|
||||
const accessTokenExp = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined;
|
||||
const refreshTokenExp = tokenData.refresh_expires_in ? Date.now() + tokenData.refresh_expires_in * 1000 : undefined;
|
||||
const tokenType = tokenData.token_type;
|
||||
|
||||
assert(tokenType && tokenType.toLowerCase() === 'bearer', '不支持的令牌类型');
|
||||
|
||||
// 2. 使用 access_token 获取用户信息
|
||||
const userInfoResponse = await fetch(providerConfig.userInfoEndpoint!, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
throw new OakUserException(`获取用户信息失败: ${userInfoResponse.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
const userInfoData = await userInfoResponse.json();
|
||||
|
||||
// TODO: 用户信息中获取唯一标识,通过注入解决: utils/oauth/index.ts
|
||||
const { id: providerUserId } = await processUserInfo(providerConfig.type!, userInfoData)
|
||||
|
||||
if (!providerUserId) {
|
||||
throw new OakUserException('用户信息中缺少唯一标识符');
|
||||
}
|
||||
|
||||
return {
|
||||
oauthUserInfo: {
|
||||
providerUserId,
|
||||
rawData: userInfoData,
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExp,
|
||||
refreshTokenExp,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue