fix: 更新oauth相关数据的组件优化,允许未提供refreshEndpoint

This commit is contained in:
Pan Qiancheng 2025-12-24 17:27:14 +08:00
parent a3ec5fc808
commit 1e5697a28e
26 changed files with 486 additions and 182 deletions

View File

@ -1,2 +1,6 @@
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../../oak-app-domain").EntityDict, "oauthApplication", false, {}>) => React.ReactElement;
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../../oak-app-domain").EntityDict, "oauthApplication", false, {
open: boolean;
onCancel: () => void;
onOk: () => void;
}>) => React.ReactElement;
export default _default;

View File

@ -6,9 +6,9 @@ export default OakComponent({
name: 1,
description: 1,
redirectUris: 1,
logo: 1,
logo: 1, // string
isConfidential: 1,
scopes: 1,
scopes: 1, // string[]
ableState: 1,
requirePKCE: 1,
},
@ -30,7 +30,11 @@ export default OakComponent({
isCreation: this.isCreation(),
};
},
properties: {},
properties: {
open: false,
onCancel: (() => { }),
onOk: (() => { }),
},
methods: {
reGenerateClientSecret() {
this.features.cache.operate("oauthApplication", {
@ -41,6 +45,15 @@ export default OakComponent({
id: this.props.oakId,
}
});
},
setHideModal(hide) {
this.setState({
hideModal: hide
});
}
},
data: {
hideModal: false,
isCreation: false,
}
});

View File

@ -1,4 +1,5 @@
{
"oauthApplication": "OAuth应用",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入应用名称",
@ -21,5 +22,8 @@
"clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。",
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看"
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看",
"create": "创建",
"update": "更新",
"cancel": "取消"
}

View File

@ -5,7 +5,12 @@ declare const Upsert: (props: WebComponentProps<EntityDict, "oauthApplication",
item: RowWithActions<EntityDict, "oauthApplication">;
clientSecret: string;
isCreation: boolean;
open: boolean;
onCancel: () => void;
onOk: () => void;
hideModal: boolean;
}, {
reGenerateClientSecret: () => void;
}>) => React.JSX.Element;
setHideModal: (hide: boolean) => void;
}>) => React.JSX.Element | null;
export default Upsert;

View File

@ -1,41 +1,75 @@
import React from 'react';
import { Form, Input, Switch, Button, Space, Select } from 'antd';
import React, { useEffect } from 'react';
import { Form, Input, Switch, Button, Space, Select, Modal } from 'antd';
import Styles from './styles.module.less';
const Upsert = (props) => {
const { item, clientSecret, isCreation } = props.data;
const { t, update, reGenerateClientSecret } = props.methods;
const { item, clientSecret, isCreation, open, onCancel, onOk, hideModal } = props.data;
const { t, update, reGenerateClientSecret, setHideModal } = props.methods;
const [form] = Form.useForm();
useEffect(() => {
if (item) {
form.setFieldsValue({
name: item.name,
description: item.description,
logo: item.logo,
redirectUris: Array.isArray(item.redirectUris) ? item.redirectUris.join('\n') : '',
scopes: item.scopes,
isConfidential: item.isConfidential,
ableState: item.ableState === 'enabled',
requirePKCE: item.requirePKCE,
});
}
}, [item, form]);
const handleOk = async () => {
try {
await form.validateFields();
setHideModal(true);
setTimeout(() => {
onOk();
}, 300);
}
catch (error) {
console.log('Validation failed:', error);
}
};
const handleCancel = () => {
setHideModal(true);
setTimeout(() => {
onCancel();
}, 300);
};
if (item === undefined) {
return <div>{t('noData')}</div>;
return null;
}
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) => {
return (<Modal open={open && !hideModal} destroyOnClose={true} width={600} onCancel={handleCancel} onOk={handleOk} title={t('oauthApplication')} okText={isCreation ? t('create') : t('update')} cancelText={t('cancel')}>
<div className={Styles.id}>
<Form form={form} layout="vertical" autoComplete="off">
<Form.Item label={t('name')} name="name" rules={[{ required: true, message: t('nameRequired') }]}>
<Input placeholder={t('namePlaceholder')} 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) => {
<Form.Item label={t('description')} name="description">
<Input.TextArea placeholder={t('descriptionPlaceholder')} 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) => {
<Form.Item label={t('logo')} name="logo">
<Input placeholder={t('logoPlaceholder')} 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) => {
<Form.Item label={t('redirectUris')} name="redirectUris" rules={[{ required: true, message: t('redirectUrisRequired') }]}>
<Input.TextArea placeholder={t('redirectUrisPlaceholder')} 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) => {
<Form.Item label={t('scopes')} name="scopes">
<Select mode="tags" placeholder={t('scopesPlaceholder')} onChange={(v) => {
update({ scopes: v });
}} tokenSeparators={[',']} open={false}/>
</Form.Item>
@ -56,19 +90,20 @@ const Upsert = (props) => {
</Space.Compact>
</Form.Item>
<Form.Item label={t('isConfidential')} valuePropName="checked">
<Switch checked={!!item.isConfidential} onChange={(checked) => update({ isConfidential: checked })}/>
<Form.Item label={t('isConfidential')} name="isConfidential" valuePropName="checked">
<Switch 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 label={t('ableState')} name="ableState" valuePropName="checked">
<Switch onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}/>
</Form.Item>
{/* requirePKCE */}
<Form.Item label={t('requirePKCE')} valuePropName="checked" tooltip={t('requirePKCETooltip')}>
<Switch checked={!!item.requirePKCE} onChange={(checked) => update({ requirePKCE: checked })}/>
<Form.Item label={t('requirePKCE')} name="requirePKCE" valuePropName="checked" tooltip={t('requirePKCETooltip')}>
<Switch onChange={(checked) => update({ requirePKCE: checked })}/>
</Form.Item>
</Form>
</div>);
</div>
</Modal>);
};
export default Upsert;

View File

@ -43,16 +43,13 @@ const OauthProvider = (props) => {
{t('common::action.create')}
</Button>
</div>}/>)}
{/* 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>
{upsertId && (<AppUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId} open={!!upsertId} onCancel={() => {
clean();
setUpsertId(null);
}} onOk={() => {
execute();
setUpsertId(null);
}}/>)}
</>);
};
export default OauthProvider;

View File

@ -1,2 +1,6 @@
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../../oak-app-domain").EntityDict, "oauthProvider", false, {}>) => React.ReactElement;
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../../../oak-app-domain").EntityDict, "oauthProvider", false, {
open: boolean;
onCancel: () => void;
onOk: () => void;
}>) => React.ReactElement;
export default _default;

View File

@ -20,11 +20,26 @@ export default OakComponent({
formData({ data }) {
return {
item: data,
isCreation: this.isCreation(),
};
},
properties: {},
properties: {
open: false,
onCancel: (() => { }),
onOk: (() => { }),
},
lifetimes: {
ready() {
},
},
methods: {
setHideModal(hide) {
this.setState({
hideModal: hide
});
}
},
data: {
hideModal: false
}
});

View File

@ -1,4 +1,5 @@
{
"oauthProvider": "OAuth提供商",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入OAuth提供商名称",
@ -34,5 +35,7 @@
"scopes": "权限范围",
"scopesPlaceholder": "请选择或输入权限范围",
"refreshEndpoint": "刷新端点",
"refreshEndpointPlaceholder": "请输入刷新端点URL"
"refreshEndpointPlaceholder": "请输入刷新端点URL",
"create": "创建",
"update": "更新"
}

View File

@ -3,5 +3,12 @@ import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../../../oak-app-domain';
declare const Upsert: (props: WebComponentProps<EntityDict, "oauthProvider", false, {
item: RowWithActions<EntityDict, "oauthProvider">;
}>) => React.JSX.Element;
open: boolean;
onCancel: () => void;
onOk: () => void;
hideModal: boolean;
isCreation: boolean;
}, {
setHideModal: (hide: boolean) => void;
}>) => React.JSX.Element | null;
export default Upsert;

View File

@ -1,26 +1,65 @@
import React from 'react';
import { Form, Input, Switch, Select, Typography } from 'antd';
import React, { useEffect } from 'react';
import { Form, Input, Switch, Select, Typography, Modal } from 'antd';
import Styles from './styles.module.less';
const { Text } = Typography;
const Upsert = (props) => {
const { item } = props.data;
const { t, update } = props.methods;
const { item, open, onCancel, onOk, hideModal, isCreation } = props.data;
const { t, update, setHideModal } = props.methods;
const [form] = Form.useForm();
useEffect(() => {
if (item) {
form.setFieldsValue({
name: item.name,
type: item.type ? [item.type] : [],
logo: item.logo,
authorizationEndpoint: item.authorizationEndpoint,
tokenEndpoint: item.tokenEndpoint,
refreshEndpoint: item.refreshEndpoint,
userInfoEndpoint: item.userInfoEndpoint,
revokeEndpoint: item.revokeEndpoint,
clientId: item.clientId,
clientSecret: item.clientSecret,
scopes: item.scopes,
redirectUri: item.redirectUri,
autoRegister: item.autoRegister,
ableState: item.ableState === 'enabled',
});
}
}, [item, form]);
const handleOk = async () => {
try {
await form.validateFields();
setHideModal(true);
setTimeout(() => {
onOk();
}, 300);
}
catch (error) {
console.log('Validation failed:', error);
}
};
const handleCancel = () => {
setHideModal(true);
setTimeout(() => {
onCancel();
}, 300);
};
if (item === undefined) {
return <div>{t('noData')}</div>;
return null;
}
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) => {
return (<Modal open={open && !hideModal} destroyOnClose={true} width={600} onCancel={handleCancel} onOk={handleOk} title={t('oauthProvider')} okText={isCreation ? t('create') : t('update')} cancelText={t('cancel')}>
<div className={Styles.id}>
<Form form={form} layout="vertical" autoComplete="off">
<Form.Item label={t('name')} name="name" rules={[{ required: true, message: t('nameRequired') }]}>
<Input placeholder={t('namePlaceholder')} onChange={(v) => {
update({ name: v.target.value });
}}/>
</Form.Item>
<Form.Item label={t('type')} rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning">
<Form.Item label={t('type')} name="type" rules={[{ required: true, message: t('typeRequired') }]} extra={item.type && item.type !== 'oak' && item.type !== 'gitea' ? (<Text type="warning">
{item.type}不是预设类型请自行注入 handler
</Text>) : undefined}>
<Select mode="tags" placeholder={t('typePlaceholder')} value={item.type ? [item.type] : []} // 保持数组形式
onChange={(v) => {
<Select mode="tags" placeholder={t('typePlaceholder')} onChange={(v) => {
// 只取最后一个输入或选择的值
const last = v.slice(-1)[0];
update({ type: last });
@ -31,85 +70,76 @@ const Upsert = (props) => {
]}/>
</Form.Item>
<Form.Item label={t('logo')}>
<Input placeholder={t('logoPlaceholder')} value={item.logo || ""} onChange={(v) => {
<Form.Item label={t('logo')} name="logo">
<Input placeholder={t('logoPlaceholder')} 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) => {
<Form.Item label={t('authorizationEndpoint')} name="authorizationEndpoint" rules={[{ required: true, message: t('authorizationEndpointRequired') }]}>
<Input placeholder={t('authorizationEndpointPlaceholder')} 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) => {
<Form.Item label={t('tokenEndpoint')} name="tokenEndpoint" rules={[{ required: true, message: t('tokenEndpointRequired') }]}>
<Input placeholder={t('tokenEndpointPlaceholder')} onChange={(v) => {
update({ tokenEndpoint: v.target.value });
}}/>
</Form.Item>
<Form.Item label={t('refreshEndpoint')}>
<Input placeholder={t('refreshEndpointPlaceholder')} value={item.refreshEndpoint || ""} onChange={(v) => {
<Form.Item label={t('refreshEndpoint')} name="refreshEndpoint">
<Input placeholder={t('refreshEndpointPlaceholder')} onChange={(v) => {
update({ refreshEndpoint: v.target.value });
}}/>
</Form.Item>
<Form.Item label={t('userInfoEndpoint')}>
<Input placeholder={t('userInfoEndpointPlaceholder')} value={item.userInfoEndpoint || ""} onChange={(v) => {
<Form.Item label={t('userInfoEndpoint')} name="userInfoEndpoint">
<Input placeholder={t('userInfoEndpointPlaceholder')} onChange={(v) => {
update({ userInfoEndpoint: v.target.value });
}}/>
</Form.Item>
<Form.Item label={t('revokeEndpoint')}>
<Input placeholder={t('revokeEndpointPlaceholder')} value={item.revokeEndpoint || ""} onChange={(v) => {
<Form.Item label={t('revokeEndpoint')} name="revokeEndpoint">
<Input placeholder={t('revokeEndpointPlaceholder')} 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) => {
<Form.Item label={t('clientId')} name="clientId" rules={[{ required: true, message: t('clientIdRequired') }]}>
<Input placeholder={t('clientIdPlaceholder')} 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) => {
<Form.Item label={t('clientSecret')} name="clientSecret" rules={[{ required: true, message: t('clientSecretRequired') }]}>
<Input.Password placeholder={t('clientSecretPlaceholder')} 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) => {
<Form.Item label={t('scopes')} name="scopes">
<Select mode="tags" placeholder={t('scopesPlaceholder')} 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) => {
<Form.Item label={t('redirectUri')} name="redirectUri" rules={[{ required: true, message: t('redirectUriRequired') }]}>
<Input placeholder={t('redirectUriPlaceholder')} 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 label={t('autoRegister')} name="autoRegister" valuePropName="checked">
<Switch 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 label={t('ableState')} name="ableState" valuePropName="checked">
<Switch 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>);
</div>
</Modal>);
};
export default Upsert;

View File

@ -44,16 +44,13 @@ const OauthProvider = (props) => {
{t('common::action.create')}
</Button>
</div>}/>)}
{/* 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>
{upsertId && (<ProviderUpsert oakPath={`${oakFullpath}.${upsertId}`} oakId={upsertId} open={!!upsertId} onCancel={() => {
clean();
setUpsertId(null);
}} onOk={() => {
execute();
setUpsertId(null);
}}/>)}
</>);
};
export default OauthProvider;

View File

@ -380,6 +380,7 @@ const i18ns = [
module: "oak-general-business",
position: "src/components/oauth/management/oauthApps/upsert",
data: {
"oauthApplication": "OAuth应用",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入应用名称",
@ -402,7 +403,10 @@ const i18ns = [
"clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。",
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看"
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看",
"create": "创建",
"update": "更新",
"cancel": "取消"
}
},
{
@ -426,6 +430,7 @@ const i18ns = [
module: "oak-general-business",
position: "src/components/oauth/management/oauthProvider/upsert",
data: {
"oauthProvider": "OAuth提供商",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入OAuth提供商名称",
@ -461,7 +466,9 @@ const i18ns = [
"scopes": "权限范围",
"scopesPlaceholder": "请选择或输入权限范围",
"refreshEndpoint": "刷新端点",
"refreshEndpointPlaceholder": "请输入刷新端点URL"
"refreshEndpointPlaceholder": "请输入刷新端点URL",
"create": "创建",
"update": "更新"
}
},
{

View File

@ -143,7 +143,10 @@ const triggers = [
assert(oauthUser.state, `oauthUser ${oauthUser.id} 关联的 state 不存在`);
assert(oauthUser.state.provider, `oauthUser ${oauthUser.id} 关联的 state 的 provider 不存在`);
const refreshEndpoint = oauthUser.state.provider.refreshEndpoint;
assert(refreshEndpoint, `oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌`);
if (!refreshEndpoint) {
console.warn(`oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌,跳过`);
continue;
}
// 根据 RFC 6749 规范 使用 refresh token 刷新 access token
const authHeaderRaw = Buffer.from(`${oauthUser.state.provider.clientId}:${oauthUser.state.provider.clientSecret}`).toString('base64');
const resp = await fetch(refreshEndpoint, {

View File

@ -382,6 +382,7 @@ const i18ns = [
module: "oak-general-business",
position: "src/components/oauth/management/oauthApps/upsert",
data: {
"oauthApplication": "OAuth应用",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入应用名称",
@ -404,7 +405,10 @@ const i18ns = [
"clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。",
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看"
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看",
"create": "创建",
"update": "更新",
"cancel": "取消"
}
},
{
@ -428,6 +432,7 @@ const i18ns = [
module: "oak-general-business",
position: "src/components/oauth/management/oauthProvider/upsert",
data: {
"oauthProvider": "OAuth提供商",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入OAuth提供商名称",
@ -463,7 +468,9 @@ const i18ns = [
"scopes": "权限范围",
"scopesPlaceholder": "请选择或输入权限范围",
"refreshEndpoint": "刷新端点",
"refreshEndpointPlaceholder": "请输入刷新端点URL"
"refreshEndpointPlaceholder": "请输入刷新端点URL",
"create": "创建",
"update": "更新"
}
},
{

View File

@ -146,7 +146,10 @@ const triggers = [
(0, assert_1.default)(oauthUser.state, `oauthUser ${oauthUser.id} 关联的 state 不存在`);
(0, assert_1.default)(oauthUser.state.provider, `oauthUser ${oauthUser.id} 关联的 state 的 provider 不存在`);
const refreshEndpoint = oauthUser.state.provider.refreshEndpoint;
(0, assert_1.default)(refreshEndpoint, `oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌`);
if (!refreshEndpoint) {
console.warn(`oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌,跳过`);
continue;
}
// 根据 RFC 6749 规范 使用 refresh token 刷新 access token
const authHeaderRaw = Buffer.from(`${oauthUser.state.provider.clientId}:${oauthUser.state.provider.clientSecret}`).toString('base64');
const resp = await fetch(refreshEndpoint, {

View File

@ -31,7 +31,11 @@ export default OakComponent({
isCreation: this.isCreation(),
};
},
properties: {},
properties: {
open: false as boolean,
onCancel: (() => {}) as () => void,
onOk: (() => {}) as () => void,
},
methods: {
reGenerateClientSecret() {
this.features.cache.operate("oauthApplication", {
@ -42,6 +46,15 @@ export default OakComponent({
id: this.props.oakId,
}
})
},
setHideModal(hide: boolean) {
this.setState({
hideModal: hide
});
}
},
data: {
hideModal: false,
isCreation: false,
}
});

View File

@ -1,4 +1,5 @@
{
"oauthApplication": "OAuth应用",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入应用名称",
@ -21,5 +22,8 @@
"clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。",
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看"
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看",
"create": "创建",
"update": "更新",
"cancel": "取消"
}

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Form, Input, Switch, Button, Space, Upload, Select } from 'antd';
import { Form, Input, Switch, Button, Space, Upload, Select, Modal } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
@ -14,32 +14,83 @@ const Upsert = (
item: RowWithActions<EntityDict, 'oauthApplication'>;
clientSecret: string;
isCreation: boolean;
open: boolean;
onCancel: () => void;
onOk: () => void;
hideModal: boolean;
},
{
reGenerateClientSecret: () => void;
setHideModal: (hide: boolean) => void;
}
>
) => {
const { item, clientSecret, isCreation } = props.data;
const { t, update, reGenerateClientSecret } = props.methods;
const { item, clientSecret, isCreation, open, onCancel, onOk, hideModal } = props.data;
const { t, update, reGenerateClientSecret, setHideModal } = props.methods;
const [form] = Form.useForm();
useEffect(() => {
if (item) {
form.setFieldsValue({
name: item.name,
description: item.description,
logo: item.logo,
redirectUris: Array.isArray(item.redirectUris) ? item.redirectUris.join('\n') : '',
scopes: item.scopes,
isConfidential: item.isConfidential,
ableState: item.ableState === 'enabled',
requirePKCE: item.requirePKCE,
});
}
}, [item, form]);
const handleOk = async () => {
try {
await form.validateFields();
setHideModal(true);
setTimeout(() => {
onOk();
}, 300);
} catch (error) {
console.log('Validation failed:', error);
}
};
const handleCancel = () => {
setHideModal(true);
setTimeout(() => {
onCancel();
}, 300);
}
if (item === undefined) {
return <div>{t('noData')}</div>;
return null;
}
return (
<div className={Styles.id}>
<Modal
open={open && !hideModal}
destroyOnClose={true}
width={600}
onCancel={handleCancel}
onOk={handleOk}
title={t('oauthApplication')}
okText={isCreation ? t('create') : t('update')}
cancelText={t('cancel')}
>
<div className={Styles.id}>
<Form
form={form}
layout="vertical"
autoComplete="off"
>
<Form.Item
label={t('name')}
name="name"
rules={[{ required: true, message: t('nameRequired') }]}
>
<Input
placeholder={t('namePlaceholder')}
value={item.name || ""}
placeholder={t('namePlaceholder')}
onChange={(v) => {
update({ name: v.target.value });
}}
@ -48,10 +99,10 @@ const Upsert = (
<Form.Item
label={t('description')}
name="description"
>
<Input.TextArea
placeholder={t('descriptionPlaceholder')}
value={item.description || ""}
placeholder={t('descriptionPlaceholder')}
rows={4}
onChange={(v) => {
update({ description: v.target.value });
@ -61,10 +112,10 @@ const Upsert = (
<Form.Item
label={t('logo')}
name="logo"
>
<Input
placeholder={t('logoPlaceholder')}
value={item.logo || ""}
placeholder={t('logoPlaceholder')}
onChange={(v) => {
update({ logo: v.target.value });
}}
@ -73,11 +124,11 @@ const Upsert = (
<Form.Item
label={t('redirectUris')}
name="redirectUris"
rules={[{ required: true, message: t('redirectUrisRequired') }]}
>
<Input.TextArea
placeholder={t('redirectUrisPlaceholder')}
value={Array.isArray(item.redirectUris) ? item.redirectUris.join('\n') : ""}
placeholder={t('redirectUrisPlaceholder')}
rows={3}
onChange={(v) => {
const uris = v.target.value.split('\n').filter(uri => uri.trim() !== '');
@ -88,11 +139,11 @@ const Upsert = (
<Form.Item
label={t('scopes')}
name="scopes"
>
<Select
mode="tags"
placeholder={t('scopesPlaceholder')}
value={item.scopes || []}
onChange={(v) => {
update({ scopes: v });
}}
@ -135,20 +186,20 @@ const Upsert = (
<Form.Item
label={t('isConfidential')}
name="isConfidential"
valuePropName="checked"
>
<Switch
checked={!!item.isConfidential}
onChange={(checked) => update({ isConfidential: checked })}
/>
</Form.Item>
<Form.Item
label={t('ableState')}
name="ableState"
valuePropName="checked"
>
<Switch
checked={!!item.ableState}
onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })}
/>
</Form.Item>
@ -156,16 +207,17 @@ const Upsert = (
{/* requirePKCE */}
<Form.Item
label={t('requirePKCE')}
name="requirePKCE"
valuePropName="checked"
tooltip={t('requirePKCETooltip')}
>
<Switch
checked={!!item.requirePKCE}
onChange={(checked) => update({ requirePKCE: checked })}
/>
</Form.Item>
</Form>
</div>
</div>
</Modal>
);
};

View File

@ -78,16 +78,21 @@ const OauthProvider = (
}
/>
)}
{/* 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>
{upsertId && (
<AppUpsert
oakPath={`${oakFullpath}.${upsertId}`}
oakId={upsertId}
open={!!upsertId}
onCancel={() => {
clean();
setUpsertId(null);
}}
onOk={() => {
execute();
setUpsertId(null);
}}
/>
)}
</>
);
};

View File

@ -20,11 +20,26 @@ export default OakComponent({
formData({ data }) {
return {
item: data,
isCreation: this.isCreation(),
};
},
properties: {},
properties: {
open: false as boolean,
onCancel: (() => { }) as () => void,
onOk: (() => { }) as () => void,
},
lifetimes: {
ready() {
},
},
methods: {
setHideModal(hide: boolean) {
this.setState({
hideModal: hide
});
}
},
data: {
hideModal: false
}
});

View File

@ -1,4 +1,5 @@
{
"oauthProvider": "OAuth提供商",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入OAuth提供商名称",
@ -34,5 +35,7 @@
"scopes": "权限范围",
"scopesPlaceholder": "请选择或输入权限范围",
"refreshEndpoint": "刷新端点",
"refreshEndpointPlaceholder": "请输入刷新端点URL"
"refreshEndpointPlaceholder": "请输入刷新端点URL",
"create": "创建",
"update": "更新"
}

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Form, Input, Switch, Button, Space, Upload, Select, Typography } from 'antd';
import { Form, Input, Switch, Button, Space, Upload, Select, Typography, Modal } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
@ -13,27 +13,88 @@ const Upsert = (
false,
{
item: RowWithActions<EntityDict, 'oauthProvider'>;
open: boolean;
onCancel: () => void;
onOk: () => void;
hideModal: boolean;
isCreation: boolean;
},
{
setHideModal: (hide: boolean) => void;
}
>
) => {
const { item } = props.data;
const { t, update } = props.methods;
const { item, open, onCancel, onOk, hideModal, isCreation } = props.data;
const { t, update, setHideModal } = props.methods;
const [form] = Form.useForm();
useEffect(() => {
if (item) {
form.setFieldsValue({
name: item.name,
type: item.type ? [item.type] : [],
logo: item.logo,
authorizationEndpoint: item.authorizationEndpoint,
tokenEndpoint: item.tokenEndpoint,
refreshEndpoint: item.refreshEndpoint,
userInfoEndpoint: item.userInfoEndpoint,
revokeEndpoint: item.revokeEndpoint,
clientId: item.clientId,
clientSecret: item.clientSecret,
scopes: item.scopes,
redirectUri: item.redirectUri,
autoRegister: item.autoRegister,
ableState: item.ableState === 'enabled',
});
}
}, [item, form]);
const handleOk = async () => {
try {
await form.validateFields();
setHideModal(true);
setTimeout(() => {
onOk();
}, 300);
} catch (error) {
console.log('Validation failed:', error);
}
};
const handleCancel = () => {
setHideModal(true);
setTimeout(() => {
onCancel();
}, 300);
}
if (item === undefined) {
return <div>{t('noData')}</div>;
return null;
}
return (
<div className={Styles.id}>
<Modal
open={open && !hideModal}
destroyOnClose={true}
width={600}
onCancel={handleCancel}
onOk={handleOk}
title={t('oauthProvider')}
okText={isCreation ? t('create') : t('update')}
cancelText={t('cancel')}
>
<div className={Styles.id}>
<Form
form={form}
layout="vertical"
autoComplete="off"
>
<Form.Item
label={t('name')}
name="name"
rules={[{ required: true, message: t('nameRequired') }]}
>
<Input placeholder={t('namePlaceholder')} value={item.name || ""}
<Input placeholder={t('namePlaceholder')}
onChange={(v) => {
update({ name: v.target.value });
}}
@ -42,6 +103,7 @@ const Upsert = (
<Form.Item
label={t('type')}
name="type"
rules={[{ required: true, message: t('typeRequired') }]}
extra={
item.type && item.type !== 'oak' && item.type !== 'gitea' ? (
@ -54,7 +116,6 @@ const Upsert = (
<Select
mode="tags"
placeholder={t('typePlaceholder')}
value={item.type ? [item.type] : []} // 保持数组形式
onChange={(v) => {
// 只取最后一个输入或选择的值
const last = v.slice(-1)[0] as
@ -77,8 +138,9 @@ const Upsert = (
<Form.Item
label={t('logo')}
name="logo"
>
<Input placeholder={t('logoPlaceholder')} value={item.logo || ""}
<Input placeholder={t('logoPlaceholder')}
onChange={(v) => {
update({ logo: v.target.value });
}}
@ -87,9 +149,10 @@ const Upsert = (
<Form.Item
label={t('authorizationEndpoint')}
name="authorizationEndpoint"
rules={[{ required: true, message: t('authorizationEndpointRequired') }]}
>
<Input placeholder={t('authorizationEndpointPlaceholder')} value={item.authorizationEndpoint || ""}
<Input placeholder={t('authorizationEndpointPlaceholder')}
onChange={(v) => {
update({ authorizationEndpoint: v.target.value });
}}
@ -98,9 +161,10 @@ const Upsert = (
<Form.Item
label={t('tokenEndpoint')}
name="tokenEndpoint"
rules={[{ required: true, message: t('tokenEndpointRequired') }]}
>
<Input placeholder={t('tokenEndpointPlaceholder')} value={item.tokenEndpoint || ""}
<Input placeholder={t('tokenEndpointPlaceholder')}
onChange={(v) => {
update({ tokenEndpoint: v.target.value });
}}
@ -109,8 +173,9 @@ const Upsert = (
<Form.Item
label={t('refreshEndpoint')}
name="refreshEndpoint"
>
<Input placeholder={t('refreshEndpointPlaceholder')} value={item.refreshEndpoint || ""}
<Input placeholder={t('refreshEndpointPlaceholder')}
onChange={(v) => {
update({ refreshEndpoint: v.target.value });
}}
@ -119,8 +184,9 @@ const Upsert = (
<Form.Item
label={t('userInfoEndpoint')}
name="userInfoEndpoint"
>
<Input placeholder={t('userInfoEndpointPlaceholder')} value={item.userInfoEndpoint || ""}
<Input placeholder={t('userInfoEndpointPlaceholder')}
onChange={(v) => {
update({ userInfoEndpoint: v.target.value });
}}
@ -129,8 +195,9 @@ const Upsert = (
<Form.Item
label={t('revokeEndpoint')}
name="revokeEndpoint"
>
<Input placeholder={t('revokeEndpointPlaceholder')} value={item.revokeEndpoint || ""}
<Input placeholder={t('revokeEndpointPlaceholder')}
onChange={(v) => {
update({ revokeEndpoint: v.target.value });
}}
@ -139,9 +206,10 @@ const Upsert = (
<Form.Item
label={t('clientId')}
name="clientId"
rules={[{ required: true, message: t('clientIdRequired') }]}
>
<Input placeholder={t('clientIdPlaceholder')} value={item.clientId || ""}
<Input placeholder={t('clientIdPlaceholder')}
onChange={(v) => {
update({ clientId: v.target.value });
}}
@ -150,9 +218,10 @@ const Upsert = (
<Form.Item
label={t('clientSecret')}
name="clientSecret"
rules={[{ required: true, message: t('clientSecretRequired') }]}
>
<Input.Password placeholder={t('clientSecretPlaceholder')} value={item.clientSecret || ""}
<Input.Password placeholder={t('clientSecretPlaceholder')}
onChange={(v) => {
update({ clientSecret: v.target.value });
}}
@ -161,11 +230,11 @@ const Upsert = (
<Form.Item
label={t('scopes')}
name="scopes"
>
<Select
mode="tags"
placeholder={t('scopesPlaceholder')}
value={item.scopes || []}
onChange={(v) => {
update({ scopes: v });
}}
@ -176,9 +245,10 @@ const Upsert = (
<Form.Item
label={t('redirectUri')}
name="redirectUri"
rules={[{ required: true, message: t('redirectUriRequired') }]}
>
<Input placeholder={t('redirectUriPlaceholder')} value={item.redirectUri || ""}
<Input placeholder={t('redirectUriPlaceholder')}
onChange={(v) => {
update({ redirectUri: v.target.value });
}}
@ -187,30 +257,23 @@ const Upsert = (
<Form.Item
label={t('autoRegister')}
name="autoRegister"
valuePropName="checked"
>
<Switch checked={!!item.autoRegister} onChange={(checked) => update({ autoRegister: checked })} />
<Switch onChange={(checked) => update({ autoRegister: checked })} />
</Form.Item>
<Form.Item
label={t('ableState')}
name="ableState"
valuePropName="checked"
>
<Switch checked={!!item.ableState} onChange={(checked) => update({ ableState: checked ? "enabled" : "disabled" })} />
<Switch 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>
</div>
</Modal>
);
};

View File

@ -79,16 +79,21 @@ const OauthProvider = (
}
/>
)}
{/* 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>
{upsertId && (
<ProviderUpsert
oakPath={`${oakFullpath}.${upsertId}`}
oakId={upsertId}
open={!!upsertId}
onCancel={() => {
clean();
setUpsertId(null);
}}
onOk={() => {
execute();
setUpsertId(null);
}}
/>
)}
</>
);
};

View File

@ -382,6 +382,7 @@ const i18ns: I18n[] = [
module: "oak-general-business",
position: "src/components/oauth/management/oauthApps/upsert",
data: {
"oauthApplication": "OAuth应用",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入应用名称",
@ -404,7 +405,10 @@ const i18ns: I18n[] = [
"clientIdPlaceholder": "自动生成的客户端ID",
"requirePKCE": "强制 PKCE",
"requirePKCETooltip": "启用后授权请求必须使用PKCE扩展以增强安全性。",
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看"
"clientSecretTooltip": "客户端密钥仅在机密客户端中使用,用于身份验证。创建成功后可以查看",
"create": "创建",
"update": "更新",
"cancel": "取消"
}
},
{
@ -428,6 +432,7 @@ const i18ns: I18n[] = [
module: "oak-general-business",
position: "src/components/oauth/management/oauthProvider/upsert",
data: {
"oauthProvider": "OAuth提供商",
"name": "名称",
"nameRequired": "请输入名称",
"namePlaceholder": "请输入OAuth提供商名称",
@ -463,7 +468,9 @@ const i18ns: I18n[] = [
"scopes": "权限范围",
"scopesPlaceholder": "请选择或输入权限范围",
"refreshEndpoint": "刷新端点",
"refreshEndpointPlaceholder": "请输入刷新端点URL"
"refreshEndpointPlaceholder": "请输入刷新端点URL",
"create": "创建",
"update": "更新"
}
},
{

View File

@ -169,7 +169,10 @@ const triggers: Trigger<EntityDict, "oauthUser", BRC<EntityDict>>[] = [
assert(oauthUser.state, `oauthUser ${oauthUser.id} 关联的 state 不存在`);
assert(oauthUser.state.provider, `oauthUser ${oauthUser.id} 关联的 state 的 provider 不存在`);
const refreshEndpoint = oauthUser.state.provider.refreshEndpoint;
assert(refreshEndpoint, `oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌`);
if (!refreshEndpoint) {
console.warn(`oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌,跳过`);
continue;
}
// 根据 RFC 6749 规范 使用 refresh token 刷新 access token
const authHeaderRaw = Buffer.from(`${oauthUser.state.provider.clientId}:${oauthUser.state.provider.clientSecret}`).toString('base64');