Compare commits
25 Commits
dev
...
modifiable
| Author | SHA1 | Date |
|---|---|---|
|
|
4c317906ca | |
|
|
70747be388 | |
|
|
db522727ec | |
|
|
a9ab8377d2 | |
|
|
aef6942a57 | |
|
|
71b25a2a54 | |
|
|
5855b5f808 | |
|
|
fb8fae0525 | |
|
|
841faface2 | |
|
|
0dc7dd4d98 | |
|
|
9a05b713ab | |
|
|
6f5dead6cc | |
|
|
d7b34fb3f6 | |
|
|
fb9489a2f5 | |
|
|
35c6b01f65 | |
|
|
a3a4837974 | |
|
|
18c8d3b670 | |
|
|
1a063c86f5 | |
|
|
82073c86e0 | |
|
|
dc3dadc824 | |
|
|
011a56c830 | |
|
|
195bbc0c24 | |
|
|
e06198eb78 | |
|
|
e0e906bd60 | |
|
|
0b54e3f208 |
|
|
@ -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,372 @@ 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";
|
||||
}, 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,30 @@
|
|||
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';
|
||||
}, context: BRC<ED>): Promise<{
|
||||
redirectUri: string;
|
||||
}>;
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
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,
|
||||
},
|
||||
}, {});
|
||||
assert(state, '无效的 state 参数');
|
||||
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
|
||||
// 如果已经使用
|
||||
if (state.usedAt) {
|
||||
throw new OakUserException('该授权请求已被使用,请重新发起授权请求');
|
||||
}
|
||||
// 更新为使用过
|
||||
await context.operate("oauthState", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
usedAt: Date.now(),
|
||||
},
|
||||
filter: {
|
||||
id: state.id,
|
||||
}
|
||||
}, {});
|
||||
// 使用 code 换取 access_token 并获取用户信息
|
||||
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider);
|
||||
const [existingOAuthUser] = await context.select("oauthUser", {
|
||||
data: {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
providerUserId: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
refId: 1,
|
||||
ref: {
|
||||
id: 1,
|
||||
userState: 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
providerUserId: oauthUserInfo.providerUserId,
|
||||
providerConfigId: state.providerId,
|
||||
}
|
||||
}, {});
|
||||
// 已登录的情况
|
||||
if (islogginedIn) {
|
||||
// 检查当前用户是否已绑定此提供商
|
||||
const [currentUserBinding] = await context.select("oauthUser", {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
userId: currentUserId,
|
||||
providerConfigId: state.providerId,
|
||||
}
|
||||
}, {});
|
||||
if (currentUserBinding) {
|
||||
throw new OakUserException('当前用户已绑定该 OAuth 平台账号');
|
||||
}
|
||||
if (existingOAuthUser) {
|
||||
throw new OakUserException('该 OAuth 账号已被其他用户绑定');
|
||||
}
|
||||
console.log("绑定 OAuth 账号到当前用户:", currentUserId, oauthUserInfo.providerUserId);
|
||||
// 创建绑定关系
|
||||
await context.operate("oauthUser", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
userId: currentUserId,
|
||||
providerConfigId: state.providerId,
|
||||
providerUserId: oauthUserInfo.providerUserId,
|
||||
rawUserInfo: oauthUserInfo.rawData,
|
||||
accessToken,
|
||||
accessExpiresAt: accessTokenExp,
|
||||
refreshToken,
|
||||
refreshExpiresAt: refreshTokenExp,
|
||||
applicationId,
|
||||
stateId: state.id,
|
||||
}
|
||||
}, {});
|
||||
// 返回当前 token
|
||||
const tokenValue = context.getTokenValue();
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
// 未登录,OAuth账号已存在,直接登录
|
||||
}
|
||||
else if (existingOAuthUser) {
|
||||
console.log("使用已绑定的 OAuth 账号登录:", existingOAuthUser.id);
|
||||
const { user } = existingOAuthUser;
|
||||
const targetUser = user?.userState === 'merged' ? user.ref : user;
|
||||
const tokenValue = await setUpTokenAndUser(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID
|
||||
undefined, targetUser // 关联的用户
|
||||
);
|
||||
// 更新登录信息
|
||||
await context.operate("oauthUser", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
rawUserInfo: oauthUserInfo.rawData,
|
||||
accessToken,
|
||||
accessExpiresAt: accessTokenExp,
|
||||
refreshToken,
|
||||
refreshExpiresAt: refreshTokenExp,
|
||||
applicationId,
|
||||
stateId: state.id,
|
||||
},
|
||||
filter: {
|
||||
id: existingOAuthUser.id,
|
||||
}
|
||||
}, {});
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
// 未登录,OAuth账号不存在,创建新用户
|
||||
else {
|
||||
if (!state.provider.autoRegister) {
|
||||
throw new OakUserException('您还没有账号,请先注册一个账号');
|
||||
}
|
||||
console.log("使用未绑定的 OAuth 账号登录:", oauthUserInfo.providerUserId);
|
||||
const newUserId = await generateNewIdAsync();
|
||||
const oauthUserCreateData = {
|
||||
id: newUserId,
|
||||
providerConfigId: state.providerId,
|
||||
providerUserId: oauthUserInfo.providerUserId,
|
||||
rawUserInfo: oauthUserInfo.rawData,
|
||||
accessToken,
|
||||
accessExpiresAt: accessTokenExp,
|
||||
refreshToken,
|
||||
refreshExpiresAt: refreshTokenExp,
|
||||
applicationId,
|
||||
stateId: state.id,
|
||||
loadState: 'unload'
|
||||
};
|
||||
// 不传 user 参数,会自动创建新用户
|
||||
const tokenValue = await setUpTokenAndUser(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser
|
||||
undefined // 不传 user,自动创建新用户
|
||||
);
|
||||
await context.operate("oauthUser", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'loadUserInfo',
|
||||
data: {},
|
||||
filter: {
|
||||
id: newUserId,
|
||||
}
|
||||
}, {});
|
||||
await loadTokenInfo(tokenValue, context);
|
||||
closeRootMode();
|
||||
return tokenValue;
|
||||
}
|
||||
}
|
||||
export async function getOAuthClientInfo(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",
|
||||
}
|
||||
}, {});
|
||||
// 如果还有正在生效的授权,说明已经授权过了
|
||||
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,
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
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
|
||||
}
|
||||
}, {});
|
||||
closeRootMode();
|
||||
return state;
|
||||
}
|
||||
export async function authorize(params, context) {
|
||||
const { response_type, client_id, redirect_uri, scope, state, action } = params;
|
||||
if (response_type !== 'code') {
|
||||
throw new OakUserException('不支持的 response_type 类型');
|
||||
}
|
||||
const closeRootMode = context.openRootMode();
|
||||
const systemId = context.getSystemId();
|
||||
const [oauthApp] = await context.select("oauthApplication", {
|
||||
data: {
|
||||
id: 1,
|
||||
redirectUris: 1,
|
||||
isConfidential: 1,
|
||||
},
|
||||
filter: {
|
||||
id: client_id,
|
||||
systemId: systemId,
|
||||
}
|
||||
}, {});
|
||||
if (!oauthApp) {
|
||||
throw new OakUserException('未经授权的客户端应用');
|
||||
}
|
||||
// 创建授权记录
|
||||
const recordId = await generateNewIdAsync();
|
||||
await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: recordId,
|
||||
userId: context.getCurrentUserId(),
|
||||
applicationId: oauthApp.id,
|
||||
usageState: action === 'grant' ? 'unused' : 'denied',
|
||||
authorizedAt: Date.now(),
|
||||
}
|
||||
}, {});
|
||||
if (action === 'deny') {
|
||||
const params = new URLSearchParams();
|
||||
params.set('error', 'access_denied');
|
||||
params.set('error_description', '用户拒绝了授权请求');
|
||||
if (state) {
|
||||
params.set('state', state);
|
||||
}
|
||||
closeRootMode();
|
||||
return {
|
||||
redirectUri: `${redirect_uri}?${params.toString()}`,
|
||||
};
|
||||
}
|
||||
if (action === 'grant') {
|
||||
// 检查redirectUri 是否在注册的列表中
|
||||
if (!oauthApp.redirectUris?.includes(redirect_uri)) {
|
||||
console.log('不合法的重定向 URI:', redirect_uri, oauthApp.redirectUris);
|
||||
throw new OakUserException('重定向 URI 不合法');
|
||||
}
|
||||
const code = randomUUID();
|
||||
const codeId = await generateNewIdAsync();
|
||||
// 存储授权码
|
||||
await context.operate("oauthAuthorizationCode", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: codeId,
|
||||
code,
|
||||
redirectUri: redirect_uri,
|
||||
oauthAppId: oauthApp.id,
|
||||
applicationId: context.getApplicationId(),
|
||||
userId: context.getCurrentUserId(),
|
||||
scope: scope === undefined ? [] : [scope],
|
||||
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
|
||||
}
|
||||
}, {});
|
||||
// 更新记录
|
||||
await context.operate("oauthUserAuthorization", {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
codeId: codeId,
|
||||
},
|
||||
filter: {
|
||||
id: recordId,
|
||||
}
|
||||
}, {});
|
||||
const params = new URLSearchParams();
|
||||
params.set('code', code);
|
||||
if (state) {
|
||||
params.set('state', state);
|
||||
}
|
||||
closeRootMode();
|
||||
return {
|
||||
redirectUri: `${redirect_uri}?${params.toString()}`,
|
||||
};
|
||||
}
|
||||
closeRootMode();
|
||||
throw new Error('unknown action');
|
||||
}
|
||||
const fetchOAuthUserInfo = async (code, 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -103,7 +103,7 @@ export async function createSession(params, context) {
|
|||
origin: 'wechat',
|
||||
type: 'image',
|
||||
tag1: 'image',
|
||||
objectId: await generateNewIdAsync(),
|
||||
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
|
||||
sort: 1000,
|
||||
uploadState: 'success',
|
||||
extra1: data.MediaId,
|
||||
|
|
@ -128,7 +128,7 @@ export async function createSession(params, context) {
|
|||
origin: 'wechat',
|
||||
type: 'video',
|
||||
tag1: 'video',
|
||||
objectId: await generateNewIdAsync(),
|
||||
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
|
||||
sort: 1000,
|
||||
uploadState: 'success',
|
||||
extra1: data.MediaId,
|
||||
|
|
@ -150,7 +150,7 @@ export async function createSession(params, context) {
|
|||
origin: 'wechat',
|
||||
type: 'audio',
|
||||
tag1: 'audio',
|
||||
objectId: await generateNewIdAsync(),
|
||||
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
|
||||
sort: 1000,
|
||||
uploadState: 'success',
|
||||
extra1: data.MediaId,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ export declare function syncMessageTemplate<ED extends EntityDict>(params: {
|
|||
example: string;
|
||||
keywordEnumValueList: {
|
||||
keywordCode: string;
|
||||
enumValueList: string[];
|
||||
enumValueList: Array<string>;
|
||||
}[];
|
||||
}[]>;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
@ -2467,8 +2467,8 @@ export async function refreshToken(params, context) {
|
|||
// 只有server模式去刷新token
|
||||
// 'development' | 'production' | 'staging'
|
||||
const intervals = {
|
||||
development: 7200 * 1000,
|
||||
staging: 600 * 1000,
|
||||
development: 7200 * 1000, // 2小时
|
||||
staging: 600 * 1000, // 十分钟
|
||||
production: 600 * 1000, // 十分钟
|
||||
};
|
||||
let applicationId = token.applicationId;
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export async function createWechatQrCode(options, context) {
|
|||
permanent,
|
||||
url,
|
||||
expired: false,
|
||||
expiresAt: Date.now() + 2592000 * 1000,
|
||||
expiresAt: Date.now() + 2592000 * 1000, // wecharQrCode里的过期时间都放到最大,由上层关联对象来主动过期(by Xc, 20230131)
|
||||
props,
|
||||
};
|
||||
// 直接创建
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "address", true, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "address", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="@uiw/react-amap-types" />
|
||||
import React from 'react';
|
||||
export type PositionProps = {
|
||||
loadUI: boolean;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="@uiw/react-amap-types" />
|
||||
import React from 'react';
|
||||
import { ModalProps } from 'antd';
|
||||
import { GeolocationProps } from '@uiw/react-amap';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="@uiw/react-amap-types" />
|
||||
import React from 'react';
|
||||
import { MapProps, APILoaderConfig } from '@uiw/react-amap';
|
||||
import './index.less';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { NativeConfig, WebConfig, WechatMpConfig, WechatPublicConfig } from '../../../entities/Application';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
export type AppConfig = WebConfig | WechatMpConfig | WechatPublicConfig | NativeConfig;
|
||||
export type CosConfig = AppConfig['cos'];
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
config: AppConfig;
|
||||
entity: string;
|
||||
entityId: string;
|
||||
name: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
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", {
|
||||
action: 'select',
|
||||
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,2 @@
|
|||
/// <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;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <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;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <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;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
"menu": "菜单管理",
|
||||
"autoReply": "被关注回复管理",
|
||||
"tag": "标签管理",
|
||||
"user": "用户管理"
|
||||
"user": "用户管理",
|
||||
"cos": "COS配置"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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 { t, update } = props.methods;
|
||||
|
|
@ -29,6 +30,12 @@ export default function Render(props) {
|
|||
key: 'style',
|
||||
children: (<StyleUpsert style={style} entity={'platform'} 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>),
|
||||
},
|
||||
];
|
||||
if (type === 'wechatPublic') {
|
||||
items.push({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "application", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "article", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
/// <reference types="react" />
|
||||
import { GenerateUrlFn } from "../../../types/Article";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "article", true, {
|
||||
entityId: string;
|
||||
articleMenuId: string | undefined;
|
||||
generateUrl: GenerateUrlFn;
|
||||
empty: import("react").ReactNode;
|
||||
empty: React.ReactNode | undefined;
|
||||
menuCheck: (isArticle: boolean) => void;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "article", true, {
|
||||
articleMenuId: string | undefined;
|
||||
onChildEditArticleChange: (data: string) => void;
|
||||
show: "preview" | "doc" | "edit";
|
||||
show: "edit" | "doc" | "preview";
|
||||
getBreadcrumbItemsByParent: (breadcrumbItems: string[]) => void;
|
||||
breadcrumbItems: string[];
|
||||
drawerOpen: boolean;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="react" />
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
import { GenerateUrlFn } from "../../../types/Article";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
|
||||
|
|
@ -6,8 +5,8 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
entityId: string;
|
||||
title: string;
|
||||
origin: string;
|
||||
menuEmpty: import("react").ReactNode;
|
||||
articleEmpty: import("react").ReactNode;
|
||||
menuEmpty: React.ReactNode | undefined;
|
||||
articleEmpty: React.ReactNode | undefined;
|
||||
generateUrl: GenerateUrlFn;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default OakComponent({
|
|||
entity: '',
|
||||
entityId: '',
|
||||
title: '',
|
||||
origin: 'qiniu',
|
||||
origin: 'qiniu', // cos origin默认七牛云
|
||||
menuEmpty: undefined,
|
||||
articleEmpty: undefined,
|
||||
generateUrl: ((mode, type, id) => { }), //构造文章显示路由
|
||||
|
|
@ -20,7 +20,7 @@ export default OakComponent({
|
|||
showAddArticle: false,
|
||||
showAddMenu: true,
|
||||
parentId: '',
|
||||
articleMenuId: '',
|
||||
articleMenuId: '', //非空时展示atricle表格
|
||||
unsub: undefined,
|
||||
},
|
||||
listeners: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="react" />
|
||||
import { GenerateUrlFn } from "../../../types/Article";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "articleMenu", true, {
|
||||
entity: string;
|
||||
|
|
@ -7,7 +6,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
origin: string;
|
||||
onMenuClick: (menuId: string, menuName: string, isArticle: boolean) => void;
|
||||
onArticleClick: (atricleId: string) => void;
|
||||
empty: import("react").ReactNode;
|
||||
empty: React.ReactNode | undefined;
|
||||
changeAddArticle: (show: boolean) => void;
|
||||
generateUrl: GenerateUrlFn;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
onRemove: () => void;
|
||||
onUpdateName: (name: string) => Promise<void>;
|
||||
onChildEditArticleChange: (data: string) => void;
|
||||
show: "preview" | "doc" | "edit";
|
||||
show: "edit" | "doc" | "preview";
|
||||
getBreadcrumbItemsByParent: (breadcrumbItems: string[]) => void;
|
||||
breadItems: string[];
|
||||
drawerOpen: boolean;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
entityId: string;
|
||||
parentId: string | undefined;
|
||||
onGrandChildEditArticleChange: (data: string) => void;
|
||||
show: "preview" | "doc" | "edit";
|
||||
show: "edit" | "doc" | "preview";
|
||||
articleMenuId: string;
|
||||
articleId: string;
|
||||
getBreadcrumbItems: (breadcrumbItems: string[]) => void;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "articleMenu", true, {
|
||||
entity: string;
|
||||
entityId: string;
|
||||
show: "preview" | "doc" | "edit";
|
||||
show: "edit" | "doc" | "preview";
|
||||
articleMenuId: string;
|
||||
articleId: string;
|
||||
tocPosition: "none" | "left" | "right";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "user", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "user", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="node" />
|
||||
import * as React from 'react';
|
||||
type IDownloadProps = {
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -9,9 +8,9 @@ type IDownloadProps = {
|
|||
};
|
||||
declare function Download(props: IDownloadProps): React.JSX.Element;
|
||||
declare namespace Download {
|
||||
var onDownload: (data: ArrayBuffer | ReadableStream<any>, filename: string) => Promise<void>;
|
||||
var onDownload: (data: ArrayBuffer | ReadableStream, filename: string) => Promise<void>;
|
||||
var base64ToBlob: (base64String: string) => Blob;
|
||||
var arrayBufferToBase64: (buffer: Buffer) => string;
|
||||
var base64ToArrayBuffer: (base64String: string) => ArrayBufferLike;
|
||||
var base64ToArrayBuffer: (base64String: string) => ArrayBuffer;
|
||||
}
|
||||
export default Download;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { ReactComponentProps } from 'oak-frontend-base';
|
|||
import { ECode } from '../../../types/ErrorPage';
|
||||
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
|
||||
code: ECode;
|
||||
title?: string | undefined;
|
||||
desc?: string | undefined;
|
||||
title?: string;
|
||||
desc?: string;
|
||||
children?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -2,21 +2,21 @@ import { EntityDict } from '../../../oak-app-domain';
|
|||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
|
||||
import { ReactComponentProps } from 'oak-frontend-base';
|
||||
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
|
||||
filename?: string | undefined;
|
||||
expiresAt?: number | undefined;
|
||||
filename?: string;
|
||||
expiresAt?: number;
|
||||
tips?: React.ReactNode;
|
||||
onDownload?: ((qrCodeImage: string, filename?: string) => void) | undefined;
|
||||
onRefresh?: (() => void) | undefined;
|
||||
size?: number | undefined;
|
||||
onDownload?: (qrCodeImage: string, filename?: string) => void;
|
||||
onRefresh?: () => void;
|
||||
size?: number;
|
||||
url: string;
|
||||
loading?: boolean | undefined;
|
||||
disableDownload?: boolean | undefined;
|
||||
loading?: boolean;
|
||||
disableDownload?: boolean;
|
||||
disabled: boolean;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
maskColor: string;
|
||||
maskTextColor: string;
|
||||
maskText: string;
|
||||
mode: 'simple' | 'default';
|
||||
mode: "simple" | "default";
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
redDot: boolean;
|
||||
text: string;
|
||||
pagePath: string;
|
||||
iconName?: string | undefined;
|
||||
selectedIconName?: string | undefined;
|
||||
iconPath?: string | undefined;
|
||||
selectedIconPath?: string | undefined;
|
||||
iconSize?: string | undefined;
|
||||
iconName?: string;
|
||||
selectedIconName?: string;
|
||||
iconPath?: string;
|
||||
selectedIconPath?: string;
|
||||
iconSize?: string;
|
||||
}[];
|
||||
color: string;
|
||||
selectedColor: string;
|
||||
|
|
|
|||
|
|
@ -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" | "application" | "system";
|
||||
entity: "system" | "platform" | "application";
|
||||
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, } from 'antd';
|
||||
import Styles from './web.module.less';
|
||||
import { isEmptyObject } from '../../../../utils/strings';
|
||||
// https://developer.qiniu.com/kodo/1671/region-endpoint-fq
|
||||
const QiniuZoneArray = [
|
||||
{
|
||||
|
|
@ -987,6 +988,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}>
|
||||
每种均可配置一个,相应的服务所使用的帐号请准确对应
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Config } from '../../../types/Config';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, {
|
||||
config: Config;
|
||||
entity: "platform" | "system";
|
||||
entity: "system" | "platform";
|
||||
name: string;
|
||||
entityId: string;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "domain", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "domain", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="react" />
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
|
||||
import { ReactComponentProps } from 'oak-frontend-base/lib/types/Page';
|
||||
|
|
@ -9,31 +8,14 @@ type AfterCommit = (() => void) | undefined;
|
|||
type BeforeCommit = (() => boolean | undefined | Promise<boolean | undefined>) | undefined;
|
||||
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, true, {
|
||||
entity: keyof ED2;
|
||||
action?: string | undefined;
|
||||
size?: ButtonProps['size'] | AmButtonProps['size'];
|
||||
block?: boolean | undefined;
|
||||
type?: ButtonProps['type'] | AmButtonProps['type'];
|
||||
executeText?: string | undefined;
|
||||
buttonProps?: (ButtonProps & {
|
||||
color?: "success" | "default" | "warning" | "danger" | "primary" | 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?: "button" | "submit" | "reset" | undefined;
|
||||
shape?: "default" | "rounded" | "rectangular" | undefined;
|
||||
children?: import("react").ReactNode;
|
||||
} & 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;
|
||||
} & import("react").AriaAttributes) | undefined;
|
||||
action?: string;
|
||||
size?: ButtonProps["size"] | AmButtonProps["size"];
|
||||
block?: boolean;
|
||||
type?: ButtonProps["type"] | AmButtonProps["type"];
|
||||
executeText?: string;
|
||||
buttonProps?: ButtonProps & AmButtonProps;
|
||||
afterCommit?: AfterCommit;
|
||||
beforeCommit?: BeforeCommit;
|
||||
messageProps?: boolean | MessageProps | undefined;
|
||||
messageProps?: MessageProps | boolean;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
extension: string[];
|
||||
selectCount: number;
|
||||
sourceType: SourceType[];
|
||||
mediaType: ('image' | 'video')[];
|
||||
mediaType: ("image" | "video")[];
|
||||
mode: ImageMode;
|
||||
size: number;
|
||||
showUploadList: boolean;
|
||||
|
|
@ -52,7 +52,7 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
aspect: number;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
cropShape: 'rect' | 'round';
|
||||
cropShape: "rect" | "round";
|
||||
cropperProps: object;
|
||||
modalTitle: string;
|
||||
modalWidth: string;
|
||||
|
|
@ -67,7 +67,7 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
minHeight: number;
|
||||
compressWidth: number;
|
||||
compressHeight: number;
|
||||
resize: 'contain' | 'cover' | 'none';
|
||||
resize: "contain" | "cover" | "none";
|
||||
compressQuality: number;
|
||||
mimeType: string;
|
||||
convertTypes: string[];
|
||||
|
|
|
|||
|
|
@ -47,19 +47,19 @@ export default OakComponent({
|
|||
bucket: '',
|
||||
autoUpload: false,
|
||||
maxNumber: 20,
|
||||
extension: [],
|
||||
selectCount: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
mediaType: ['image'],
|
||||
mode: 'aspectFit',
|
||||
size: 3,
|
||||
showUploadList: true,
|
||||
showUploadProgress: false,
|
||||
accept: 'image/*',
|
||||
disablePreview: false,
|
||||
disableDelete: false,
|
||||
disableAdd: false,
|
||||
disableDownload: false,
|
||||
extension: [], //小程序独有 chooseMessageFile
|
||||
selectCount: 1, // 每次打开图片时,可选中的数量 小程序独有
|
||||
sourceType: ['album', 'camera'], // 小程序独有 chooseMedia
|
||||
mediaType: ['image'], // 小程序独有 chooseMedia
|
||||
mode: 'aspectFit', // 图片显示模式
|
||||
size: 3, // 每行可显示的个数 小程序独有
|
||||
showUploadList: true, //web独有
|
||||
showUploadProgress: false, // web独有
|
||||
accept: 'image/*', // web独有
|
||||
disablePreview: false, // 图片是否可预览
|
||||
disableDelete: false, // 图片是否可删除
|
||||
disableAdd: false, // 上传按钮隐藏
|
||||
disableDownload: false, // 下载按钮隐藏
|
||||
type: 'image',
|
||||
origin: 'qiniu',
|
||||
tag1: '',
|
||||
|
|
@ -67,40 +67,40 @@ export default OakComponent({
|
|||
entity: '',
|
||||
entityId: '',
|
||||
theme: 'image',
|
||||
enableCrop: false,
|
||||
enableCompross: false,
|
||||
enableCrop: false, //启用裁剪
|
||||
enableCompross: false, //启用压缩
|
||||
//图片裁剪
|
||||
cropQuality: 1,
|
||||
showRest: false,
|
||||
showGrid: false,
|
||||
fillColor: 'white',
|
||||
rotationSlider: false,
|
||||
aspectSlider: false,
|
||||
zoomSlider: true,
|
||||
resetText: '重置',
|
||||
aspect: 1 / 1,
|
||||
minZoom: 1,
|
||||
maxZoom: 3,
|
||||
cropShape: 'rect',
|
||||
cropperProps: {},
|
||||
modalTitle: '编辑图片',
|
||||
modalWidth: '40vw',
|
||||
modalOk: '确定',
|
||||
modalCancel: '取消',
|
||||
cropQuality: 1, //图片裁剪质量,范围:0 ~ 1
|
||||
showRest: false, //显示重置按钮,重置缩放及旋转
|
||||
showGrid: false, //显示裁切区域网格(九宫格)
|
||||
fillColor: 'white', //裁切图像填充色
|
||||
rotationSlider: false, //图片旋转控制
|
||||
aspectSlider: false, //裁切比率控制
|
||||
zoomSlider: true, //图片缩放控制
|
||||
resetText: '重置', //重置按钮文字
|
||||
aspect: 1 / 1, //裁切区域宽高比,width / height
|
||||
minZoom: 1, //最小缩放倍数
|
||||
maxZoom: 3, //最大缩放倍数
|
||||
cropShape: 'rect', //裁切区域形状,'rect' 或 'round'
|
||||
cropperProps: {}, //recat-easy-crop的props
|
||||
modalTitle: '编辑图片', //弹窗标题
|
||||
modalWidth: '40vw', //弹窗宽度
|
||||
modalOk: '确定', //确定按钮文字
|
||||
modalCancel: '取消', //取消按钮的文字
|
||||
//图片压缩
|
||||
strict: true,
|
||||
checkOrientation: true,
|
||||
retainExif: false,
|
||||
maxWidth: Infinity,
|
||||
maxHeight: Infinity,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
compressWidth: undefined,
|
||||
compressHeight: undefined,
|
||||
resize: 'none',
|
||||
compressQuality: 0.8,
|
||||
mimeType: 'auto',
|
||||
convertTypes: ['image/png'],
|
||||
strict: true, //当压缩后的图片尺寸大于原图尺寸时输出原图
|
||||
checkOrientation: true, //读取图像的Exif方向值并自动旋转或翻转图像(仅限 JPEG 图像)
|
||||
retainExif: false, //压缩后保留图片的Exif信息
|
||||
maxWidth: Infinity, //输出图片的最大宽度,值需大于0
|
||||
maxHeight: Infinity, //输出图片的最大高度,值需大于0
|
||||
minWidth: 0, //输出图片的最小宽度,值需大于0且不应大于maxWidth
|
||||
minHeight: 0, //输出图片的最小高度。值需大于0且不应大于maxHeight
|
||||
compressWidth: undefined, //输出图像的宽度。如果未指定,则将使用原始图像的宽度,若设置了height,则宽度将根据自然纵横比自动计算。
|
||||
compressHeight: undefined, //输出图像的高度。如果未指定,则将使用原始图像的高度,若设置了width,则高度将根据自然纵横比自动计算。
|
||||
resize: 'none', //仅在同时指定了width和height时生效
|
||||
compressQuality: 0.8, //输出图像的质量。范围:0 ~ 1
|
||||
mimeType: 'auto', //输出图片的 MIME 类型。默认情况下,将使用源图片文件的原始 MIME 类型。
|
||||
convertTypes: ['image/png'], //文件类型包含在其中且文件大小超过该convertSize值的文件将被转换为 JPEG。
|
||||
convertSize: Infinity, //文件类型包含在convertTypes中且文件大小超过此值的文件将转换为 JPEG,Infinity表示禁用该功能
|
||||
},
|
||||
features: ['extraFile'],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
tag2: string;
|
||||
entity: keyof ED2;
|
||||
entityId: string;
|
||||
style?: string | undefined;
|
||||
style?: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ export default OakComponent({
|
|||
},
|
||||
],
|
||||
properties: {
|
||||
mode: 'aspectFit',
|
||||
size: 3,
|
||||
disablePreview: false,
|
||||
disableDownload: false,
|
||||
mode: 'aspectFit', // 图片显示模式
|
||||
size: 3, // 每行可显示的个数 小程序独有
|
||||
disablePreview: false, // 图片是否可预览
|
||||
disableDownload: false, // 下载按钮隐藏
|
||||
tag1: '',
|
||||
tag2: '',
|
||||
entity: '',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="react" />
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { FileState } from '../../../features/extraFile';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
|
||||
|
|
@ -21,7 +20,7 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
extension: string[];
|
||||
selectCount: number;
|
||||
sourceType: SourceType[];
|
||||
mediaType: ('image' | 'video')[];
|
||||
mediaType: ("image" | "video")[];
|
||||
mode: ImageMode;
|
||||
size: number;
|
||||
showUploadList: boolean;
|
||||
|
|
@ -40,8 +39,8 @@ declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends key
|
|||
entityId: string;
|
||||
theme: Theme;
|
||||
children?: React.ReactNode;
|
||||
style?: import("react").CSSProperties | undefined;
|
||||
className?: string | undefined;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
beforeUpload: (file: File) => Promise<string>;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -50,21 +50,21 @@ export default OakComponent({
|
|||
bucket: '',
|
||||
autoUpload: false,
|
||||
maxNumber: 20,
|
||||
extension: [],
|
||||
selectCount: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
mediaType: ['image'],
|
||||
mode: 'aspectFit',
|
||||
size: 3,
|
||||
showUploadList: true,
|
||||
showUploadProgress: false,
|
||||
accept: 'image/*',
|
||||
disablePreview: false,
|
||||
disableDelete: false,
|
||||
disableAdd: false,
|
||||
disableDownload: false,
|
||||
extension: [], //小程序独有 chooseMessageFile
|
||||
selectCount: 1, // 每次打开图片时,可选中的数量 小程序独有
|
||||
sourceType: ['album', 'camera'], // 小程序独有 chooseMedia
|
||||
mediaType: ['image'], // 小程序独有 chooseMedia
|
||||
mode: 'aspectFit', // 图片显示模式
|
||||
size: 3, // 每行可显示的个数 小程序独有
|
||||
showUploadList: true, //web独有
|
||||
showUploadProgress: false, // web独有
|
||||
accept: 'image/*', // web独有
|
||||
disablePreview: false, // 图片是否可预览
|
||||
disableDelete: false, // 图片是否可删除
|
||||
disableAdd: false, // 上传按钮隐藏
|
||||
disableDownload: false, // 下载按钮隐藏
|
||||
type: 'image',
|
||||
origin: 'qiniu',
|
||||
origin: null,
|
||||
tag1: '',
|
||||
tag2: '',
|
||||
entity: '',
|
||||
|
|
@ -147,7 +147,7 @@ export default OakComponent({
|
|||
type,
|
||||
tag1,
|
||||
tag2,
|
||||
objectId: generateNewId(),
|
||||
objectId: generateNewId(), // 这个域用来标识唯一性
|
||||
entity,
|
||||
filename,
|
||||
size,
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ export interface UploadHandle {
|
|||
}
|
||||
declare const _default: React.ForwardRefExoticComponent<WebComponentProps<EntityDict, "extraFile", true, {
|
||||
files: EnhancedExtraFile[];
|
||||
accept?: string | undefined;
|
||||
maxNumber?: number | undefined;
|
||||
multiple?: boolean | undefined;
|
||||
draggable?: boolean | undefined;
|
||||
theme?: Theme | undefined;
|
||||
beforeUpload?: ((file: File) => Promise<boolean> | boolean) | undefined;
|
||||
style?: React.CSSProperties | undefined;
|
||||
className?: string | undefined;
|
||||
directory?: boolean | undefined;
|
||||
onPreview?: ((file: UploadFile<any>) => void) | undefined;
|
||||
onDownload?: ((file: UploadFile<any>) => void) | undefined;
|
||||
showUploadList?: boolean | undefined;
|
||||
children?: JSX.Element | undefined;
|
||||
disableInsert?: boolean | undefined;
|
||||
disableDownload?: boolean | undefined;
|
||||
disableDelete?: boolean | undefined;
|
||||
disablePreview?: boolean | undefined;
|
||||
accept?: string;
|
||||
maxNumber?: number;
|
||||
multiple?: boolean;
|
||||
draggable?: boolean;
|
||||
theme?: Theme;
|
||||
beforeUpload?: (file: File) => Promise<boolean> | boolean;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
directory?: boolean;
|
||||
onPreview?: (file: UploadFile<any>) => void;
|
||||
onDownload?: (file: UploadFile<any>) => void;
|
||||
showUploadList?: boolean;
|
||||
children?: JSX.Element;
|
||||
disableInsert?: boolean;
|
||||
disableDownload?: boolean;
|
||||
disableDelete?: boolean;
|
||||
disablePreview?: boolean;
|
||||
}, {
|
||||
onRemove: (file: UploadFile) => void;
|
||||
addFileByWeb: (file: UploadFile) => void;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, boolean, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "message", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "message", true, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EntityDict } from '../../../oak-app-domain';
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "messageTypeSmsTemplate", true, {
|
||||
systemId: string;
|
||||
origin: import("../../../oak-app-domain/SmsTemplate/_baseSchema").Origin;
|
||||
origin: EntityDict["smsTemplate"]["Schema"]["origin"];
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
onlyCaptcha: boolean;
|
||||
onlyPassword: boolean;
|
||||
eventLoggedIn: string;
|
||||
callback: (() => void) | undefined;
|
||||
callback: ((() => void) | undefined);
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "mobile", true, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, {
|
||||
shape: string;
|
||||
size: string | number;
|
||||
size: number | string;
|
||||
iconColor: string;
|
||||
iconName: string;
|
||||
}>) => React.ReactElement;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -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,33 @@
|
|||
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,
|
||||
},
|
||||
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,44 @@
|
|||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
export default OakComponent({
|
||||
entity: 'oauthApplication',
|
||||
isList: false,
|
||||
projection: {
|
||||
name: 1,
|
||||
description: 1,
|
||||
redirectUris: 1,
|
||||
logo: 1, // string
|
||||
isConfidential: 1,
|
||||
scopes: 1, // string[]
|
||||
ableState: 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 || "",
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
methods: {
|
||||
reGenerateClientSecret() {
|
||||
this.features.cache.operate("oauthApplication", {
|
||||
id: generateNewId(),
|
||||
action: "resetSecret",
|
||||
data: {},
|
||||
filter: {
|
||||
id: this.props.oakId,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
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;
|
||||
}, {
|
||||
reGenerateClientSecret: () => void;
|
||||
}>) => React.JSX.Element;
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
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 } = 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')}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder={t('clientSecretPlaceholder')} value={clientSecret || "已隐藏"} disabled/>
|
||||
<Button type="primary" onClick={reGenerateClientSecret}>
|
||||
{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>
|
||||
</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"
|
||||
];
|
||||
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,38 @@
|
|||
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,
|
||||
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,29 @@
|
|||
export default OakComponent({
|
||||
entity: 'oauthProvider',
|
||||
isList: false,
|
||||
projection: {
|
||||
name: 1,
|
||||
type: 1,
|
||||
logo: 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,121 @@
|
|||
import React from 'react';
|
||||
import { Form, Input, Switch, Select } from 'antd';
|
||||
import Styles from './styles.module.less';
|
||||
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') }]}>
|
||||
<Select placeholder={t('typePlaceholder')} value={item.type || ""} onChange={(v) => {
|
||||
update({ type: v });
|
||||
}}>
|
||||
<Select.Option value="oak">Oak</Select.Option>
|
||||
<Select.Option value="gitea">Gitea</Select.Option>
|
||||
<Select.Option value="github">GitHub</Select.Option>
|
||||
<Select.Option value="google">Google</Select.Option>
|
||||
<Select.Option value="facebook">Facebook</Select.Option>
|
||||
<Select.Option value="twitter">Twitter</Select.Option>
|
||||
<Select.Option value="linkedin">LinkedIn</Select.Option>
|
||||
<Select.Option value="custom">Custom</Select.Option>
|
||||
<Select.Option value="gitlab">GitLab</Select.Option>
|
||||
<Select.Option value="microsoft">Microsoft</Select.Option>
|
||||
<Select.Option value="apple">Apple</Select.Option>
|
||||
<Select.Option value="tencent">Tencent</Select.Option>
|
||||
<Select.Option value="weixin">Weixin</Select.Option>
|
||||
<Select.Option value="weibo">Weibo</Select.Option>
|
||||
<Select.Option value="dingtalk">DingTalk</Select.Option>
|
||||
</Select>
|
||||
</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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue