Compare commits

...

25 Commits

Author SHA1 Message Date
Pan Qiancheng 4c317906ca feat: 支持了application, system,platform的defaultOrigin配置 2025-10-24 15:35:48 +08:00
Pan Qiancheng 70747be388 fix: 因编译器暂不支持嵌套引用类型,故重新在entity内声明CosOrigin(以后适配) 2025-10-24 14:20:07 +08:00
Pan Qiancheng db522727ec fix: 赋值 2025-10-24 14:10:03 +08:00
Pan Qiancheng a9ab8377d2 fix: 优化了查询逻辑,如果提供origin则不需要再去查询数据库 2025-10-24 14:09:07 +08:00
Pan Qiancheng aef6942a57 feat: 在application, system, platform中新增了defaultOrigin的配置,并在extrafile的create-before trigger中获取配置并应用 2025-10-24 14:04:34 +08:00
Pan Qiancheng 71b25a2a54 feat: 新增我的授权历史组件 2025-10-24 11:44:21 +08:00
Pan Qiancheng 5855b5f808 feat: 修复oauthUserAuth中的revoke逻辑,在撤销时还需要将unused的所用的code都标记为已使用 2025-10-24 11:04:20 +08:00
Pan Qiancheng fb8fae0525 feat: 修复revoke端口相关逻辑 2025-10-24 10:49:33 +08:00
Pan Qiancheng 841faface2 feat: build 2025-10-23 22:25:45 +08:00
Pan Qiancheng 0dc7dd4d98 feat: 如果是在某一应用下反复授权则直接允许,并修改实体添加了更多认真状态 2025-10-23 22:25:38 +08:00
Pan Qiancheng 9a05b713ab feat: 实现派发方的revokr逻辑 2025-10-23 17:54:55 +08:00
Pan Qiancheng 6f5dead6cc fix: 获取token之后要回填到userAuth记录中 2025-10-23 17:36:03 +08:00
Pan Qiancheng d7b34fb3f6 feat: 复制几个固定页面到模板中 2025-10-23 16:21:17 +08:00
Pan Qiancheng fb9489a2f5 feat: build 2025-10-23 15:31:31 +08:00
Pan Qiancheng 35c6b01f65 feat: oauth相关aspect实现 && 给其他aspect定义加上了注释 2025-10-23 15:31:19 +08:00
Pan Qiancheng a3a4837974 feat: oauth配置相关增删改查组件实现 2025-10-23 15:30:54 +08:00
Pan Qiancheng 18c8d3b670 feat: 定期刷新要过期的oauth授权用户 2025-10-23 15:30:13 +08:00
Pan Qiancheng 1a063c86f5 feat: 新增oauth用户信息注册点工具包 2025-10-23 15:29:44 +08:00
Pan Qiancheng 82073c86e0 feat: oauth相关trigger实现 2025-10-23 15:29:18 +08:00
Pan Qiancheng dc3dadc824 feat: 更新oauth相关实体定义 2025-10-23 15:28:53 +08:00
Pan Qiancheng 011a56c830 feat: 获取OAuth Token、获取OAuth用户信息、刷新OAuth令牌
endpoint实现
2025-10-23 15:28:28 +08:00
Pan Qiancheng 195bbc0c24 feat: 修改了oauth服务相关的实体定义 2025-10-22 18:17:28 +08:00
Pan Qiancheng e06198eb78 feat: 在feature/token中新增loginByOAuth方法,build 2025-10-22 09:14:24 +08:00
Pan Qiancheng e0e906bd60 feat: 导出所需函数供项目中使用 2025-10-22 09:13:59 +08:00
Pan Qiancheng 0b54e3f208 feat: 新增oauth相关的实体定义及类型定义 2025-10-22 09:13:14 +08:00
656 changed files with 21546 additions and 474 deletions

View File

@ -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 signaturenoncestrtimestampappId
*/
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;

View File

@ -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';

View File

@ -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;

30
es/aspects/oauth.d.ts vendored Normal file
View File

@ -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;
}>;

420
es/aspects/oauth.js Normal file
View File

@ -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,
};
};

View File

@ -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,

View File

@ -19,6 +19,6 @@ export declare function syncMessageTemplate<ED extends EntityDict>(params: {
example: string;
keywordEnumValueList: {
keywordCode: string;
enumValueList: string[];
enumValueList: Array<string>;
}[];
}[]>;

11
es/aspects/token.d.ts vendored
View File

@ -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;

View File

@ -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;

View File

@ -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,
};
// 直接创建

View File

@ -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;

View File

@ -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;

View File

@ -1,4 +1,3 @@
/// <reference types="@uiw/react-amap-types" />
import React from 'react';
export type PositionProps = {
loadUI: boolean;

View File

@ -1,4 +1,3 @@
/// <reference types="@uiw/react-amap-types" />
import React from 'react';
import { ModalProps } from 'antd';
import { GeolocationProps } from '@uiw/react-amap';

View File

@ -1,4 +1,3 @@
/// <reference types="@uiw/react-amap-types" />
import React from 'react';
import { MapProps, APILoaderConfig } from '@uiw/react-amap';
import './index.less';

View File

@ -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;

View File

@ -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,
});
},
},
});

View File

@ -0,0 +1,8 @@
{
"qiniu": "七牛云",
"ctyun": "天翼云",
"aliyun": "阿里云",
"tencent": "腾讯云",
"local": "本地存储",
"s3": "S3存储"
}

View File

@ -0,0 +1,5 @@
.contains {
margin-top: 20px;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -6,5 +6,6 @@
"menu": "菜单管理",
"autoReply": "被关注回复管理",
"tag": "标签管理",
"user": "用户管理"
"user": "用户管理",
"cos": "COS配置"
}

View File

@ -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({

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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: {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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}>
每种均可配置一个相应的服务所使用的帐号请准确对应

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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[];

View File

@ -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中且文件大小超过此值的文件将转换为 JPEGInfinity表示禁用该功能
},
features: ['extraFile'],

View File

@ -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;

View File

@ -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: '',

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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',
});
});
}
},
});

View File

@ -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": "未知错误,请稍后重试"
}
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

2
es/components/oauth/index.d.ts vendored Normal file
View File

@ -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;

View File

@ -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: '/',
});
}
}
});

View File

@ -0,0 +1,20 @@
{
"Invalid state parameter": "无效的状态参数",
"oauth": {
"loading": {
"title": "授权中..."
},
"loadingMessage": "正在处理授权请求,请稍候",
"error": {
"title": "授权失败"
},
"success": {
"title": "授权成功"
},
"successMessage": "授权已成功完成",
"return": "返回首页",
"confirm": "确认登录",
"cancel": "取消",
"close": "关闭窗口"
}
}

View File

@ -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;

View File

@ -0,0 +1,9 @@
export default OakComponent({
// Virtual Component
isList: false,
filters: [],
properties: {
systemId: '',
systemName: '',
}
});

View File

@ -0,0 +1,5 @@
{
"systemInfo": "系统信息",
"applications": "OAuth应用",
"providers": "OAuth供应商"
}

View File

@ -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;

View File

@ -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"]
});

View File

@ -0,0 +1,7 @@
{
"oauthAppConfig": "OAuth应用程序配置",
"confirm": {
"deleteTitle": "确认删除",
"deleteContent": "您确定要删除此OAuth应用程序配置吗"
}
}

View File

@ -0,0 +1,7 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}

View File

@ -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;

View File

@ -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,
}
});
}
}
});

View File

@ -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"
}

View File

@ -0,0 +1,7 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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"]
});

View File

@ -0,0 +1,7 @@
{
"oauthProviderConfig": "OAuth提供商配置",
"confirm": {
"deleteTitle": "确认删除",
"deleteContent": "您确定要删除此OAuth提供商配置吗"
}
}

View File

@ -0,0 +1,12 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}
.actions {
display: flex;
gap: 8px;
}

View File

@ -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;

View File

@ -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() {
},
}
});

View File

@ -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"
}

View File

@ -0,0 +1,7 @@
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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