merge
This commit is contained in:
commit
5f33319f75
|
|
@ -7,66 +7,157 @@ import { MaterialType } from '../types/WeChat';
|
|||
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;
|
||||
env: WebEnv;
|
||||
}, 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;
|
||||
captcha: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 通过邮箱绑定当前登录用户
|
||||
* @param email 邮箱地址
|
||||
* @param captcha 验证码
|
||||
* @param env 环境信息
|
||||
*/
|
||||
bindByEmail: (params: {
|
||||
email: string;
|
||||
captcha: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 通过手机号和验证码登录
|
||||
* @param mobile 手机号
|
||||
* @param captcha 验证码
|
||||
* @param disableRegister 是否禁止自动注册,true 时账号不存在会报错
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByMobile: (params: {
|
||||
mobile: string;
|
||||
captcha: string;
|
||||
disableRegister?: boolean;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 验证用户密码是否正确
|
||||
* @param password 密码(明文或 SHA1 密文,根据系统配置)
|
||||
* @param env 环境信息
|
||||
*/
|
||||
verifyPassword: (params: {
|
||||
password: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 通过账号(手机号/邮箱/登录名)和密码登录
|
||||
* @param account 账号(可以是手机号、邮箱或登录名)
|
||||
* @param password 密码(明文或 SHA1 密文,根据系统配置)
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByAccount: (params: {
|
||||
account: string;
|
||||
password: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 通过邮箱和验证码登录
|
||||
* @param email 邮箱地址
|
||||
* @param captcha 验证码
|
||||
* @param disableRegister 是否禁止自动注册,true 时账号不存在会报错
|
||||
* @param env 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByEmail: (params: {
|
||||
email: string;
|
||||
captcha: string;
|
||||
disableRegister?: boolean;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 微信公众号登录
|
||||
* @param code 微信授权 code
|
||||
* @param env Web 环境信息
|
||||
* @param wechatLoginId 可选的微信登录 ID(用于扫码登录场景)
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginWechat: ({ code, env, wechatLoginId, }: {
|
||||
code: string;
|
||||
env: WebEnv;
|
||||
wechatLoginId?: string;
|
||||
}, 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, env, }: {
|
||||
code: string;
|
||||
env: WechatMpEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 微信原生 APP 登录
|
||||
* @param code 微信授权 code
|
||||
* @param env 原生 APP 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginWechatNative: ({ code, env, }: {
|
||||
code: string;
|
||||
env: NativeEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 同步微信小程序用户信息(昵称、头像等)
|
||||
* @param nickname 昵称
|
||||
* @param avatarUrl 头像 URL
|
||||
* @param encryptedData 加密数据
|
||||
* @param iv 加密算法的初始向量
|
||||
* @param signature 签名
|
||||
*/
|
||||
syncUserInfoWechatMp: ({ nickname, avatarUrl, encryptedData, iv, signature, }: {
|
||||
nickname: string;
|
||||
avatarUrl: string;
|
||||
|
|
@ -74,25 +165,61 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
iv: string;
|
||||
signature: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 唤醒寄生用户(将 shadow 状态的用户激活)
|
||||
* @param id 用户 ID
|
||||
* @param env 环境信息
|
||||
* @returns 返回 token
|
||||
*/
|
||||
wakeupParasite: (params: {
|
||||
id: string;
|
||||
env: WebEnv | WechatMpEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 刷新 token,延长有效期
|
||||
* @param tokenValue 当前 token
|
||||
* @param env 环境信息
|
||||
* @param applicationId 应用 ID
|
||||
* @returns 返回新的 token
|
||||
*/
|
||||
refreshToken: (params: {
|
||||
tokenValue: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
applicationId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 通过手机号发送验证码
|
||||
* @param mobile 手机号
|
||||
* @param env 环境信息
|
||||
* @param type 验证码类型:login-登录,changePassword-修改密码,confirm-确认操作
|
||||
* @returns 返回验证码 ID
|
||||
*/
|
||||
sendCaptchaByMobile: (params: {
|
||||
mobile: string;
|
||||
env: WechatMpEnv | WebEnv;
|
||||
type: 'login' | 'changePassword' | 'confirm';
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 通过邮箱发送验证码
|
||||
* @param email 邮箱地址
|
||||
* @param env 环境信息
|
||||
* @param type 验证码类型:login-登录,changePassword-修改密码,confirm-确认操作
|
||||
* @returns 返回验证码 ID
|
||||
*/
|
||||
sendCaptchaByEmail: (params: {
|
||||
email: string;
|
||||
env: WechatMpEnv | WebEnv;
|
||||
type: 'login' | 'changePassword' | 'confirm';
|
||||
}, 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;
|
||||
type: AppType;
|
||||
|
|
@ -100,6 +227,12 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
data: ED['application']['Projection'];
|
||||
appId?: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 生成微信 JS-SDK 签名,用于调用微信 JS 接口
|
||||
* @param url 当前页面 URL
|
||||
* @param env Web 环境信息
|
||||
* @returns 返回签名信息(signature、noncestr、timestamp、appId)
|
||||
*/
|
||||
signatureJsSDK: (params: {
|
||||
url: string;
|
||||
env: WebEnv;
|
||||
|
|
@ -109,38 +242,88 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
timestamp: number;
|
||||
appId: string;
|
||||
}>;
|
||||
/**
|
||||
* 更新平台或系统的配置信息
|
||||
* @param entity 实体类型(platform 或 system)
|
||||
* @param entityId 实体 ID
|
||||
* @param config 配置对象
|
||||
*/
|
||||
updateConfig: (params: {
|
||||
entity: 'platform' | 'system';
|
||||
entityId: string;
|
||||
config: Config;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 更新平台、系统或应用的样式配置
|
||||
* @param entity 实体类型(platform/system/application)
|
||||
* @param entityId 实体 ID
|
||||
* @param style 样式对象
|
||||
*/
|
||||
updateStyle: (params: {
|
||||
entity: 'platform' | 'system' | 'application';
|
||||
entityId: string;
|
||||
style: Style;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 更新应用的配置信息
|
||||
* @param entity 实体类型(application)
|
||||
* @param entityId 应用 ID
|
||||
* @param config 应用配置对象
|
||||
*/
|
||||
updateApplicationConfig: (params: {
|
||||
entity: 'application';
|
||||
entityId: string;
|
||||
config: EntityDict['application']['Schema']['config'];
|
||||
}, 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'];
|
||||
interval: number;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 解绑微信用户
|
||||
* @param wechatUserId 微信用户 ID
|
||||
* @param captcha 可选的验证码
|
||||
* @param mobile 可选的手机号
|
||||
*/
|
||||
unbindingWechat: (params: {
|
||||
wechatUserId: string;
|
||||
captcha?: string;
|
||||
mobile?: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<void>;
|
||||
/**
|
||||
* 通过微信登录会话 ID 完成登录(Web 端扫码登录确认)
|
||||
* @param wechatLoginId 微信登录会话 ID
|
||||
* @param env Web 环境信息
|
||||
* @returns 返回登录 token
|
||||
*/
|
||||
loginByWechat: (params: {
|
||||
wechatLoginId: string;
|
||||
env: WebEnv;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<string>;
|
||||
/**
|
||||
* 从 URL 中提取网页信息(标题、发布时间、图片列表)
|
||||
* @param url 网页 URL
|
||||
* @returns 返回网页信息
|
||||
*/
|
||||
getInfoByUrl: (params: {
|
||||
url: string;
|
||||
}) => Promise<{
|
||||
|
|
@ -148,9 +331,23 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
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;
|
||||
prevPassword?: string;
|
||||
|
|
@ -161,136 +358,374 @@ export type AspectDict<ED extends EntityDict> = {
|
|||
result: string;
|
||||
times?: number;
|
||||
}>;
|
||||
/**
|
||||
* 创建或获取会话(用于客服系统等场景)
|
||||
* @param data 微信事件数据(可选)
|
||||
* @param type 应用类型
|
||||
* @param entity 关联实体类型(可选)
|
||||
* @param entityId 关联实体 ID(可选)
|
||||
* @returns 返回会话 ID
|
||||
*/
|
||||
createSession: (params: {
|
||||
data?: WechatPublicEventData | WechatMpEventData;
|
||||
type: AppType;
|
||||
entity?: string;
|
||||
entityId?: string;
|
||||
}, 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;
|
||||
menuConfig: any;
|
||||
id: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 创建微信公众号个性化菜单(针对特定用户群体)
|
||||
* @param applicationId 应用 ID
|
||||
* @param menuConfig 菜单配置
|
||||
* @param id 菜单记录 ID
|
||||
*/
|
||||
createConditionalMenu: (params: {
|
||||
applicationId: string;
|
||||
menuConfig: any;
|
||||
id: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 删除微信公众号个性化菜单
|
||||
* @param applicationId 应用 ID
|
||||
* @param menuId 微信菜单 ID
|
||||
*/
|
||||
deleteConditionalMenu: (params: {
|
||||
applicationId: string;
|
||||
menuId: number;
|
||||
}, 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;
|
||||
offset?: number;
|
||||
count: number;
|
||||
noContent?: 0 | 1;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 获取微信公众号单个图文消息素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param articleId 图文消息 ID
|
||||
* @returns 返回图文消息详情
|
||||
*/
|
||||
getArticle: (params: {
|
||||
applicationId: string;
|
||||
articleId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 批量获取微信素材列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param type 素材类型(image-图片,voice-语音,video-视频,news-图文)
|
||||
* @param offset 起始位置(可选)
|
||||
* @param count 获取数量
|
||||
* @returns 返回素材列表
|
||||
*/
|
||||
batchGetMaterialList: (params: {
|
||||
applicationId: string;
|
||||
type: MaterialType;
|
||||
offset?: number;
|
||||
count: number;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 获取微信素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param mediaId 素材 ID
|
||||
* @param isPermanent 是否为永久素材(默认获取临时素材)
|
||||
* @returns 返回素材数据
|
||||
*/
|
||||
getMaterial: (params: {
|
||||
applicationId: string;
|
||||
mediaId: string;
|
||||
isPermanent?: boolean;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 删除微信永久素材
|
||||
* @param applicationId 应用 ID
|
||||
* @param mediaId 素材 ID
|
||||
*/
|
||||
deleteMaterial: (params: {
|
||||
applicationId: string;
|
||||
mediaId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 创建微信公众号用户标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param name 标签名称
|
||||
* @returns 返回创建结果
|
||||
*/
|
||||
createTag: (params: {
|
||||
applicationId: string;
|
||||
name: string;
|
||||
}, 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;
|
||||
id: number;
|
||||
name: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 删除微信公众号用户标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param id 本地标签 ID
|
||||
* @param wechatId 微信标签 ID
|
||||
*/
|
||||
deleteTag: (params: {
|
||||
applicationId: string;
|
||||
id: string;
|
||||
wechatId: number;
|
||||
}, 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;
|
||||
id: string;
|
||||
}, 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;
|
||||
tagId: number;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 批量为用户打标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param openIdList 微信用户 openId 列表
|
||||
* @param tagId 微信标签 ID
|
||||
*/
|
||||
batchtagging: (params: {
|
||||
applicationId: string;
|
||||
openIdList: string[];
|
||||
tagId: number;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 批量为用户取消标签
|
||||
* @param applicationId 应用 ID
|
||||
* @param openIdList 微信用户 openId 列表
|
||||
* @param tagId 微信标签 ID
|
||||
*/
|
||||
batchuntagging: (params: {
|
||||
applicationId: string;
|
||||
openIdList: string[];
|
||||
tagId: number;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 获取用户身上的标签列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param openId 微信用户 openId
|
||||
* @returns 返回用户的标签 ID 列表
|
||||
*/
|
||||
getUserTags: (params: {
|
||||
applicationId: string;
|
||||
openId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 获取微信公众号用户列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param nextOpenId 下一个用户的 openId(用于分页)
|
||||
* @returns 返回用户列表
|
||||
*/
|
||||
getUsers: (params: {
|
||||
applicationId: string;
|
||||
nextOpenId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 为单个用户设置标签列表
|
||||
* @param applicationId 应用 ID
|
||||
* @param openId 微信用户 openId
|
||||
* @param tagIdList 标签 ID 列表
|
||||
*/
|
||||
tagging: (params: {
|
||||
applicationId: string;
|
||||
openId: string;
|
||||
tagIdList: number[];
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 从微信服务器同步用户标签到本地
|
||||
* @param applicationId 应用 ID
|
||||
* @param openId 微信用户 openId
|
||||
*/
|
||||
syncToLocale: (params: {
|
||||
applicationId: string;
|
||||
openId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 将本地用户标签同步到微信服务器
|
||||
* @param applicationId 应用 ID
|
||||
* @param id 本地用户标签关联 ID
|
||||
* @param openId 微信用户 openId
|
||||
*/
|
||||
syncToWechat: (params: {
|
||||
applicationId: string;
|
||||
id: string;
|
||||
openId: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<any>;
|
||||
/**
|
||||
* 同步短信模板(从服务商同步到本地)
|
||||
* @param systemId 系统 ID
|
||||
* @param origin 短信服务商(如阿里云、腾讯云等)
|
||||
*/
|
||||
syncSmsTemplate: (params: {
|
||||
systemId: string;
|
||||
origin: EntityDict['smsTemplate']['Schema']['origin'];
|
||||
}, 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";
|
||||
code_challenge?: string;
|
||||
code_challenge_method?: 'plain' | 'S256';
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<{
|
||||
redirectUri: string;
|
||||
}>;
|
||||
/**
|
||||
* 获取 OAuth 客户端应用信息
|
||||
* @param client_id 客户端应用 ID
|
||||
* @returns 返回客户端应用信息,不存在则返回 null
|
||||
*/
|
||||
getOAuthClientInfo: (params: {
|
||||
client_id: string;
|
||||
currentUserId?: string;
|
||||
}, context: BackendRuntimeContext<ED>) => Promise<{
|
||||
data: EntityDict['oauthApplication']['Schema'] | null;
|
||||
alreadyAuth: boolean;
|
||||
}>;
|
||||
};
|
||||
export default AspectDict;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { createTag, getTags, editTag, deleteTag, syncTag, oneKeySync } from './w
|
|||
import { getTagUsers, batchtagging, batchuntagging, getUserTags, getUsers, tagging, syncToLocale, syncToWechat } from './userWechatPublicTag';
|
||||
import { wechatMpJump } from './wechatMpJump';
|
||||
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
|
||||
import { authorize, createOAuthState, getOAuthClientInfo, loginByOauth } from './oauth';
|
||||
declare const aspectDict: {
|
||||
bindByEmail: typeof bindByEmail;
|
||||
bindByMobile: typeof bindByMobile;
|
||||
|
|
@ -80,6 +81,10 @@ declare const aspectDict: {
|
|||
removeApplicationPassportsByPIds: typeof removeApplicationPassportsByPIds;
|
||||
verifyPassword: typeof verifyPassword;
|
||||
loginWebByMpToken: typeof loginWebByMpToken;
|
||||
loginByOauth: typeof loginByOauth;
|
||||
getOAuthClientInfo: typeof getOAuthClientInfo;
|
||||
createOAuthState: typeof createOAuthState;
|
||||
authorize: typeof authorize;
|
||||
};
|
||||
export default aspectDict;
|
||||
export { AspectDict } from './AspectDict';
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { createTag, getTags, editTag, deleteTag, syncTag, oneKeySync, } from './
|
|||
import { getTagUsers, batchtagging, batchuntagging, getUserTags, getUsers, tagging, syncToLocale, syncToWechat, } from './userWechatPublicTag';
|
||||
import { wechatMpJump, } from './wechatMpJump';
|
||||
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
|
||||
import { authorize, createOAuthState, getOAuthClientInfo, loginByOauth } from './oauth';
|
||||
const aspectDict = {
|
||||
bindByEmail,
|
||||
bindByMobile,
|
||||
|
|
@ -80,5 +81,10 @@ const aspectDict = {
|
|||
removeApplicationPassportsByPIds,
|
||||
verifyPassword,
|
||||
loginWebByMpToken,
|
||||
// oauth
|
||||
loginByOauth,
|
||||
getOAuthClientInfo,
|
||||
createOAuthState,
|
||||
authorize,
|
||||
};
|
||||
export default aspectDict;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { BRC } from "../types/RuntimeCxt";
|
||||
import { EntityDict } from "../oak-app-domain";
|
||||
import { NativeEnv, WebEnv, WechatMpEnv } from "oak-domain/lib/types";
|
||||
export declare function loginByOauth<ED extends EntityDict>(params: {
|
||||
code: string;
|
||||
state: string;
|
||||
env: WebEnv | WechatMpEnv | NativeEnv;
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
export declare function getOAuthClientInfo<ED extends EntityDict>(params: {
|
||||
client_id: string;
|
||||
currentUserId?: string;
|
||||
}, context: BRC<ED>): Promise<{
|
||||
data: Partial<ED["oauthApplication"]["Schema"]>;
|
||||
alreadyAuth: boolean;
|
||||
}>;
|
||||
export declare function createOAuthState<ED extends EntityDict>(params: {
|
||||
providerId: string;
|
||||
userId?: string;
|
||||
type: 'login' | 'bind';
|
||||
}, context: BRC<ED>): Promise<string>;
|
||||
export declare function authorize<ED extends EntityDict>(params: {
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
scope?: string;
|
||||
state?: string;
|
||||
action: 'grant' | 'deny';
|
||||
code_challenge?: string;
|
||||
code_challenge_method?: 'plain' | 'S256';
|
||||
}, context: BRC<ED>): Promise<{
|
||||
redirectUri: string;
|
||||
}>;
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
import assert from "assert";
|
||||
import { OakUserException } 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(params, context) {
|
||||
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,
|
||||
},
|
||||
}, { dontCollect: true });
|
||||
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,
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
// 使用 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,
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
// 已登录的情况
|
||||
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,
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
// 返回当前 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,
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
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 = {
|
||||
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,
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
}
|
||||
export async function getOAuthClientInfo(params, context) {
|
||||
const { client_id, currentUserId } = params;
|
||||
const closeRootMode = context.openRootMode();
|
||||
const systemId = context.getSystemId();
|
||||
const applicationId = context.getApplicationId();
|
||||
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",
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
// 如果还有正在生效的授权,说明已经授权过了
|
||||
const [hasAuth] = await context.select("oauthUserAuthorization", {
|
||||
data: {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
usageState: 1,
|
||||
authorizedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
// 如果 已经授权过token并且 没有被撤销
|
||||
tokenId: {
|
||||
$exists: true
|
||||
},
|
||||
token: {
|
||||
revokedAt: {
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
usageState: 'granted',
|
||||
code: {
|
||||
// 当前应用下的认证客户端
|
||||
oauthApp: {
|
||||
id: client_id
|
||||
},
|
||||
applicationId: applicationId,
|
||||
userId: currentUserId,
|
||||
}
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
if (hasAuth) {
|
||||
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
|
||||
}
|
||||
if (!oauthApp) {
|
||||
throw new OakUserException('未经授权的客户端应用');
|
||||
}
|
||||
closeRootMode();
|
||||
return {
|
||||
data: oauthApp,
|
||||
alreadyAuth: !!hasAuth,
|
||||
};
|
||||
}
|
||||
export async function createOAuthState(params, context) {
|
||||
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
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
closeRootMode();
|
||||
return state;
|
||||
}
|
||||
export async function authorize(params, context) {
|
||||
const { response_type, client_id, redirect_uri, scope, state, action, code_challenge, code_challenge_method } = 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,
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
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' ? 'unused' : 'denied',
|
||||
authorizedAt: Date.now(),
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
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 === undefined ? [] : [scope],
|
||||
expiresAt: Date.now() + 10 * 60 * 1000,
|
||||
// PKCE 支持
|
||||
codeChallenge: code_challenge,
|
||||
codeChallengeMethod: code_challenge_method || 'plain',
|
||||
}
|
||||
}, { dontCollect: true });
|
||||
// 更新记录
|
||||
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, providerConfig) => {
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,17 @@
|
|||
import { EntityDict } from '../oak-app-domain';
|
||||
import { NativeEnv, WebEnv, WechatMpEnv } from 'oak-domain/lib/types/Environment';
|
||||
import { BRC } from '../types/RuntimeCxt';
|
||||
/**
|
||||
* 根据user的不同情况,完成登录动作
|
||||
* @param env
|
||||
* @param context
|
||||
* @param user
|
||||
* @return tokenValue
|
||||
*/
|
||||
export declare function setUpTokenAndUser<ED extends EntityDict>(env: WebEnv | WechatMpEnv | NativeEnv, context: BRC<ED>, entity: string, // 支持更多的登录渠道使用此函数创建token
|
||||
entityId?: string, // 如果是现有对象传id,如果没有对象传createData
|
||||
createData?: any, user?: Partial<ED['user']['Schema']>): Promise<string>;
|
||||
export declare function loadTokenInfo<ED extends EntityDict>(tokenValue: string, context: BRC<ED>): Promise<Partial<ED["token"]["Schema"]>[]>;
|
||||
export declare function loginByMobile<ED extends EntityDict>(params: {
|
||||
mobile: string;
|
||||
captcha?: string;
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ function autoMergeUser(context) {
|
|||
* @param user
|
||||
* @return tokenValue
|
||||
*/
|
||||
async function setUpTokenAndUser(env, context, entity, // 支持更多的登录渠道使用此函数创建token
|
||||
export async function setUpTokenAndUser(env, context, entity, // 支持更多的登录渠道使用此函数创建token
|
||||
entityId, // 如果是现有对象传id,如果没有对象传createData
|
||||
createData, user) {
|
||||
const currentToken = context.getToken(true);
|
||||
|
|
@ -431,7 +431,7 @@ async function setupMobile(mobile, env, context) {
|
|||
});
|
||||
}
|
||||
}
|
||||
async function loadTokenInfo(tokenValue, context) {
|
||||
export async function loadTokenInfo(tokenValue, context) {
|
||||
return await context.select('token', {
|
||||
data: cloneDeep(tokenProjection),
|
||||
filter: {
|
||||
|
|
@ -566,12 +566,12 @@ export async function verifyPassword(params, context) {
|
|||
]
|
||||
};
|
||||
}
|
||||
else if (pwdFilter === 'plain') {
|
||||
else if (pwdMode === 'plain') {
|
||||
pwdFilter = {
|
||||
password,
|
||||
};
|
||||
}
|
||||
else if (pwdFilter === 'sha1') {
|
||||
else if (pwdMode === 'sha1') {
|
||||
pwdFilter = {
|
||||
passwordSha1: password,
|
||||
};
|
||||
|
|
@ -909,11 +909,11 @@ export async function loginByAccount(params, context) {
|
|||
passwordSha1: encryptPasswordSha1(password),
|
||||
},
|
||||
],
|
||||
},
|
||||
updateData = {
|
||||
password,
|
||||
passwordSha1: encryptPasswordSha1(password),
|
||||
};
|
||||
};
|
||||
updateData = {
|
||||
password,
|
||||
passwordSha1: encryptPasswordSha1(password),
|
||||
};
|
||||
}
|
||||
else if (pwdMode === 'plain') {
|
||||
pwdFilter = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { NativeConfig, WebConfig, WechatMpConfig, WechatPublicConfig } from '../../../entities/Application';
|
||||
export type AppConfig = WebConfig | WechatMpConfig | WechatPublicConfig | NativeConfig;
|
||||
export type CosConfig = AppConfig['cos'];
|
||||
declare const _default: any;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import { cloneDeep, set } from 'oak-domain/lib/utils/lodash';
|
||||
import { generateNewId } from 'oak-domain/lib/utils/uuid';
|
||||
import { isEmptyJsonObject } from '../../../utils/strings';
|
||||
export default OakComponent({
|
||||
isList: false,
|
||||
properties: {
|
||||
config: {},
|
||||
entity: 'application',
|
||||
entityId: '',
|
||||
name: '',
|
||||
},
|
||||
data: {
|
||||
initialConfig: {},
|
||||
dirty: false,
|
||||
currentConfig: {},
|
||||
selections: [],
|
||||
},
|
||||
lifetimes: {
|
||||
async ready() {
|
||||
const { config } = this.props;
|
||||
this.setState({
|
||||
initialConfig: config,
|
||||
dirty: false,
|
||||
currentConfig: cloneDeep(config),
|
||||
});
|
||||
const systemId = this.features.application.getApplication().systemId;
|
||||
const { data: [system] } = await this.features.cache.refresh("system", {
|
||||
data: {
|
||||
config: {
|
||||
Cos: {
|
||||
qiniu: 1,
|
||||
ctyun: 1,
|
||||
aliyun: 1,
|
||||
tencent: 1,
|
||||
local: 1,
|
||||
s3: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id: systemId,
|
||||
}
|
||||
});
|
||||
const cosConfig = system?.config?.Cos;
|
||||
// 如果key存在并且不为defaultOrigin并且value的keys长度大于0,则加入选择项
|
||||
const selections = [];
|
||||
if (cosConfig) {
|
||||
for (const [key, value] of Object.entries(cosConfig)) {
|
||||
if (key === 'defaultOrigin') {
|
||||
continue;
|
||||
}
|
||||
if (value && !isEmptyJsonObject(value)) {
|
||||
selections.push({
|
||||
name: key,
|
||||
value: key,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
selections,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setValue(path, value) {
|
||||
const { currentConfig } = this.state;
|
||||
const newConfig = cloneDeep(currentConfig || {});
|
||||
set(newConfig, path, value);
|
||||
this.setState({
|
||||
currentConfig: newConfig,
|
||||
dirty: true,
|
||||
});
|
||||
},
|
||||
resetConfig() {
|
||||
const { initialConfig } = this.state;
|
||||
this.setState({
|
||||
dirty: false,
|
||||
currentConfig: cloneDeep(initialConfig),
|
||||
});
|
||||
},
|
||||
async updateConfig() {
|
||||
const { currentConfig } = this.state;
|
||||
const { entity, entityId } = this.props;
|
||||
if (!entityId) {
|
||||
this.setMessage({
|
||||
content: '缺少实体ID,无法更新配置',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.features.cache.operate("application", {
|
||||
id: generateNewId(),
|
||||
action: 'update',
|
||||
data: {
|
||||
config: currentConfig,
|
||||
},
|
||||
filter: {
|
||||
id: entityId,
|
||||
}
|
||||
}, {});
|
||||
this.setMessage({
|
||||
content: '操作成功',
|
||||
type: 'success',
|
||||
});
|
||||
this.setState({
|
||||
dirty: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"qiniu": "七牛云",
|
||||
"ctyun": "天翼云",
|
||||
"aliyun": "阿里云",
|
||||
"tencent": "腾讯云",
|
||||
"local": "本地存储",
|
||||
"s3": "S3存储"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
.contains {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
declare const Cos: (props: WebComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
currentConfig: EntityDict['application']['OpSchema']['config'];
|
||||
dirty: boolean;
|
||||
entity: string;
|
||||
name: string;
|
||||
selections: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
}, {
|
||||
setValue: (path: string, value: any) => void;
|
||||
resetConfig: () => void;
|
||||
updateConfig: () => void;
|
||||
}>) => React.JSX.Element;
|
||||
export default Cos;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import Styles from './styles.module.less';
|
||||
import { Affix, Alert, Button, Select, Space, Typography } from 'antd';
|
||||
const Cos = (props) => {
|
||||
const { currentConfig, dirty, entity, name, selections } = props.data;
|
||||
const { t, setValue, resetConfig, updateConfig } = props.methods;
|
||||
return (<>
|
||||
<Affix offsetTop={64}>
|
||||
<Alert message={<div>
|
||||
<text>
|
||||
您正在更新
|
||||
<Typography.Text keyboard>
|
||||
{entity}
|
||||
</Typography.Text>
|
||||
对象
|
||||
<Typography.Text keyboard>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
的COS配置,请谨慎操作
|
||||
</text>
|
||||
</div>} type="info" showIcon action={<Space>
|
||||
<Button disabled={!dirty} type="primary" danger onClick={() => resetConfig()} style={{
|
||||
marginRight: 10,
|
||||
}}>
|
||||
{t('common::reset')}
|
||||
</Button>
|
||||
<Button disabled={!dirty} type="primary" onClick={() => updateConfig()}>
|
||||
{t('common::action.confirm')}
|
||||
</Button>
|
||||
</Space>}/>
|
||||
</Affix>
|
||||
<div className={Styles.contains}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>默认COS源</Typography.Text>
|
||||
<Select value={currentConfig?.cos?.defaultOrigin} onChange={(v) => {
|
||||
setValue('cos.defaultOrigin', v);
|
||||
}} style={{ width: '100%' }} allowClear placeholder="请选择默认COS源">
|
||||
{selections.map((item) => (<Select.Option key={item.value} value={item.value}>
|
||||
{t(item.name)}
|
||||
</Select.Option>))}
|
||||
</Select>
|
||||
</Space>
|
||||
</div>
|
||||
</>);
|
||||
};
|
||||
export default Cos;
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "application", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
import React from 'react';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "application", false, {
|
||||
tabs: {
|
||||
label: React.ReactNode;
|
||||
key: string;
|
||||
children: React.ReactNode;
|
||||
}[];
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -16,4 +16,7 @@ export default OakComponent({
|
|||
formData({ data }) {
|
||||
return data || {};
|
||||
},
|
||||
properties: {
|
||||
tabs: [],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
"menu": "菜单管理",
|
||||
"autoReply": "被关注回复管理",
|
||||
"tag": "标签管理",
|
||||
"user": "用户管理"
|
||||
"user": "用户管理",
|
||||
"cos": "COS配置"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,9 @@ export default function Render(props: WebComponentProps<EntityDict, 'application
|
|||
name: string;
|
||||
style: Style;
|
||||
type: string;
|
||||
tabs: {
|
||||
label: React.ReactNode;
|
||||
key: string;
|
||||
children: React.ReactNode;
|
||||
}[];
|
||||
}>): React.JSX.Element | undefined;
|
||||
|
|
|
|||
|
|
@ -9,15 +9,16 @@ import WechatMenu from '../../wechatMenu';
|
|||
import UserWechatPublicTag from '../../userWechatPublicTag';
|
||||
import WechatPublicTag from '../..//wechatPublicTag/list';
|
||||
import WechatPublicAutoReply from '../../wechatPublicAutoReply';
|
||||
import Cos from '../cos';
|
||||
export default function Render(props) {
|
||||
const { id, config, oakFullpath, name, style, type } = props.data;
|
||||
const { id, config, oakFullpath, name, style, type, tabs } = props.data;
|
||||
const { t, update } = props.methods;
|
||||
const [tabKey, setTabKey] = useState('detail');
|
||||
const items = [
|
||||
{
|
||||
label: <div className={Styles.tabLabel}>{t('detail')}</div>,
|
||||
key: 'detail',
|
||||
children: (<ApplicationDetail oakId={id} oakPath={oakFullpath}/>),
|
||||
children: <ApplicationDetail oakId={id} oakPath={oakFullpath}/>,
|
||||
},
|
||||
{
|
||||
label: <div className={Styles.tabLabel}>{t('config')}</div>,
|
||||
|
|
@ -27,8 +28,15 @@ export default function Render(props) {
|
|||
{
|
||||
label: <div className={Styles.tabLabel}>{t('style')}</div>,
|
||||
key: 'style',
|
||||
children: (<StyleUpsert style={style} entity={'platform'} entityId={id} name={name}/>),
|
||||
children: (<StyleUpsert style={style} entity={'application'} entityId={id} name={name}/>),
|
||||
},
|
||||
{
|
||||
label: <div className={Styles.tabLabel}>{t('cos')}</div>,
|
||||
key: 'cos',
|
||||
children: (<Cos oakPath={`#application-panel-cos-${id}`} config={config} entity="application" entityId={id} name={name}>
|
||||
</Cos>),
|
||||
},
|
||||
...(tabs || [])
|
||||
];
|
||||
if (type === 'wechatPublic') {
|
||||
items.push({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
tocPosition: "none" | "left" | "right";
|
||||
highlightBgColor: string;
|
||||
onArticlePreview: (content?: string, title?: string) => void;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
scrollId: string;
|
||||
height: number | "auto";
|
||||
activeColor: string | undefined;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default OakComponent({
|
|||
tocPosition: 'none',
|
||||
highlightBgColor: 'none',
|
||||
onArticlePreview: (content, title) => undefined,
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
scrollId: '',
|
||||
height: 600,
|
||||
activeColor: undefined,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'article', f
|
|||
name: string;
|
||||
editor: any;
|
||||
content?: string;
|
||||
origin?: string;
|
||||
origin?: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
contentTip: boolean;
|
||||
articleMenuId: string;
|
||||
oakId: string;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function customCheckImageFn(src, alt, url) {
|
|||
export default function Render(props) {
|
||||
const { methods, data } = props;
|
||||
const { t, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
|
||||
const { oakId, oakFullpath, id, name, content, editor, origin = 'qiniu', tocPosition = 'none', highlightBgColor, scrollId, tocWidth, tocHeight, height = 600, tocClosed = false, execuable, activeColor, } = data;
|
||||
const { oakId, oakFullpath, id, name, content, editor, origin, tocPosition = 'none', highlightBgColor, scrollId, tocWidth, tocHeight, height = 600, tocClosed = false, execuable, activeColor, } = data;
|
||||
const [articleId, setArticleId] = useState('');
|
||||
const [toc, setToc] = useState([]);
|
||||
const [showToc, setShowToc] = useState(false);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
tocPosition: "none" | "left" | "right";
|
||||
highlightBgColor: string;
|
||||
onArticlePreview: (content?: string, title?: string) => void;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
scrollId: string;
|
||||
height: number | "auto";
|
||||
activeColor: string | undefined;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default OakComponent({
|
|||
tocPosition: 'none',
|
||||
highlightBgColor: 'none',
|
||||
onArticlePreview: (content, title) => undefined,
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
scrollId: '',
|
||||
height: 600,
|
||||
activeColor: undefined,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'article', f
|
|||
name: string;
|
||||
editor: any;
|
||||
content?: string;
|
||||
origin?: string;
|
||||
origin?: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
contentTip: boolean;
|
||||
articleMenuId: string;
|
||||
oakId: string;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function customCheckImageFn(src, alt, url) {
|
|||
export default function Render(props) {
|
||||
const { methods, data } = props;
|
||||
const { t, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
|
||||
const { oakId, oakFullpath, id, content, editor, origin = 'qiniu', tocPosition = 'none', highlightBgColor, activeColor, scrollId, tocWidth, tocHeight, height = 600 } = data;
|
||||
const { oakId, oakFullpath, id, content, editor, origin, tocPosition = 'none', highlightBgColor, activeColor, scrollId, tocWidth, tocHeight, height = 600 } = data;
|
||||
const [articleId, setArticleId] = useState('');
|
||||
const [toc, setToc] = useState([]);
|
||||
const [showToc, setShowToc] = useState(false);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
entity: string;
|
||||
entityId: string;
|
||||
title: string;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
menuEmpty: import("react").ReactNode;
|
||||
articleEmpty: import("react").ReactNode;
|
||||
generateUrl: GenerateUrlFn;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default OakComponent({
|
|||
entity: '',
|
||||
entityId: '',
|
||||
title: '',
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
menuEmpty: undefined,
|
||||
articleEmpty: undefined,
|
||||
generateUrl: ((mode, type, id) => { }), //构造文章显示路由
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'articleMenu
|
|||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
origin: 'qiniu';
|
||||
origin: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
menuEmpty: React.ReactNode | undefined;
|
||||
articleEmpty: React.ReactNode | undefined;
|
||||
parentId: string | undefined;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import MenuList from '../list';
|
|||
import ArticleList from '../../article/list';
|
||||
export default function Render(props) {
|
||||
const { data, methods } = props;
|
||||
const { oakFullpath, oakLoading, oakExecuting, entity, entityId, title, breadcrumbItems, origin = 'qiniu', menuEmpty, articleEmpty, parentId, articleMenuId, showAddArticle, showAddMenu, generateUrl, } = data;
|
||||
const { oakFullpath, oakLoading, oakExecuting, entity, entityId, title, breadcrumbItems, origin, menuEmpty, articleEmpty, parentId, articleMenuId, showAddArticle, showAddMenu, generateUrl, } = data;
|
||||
const { t, onBreadcrumItemClick, onMenuClick, onAddMenu, changeAddArticle, onAddArticle, menuCheck } = methods;
|
||||
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
||||
const [menuName, setMenuName] = useState('');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
/// <reference types="react" />
|
||||
import { GenerateUrlFn } from "../../../types/Article";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "articleMenu", true, {
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "articleMenu", true, {
|
||||
entity: string;
|
||||
entityId: string;
|
||||
parentId: string | undefined;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
onMenuClick: (menuId: string, menuName: string, isArticle: boolean) => void;
|
||||
onArticleClick: (atricleId: string) => void;
|
||||
empty: import("react").ReactNode;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export default OakComponent({
|
|||
entity: '',
|
||||
entityId: '',
|
||||
parentId: '',
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
onMenuClick: (menuId, menuName, isArticle) => undefined,
|
||||
onArticleClick: (atricleId) => undefined,
|
||||
empty: undefined,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'articleMenu
|
|||
})[];
|
||||
entity: string;
|
||||
entityId: string;
|
||||
origin: 'qiniu';
|
||||
origin: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
onMenuClick: (menuId: string, menuName: string, isArticle: boolean) => void;
|
||||
empty: React.ReactNode | undefined;
|
||||
execuable: boolean;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Pagination from 'oak-frontend-base/es/components/pagination';
|
|||
const { confirm } = Modal;
|
||||
export default function Render(props) {
|
||||
const { data, methods } = props;
|
||||
const { articleMenus, oakFullpath, oakPagination, oakLoading, oakExecuting, oakEntity, entity, entityId, origin = 'qiniu', onMenuClick, empty, execuable } = data;
|
||||
const { articleMenus, oakFullpath, oakPagination, oakLoading, oakExecuting, oakEntity, entity, entityId, onMenuClick, empty, execuable } = data;
|
||||
const { t, goDetail, onCopy } = props.methods;
|
||||
const [editorId, setEditorId] = useState('');
|
||||
const IconButton = (props) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "articleMenu", false, {
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "articleMenu", false, {
|
||||
onRemove: () => void;
|
||||
onUpdateName: (name: string) => Promise<void>;
|
||||
onChildEditArticleChange: (data: string) => void;
|
||||
|
|
@ -24,6 +25,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
setCurrentArticle: (id: string) => void;
|
||||
onMenuViewById: (articleMenuId: string) => void;
|
||||
setCopyArticleUrl: (id: string) => string;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default OakComponent({
|
|||
setCurrentArticle: (id) => undefined,
|
||||
onMenuViewById: (articleMenuId) => undefined,
|
||||
setCopyArticleUrl: (id) => '',
|
||||
origin: 'qiniu', // cos origin默认七牛云
|
||||
origin: null, // cos origin默认由系统决定
|
||||
},
|
||||
formData({ data: row }) {
|
||||
const { articleMenu$parent, article$articleMenu } = row || {};
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'articleMenu
|
|||
currentArticle: string;
|
||||
setCurrentArticle: (id: string) => void;
|
||||
setCopyArticleUrl: (id: string) => string;
|
||||
origin: 'qiniu';
|
||||
origin: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
}, {
|
||||
createSubArticle: (name: string) => Promise<void>;
|
||||
createSubArticleMenu: (name: string) => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import ExtraFileUpload from '../../extraFile/upload';
|
|||
import ExtraFileCommit from '../../extraFile/commit';
|
||||
import Styles from './web.pc.module.less';
|
||||
export default function Render(props) {
|
||||
const { row, allowCreateSubArticle, allowCreateSubMenu, allowRemove, onRemove, onUpdateName, oakFullpath, logo, onChildEditArticleChange, editArticleId, show, getBreadcrumbItemsByParent, breadItems, drawerOpen, changeDrawerOpen, selectedArticleId, openArray, getTopInfo, articleId, articleMenuId, getSideInfo, currentArticle, setCurrentArticle, setCopyArticleUrl, oakEntity, origin = 'qiniu' } = props.data;
|
||||
const { row, allowCreateSubArticle, allowCreateSubMenu, allowRemove, onRemove, onUpdateName, oakFullpath, logo, onChildEditArticleChange, editArticleId, show, getBreadcrumbItemsByParent, breadItems, drawerOpen, changeDrawerOpen, selectedArticleId, openArray, getTopInfo, articleId, articleMenuId, getSideInfo, currentArticle, setCurrentArticle, setCopyArticleUrl, oakEntity, origin } = props.data;
|
||||
const { update, execute, createSubArticle, createSubArticleMenu, setMessage, gotoDoc } = props.methods;
|
||||
useEffect(() => {
|
||||
if (editArticleId) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
setCurrentArticle: (id: string) => void;
|
||||
onMenuViewById: (articleMenuId: string) => void;
|
||||
setCopyArticleUrl: (id: string) => string;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default OakComponent({
|
|||
setCurrentArticle: (id) => undefined,
|
||||
onMenuViewById: (articleMenuId) => undefined,
|
||||
setCopyArticleUrl: (id) => '',
|
||||
origin: 'qiniu', // cos origin默认七牛云
|
||||
origin: null, // cos origin默认由系统决定
|
||||
},
|
||||
projection: {
|
||||
id: 1,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'articleMenu
|
|||
setCurrentArticle: (id: string) => void;
|
||||
onMenuViewById: (articleMenuId: string) => void;
|
||||
setCopyArticleUrl: (id: string) => string;
|
||||
origin: 'qiniu';
|
||||
origin: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
}, {
|
||||
createOne: (name?: string) => Promise<void>;
|
||||
getDefaultArticle: (rows: EntityDict['articleMenu']['OpSchema'][]) => void;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Divider, Modal, Input, Form, Empty } from 'antd';
|
|||
import TreeCell from '../treeCell';
|
||||
import Styles from './web.pc.module.less';
|
||||
export default function Render(props) {
|
||||
const { rows, oakFullpath, parentId, onGrandChildEditArticleChange, show, getBreadcrumbItems, breadcrumbItems, drawerOpen, changeDrawerOpen, addOpen, changeAddOpen, selectedArticleId, defaultOpen, changeDefaultOpen, openArray, getTopInfo, articleId, articleMenuId, getSearchOpen, getSideInfo, currentArticle, setCurrentArticle, onMenuViewById, setCopyArticleUrl, origin = 'qiniu' } = props.data;
|
||||
const { rows, oakFullpath, parentId, onGrandChildEditArticleChange, show, getBreadcrumbItems, breadcrumbItems, drawerOpen, changeDrawerOpen, addOpen, changeAddOpen, selectedArticleId, defaultOpen, changeDefaultOpen, openArray, getTopInfo, articleId, articleMenuId, getSearchOpen, getSideInfo, currentArticle, setCurrentArticle, onMenuViewById, setCopyArticleUrl, origin } = props.data;
|
||||
const { t, createOne, removeItem, updateItem, execute, setMessage, getDefaultArticle, getSearchArticle } = props.methods;
|
||||
useEffect(() => {
|
||||
if (rows && rows.length > 0 && defaultOpen && !articleId) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "articleMenu", true, {
|
||||
import { EntityDict } from "../../../oak-app-domain/EntityDict";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "articleMenu", true, {
|
||||
entity: string;
|
||||
entityId: string;
|
||||
show: "edit" | "doc" | "preview";
|
||||
|
|
@ -12,7 +13,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
onArticlePreview: (content?: string, title?: string) => void;
|
||||
onArticleEdit: (articleId: string) => void;
|
||||
setCopyArticleUrl: (articleId: string) => string;
|
||||
origin: string;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
scrollId: string;
|
||||
activeColor: string | undefined;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default OakComponent({
|
|||
onArticlePreview: (content, title) => undefined,
|
||||
onArticleEdit: (articleId) => undefined,
|
||||
setCopyArticleUrl: (articleId) => '',
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
scrollId: '',
|
||||
activeColor: undefined, //目录高亮颜色
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'articleMenu
|
|||
articleId: string;
|
||||
tocPosition: 'none' | 'left' | 'right';
|
||||
highlightBgColor: string;
|
||||
origin: 'qiniu';
|
||||
origin: null | EntityDict['extraFile']['Schema']['origin'];
|
||||
onMenuViewById: (articleMenuId: string) => void;
|
||||
onArticlePreview: (content?: string, title?: string) => void;
|
||||
onArticleEdit: (oakId: string) => void;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Style } from '../../../../types/Style';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../oak-app-domain").EntityDict, keyof import("../../../../oak-app-domain").EntityDict, false, {
|
||||
style: Style;
|
||||
entity: "platform" | "system" | "application";
|
||||
entity: "application" | "platform" | "system";
|
||||
entityId: string;
|
||||
name: string;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Tabs, Row, Col, Card, Divider, Input, Form, Space, Select, } from 'antd';
|
||||
import { Tabs, Row, Col, Card, Divider, Input, Form, Space, Select, Typography, Switch, } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
import { isEmptyObject } from '../../../../utils/strings';
|
||||
// https://developer.qiniu.com/kodo/1671/region-endpoint-fq
|
||||
const QiniuZoneArray = [
|
||||
{
|
||||
|
|
@ -947,6 +948,11 @@ function S3Cos(props) {
|
|||
.value)}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="pathStyle" help="使用路径样式访问(MinIO必须开启)">
|
||||
<>
|
||||
<Switch checked={ele.pathStyle} onChange={(checked) => setValue(`buckets.${idx}.pathStyle`, checked)}/>
|
||||
</>
|
||||
</Form.Item>
|
||||
<Form.Item label="protocol">
|
||||
<>
|
||||
<Select mode="multiple" allowClear style={{
|
||||
|
|
@ -987,6 +993,24 @@ export default function Cos(props) {
|
|||
const { cos, setValue, removeItem } = props;
|
||||
const { qiniu, ctyun, aliyun, tencent, local, s3 } = cos;
|
||||
return (<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||
{/* 默认项选择 */}
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>默认COS源</Typography.Text>
|
||||
<Select value={cos?.defaultOrigin} onChange={(v) => {
|
||||
setValue('defaultOrigin', v);
|
||||
}} style={{ width: '100%' }} allowClear placeholder="请选择默认COS源">
|
||||
{Object.entries(cos).map(([key, value]) => {
|
||||
if (key === 'defaultOrigin') {
|
||||
return null;
|
||||
}
|
||||
if (value && !isEmptyObject(value)) {
|
||||
return (<Select.Option key={key} value={key}>
|
||||
{key}
|
||||
</Select.Option>);
|
||||
}
|
||||
}).filter(Boolean)}
|
||||
</Select>
|
||||
</Space>
|
||||
<Row>
|
||||
<Card className={Styles.tips}>
|
||||
每种均可配置一个,相应的服务所使用的帐号请准确对应
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export default function Render(props) {
|
|||
</Space>}/>
|
||||
</Affix>
|
||||
<div className={Style.container}>
|
||||
<Tabs tabPosition="left" items={[
|
||||
<Tabs tabPosition="top" items={[
|
||||
{
|
||||
key: '云平台帐号',
|
||||
label: '云平台帐号',
|
||||
|
|
@ -51,13 +51,13 @@ export default function Render(props) {
|
|||
children: (<Cos cos={cos || {}} setValue={(path, value) => setValue(`Cos.${path}`, value)} removeItem={(path, index) => removeItem(`Cos.${path}`, index)}/>),
|
||||
},
|
||||
{
|
||||
key: '直播api设置',
|
||||
label: '直播api设置',
|
||||
key: '直播设置',
|
||||
label: '直播设置',
|
||||
children: (<Live live={live || {}} setValue={(path, value) => setValue(`Live.${path}`, value)}/>),
|
||||
},
|
||||
{
|
||||
key: '地图api设置',
|
||||
label: '地图api设置',
|
||||
key: '地图设置',
|
||||
label: '地图设置',
|
||||
children: (<Map map={map || {}} setValue={(path, value) => setValue(`Map.${path}`, value)} removeItem={(path, index) => removeItem(`Map.${path}`, index)} cleanKey={(path, key) => cleanKey(`Map${path ? `.${path}` : ''}`, key)}/>),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
entityId: string;
|
||||
tag1: string;
|
||||
tag2: string;
|
||||
origin: import("../../types/Config").CosOrigin | null;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default OakComponent({
|
|||
entityId: '',
|
||||
tag1: '',
|
||||
tag2: '',
|
||||
origin: null,
|
||||
},
|
||||
methods: {
|
||||
onEditReady(e) {
|
||||
|
|
@ -69,7 +70,7 @@ export default OakComponent({
|
|||
const extension = tempFilePath.substring(tempFilePath.lastIndexOf('.') + 1);
|
||||
const filename = tempFilePath.substring(0, tempFilePath.lastIndexOf('.'));
|
||||
const extraFile = {
|
||||
origin: 'qiniu',
|
||||
origin: this.props.origin,
|
||||
type: 'image',
|
||||
tag1: this.props.tag1 || 'editorImg',
|
||||
tag2: this.props.tag2,
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
entity: keyof EntityDict;
|
||||
entityId: string;
|
||||
autoUpload: boolean;
|
||||
origin: import("../../../types/Config").CosOrigin | null;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export default OakComponent({
|
|||
};
|
||||
},
|
||||
data: {
|
||||
origin: 'qiniu',
|
||||
type: 'image',
|
||||
tag1: 'avatar',
|
||||
},
|
||||
|
|
@ -43,6 +42,7 @@ export default OakComponent({
|
|||
entity: '',
|
||||
entityId: '',
|
||||
autoUpload: false,
|
||||
origin: null,
|
||||
},
|
||||
methods: {
|
||||
async onPickByMp() {
|
||||
|
|
@ -93,8 +93,8 @@ export default OakComponent({
|
|||
}));
|
||||
},
|
||||
async pushExtraFile(options) {
|
||||
const { origin, type, tag1, avatar } = this.state;
|
||||
const { entityId, entity, autoUpload = false } = this.props;
|
||||
const { type, tag1, avatar } = this.state;
|
||||
const { entityId, entity, autoUpload = false, origin } = this.props;
|
||||
const { name, extra1, fileType, size } = options;
|
||||
const extension = name.substring(name.lastIndexOf('.') + 1);
|
||||
const filename = name.substring(0, name.lastIndexOf('.'));
|
||||
|
|
|
|||
|
|
@ -15,19 +15,19 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
type?: ButtonProps['type'] | AmButtonProps['type'];
|
||||
executeText?: string | undefined;
|
||||
buttonProps?: (ButtonProps & {
|
||||
color?: "default" | "primary" | "success" | "warning" | "danger" | undefined;
|
||||
fill?: "none" | "solid" | "outline" | undefined;
|
||||
size?: "small" | "large" | "middle" | "mini" | undefined;
|
||||
color?: "default" | "success" | "warning" | "primary" | "danger" | undefined;
|
||||
fill?: "none" | "outline" | "solid" | undefined;
|
||||
size?: "small" | "middle" | "large" | "mini" | undefined;
|
||||
block?: boolean | undefined;
|
||||
loading?: boolean | "auto" | undefined;
|
||||
loadingText?: string | undefined;
|
||||
loadingIcon?: import("react").ReactNode;
|
||||
disabled?: boolean | undefined;
|
||||
onClick?: ((event: import("react").MouseEvent<HTMLButtonElement, MouseEvent>) => unknown) | undefined;
|
||||
type?: "reset" | "submit" | "button" | undefined;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
shape?: "default" | "rounded" | "rectangular" | undefined;
|
||||
children?: import("react").ReactNode;
|
||||
} & Pick<import("react").ClassAttributes<HTMLButtonElement> & import("react").ButtonHTMLAttributes<HTMLButtonElement>, "id" | "onMouseUp" | "onMouseDown" | "onTouchStart" | "onTouchEnd"> & {
|
||||
} & Pick<import("react").ClassAttributes<HTMLButtonElement> & import("react").ButtonHTMLAttributes<HTMLButtonElement>, "id" | "onMouseDown" | "onMouseUp" | "onTouchEnd" | "onTouchStart"> & {
|
||||
className?: string | undefined;
|
||||
style?: (import("react").CSSProperties & Partial<Record<"--text-color" | "--background-color" | "--border-radius" | "--border-width" | "--border-style" | "--border-color", string>>) | undefined;
|
||||
tabIndex?: number | undefined;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default OakComponent({
|
|||
disableAdd: false,
|
||||
disableDownload: false,
|
||||
type: 'image',
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
tag1: '',
|
||||
tag2: '',
|
||||
entity: '',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export default function render(props: WebComponentProps<EntityDict, 'extraFile',
|
|||
disableDownload: boolean;
|
||||
disabled: boolean;
|
||||
type: string;
|
||||
origin: string;
|
||||
origin: EntityDict['extraFile']['Schema']['origin'] | null;
|
||||
tag1: string;
|
||||
tag2: string;
|
||||
entity: keyof EntityDict;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export default OakComponent({
|
|||
entity: '',
|
||||
entityId: '',
|
||||
imgUrls: [],
|
||||
origin: null,
|
||||
},
|
||||
lifetimes: {},
|
||||
listeners: {
|
||||
|
|
@ -154,7 +155,7 @@ export default OakComponent({
|
|||
},
|
||||
createExtraFileData(file) {
|
||||
const { methodsType } = this.state;
|
||||
const { tag1, tag2, entity, entityId } = this.props;
|
||||
const { tag1, tag2, entity, entityId, origin } = this.props;
|
||||
let extension = '';
|
||||
let filename = '';
|
||||
const applicationId = this.features.application.getApplicationId();
|
||||
|
|
@ -177,7 +178,7 @@ export default OakComponent({
|
|||
extension = name.substring(name.lastIndexOf('.') + 1);
|
||||
filename = name.substring(0, name.lastIndexOf('.'));
|
||||
Object.assign(createData, {
|
||||
origin: 'qiniu',
|
||||
origin,
|
||||
extension,
|
||||
filename,
|
||||
size,
|
||||
|
|
@ -207,7 +208,10 @@ export default OakComponent({
|
|||
},
|
||||
async myAddItem(createData) {
|
||||
// 目前只支持七牛上传
|
||||
if (createData.origin === 'qiniu') {
|
||||
if (createData.origin === 'unknown') {
|
||||
this.addItem(createData);
|
||||
}
|
||||
else {
|
||||
const file = createData.extra1;
|
||||
const id = this.addItem(Object.assign(createData, {
|
||||
extra1: null,
|
||||
|
|
@ -215,9 +219,6 @@ export default OakComponent({
|
|||
}));
|
||||
this.features.extraFile.addLocalFile(id, file);
|
||||
}
|
||||
else {
|
||||
this.addItem(createData);
|
||||
}
|
||||
},
|
||||
async myUpdateItem(params) {
|
||||
const { file } = this.state;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
disableDownload: boolean;
|
||||
disabled: boolean;
|
||||
type: string;
|
||||
origin: string;
|
||||
origin: ED2["extraFile"]["Schema"]["origin"] | null;
|
||||
tag1: string;
|
||||
tag2: string;
|
||||
entity: keyof ED2;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default OakComponent({
|
|||
disableAdd: false,
|
||||
disableDownload: false,
|
||||
type: 'image',
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
tag1: '',
|
||||
tag2: '',
|
||||
entity: '',
|
||||
|
|
@ -133,7 +133,7 @@ export default OakComponent({
|
|||
this.features.extraFile.removeLocalFiles([file.id]);
|
||||
},
|
||||
async addExtraFileInner(options, file) {
|
||||
const { type, origin = 'qiniu', // 默认qiniu
|
||||
const { type, origin, // 默认由系统决定
|
||||
tag1, tag2, entity, entityId, bucket, autoUpload, } = this.props;
|
||||
const { name, fileType, size, sort } = options;
|
||||
const extension = name.substring(name.lastIndexOf('.') + 1);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
import assert from "assert";
|
||||
export default OakComponent({
|
||||
// Virtual Component
|
||||
isList: false,
|
||||
filters: [],
|
||||
properties: {},
|
||||
data: {
|
||||
clientInfo: null,
|
||||
loading: true,
|
||||
userInfo: null,
|
||||
hasError: false,
|
||||
errorMsg: '',
|
||||
name: '',
|
||||
nickname: '',
|
||||
mobile: '',
|
||||
avatarUrl: '',
|
||||
response_type: '',
|
||||
client_id: '',
|
||||
redirect_uri: '',
|
||||
scope: '',
|
||||
state: '',
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const clientId = searchParams.get('client_id') || '';
|
||||
const responseType = searchParams.get('response_type') || '';
|
||||
const redirectUri = searchParams.get('redirect_uri') || '';
|
||||
const scope = searchParams.get('scope') || '';
|
||||
const state = searchParams.get('state') || '';
|
||||
this.setState({
|
||||
client_id: clientId,
|
||||
response_type: responseType,
|
||||
redirect_uri: redirectUri,
|
||||
scope: scope,
|
||||
state: state,
|
||||
});
|
||||
// load userinfo
|
||||
const userId = this.features.token.getUserId(true);
|
||||
if (!userId) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('response_type', responseType || "");
|
||||
params.set('client_id', clientId || "");
|
||||
params.set('redirect_uri', redirectUri || "");
|
||||
params.set('scope', scope || "");
|
||||
params.set('state', state || "");
|
||||
const redirectUrl = `/login/oauth/authorize?${params.toString()}`;
|
||||
console.log('Not logged in, redirecting to login page:', redirectUrl);
|
||||
const encoded = btoa(encodeURIComponent(redirectUrl));
|
||||
this.features.navigator.navigateTo({
|
||||
url: `/login?redirect=${encoded}`,
|
||||
}, undefined, true);
|
||||
return;
|
||||
}
|
||||
const userInfo = this.features.token.getUserInfo();
|
||||
const { mobile } = (userInfo?.mobile$user && userInfo?.mobile$user[0]) ||
|
||||
(userInfo?.user$ref &&
|
||||
userInfo?.user$ref[0] &&
|
||||
userInfo?.user$ref[0].mobile$user &&
|
||||
userInfo?.user$ref[0].mobile$user[0]) ||
|
||||
{};
|
||||
const extraFile = userInfo?.extraFile$entity?.find((ele) => ele.tag1 === 'avatar');
|
||||
const avatarUrl = this.features.extraFile.getUrl(extraFile);
|
||||
this.setState({
|
||||
userInfo: userId ? this.features.token.getUserInfo() : null,
|
||||
name: userInfo?.name || '',
|
||||
nickname: userInfo?.nickname || '',
|
||||
mobile: mobile || '',
|
||||
avatarUrl,
|
||||
});
|
||||
// end load userinfo
|
||||
if (!clientId) {
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: 'oauth.authorize.error.missing_client_id',
|
||||
});
|
||||
this.setState({ loading: false });
|
||||
return;
|
||||
}
|
||||
if (!responseType) {
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: 'oauth.authorize.error.missing_response_type',
|
||||
});
|
||||
this.setState({ loading: false });
|
||||
return;
|
||||
}
|
||||
this.features.cache.exec("getOAuthClientInfo", {
|
||||
client_id: clientId,
|
||||
currentUserId: userId,
|
||||
}).then((clientInfo) => {
|
||||
if (!clientInfo.result) {
|
||||
this.setState({ loading: false });
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: 'oauth.authorize.error.invalid_client_id',
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
clientInfo: clientInfo.result.data,
|
||||
});
|
||||
if (clientInfo.result.alreadyAuth) {
|
||||
// 已经授权过,直接跳转
|
||||
this.handleGrant();
|
||||
}
|
||||
else {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.setState({ loading: false });
|
||||
console.error('Error loading OAuth client info:', err);
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: err.message || 'oauth.authorize.error.unknown',
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleGrant() {
|
||||
this.callAspectAuthorize("grant");
|
||||
},
|
||||
handleDeny() {
|
||||
this.callAspectAuthorize("deny");
|
||||
},
|
||||
callAspectAuthorize(action) {
|
||||
this.features.cache.exec("authorize", {
|
||||
response_type: this.state.response_type || "",
|
||||
client_id: this.state.client_id || "",
|
||||
redirect_uri: this.state.redirect_uri || "",
|
||||
scope: this.state.scope || "",
|
||||
state: this.state.state || "",
|
||||
action: action,
|
||||
}).then((result) => {
|
||||
const { redirectUri } = result.result;
|
||||
assert(redirectUri, 'redirectUri should be present in authorize result');
|
||||
window.location.replace(redirectUri);
|
||||
}).catch((err) => {
|
||||
console.error('Error during OAuth authorization:', err);
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: err.message || 'oauth.authorize.error.unknown',
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"oauth": {
|
||||
"authorize": {
|
||||
"title": "授权确认",
|
||||
"loading": "正在加载...",
|
||||
"description": "第三方应用请求访问您的账户",
|
||||
"clientName": "应用名称",
|
||||
"clientDescription": "应用介绍",
|
||||
"scope": "授权范围",
|
||||
"allPermissions": "该应用将获得您账户的完整访问权限",
|
||||
"confirm": "同意授权",
|
||||
"deny": "拒绝",
|
||||
"error": {
|
||||
"title": "授权失败",
|
||||
"missing_response_type": "缺少 response_type 参数",
|
||||
"missing_client_id": "缺少 client_id 参数",
|
||||
"unknown": "未知错误,请稍后重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
background-color: #f0f2f5;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.loadingBox,
|
||||
.errorBox,
|
||||
.authorizeBox {
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.loadingBox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 20px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top: 3px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.errorBox {
|
||||
border-top: 3px solid #f5222d;
|
||||
}
|
||||
|
||||
.errorTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
line-height: 1.8;
|
||||
padding: 16px;
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.authorizeBox {
|
||||
border-top: 3px solid #faad14;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.appInfo {
|
||||
background: #fafafa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
background: #fafafa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 15px;
|
||||
color: #262626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.descValue {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.scopeSection {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fffbe6;
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
border-left: 4px solid #faad14;
|
||||
}
|
||||
|
||||
.scopeTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.scopeItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.scopeIcon {
|
||||
color: #faad14;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.denyButton,
|
||||
.grantButton {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.denyButton {
|
||||
background: #ff5e5e;
|
||||
color: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.denyButton:hover {
|
||||
color: #262626;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.denyButton:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.grantButton {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
.grantButton:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.grantButton:active {
|
||||
background: #096dd9;
|
||||
border-color: #096dd9;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
declare const Authorize: (props: WebComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
loading: boolean;
|
||||
hasError: boolean;
|
||||
errorMsg: string;
|
||||
userInfo: EntityDict['token']['Schema']['user'] | null;
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
scope: string;
|
||||
state: string;
|
||||
clientInfo: EntityDict['oauthApplication']['Schema'] | null;
|
||||
name: string;
|
||||
nickname: string;
|
||||
mobile: string;
|
||||
avatarUrl: string;
|
||||
}, {
|
||||
handleGrant: () => void;
|
||||
handleDeny: () => void;
|
||||
}>) => React.JSX.Element;
|
||||
export default Authorize;
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import Styles from './styles.module.less';
|
||||
import { Avatar } from 'antd';
|
||||
const Authorize = (props) => {
|
||||
const { oakFullpath, loading, hasError, errorMsg, userInfo, response_type, client_id, redirect_uri, scope, state, clientInfo, name, nickname, mobile, avatarUrl } = props.data;
|
||||
const { t, handleGrant, handleDeny } = props.methods;
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (<div className={Styles.container}>
|
||||
<div className={Styles.loadingBox}>
|
||||
<div className={Styles.spinner}></div>
|
||||
<div className={Styles.loadingText}>{t('oauth.authorize.loading')}</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
// Error state
|
||||
if (hasError) {
|
||||
return (<div className={Styles.container}>
|
||||
<div className={Styles.errorBox}>
|
||||
<div className={Styles.errorTitle}>{t('oauth.authorize.error.title')}</div>
|
||||
<div className={Styles.errorMessage}>{t(errorMsg)}</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
// Logged in - show authorization confirmation
|
||||
return (<div className={Styles.container}>
|
||||
<div className={Styles.authorizeBox}>
|
||||
<div className={Styles.title}>{t('oauth.authorize.title')}</div>
|
||||
<div className={Styles.description}>
|
||||
{t('oauth.authorize.description')}
|
||||
</div>
|
||||
|
||||
<div className={Styles.appInfo}>
|
||||
<div className={Styles.infoLabel}>{t('oauth.authorize.clientName')}:</div>
|
||||
<div className={Styles.infoValue}>{clientInfo?.name || client_id}</div>
|
||||
{clientInfo?.description && (<>
|
||||
<div className={Styles.infoLabel}>{t('oauth.authorize.clientDescription')}:</div>
|
||||
<div className={Styles.descValue}>{clientInfo.description}</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
<div className={Styles.scopeSection}>
|
||||
<div className={Styles.scopeTitle}>{t('oauth.authorize.scope')}</div>
|
||||
<div className={Styles.scopeItem}>
|
||||
<span className={Styles.scopeIcon}>✓</span>
|
||||
<span>{t('oauth.authorize.allPermissions')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={Styles.userInfo}>
|
||||
{avatarUrl ? (<Avatar className={Styles.avatar} src={avatarUrl}></Avatar>) : (<Avatar className={Styles.avatar}>
|
||||
<span className={Styles.text}>
|
||||
{nickname?.[0]}
|
||||
</span>
|
||||
</Avatar>)}
|
||||
<div className={Styles.userDetails}>
|
||||
<div className={Styles.userName}>{name || nickname}</div>
|
||||
{mobile && <div className={Styles.userMobile}>{mobile}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={Styles.actions}>
|
||||
<button className={Styles.denyButton} onClick={handleDeny}>
|
||||
{t('oauth.authorize.deny')}
|
||||
</button>
|
||||
<button className={Styles.grantButton} onClick={handleGrant}>
|
||||
{t('oauth.authorize.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
export default Authorize;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../oak-app-domain").EntityDict, keyof import("../../oak-app-domain").EntityDict, false, {}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import assert from "assert";
|
||||
export default OakComponent({
|
||||
// Virtual Component
|
||||
isList: false,
|
||||
filters: [],
|
||||
properties: {},
|
||||
data: {
|
||||
hasError: false,
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
},
|
||||
lifetimes: {
|
||||
async ready() {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const error = urlParams.get('error');
|
||||
const errorDescription = urlParams.get('error_description');
|
||||
if (error) {
|
||||
this.setErrorMsg(errorDescription || 'OAuth authorization error: ' + error);
|
||||
this.setState({ loading: false });
|
||||
return;
|
||||
}
|
||||
assert(state, 'State parameter is missing');
|
||||
assert(code, 'Code parameter is missing');
|
||||
this.setState({ hasError: false, errorMessage: '' });
|
||||
if (!state) {
|
||||
this.setErrorMsg('Invalid state parameter');
|
||||
return;
|
||||
}
|
||||
this.performLogin(code, state);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
performLogin(code, state) {
|
||||
this.features.token.loginByOAuth(code, state).then(() => {
|
||||
console.log('OAuth login successful');
|
||||
}).catch((err) => {
|
||||
console.error('OAuth login failed:', err);
|
||||
this.setErrorMsg(err.message || 'OAuth login failed');
|
||||
}).finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
},
|
||||
setErrorMsg(message) {
|
||||
if (!message) {
|
||||
this.setState({ hasError: false, errorMessage: '' });
|
||||
return;
|
||||
}
|
||||
this.setState({ hasError: true, errorMessage: message });
|
||||
},
|
||||
retry() {
|
||||
this.features.navigator.redirectTo({
|
||||
url: '/login',
|
||||
});
|
||||
},
|
||||
returnToIndex() {
|
||||
this.features.navigator.redirectTo({
|
||||
url: '/',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"Invalid state parameter": "无效的状态参数",
|
||||
"oauth": {
|
||||
"loading": {
|
||||
"title": "授权中..."
|
||||
},
|
||||
"loadingMessage": "正在处理授权请求,请稍候",
|
||||
"error": {
|
||||
"title": "授权失败"
|
||||
},
|
||||
"success": {
|
||||
"title": "授权成功"
|
||||
},
|
||||
"successMessage": "授权已成功完成",
|
||||
"return": "返回首页",
|
||||
"confirm": "确认登录",
|
||||
"cancel": "取消",
|
||||
"close": "关闭窗口"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, {
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export default OakComponent({
|
||||
// Virtual Component
|
||||
isList: false,
|
||||
filters: [],
|
||||
properties: {
|
||||
systemId: '',
|
||||
systemName: '',
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"systemInfo": "系统信息",
|
||||
"applications": "OAuth应用",
|
||||
"providers": "OAuth供应商"
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../oak-app-domain").EntityDict, "oauthApplication", true, {
|
||||
systemId: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import assert from "assert";
|
||||
export default OakComponent({
|
||||
entity: 'oauthApplication',
|
||||
isList: true,
|
||||
projection: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
description: 1,
|
||||
redirectUris: 1,
|
||||
logo: 1,
|
||||
isConfidential: 1,
|
||||
scopes: 1,
|
||||
ableState: 1,
|
||||
requirePKCE: 1,
|
||||
},
|
||||
filters: [{
|
||||
filter() {
|
||||
const systemId = this.props.systemId;
|
||||
assert(systemId, 'systemId is required');
|
||||
return {
|
||||
systemId: systemId,
|
||||
};
|
||||
},
|
||||
}],
|
||||
formData({ data }) {
|
||||
return {
|
||||
list: data?.filter(item => item.$$createAt$$ > 1) || [],
|
||||
};
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
actions: ["remove", "update"]
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"oauthAppConfig": "OAuth应用程序配置",
|
||||
"confirm": {
|
||||
"deleteTitle": "确认删除",
|
||||
"deleteContent": "您确定要删除此OAuth应用程序配置吗?"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../../oak-app-domain").EntityDict, "oauthApplication", false, {}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
export default OakComponent({
|
||||
entity: 'oauthApplication',
|
||||
isList: false,
|
||||
projection: {
|
||||
name: 1,
|
||||
description: 1,
|
||||
redirectUris: 1,
|
||||
logo: 1,
|
||||
isConfidential: 1,
|
||||
scopes: 1,
|
||||
ableState: 1,
|
||||
requirePKCE: 1,
|
||||
},
|
||||
formData({ data, features }) {
|
||||
if (!data) {
|
||||
return { item: {}, clientSecret: "" };
|
||||
}
|
||||
const [client] = features.cache.get("oauthApplication", {
|
||||
data: {
|
||||
clientSecret: 1,
|
||||
},
|
||||
filter: {
|
||||
id: data.id,
|
||||
}
|
||||
});
|
||||
return {
|
||||
item: data,
|
||||
clientSecret: client?.clientSecret || "",
|
||||
isCreation: this.isCreation(),
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
methods: {
|
||||
reGenerateClientSecret() {
|
||||
this.features.cache.operate("oauthApplication", {
|
||||
id: generateNewId(),
|
||||
action: "resetSecret",
|
||||
data: {},
|
||||
filter: {
|
||||
id: this.props.oakId,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入名称",
|
||||
"namePlaceholder": "请输入应用名称",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "请输入应用描述",
|
||||
"logo": "Logo",
|
||||
"logoPlaceholder": "请输入Logo URL",
|
||||
"redirectUris": "重定向URI列表",
|
||||
"redirectUrisRequired": "请输入重定向URI列表",
|
||||
"redirectUrisPlaceholder": "请输入重定向URI,每行一个",
|
||||
"scopes": "权限范围",
|
||||
"scopesPlaceholder": "请选择或输入权限范围",
|
||||
"clientSecret": "客户端密钥",
|
||||
"clientSecretPlaceholder": "自动生成的客户端密钥",
|
||||
"regenerate": "重新生成",
|
||||
"isConfidential": "机密客户端",
|
||||
"ableState": "启用状态",
|
||||
"noData": "无数据",
|
||||
"clientId": "客户端ID",
|
||||
"clientIdPlaceholder": "自动生成的客户端ID",
|
||||
"requirePKCE": "强制 PKCE",
|
||||
"requirePKCETooltip": "启用后,授权请求必须使用PKCE扩展以增强安全性。",
|
||||
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../../../oak-app-domain';
|
||||
declare const Upsert: (props: WebComponentProps<EntityDict, 'oauthApplication', false, {
|
||||
item: RowWithActions<EntityDict, 'oauthApplication'>;
|
||||
clientSecret: string;
|
||||
isCreation: boolean;
|
||||
}, {
|
||||
reGenerateClientSecret: () => void;
|
||||
}>) => React.JSX.Element;
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import { Form, Input, Switch, Button, Space, Select } from 'antd';
|
||||
import Styles from './styles.module.less';
|
||||
const Upsert = (props) => {
|
||||
const { item, clientSecret, isCreation } = props.data;
|
||||
const { t, update, reGenerateClientSecret } = props.methods;
|
||||
if (item === undefined) {
|
||||
return <div>{t('noData')}</div>;
|
||||
}
|
||||
return (<div className={Styles.id}>
|
||||
<Form layout="vertical" autoComplete="off">
|
||||
<Form.Item label={t('name')} rules={[{ required: true, message: t('nameRequired') }]}>
|
||||
<Input placeholder={t('namePlaceholder')} value={item.name || ""} onChange={(v) => {
|
||||
update({ name: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('description')}>
|
||||
<Input.TextArea placeholder={t('descriptionPlaceholder')} value={item.description || ""} rows={4} onChange={(v) => {
|
||||
update({ description: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('logo')}>
|
||||
<Input placeholder={t('logoPlaceholder')} value={item.logo || ""} onChange={(v) => {
|
||||
update({ logo: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('redirectUris')} rules={[{ required: true, message: t('redirectUrisRequired') }]}>
|
||||
<Input.TextArea placeholder={t('redirectUrisPlaceholder')} value={Array.isArray(item.redirectUris) ? item.redirectUris.join('\n') : ""} rows={3} onChange={(v) => {
|
||||
const uris = v.target.value.split('\n').filter(uri => uri.trim() !== '');
|
||||
update({ redirectUris: uris });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('scopes')}>
|
||||
<Select mode="tags" placeholder={t('scopesPlaceholder')} value={item.scopes || []} onChange={(v) => {
|
||||
update({ scopes: v });
|
||||
}} tokenSeparators={[',']} open={false}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('clientId')}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder={t('clientIdPlaceholder')} value={item.id || "已隐藏"} disabled/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('clientSecret')} tooltip={t('clientSecretTooltip')}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder={t('clientSecretPlaceholder')} value={clientSecret || "已隐藏"} disabled/>
|
||||
<Button type="primary" onClick={reGenerateClientSecret} disabled={isCreation} // 只能在更新时重新生成
|
||||
>
|
||||
{t('regenerate')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('isConfidential')} valuePropName="checked">
|
||||
<Switch checked={!!item.isConfidential} onChange={(checked) => update({ isConfidential: checked })}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('ableState')} valuePropName="checked">
|
||||
<Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/>
|
||||
</Form.Item>
|
||||
|
||||
{/* requirePKCE */}
|
||||
<Form.Item label={t('requirePKCE')} valuePropName="checked" tooltip={t('requirePKCETooltip')}>
|
||||
<Switch checked={!!item.requirePKCE} onChange={(checked) => update({ requirePKCE: checked })}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>);
|
||||
};
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
declare const OauthProvider: (props: WebComponentProps<EntityDict, 'oauthApplication', true, {
|
||||
list: RowWithActions<EntityDict, 'oauthApplication'>[];
|
||||
systemId: string;
|
||||
}>) => React.JSX.Element;
|
||||
export default OauthProvider;
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import Styles from './styles.module.less';
|
||||
import { Button, Modal } from 'antd';
|
||||
import AppUpsert from "./upsert";
|
||||
import ListPro from 'oak-frontend-base/es/components/listPro';
|
||||
const OauthProvider = (props) => {
|
||||
const { oakFullpath, systemId } = props.data;
|
||||
const { list, oakLoading } = props.data;
|
||||
const { t, addItem, removeItem, execute, clean } = props.methods;
|
||||
const attrs = [
|
||||
"id", "name", "description", "redirectUris",
|
||||
"logo", "isConfidential", "scopes",
|
||||
"ableState", "requirePKCE",
|
||||
];
|
||||
const [upsertId, setUpsertId] = React.useState(null);
|
||||
const handleAction = (row, action) => {
|
||||
switch (action) {
|
||||
case "update": {
|
||||
setUpsertId(row.id);
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
Modal.confirm({
|
||||
title: t('confirm.deleteTitle'),
|
||||
content: t('confirm.deleteContent'),
|
||||
onOk: () => {
|
||||
removeItem(row.id);
|
||||
execute();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
return (<>
|
||||
{list && (<ListPro entity='oauthApplication' attributes={attrs} data={list} loading={oakLoading} oakPath={`${oakFullpath}`} onAction={handleAction} extraContent={<div className={Styles.actions}>
|
||||
<Button type="primary" onClick={() => {
|
||||
setUpsertId(addItem({
|
||||
systemId: systemId,
|
||||
isConfidential: true,
|
||||
}));
|
||||
}}>
|
||||
{t('common::action.create')}
|
||||
</Button>
|
||||
</div>}>
|
||||
</ListPro>)}
|
||||
{/* antd model */}
|
||||
<Modal open={!!upsertId} destroyOnClose={true} width={600} onCancel={() => {
|
||||
clean();
|
||||
setUpsertId(null);
|
||||
}} onOk={() => {
|
||||
execute();
|
||||
setUpsertId(null);
|
||||
}}>
|
||||
{upsertId && <AppUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId}/>}
|
||||
</Modal>
|
||||
</>);
|
||||
};
|
||||
export default OauthProvider;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../oak-app-domain").EntityDict, "oauthProvider", true, {
|
||||
systemId: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import assert from "assert";
|
||||
export default OakComponent({
|
||||
entity: 'oauthProvider',
|
||||
isList: true,
|
||||
projection: {
|
||||
name: 1,
|
||||
type: 1,
|
||||
logo: 1,
|
||||
authorizationEndpoint: 1,
|
||||
tokenEndpoint: 1,
|
||||
userInfoEndpoint: 1,
|
||||
revokeEndpoint: 1,
|
||||
refreshEndpoint: 1,
|
||||
clientId: 1,
|
||||
scopes: 1,
|
||||
clientSecret: 1,
|
||||
redirectUri: 1,
|
||||
autoRegister: 1,
|
||||
ableState: 1,
|
||||
},
|
||||
filters: [{
|
||||
filter() {
|
||||
const systemId = this.props.systemId;
|
||||
assert(systemId, 'systemId is required');
|
||||
return {
|
||||
systemId: systemId,
|
||||
};
|
||||
},
|
||||
}],
|
||||
formData({ data }) {
|
||||
return {
|
||||
list: data?.filter(item => item.$$createAt$$ > 1) || [],
|
||||
};
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
actions: ["remove", "update"]
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"oauthProviderConfig": "OAuth提供商配置",
|
||||
"confirm": {
|
||||
"deleteTitle": "确认删除",
|
||||
"deleteContent": "您确定要删除此OAuth提供商配置吗?"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../../oak-app-domain").EntityDict, "oauthProvider", false, {}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
export default OakComponent({
|
||||
entity: 'oauthProvider',
|
||||
isList: false,
|
||||
projection: {
|
||||
name: 1,
|
||||
type: 1,
|
||||
logo: 1,
|
||||
scopes: 1,
|
||||
authorizationEndpoint: 1,
|
||||
tokenEndpoint: 1,
|
||||
userInfoEndpoint: 1,
|
||||
revokeEndpoint: 1,
|
||||
refreshEndpoint: 1,
|
||||
clientId: 1,
|
||||
clientSecret: 1,
|
||||
redirectUri: 1,
|
||||
autoRegister: 1,
|
||||
ableState: 1,
|
||||
},
|
||||
formData({ data }) {
|
||||
return {
|
||||
item: data,
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入名称",
|
||||
"namePlaceholder": "请输入OAuth提供商名称",
|
||||
"type": "类型",
|
||||
"typeRequired": "请输入类型",
|
||||
"typePlaceholder": "请输入OAuth类型",
|
||||
"logo": "Logo",
|
||||
"logoPlaceholder": "请输入Logo URL",
|
||||
"authorizationEndpoint": "授权端点",
|
||||
"authorizationEndpointRequired": "请输入授权端点",
|
||||
"authorizationEndpointPlaceholder": "请输入授权端点URL",
|
||||
"tokenEndpoint": "令牌端点",
|
||||
"tokenEndpointRequired": "请输入令牌端点",
|
||||
"tokenEndpointPlaceholder": "请输入令牌端点URL",
|
||||
"userInfoEndpoint": "用户信息端点",
|
||||
"userInfoEndpointPlaceholder": "请输入用户信息端点URL",
|
||||
"revokeEndpoint": "撤销端点",
|
||||
"revokeEndpointPlaceholder": "请输入撤销端点URL",
|
||||
"clientId": "客户端ID",
|
||||
"clientIdRequired": "请输入客户端ID",
|
||||
"clientIdPlaceholder": "请输入客户端ID",
|
||||
"clientSecret": "客户端密钥",
|
||||
"clientSecretRequired": "请输入客户端密钥",
|
||||
"clientSecretPlaceholder": "请输入客户端密钥",
|
||||
"redirectUri": "重定向URI",
|
||||
"redirectUriRequired": "请输入重定向URI",
|
||||
"redirectUriPlaceholder": "请输入重定向URI",
|
||||
"autoRegister": "自动注册",
|
||||
"ableState": "启用状态",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"noData": "无数据",
|
||||
"scopes": "权限范围",
|
||||
"scopesPlaceholder": "请选择或输入权限范围",
|
||||
"refreshEndpoint": "刷新端点",
|
||||
"refreshEndpointPlaceholder": "请输入刷新端点URL"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../../../oak-app-domain';
|
||||
declare const Upsert: (props: WebComponentProps<EntityDict, 'oauthProvider', false, {
|
||||
item: RowWithActions<EntityDict, 'oauthProvider'>;
|
||||
}>) => React.JSX.Element;
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import React from 'react';
|
||||
import { Form, Input, Switch, Select, Typography } from 'antd';
|
||||
import Styles from './styles.module.less';
|
||||
const { Text } = Typography;
|
||||
const Upsert = (props) => {
|
||||
const { item } = props.data;
|
||||
const { t, update } = props.methods;
|
||||
if (item === undefined) {
|
||||
return <div>{t('noData')}</div>;
|
||||
}
|
||||
return (<div className={Styles.id}>
|
||||
<Form layout="vertical" autoComplete="off">
|
||||
<Form.Item label={t('name')} rules={[{ required: true, message: t('nameRequired') }]}>
|
||||
<Input placeholder={t('namePlaceholder')} value={item.name || ""} onChange={(v) => {
|
||||
update({ name: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('type')} rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning">
|
||||
「{item.type}」不是预设类型,请自行注入 handler。
|
||||
</Text>) : undefined}>
|
||||
<Select mode="tags" placeholder={t('typePlaceholder')} value={item.type ? [item.type] : []} // 保持数组形式
|
||||
onChange={(v) => {
|
||||
// 只取最后一个输入或选择的值
|
||||
const last = v.slice(-1)[0];
|
||||
update({ type: last });
|
||||
}} tokenSeparators={[',']} maxTagCount={1} // 只显示一个标签
|
||||
options={[
|
||||
{ value: 'oak', label: 'Oak' },
|
||||
{ value: 'gitea', label: 'Gitea' },
|
||||
]}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('logo')}>
|
||||
<Input placeholder={t('logoPlaceholder')} value={item.logo || ""} onChange={(v) => {
|
||||
update({ logo: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('authorizationEndpoint')} rules={[{ required: true, message: t('authorizationEndpointRequired') }]}>
|
||||
<Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""} onChange={(v) => {
|
||||
update({ authorizationEndpoint: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('tokenEndpoint')} rules={[{ required: true, message: t('tokenEndpointRequired') }]}>
|
||||
<Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""} onChange={(v) => {
|
||||
update({ tokenEndpoint: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('refreshEndpoint')}>
|
||||
<Input placeholder={t('refreshEndpointPlaceholder')} value={item.refreshEndpoint || ""} onChange={(v) => {
|
||||
update({ refreshEndpoint: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('userInfoEndpoint')}>
|
||||
<Input placeholder={t('userInfoEndpointPlaceholder')} value={item.userInfoEndpoint || ""} onChange={(v) => {
|
||||
update({ userInfoEndpoint: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('revokeEndpoint')}>
|
||||
<Input placeholder={t('revokeEndpointPlaceholder')} value={item.revokeEndpoint || ""} onChange={(v) => {
|
||||
update({ revokeEndpoint: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('clientId')} rules={[{ required: true, message: t('clientIdRequired') }]}>
|
||||
<Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""} onChange={(v) => {
|
||||
update({ clientId: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('clientSecret')} rules={[{ required: true, message: t('clientSecretRequired') }]}>
|
||||
<Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""} onChange={(v) => {
|
||||
update({ clientSecret: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('scopes')}>
|
||||
<Select mode="tags" placeholder={t('scopesPlaceholder')} value={item.scopes || []} onChange={(v) => {
|
||||
update({ scopes: v });
|
||||
}} tokenSeparators={[',']} open={false}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('redirectUri')} rules={[{ required: true, message: t('redirectUriRequired') }]}>
|
||||
<Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""} onChange={(v) => {
|
||||
update({ redirectUri: v.target.value });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('autoRegister')} valuePropName="checked">
|
||||
<Switch checked={!!item.autoRegister} onChange={(checked) => update({ autoRegister: checked })}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('ableState')} valuePropName="checked">
|
||||
<Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/>
|
||||
</Form.Item>
|
||||
|
||||
{/* <Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item> */}
|
||||
</Form>
|
||||
</div>);
|
||||
};
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
declare const OauthProvider: (props: WebComponentProps<EntityDict, 'oauthProvider', true, {
|
||||
list: RowWithActions<EntityDict, 'oauthProvider'>[];
|
||||
systemId: string;
|
||||
}>) => React.JSX.Element;
|
||||
export default OauthProvider;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import Styles from './styles.module.less';
|
||||
import { Button, Modal } from 'antd';
|
||||
import ProviderUpsert from "./upsert";
|
||||
import ListPro from 'oak-frontend-base/es/components/listPro';
|
||||
const OauthProvider = (props) => {
|
||||
const { oakFullpath, systemId } = props.data;
|
||||
const { list, oakLoading } = props.data;
|
||||
const { t, addItem, removeItem, execute, clean } = props.methods;
|
||||
const attrs = [
|
||||
"id", "name", "logo", "authorizationEndpoint",
|
||||
"tokenEndpoint", "userInfoEndpoint", "clientId",
|
||||
"clientSecret", "redirectUri",
|
||||
"autoRegister", "ableState", "$$createAt$$"
|
||||
];
|
||||
const [upsertId, setUpsertId] = React.useState(null);
|
||||
const handleAction = (row, action) => {
|
||||
switch (action) {
|
||||
case "update": {
|
||||
setUpsertId(row.id);
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
Modal.confirm({
|
||||
title: t('confirm.deleteTitle'),
|
||||
content: t('confirm.deleteContent'),
|
||||
onOk: () => {
|
||||
removeItem(row.id);
|
||||
execute();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
return (<>
|
||||
{list && (<ListPro entity='oauthProvider' attributes={attrs} data={list} loading={oakLoading} oakPath={`${oakFullpath}`} onAction={handleAction} extraContent={<div className={Styles.actions}>
|
||||
<Button type="primary" onClick={() => {
|
||||
setUpsertId(addItem({
|
||||
systemId: systemId,
|
||||
autoRegister: true,
|
||||
}));
|
||||
}}>
|
||||
{t('common::action.create')}
|
||||
</Button>
|
||||
</div>}>
|
||||
</ListPro>)}
|
||||
{/* antd model */}
|
||||
<Modal open={!!upsertId} destroyOnClose={true} width={600} onCancel={() => {
|
||||
clean();
|
||||
setUpsertId(null);
|
||||
}} onOk={() => {
|
||||
execute();
|
||||
setUpsertId(null);
|
||||
}}>
|
||||
{upsertId && <ProviderUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId}/>}
|
||||
</Modal>
|
||||
</>);
|
||||
};
|
||||
export default OauthProvider;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
declare const Management: (props: WebComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}>) => React.JSX.Element;
|
||||
export default Management;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import OauthApps from './oauthApps';
|
||||
import OauthProvider from './oauthProvider';
|
||||
import { Tabs } from 'antd';
|
||||
const Management = (props) => {
|
||||
const { oakFullpath, oakDirty } = props.data;
|
||||
const { t, execute } = props.methods;
|
||||
return <Tabs items={[
|
||||
{
|
||||
key: '1',
|
||||
label: t('providers'),
|
||||
children: (<OauthProvider systemId={props.data.systemId} oakPath={`${oakFullpath}.oauthProviders:list`}></OauthProvider>)
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('applications'),
|
||||
children: (<OauthApps systemId={props.data.systemId} oakPath={`${oakFullpath}.oauthApplications:list`}></OauthApps>)
|
||||
}
|
||||
]}/>;
|
||||
};
|
||||
export default Management;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "oauthUserAuthorization", true, {}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import assert from "assert";
|
||||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
export default OakComponent({
|
||||
entity: 'oauthUserAuthorization',
|
||||
isList: true,
|
||||
projection: {
|
||||
userId: 1,
|
||||
applicationId: 1,
|
||||
authorizedAt: 1,
|
||||
codeId: 1,
|
||||
tokenId: 1,
|
||||
usageState: 1,
|
||||
$$createAt$$: 1,
|
||||
application: {
|
||||
logo: 1,
|
||||
name: 1,
|
||||
description: 1,
|
||||
isConfidential: 1,
|
||||
},
|
||||
code: {
|
||||
scope: 1,
|
||||
},
|
||||
token: {
|
||||
accessExpiresAt: 1,
|
||||
refreshExpiresAt: 1,
|
||||
lastUsedAt: 1,
|
||||
revokedAt: 1,
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
pageSize: 5,
|
||||
currentPage: 1,
|
||||
},
|
||||
filters: [{
|
||||
filter() {
|
||||
const userId = this.features.token.getUserId();
|
||||
const systemId = this.features.application.getApplication()?.systemId;
|
||||
return {
|
||||
userId,
|
||||
application: {
|
||||
systemId: systemId
|
||||
},
|
||||
usageState: {
|
||||
$in: ['granted', 'denied', 'revoked']
|
||||
}
|
||||
};
|
||||
},
|
||||
}],
|
||||
formData({ data }) {
|
||||
return {
|
||||
list: data,
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
methods: {
|
||||
async revoke(item) {
|
||||
assert(item.id, 'No id found for this authorization record');
|
||||
await this.features.cache.operate("oauthUserAuthorization", {
|
||||
action: "revoke",
|
||||
id: generateNewId(),
|
||||
data: {},
|
||||
filter: {
|
||||
id: item.id
|
||||
}
|
||||
});
|
||||
console.log('Revoking authorization for:', item.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"page_title": "My Authorization Records",
|
||||
"page_description": "Manage access permissions you've granted to third-party applications",
|
||||
"no_records": "No authorization records",
|
||||
"unknown_app": "Unknown Application",
|
||||
"revoke": "Revoke",
|
||||
"revoke_confirm_title": "Confirm Revoke Authorization",
|
||||
"revoke_confirm_content": "Are you sure you want to revoke authorization for '%{appName}'? The application will no longer be able to access your data.",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"status_active": "Active",
|
||||
"status_revoked": "Revoked",
|
||||
"status_denied": "Denied",
|
||||
"status_expired": "Expired",
|
||||
"status_unknown": "Unknown",
|
||||
"authorized_at": "Authorized At",
|
||||
"scope": "Scope",
|
||||
"full_access": "Full Access",
|
||||
"last_used_at": "Last Used",
|
||||
"access_expires_at": "Access Expires At",
|
||||
"revoked_at": "Revoked At",
|
||||
"pagination_total": "Total %{total} items"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue