feat: oauth配置相关增删改查组件实现
This commit is contained in:
parent
91ff162908
commit
67fbd49c26
|
|
@ -0,0 +1,162 @@
|
|||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
import assert from "assert";
|
||||
|
||||
export default OakComponent({
|
||||
// Virtual Component
|
||||
isList: false,
|
||||
filters: [],
|
||||
properties: {
|
||||
},
|
||||
data: {
|
||||
clientInfo: null as EntityDict['oauthApplication']['Schema'] | null,
|
||||
loading: true,
|
||||
userInfo: null as EntityDict['token']['Schema']['user'] | 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,
|
||||
}).then((clientInfo) => {
|
||||
if (!clientInfo.result) {
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: 'oauth.authorize.error.invalid_client_id',
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
clientInfo: clientInfo.result as any,
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
console.error('Error loading OAuth client info:', err);
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: err.message || 'oauth.authorize.error.unknown',
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleGrant() {
|
||||
this.callAspectAuthorize("grant");
|
||||
},
|
||||
handleDeny() {
|
||||
this.callAspectAuthorize("deny");
|
||||
},
|
||||
callAspectAuthorize(action: "grant" | "deny") {
|
||||
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.href = redirectUri;
|
||||
|
||||
}).catch((err: Error) => {
|
||||
console.error('Error during OAuth authorization:', err);
|
||||
this.setState({
|
||||
hasError: true,
|
||||
errorMsg: err.message || 'oauth.authorize.error.unknown',
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"oauth": {
|
||||
"authorize": {
|
||||
"title": "授权确认",
|
||||
"loading": "正在加载...",
|
||||
"description": "第三方应用请求访问您的账户",
|
||||
"clientName": "应用名称",
|
||||
"clientDescription": "应用介绍",
|
||||
"scope": "授权范围",
|
||||
"allPermissions": "该应用将获得您账户的完整访问权限",
|
||||
"confirm": "同意授权",
|
||||
"deny": "拒绝",
|
||||
"error": {
|
||||
"title": "授权失败",
|
||||
"missing_response_type": "缺少 response_type 参数",
|
||||
"missing_client_id": "缺少 client_id 参数",
|
||||
"unknown": "未知错误,请稍后重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
background-color: #f0f2f5;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.loadingBox,
|
||||
.errorBox,
|
||||
.authorizeBox {
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.loadingBox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 20px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top: 3px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.errorBox {
|
||||
border-top: 3px solid #f5222d;
|
||||
}
|
||||
|
||||
.errorTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
line-height: 1.8;
|
||||
padding: 16px;
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.authorizeBox {
|
||||
border-top: 3px solid #faad14;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.appInfo {
|
||||
background: #fafafa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
background: #fafafa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 15px;
|
||||
color: #262626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.descValue {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.scopeSection {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fffbe6;
|
||||
padding: 8px 12px;
|
||||
border-radius: 2px;
|
||||
border-left: 4px solid #faad14;
|
||||
}
|
||||
|
||||
.scopeTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.scopeItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.scopeIcon {
|
||||
color: #faad14;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.denyButton,
|
||||
.grantButton {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.denyButton {
|
||||
background: #ff5e5e;
|
||||
color: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.denyButton:hover {
|
||||
color: #262626;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.denyButton:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.grantButton {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
.grantButton:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.grantButton:active {
|
||||
background: #096dd9;
|
||||
border-color: #096dd9;
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import { Avatar } from 'antd';
|
||||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
|
||||
const Authorize = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
keyof EntityDict,
|
||||
false,
|
||||
{
|
||||
// virtual
|
||||
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;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const {
|
||||
oakFullpath, loading, hasError, errorMsg, userInfo,
|
||||
response_type, client_id, redirect_uri, scope, state, clientInfo,
|
||||
name, nickname, mobile, avatarUrl
|
||||
} = props.data;
|
||||
const { t, handleGrant, handleDeny } = props.methods;
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<div className={Styles.loadingBox}>
|
||||
<div className={Styles.spinner}></div>
|
||||
<div className={Styles.loadingText}>{t('oauth.authorize.loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<div className={Styles.errorBox}>
|
||||
<div className={Styles.errorTitle}>{t('oauth.authorize.error.title')}</div>
|
||||
<div className={Styles.errorMessage}>{t(errorMsg)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Logged in - show authorization confirmation
|
||||
return (
|
||||
<div className={Styles.container}>
|
||||
<div className={Styles.authorizeBox}>
|
||||
<div className={Styles.title}>{t('oauth.authorize.title')}</div>
|
||||
<div className={Styles.description}>
|
||||
{t('oauth.authorize.description')}
|
||||
</div>
|
||||
|
||||
<div className={Styles.appInfo}>
|
||||
<div className={Styles.infoLabel}>{t('oauth.authorize.clientName')}:</div>
|
||||
<div className={Styles.infoValue}>{clientInfo?.name || client_id}</div>
|
||||
{clientInfo?.description && (
|
||||
<>
|
||||
<div className={Styles.infoLabel}>{t('oauth.authorize.clientDescription')}:</div>
|
||||
<div className={Styles.descValue}>{clientInfo.description}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={Styles.scopeSection}>
|
||||
<div className={Styles.scopeTitle}>{t('oauth.authorize.scope')}</div>
|
||||
<div className={Styles.scopeItem}>
|
||||
<span className={Styles.scopeIcon}>✓</span>
|
||||
<span>{t('oauth.authorize.allPermissions')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={Styles.userInfo}>
|
||||
{avatarUrl ? (
|
||||
<Avatar className={Styles.avatar} src={avatarUrl}></Avatar>
|
||||
) : (
|
||||
<Avatar className={Styles.avatar}>
|
||||
<span className={Styles.text}>
|
||||
{nickname?.[0]}
|
||||
</span>
|
||||
</Avatar>
|
||||
)}
|
||||
<div className={Styles.userDetails}>
|
||||
<div className={Styles.userName}>{name || nickname}</div>
|
||||
{mobile && <div className={Styles.userMobile}>{mobile}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={Styles.actions}>
|
||||
<button
|
||||
className={Styles.denyButton}
|
||||
onClick={handleDeny}
|
||||
>
|
||||
{t('oauth.authorize.deny')}
|
||||
</button>
|
||||
<button
|
||||
className={Styles.grantButton}
|
||||
onClick={handleGrant}
|
||||
>
|
||||
{t('oauth.authorize.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Authorize;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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: string, state: string) {
|
||||
this.features.token.loginByOAuth(code, state).then(() => {
|
||||
console.log('OAuth login successful');
|
||||
}).catch((err: Error) => {
|
||||
console.error('OAuth login failed:', err);
|
||||
this.setErrorMsg(err.message || 'OAuth login failed');
|
||||
}).finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
},
|
||||
setErrorMsg(message: string) {
|
||||
if (!message) {
|
||||
this.setState({ hasError: false, errorMessage: '' });
|
||||
return;
|
||||
}
|
||||
this.setState({ hasError: true, errorMessage: message });
|
||||
},
|
||||
retry() {
|
||||
this.features.navigator.redirectTo({
|
||||
url: '/login',
|
||||
});
|
||||
},
|
||||
returnToIndex() {
|
||||
this.features.navigator.redirectTo({
|
||||
url: '/',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"Invalid state parameter": "无效的状态参数",
|
||||
"oauth": {
|
||||
"loading": {
|
||||
"title": "授权中..."
|
||||
},
|
||||
"loadingMessage": "正在处理授权请求,请稍候",
|
||||
"error": {
|
||||
"title": "授权失败"
|
||||
},
|
||||
"success": {
|
||||
"title": "授权成功"
|
||||
},
|
||||
"successMessage": "授权已成功完成",
|
||||
"return": "返回首页",
|
||||
"confirm": "确认登录",
|
||||
"cancel": "取消",
|
||||
"close": "关闭窗口"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export default OakComponent({
|
||||
// Virtual Component
|
||||
isList: false,
|
||||
filters: [],
|
||||
properties: {
|
||||
systemId: '',
|
||||
systemName: '',
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"systemInfo": "系统信息",
|
||||
"applications": "OAuth应用",
|
||||
"providers": "OAuth供应商"
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import assert from "assert";
|
||||
|
||||
export default OakComponent({
|
||||
entity: 'oauthApplication',
|
||||
isList: true,
|
||||
projection: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
description: 1,
|
||||
redirectUris: 1,
|
||||
logo: 1,
|
||||
isConfidential: 1,
|
||||
scopes: 1,
|
||||
ableState: 1,
|
||||
},
|
||||
filters: [{
|
||||
filter() {
|
||||
const systemId = this.props.systemId;
|
||||
assert(systemId, 'systemId is required');
|
||||
return {
|
||||
systemId: systemId,
|
||||
}
|
||||
},
|
||||
}],
|
||||
formData({ data }) {
|
||||
return {
|
||||
list: data?.filter(item => (item.$$createAt$$ as number) > 1) || [],
|
||||
};
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
actions: ["remove", "update"]
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"oauthAppConfig": "OAuth应用程序配置",
|
||||
"confirm": {
|
||||
"deleteTitle": "确认删除",
|
||||
"deleteContent": "您确定要删除此OAuth应用程序配置吗?"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||||
|
||||
export default OakComponent({
|
||||
entity: 'oauthApplication',
|
||||
isList: false,
|
||||
projection: {
|
||||
name: 1,
|
||||
description: 1,
|
||||
redirectUris: 1,
|
||||
logo: 1, // string
|
||||
isConfidential: 1,
|
||||
scopes: 1, // string[]
|
||||
ableState: 1,
|
||||
},
|
||||
formData({ data, features }) {
|
||||
if (!data) {
|
||||
return { item: {}, clientSecret: ""};
|
||||
}
|
||||
const [client] = features.cache.get("oauthApplication", {
|
||||
data: {
|
||||
clientSecret: 1,
|
||||
},
|
||||
filter: {
|
||||
id: data.id,
|
||||
}
|
||||
})
|
||||
return {
|
||||
item: data,
|
||||
clientSecret: client?.clientSecret || "",
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
methods: {
|
||||
reGenerateClientSecret() {
|
||||
this.features.cache.operate("oauthApplication", {
|
||||
id: generateNewId(),
|
||||
action: "resetSecret",
|
||||
data: {},
|
||||
filter: {
|
||||
id: this.props.oakId,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入名称",
|
||||
"namePlaceholder": "请输入应用名称",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "请输入应用描述",
|
||||
"logo": "Logo",
|
||||
"logoPlaceholder": "请输入Logo URL",
|
||||
"redirectUris": "重定向URI列表",
|
||||
"redirectUrisRequired": "请输入重定向URI列表",
|
||||
"redirectUrisPlaceholder": "请输入重定向URI,每行一个",
|
||||
"scopes": "权限范围",
|
||||
"scopesPlaceholder": "请选择或输入权限范围",
|
||||
"clientSecret": "客户端密钥",
|
||||
"clientSecretPlaceholder": "自动生成的客户端密钥",
|
||||
"regenerate": "重新生成",
|
||||
"isConfidential": "机密客户端",
|
||||
"ableState": "启用状态",
|
||||
"noData": "无数据",
|
||||
"clientId": "客户端ID",
|
||||
"clientIdPlaceholder": "自动生成的客户端ID"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Form, Input, Switch, Button, Space, Upload, Select } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import { EntityDict } from '../../../../../oak-app-domain';
|
||||
|
||||
const Upsert = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
'oauthApplication',
|
||||
false,
|
||||
{
|
||||
item: RowWithActions<EntityDict, 'oauthApplication'>;
|
||||
clientSecret: string;
|
||||
},
|
||||
{
|
||||
reGenerateClientSecret: () => void;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { item, clientSecret } = props.data;
|
||||
const { t, update, reGenerateClientSecret } = props.methods;
|
||||
|
||||
if (item === undefined) {
|
||||
return <div>{t('noData')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Styles.id}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label={t('name')}
|
||||
rules={[{ required: true, message: t('nameRequired') }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('namePlaceholder')}
|
||||
value={item.name || ""}
|
||||
onChange={(v) => {
|
||||
update({ name: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('description')}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('descriptionPlaceholder')}
|
||||
value={item.description || ""}
|
||||
rows={4}
|
||||
onChange={(v) => {
|
||||
update({ description: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('logo')}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('logoPlaceholder')}
|
||||
value={item.logo || ""}
|
||||
onChange={(v) => {
|
||||
update({ logo: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('redirectUris')}
|
||||
rules={[{ required: true, message: t('redirectUrisRequired') }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('redirectUrisPlaceholder')}
|
||||
value={Array.isArray(item.redirectUris) ? item.redirectUris.join('\n') : ""}
|
||||
rows={3}
|
||||
onChange={(v) => {
|
||||
const uris = v.target.value.split('\n').filter(uri => uri.trim() !== '');
|
||||
update({ redirectUris: uris });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('scopes')}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={t('scopesPlaceholder')}
|
||||
value={item.scopes || []}
|
||||
onChange={(v) => {
|
||||
update({ scopes: v });
|
||||
}}
|
||||
tokenSeparators={[',']}
|
||||
open={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('clientId')}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder={t('clientIdPlaceholder')}
|
||||
value={item.id || "已隐藏"}
|
||||
disabled
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('clientSecret')}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder={t('clientSecretPlaceholder')}
|
||||
value={clientSecret || "已隐藏"}
|
||||
disabled
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={reGenerateClientSecret}
|
||||
>
|
||||
{t('regenerate')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('isConfidential')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
checked={!!item.isConfidential}
|
||||
onChange={(checked) => update({ isConfidential: checked })}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('ableState')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
checked={!!item.ableState}
|
||||
onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import { onActionFnDef, RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import PageHeader from 'oak-frontend-base/es/components/pageHeader';
|
||||
import { Button, Modal } from 'antd';
|
||||
import AppUpsert from "./upsert"
|
||||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
import ListPro from 'oak-frontend-base/es/components/listPro';
|
||||
|
||||
const OauthProvider = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
'oauthApplication',
|
||||
true,
|
||||
{
|
||||
list: RowWithActions<EntityDict, 'oauthApplication'>[];
|
||||
systemId: string;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
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<string | null>(null);
|
||||
|
||||
const handleAction: onActionFnDef = (row, action: string) => {
|
||||
switch (action) {
|
||||
case "update": {
|
||||
setUpsertId(row.id);
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
Modal.confirm({
|
||||
title: t('confirm.deleteTitle'),
|
||||
content: t('confirm.deleteContent'),
|
||||
onOk: () => {
|
||||
removeItem(row.id);
|
||||
execute();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{list && (
|
||||
<ListPro
|
||||
entity='oauthApplication'
|
||||
attributes={attrs}
|
||||
data={list}
|
||||
loading={oakLoading}
|
||||
oakPath={`${oakFullpath}`}
|
||||
onAction={handleAction}
|
||||
extraContent={
|
||||
<div className={Styles.actions}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setUpsertId(addItem({
|
||||
systemId: systemId,
|
||||
isConfidential: true,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{t('common::action.create')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</ListPro>
|
||||
)}
|
||||
{/* antd model */}
|
||||
<Modal open={!!upsertId} destroyOnClose={true} width={600} onCancel={() => {
|
||||
clean()
|
||||
setUpsertId(null);
|
||||
}} onOk={() => {
|
||||
execute()
|
||||
setUpsertId(null);
|
||||
}}>
|
||||
{upsertId && <AppUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId} />}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OauthProvider;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import assert from "assert";
|
||||
|
||||
export default OakComponent({
|
||||
entity: 'oauthProvider',
|
||||
isList: true,
|
||||
projection: {
|
||||
name: 1,
|
||||
type: 1,
|
||||
logo: 1,
|
||||
authorizationEndpoint: 1,
|
||||
tokenEndpoint: 1,
|
||||
userInfoEndpoint: 1,
|
||||
revokeEndpoint: 1,
|
||||
refreshEndpoint: 1,
|
||||
clientId: 1,
|
||||
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$$ as number) > 1) || [],
|
||||
};
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
actions: ["remove", "update"]
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"oauthProviderConfig": "OAuth提供商配置",
|
||||
"confirm": {
|
||||
"deleteTitle": "确认删除",
|
||||
"deleteContent": "您确定要删除此OAuth提供商配置吗?"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
export default OakComponent({
|
||||
entity: 'oauthProvider',
|
||||
isList: false,
|
||||
projection: {
|
||||
name: 1,
|
||||
type: 1,
|
||||
logo: 1,
|
||||
authorizationEndpoint: 1,
|
||||
tokenEndpoint: 1,
|
||||
userInfoEndpoint: 1,
|
||||
revokeEndpoint: 1,
|
||||
refreshEndpoint: 1,
|
||||
clientId: 1,
|
||||
clientSecret: 1,
|
||||
redirectUri: 1,
|
||||
autoRegister: 1,
|
||||
ableState: 1,
|
||||
},
|
||||
formData({ data }) {
|
||||
return {
|
||||
item: data,
|
||||
};
|
||||
},
|
||||
properties: {},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入名称",
|
||||
"namePlaceholder": "请输入OAuth提供商名称",
|
||||
"type": "类型",
|
||||
"typeRequired": "请输入类型",
|
||||
"typePlaceholder": "请输入OAuth类型",
|
||||
"logo": "Logo",
|
||||
"logoPlaceholder": "请输入Logo URL",
|
||||
"authorizationEndpoint": "授权端点",
|
||||
"authorizationEndpointRequired": "请输入授权端点",
|
||||
"authorizationEndpointPlaceholder": "请输入授权端点URL",
|
||||
"tokenEndpoint": "令牌端点",
|
||||
"tokenEndpointRequired": "请输入令牌端点",
|
||||
"tokenEndpointPlaceholder": "请输入令牌端点URL",
|
||||
"userInfoEndpoint": "用户信息端点",
|
||||
"userInfoEndpointPlaceholder": "请输入用户信息端点URL",
|
||||
"revokeEndpoint": "撤销端点",
|
||||
"revokeEndpointPlaceholder": "请输入撤销端点URL",
|
||||
"clientId": "客户端ID",
|
||||
"clientIdRequired": "请输入客户端ID",
|
||||
"clientIdPlaceholder": "请输入客户端ID",
|
||||
"clientSecret": "客户端密钥",
|
||||
"clientSecretRequired": "请输入客户端密钥",
|
||||
"clientSecretPlaceholder": "请输入客户端密钥",
|
||||
"redirectUri": "重定向URI",
|
||||
"redirectUriRequired": "请输入重定向URI",
|
||||
"redirectUriPlaceholder": "请输入重定向URI",
|
||||
"autoRegister": "自动注册",
|
||||
"ableState": "启用状态",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"noData": "无数据",
|
||||
"scopes": "权限范围",
|
||||
"scopesPlaceholder": "请选择或输入权限范围",
|
||||
"refreshEndpoint": "刷新端点",
|
||||
"refreshEndpointPlaceholder": "请输入刷新端点URL"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Form, Input, Switch, Button, Space, Upload, Select } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import { EntityDict } from '../../../../../oak-app-domain';
|
||||
|
||||
const Upsert = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
'oauthProvider',
|
||||
false,
|
||||
{
|
||||
item: RowWithActions<EntityDict, 'oauthProvider'>;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
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 as "oak" | "gitea" | "github" | "google" | "facebook" | "twitter" | "linkedin" | "custom" | "gitlab" | "microsoft" | "apple" | "tencent" | "weixin" | "weibo" | "dingtalk" | null | undefined });
|
||||
}}
|
||||
>
|
||||
<Select.Option value="oak">Oak</Select.Option>
|
||||
<Select.Option value="gitea">Gitea</Select.Option>
|
||||
<Select.Option value="github">GitHub</Select.Option>
|
||||
<Select.Option value="google">Google</Select.Option>
|
||||
<Select.Option value="facebook">Facebook</Select.Option>
|
||||
<Select.Option value="twitter">Twitter</Select.Option>
|
||||
<Select.Option value="linkedin">LinkedIn</Select.Option>
|
||||
<Select.Option value="custom">Custom</Select.Option>
|
||||
<Select.Option value="gitlab">GitLab</Select.Option>
|
||||
<Select.Option value="microsoft">Microsoft</Select.Option>
|
||||
<Select.Option value="apple">Apple</Select.Option>
|
||||
<Select.Option value="tencent">Tencent</Select.Option>
|
||||
<Select.Option value="weixin">Weixin</Select.Option>
|
||||
<Select.Option value="weibo">Weibo</Select.Option>
|
||||
<Select.Option value="dingtalk">DingTalk</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('logo')}
|
||||
>
|
||||
<Input placeholder={t('logoPlaceholder')} value={item.logo || ""}
|
||||
onChange={(v) => {
|
||||
update({ logo: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('authorizationEndpoint')}
|
||||
rules={[{ required: true, message: t('authorizationEndpointRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""}
|
||||
onChange={(v) => {
|
||||
update({ authorizationEndpoint: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('tokenEndpoint')}
|
||||
rules={[{ required: true, message: t('tokenEndpointRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""}
|
||||
onChange={(v) => {
|
||||
update({ tokenEndpoint: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('refreshEndpoint')}
|
||||
>
|
||||
<Input placeholder={t('refreshEndpointPlaceholder')} value={item.refreshEndpoint || ""}
|
||||
onChange={(v) => {
|
||||
update({ refreshEndpoint: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('userInfoEndpoint')}
|
||||
>
|
||||
<Input placeholder={t('userInfoEndpointPlaceholder')} value={item.userInfoEndpoint || ""}
|
||||
onChange={(v) => {
|
||||
update({ userInfoEndpoint: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('revokeEndpoint')}
|
||||
>
|
||||
<Input placeholder={t('revokeEndpointPlaceholder')} value={item.revokeEndpoint || ""}
|
||||
onChange={(v) => {
|
||||
update({ revokeEndpoint: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('clientId')}
|
||||
rules={[{ required: true, message: t('clientIdRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""}
|
||||
onChange={(v) => {
|
||||
update({ clientId: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('clientSecret')}
|
||||
rules={[{ required: true, message: t('clientSecretRequired') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""}
|
||||
onChange={(v) => {
|
||||
update({ clientSecret: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('scopes')}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={t('scopesPlaceholder')}
|
||||
value={item.scopes || []}
|
||||
onChange={(v) => {
|
||||
update({ scopes: v });
|
||||
}}
|
||||
tokenSeparators={[',']}
|
||||
open={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('redirectUri')}
|
||||
rules={[{ required: true, message: t('redirectUriRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""}
|
||||
onChange={(v) => {
|
||||
update({ redirectUri: v.target.value });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('autoRegister')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checked={!!item.autoRegister} onChange={(checked) => update({ autoRegister: checked })} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('ableState')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })} />
|
||||
</Form.Item>
|
||||
|
||||
{/* <Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item> */}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Upsert;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import React from 'react';
|
||||
import { onActionFnDef, RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import PageHeader from 'oak-frontend-base/es/components/pageHeader';
|
||||
import { Button, Modal } from 'antd';
|
||||
import ProviderUpsert from "./upsert"
|
||||
import { EntityDict } from '../../../../oak-app-domain';
|
||||
import ListPro from 'oak-frontend-base/es/components/listPro';
|
||||
|
||||
const OauthProvider = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
'oauthProvider',
|
||||
true,
|
||||
{
|
||||
list: RowWithActions<EntityDict, 'oauthProvider'>[];
|
||||
systemId: string;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
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<string | null>(null);
|
||||
|
||||
const handleAction: onActionFnDef = (row, action: string) => {
|
||||
switch (action) {
|
||||
case "update": {
|
||||
setUpsertId(row.id);
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
Modal.confirm({
|
||||
title: t('confirm.deleteTitle'),
|
||||
content: t('confirm.deleteContent'),
|
||||
onOk: () => {
|
||||
removeItem(row.id);
|
||||
execute();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{list && (
|
||||
<ListPro
|
||||
entity='oauthProvider'
|
||||
attributes={attrs}
|
||||
data={list}
|
||||
loading={oakLoading}
|
||||
oakPath={`${oakFullpath}`}
|
||||
onAction={handleAction}
|
||||
extraContent={
|
||||
<div className={Styles.actions}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setUpsertId(addItem({
|
||||
systemId: systemId,
|
||||
autoRegister: true,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{t('common::action.create')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</ListPro>
|
||||
)}
|
||||
{/* antd model */}
|
||||
<Modal open={!!upsertId} destroyOnClose={true} width={600} onCancel={() => {
|
||||
clean()
|
||||
setUpsertId(null);
|
||||
}} onOk={() => {
|
||||
execute()
|
||||
setUpsertId(null);
|
||||
}}>
|
||||
{upsertId && <ProviderUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId} />}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OauthProvider;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import OauthApps from './oauthApps';
|
||||
import OauthProvider from './oauthProvider';
|
||||
import { Tabs } from 'antd';
|
||||
|
||||
const Management = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
keyof EntityDict,
|
||||
false,
|
||||
{
|
||||
// virtual
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { oakFullpath, oakDirty } = props.data;
|
||||
const { t, execute } = props.methods;
|
||||
|
||||
return <Tabs
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: t('providers'),
|
||||
children: (
|
||||
<OauthProvider
|
||||
systemId={props.data.systemId}
|
||||
oakPath={`${oakFullpath}.oauthProviders:list`}
|
||||
></OauthProvider>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('applications'),
|
||||
children: (
|
||||
<OauthApps
|
||||
systemId={props.data.systemId}
|
||||
oakPath={`${oakFullpath}.oauthApplications:list`}
|
||||
></OauthApps>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
};
|
||||
|
||||
export default Management;
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
// OAuth 回调页面样式
|
||||
.oauthContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.oauthCard {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 48px 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.successIcon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #52c41a;
|
||||
animation: scaleIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #ff4d4f;
|
||||
animation: scaleIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.successButton {
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #73d13d;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingIcon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #1890ff;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 16px 0;
|
||||
animation: fadeIn 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.successMessage {
|
||||
font-size: 16px;
|
||||
color: #52c41a;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.8s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: 16px;
|
||||
color: #ff4d4f;
|
||||
margin: 0 0 24px 0;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.8s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
animation: fadeIn 0.8s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top: 24px;
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.8s ease-out 0.5s both;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.errorButton {
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #ff7875;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.8s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
.confirmButton {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
flex: 1;
|
||||
max-width: 140px;
|
||||
|
||||
&:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
background-color: #fff;
|
||||
color: #595959;
|
||||
border: 1px solid #d9d9d9;
|
||||
flex: 1;
|
||||
max-width: 140px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 640px) {
|
||||
.oauthCard {
|
||||
padding: 36px 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.successIcon,
|
||||
.errorIcon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.successMessage,
|
||||
.errorMessage {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 保留原有的样式以防其他地方使用
|
||||
.id {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import Styles from './styles.module.less';
|
||||
import { EntityDict } from '../../oak-app-domain';
|
||||
|
||||
const Oauth = (
|
||||
props: WebComponentProps<
|
||||
EntityDict,
|
||||
keyof EntityDict,
|
||||
false,
|
||||
{
|
||||
// virtual
|
||||
hasError: boolean;
|
||||
errorMessage: string;
|
||||
loading: boolean;
|
||||
},
|
||||
{
|
||||
retry: () => void;
|
||||
returnToIndex: () => void;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
const { loading, hasError, errorMessage } = props.data;
|
||||
const { t, retry, returnToIndex } = props.methods;
|
||||
const tErrMsg = t(errorMessage);
|
||||
|
||||
return (
|
||||
<div className={Styles.oauthContainer}>
|
||||
<div className={Styles.oauthCard}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className={Styles.iconWrapper}>
|
||||
<svg className={Styles.loadingIcon} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeOpacity="0.2" />
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className={Styles.title}>{t('oauth.loading.title')}</h2>
|
||||
<p className={Styles.description}>
|
||||
{t('oauth.loadingMessage')}
|
||||
</p>
|
||||
</>
|
||||
) : hasError ? (
|
||||
<>
|
||||
<div className={Styles.iconWrapper}>
|
||||
<svg className={Styles.errorIcon} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M15 9L9 15M9 9L15 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className={Styles.title}>{t('oauth.error.title')}</h2>
|
||||
<p className={Styles.errorMessage}>{tErrMsg}</p>
|
||||
<button
|
||||
className={`${Styles.button} ${Styles.errorButton}`}
|
||||
onClick={() => retry()}
|
||||
>
|
||||
{t('oauth.close')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={Styles.iconWrapper}>
|
||||
<svg className={Styles.successIcon} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M8 12L11 15L16 9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className={Styles.title}>{t('oauth.success.title')}</h2>
|
||||
<p className={Styles.successMessage}>{t('oauth.successMessage')}</p>
|
||||
<button
|
||||
className={`${Styles.button} ${Styles.successButton}`}
|
||||
onClick={() => returnToIndex()}
|
||||
>
|
||||
{t('oauth.return')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Oauth;
|
||||
|
|
@ -5,5 +5,6 @@
|
|||
"style": "样式管理",
|
||||
"application-list": "应用管理",
|
||||
"smsTemplate-list": "短信模板管理",
|
||||
"login": "登录管理"
|
||||
"login": "登录管理",
|
||||
"oauth": "OAuth管理"
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { EntityDict } from '../../../oak-app-domain';
|
|||
import { Config } from '../../../types/Config';
|
||||
import { Style } from '../../../types/Style';
|
||||
import Styles from './web.pc.module.less';
|
||||
import OAuthManagement from '../../oauth/management';
|
||||
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'system', false, {
|
||||
id: string;
|
||||
|
|
@ -135,6 +136,22 @@ export default function Render(props: WebComponentProps<EntityDict, 'system', fa
|
|||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className={Styles.tabLabel}>
|
||||
{t('oauth')}
|
||||
</div>
|
||||
),
|
||||
key: 'oauth-manage',
|
||||
destroyInactiveTabPane: true,
|
||||
children: (
|
||||
<OAuthManagement
|
||||
oakPath={`$system-oauth-${id}`}
|
||||
systemId={id}
|
||||
systemName={name}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue