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": "样式管理",
|
"style": "样式管理",
|
||||||
"application-list": "应用管理",
|
"application-list": "应用管理",
|
||||||
"smsTemplate-list": "短信模板管理",
|
"smsTemplate-list": "短信模板管理",
|
||||||
"login": "登录管理"
|
"login": "登录管理",
|
||||||
|
"oauth": "OAuth管理"
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import { EntityDict } from '../../../oak-app-domain';
|
||||||
import { Config } from '../../../types/Config';
|
import { Config } from '../../../types/Config';
|
||||||
import { Style } from '../../../types/Style';
|
import { Style } from '../../../types/Style';
|
||||||
import Styles from './web.pc.module.less';
|
import Styles from './web.pc.module.less';
|
||||||
|
import OAuthManagement from '../../oauth/management';
|
||||||
|
|
||||||
export default function Render(props: WebComponentProps<EntityDict, 'system', false, {
|
export default function Render(props: WebComponentProps<EntityDict, 'system', false, {
|
||||||
id: string;
|
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>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue