diff --git a/src/components/login/oauth/authorize/index.ts b/src/components/login/oauth/authorize/index.ts
new file mode 100644
index 000000000..86d181b70
--- /dev/null
+++ b/src/components/login/oauth/authorize/index.ts
@@ -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',
+ });
+ });
+ }
+ },
+});
diff --git a/src/components/login/oauth/authorize/locales/zh_CN.json b/src/components/login/oauth/authorize/locales/zh_CN.json
new file mode 100644
index 000000000..34d0f8371
--- /dev/null
+++ b/src/components/login/oauth/authorize/locales/zh_CN.json
@@ -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": "未知错误,请稍后重试"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/login/oauth/authorize/styles.module.less b/src/components/login/oauth/authorize/styles.module.less
new file mode 100644
index 000000000..addd395bb
--- /dev/null
+++ b/src/components/login/oauth/authorize/styles.module.less
@@ -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;
+}
\ No newline at end of file
diff --git a/src/components/login/oauth/authorize/web.pc.tsx b/src/components/login/oauth/authorize/web.pc.tsx
new file mode 100644
index 000000000..e8fdd237e
--- /dev/null
+++ b/src/components/login/oauth/authorize/web.pc.tsx
@@ -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 (
+
+
+
+
{t('oauth.authorize.loading')}
+
+
+ );
+ }
+
+ // Error state
+ if (hasError) {
+ return (
+
+
+
{t('oauth.authorize.error.title')}
+
{t(errorMsg)}
+
+
+ );
+ }
+
+ // Logged in - show authorization confirmation
+ return (
+
+
+
{t('oauth.authorize.title')}
+
+ {t('oauth.authorize.description')}
+
+
+
+
{t('oauth.authorize.clientName')}:
+
{clientInfo?.name || client_id}
+ {clientInfo?.description && (
+ <>
+
{t('oauth.authorize.clientDescription')}:
+
{clientInfo.description}
+ >
+ )}
+
+
+
+
{t('oauth.authorize.scope')}
+
+ ✓
+ {t('oauth.authorize.allPermissions')}
+
+
+
+
+ {avatarUrl ? (
+
+ ) : (
+
+
+ {nickname?.[0]}
+
+
+ )}
+
+
{name || nickname}
+ {mobile &&
{mobile}
}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Authorize;
\ No newline at end of file
diff --git a/src/components/oauth/index.ts b/src/components/oauth/index.ts
new file mode 100644
index 000000000..24d3dfc86
--- /dev/null
+++ b/src/components/oauth/index.ts
@@ -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: '/',
+ });
+ }
+ }
+});
diff --git a/src/components/oauth/locales/zh_CN.json b/src/components/oauth/locales/zh_CN.json
new file mode 100644
index 000000000..b2ad20da6
--- /dev/null
+++ b/src/components/oauth/locales/zh_CN.json
@@ -0,0 +1,20 @@
+{
+ "Invalid state parameter": "无效的状态参数",
+ "oauth": {
+ "loading": {
+ "title": "授权中..."
+ },
+ "loadingMessage": "正在处理授权请求,请稍候",
+ "error": {
+ "title": "授权失败"
+ },
+ "success": {
+ "title": "授权成功"
+ },
+ "successMessage": "授权已成功完成",
+ "return": "返回首页",
+ "confirm": "确认登录",
+ "cancel": "取消",
+ "close": "关闭窗口"
+ }
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/index.ts b/src/components/oauth/management/index.ts
new file mode 100644
index 000000000..d77da5f50
--- /dev/null
+++ b/src/components/oauth/management/index.ts
@@ -0,0 +1,9 @@
+export default OakComponent({
+ // Virtual Component
+ isList: false,
+ filters: [],
+ properties: {
+ systemId: '',
+ systemName: '',
+ }
+});
diff --git a/src/components/oauth/management/locales/zh_CN.json b/src/components/oauth/management/locales/zh_CN.json
new file mode 100644
index 000000000..5de2ea6bb
--- /dev/null
+++ b/src/components/oauth/management/locales/zh_CN.json
@@ -0,0 +1,5 @@
+{
+ "systemInfo": "系统信息",
+ "applications": "OAuth应用",
+ "providers": "OAuth供应商"
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthApps/index.ts b/src/components/oauth/management/oauthApps/index.ts
new file mode 100644
index 000000000..12dc62fc5
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/index.ts
@@ -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"]
+});
diff --git a/src/components/oauth/management/oauthApps/locales/zh_CN.json b/src/components/oauth/management/oauthApps/locales/zh_CN.json
new file mode 100644
index 000000000..2017f11d2
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/locales/zh_CN.json
@@ -0,0 +1,7 @@
+{
+ "oauthAppConfig": "OAuth应用程序配置",
+ "confirm": {
+ "deleteTitle": "确认删除",
+ "deleteContent": "您确定要删除此OAuth应用程序配置吗?"
+ }
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthApps/styles.module.less b/src/components/oauth/management/oauthApps/styles.module.less
new file mode 100644
index 000000000..b13913541
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/styles.module.less
@@ -0,0 +1,7 @@
+.id {
+ font-size: 18px;
+}
+
+.item {
+ font-size: 18px;
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthApps/upsert/index.ts b/src/components/oauth/management/oauthApps/upsert/index.ts
new file mode 100644
index 000000000..a74830548
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/upsert/index.ts
@@ -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,
+ }
+ })
+ }
+ }
+});
diff --git a/src/components/oauth/management/oauthApps/upsert/locales/zh_CN.json b/src/components/oauth/management/oauthApps/upsert/locales/zh_CN.json
new file mode 100644
index 000000000..eef45117a
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/upsert/locales/zh_CN.json
@@ -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"
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthApps/upsert/styles.module.less b/src/components/oauth/management/oauthApps/upsert/styles.module.less
new file mode 100644
index 000000000..b13913541
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/upsert/styles.module.less
@@ -0,0 +1,7 @@
+.id {
+ font-size: 18px;
+}
+
+.item {
+ font-size: 18px;
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthApps/upsert/web.pc.tsx b/src/components/oauth/management/oauthApps/upsert/web.pc.tsx
new file mode 100644
index 000000000..f221496e4
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/upsert/web.pc.tsx
@@ -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;
+ clientSecret: string;
+ },
+ {
+ reGenerateClientSecret: () => void;
+ }
+ >
+) => {
+ const { item, clientSecret } = props.data;
+ const { t, update, reGenerateClientSecret } = props.methods;
+
+ if (item === undefined) {
+ return {t('noData')}
;
+ }
+
+ return (
+
+
+ {
+ update({ name: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ description: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ logo: v.target.value });
+ }}
+ />
+
+
+
+ {
+ const uris = v.target.value.split('\n').filter(uri => uri.trim() !== '');
+ update({ redirectUris: uris });
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ update({ isConfidential: checked })}
+ />
+
+
+
+ update({ ableState: checked ? "enabled" : "disabled" })}
+ />
+
+
+
+ );
+};
+
+export default Upsert;
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthApps/web.pc.tsx b/src/components/oauth/management/oauthApps/web.pc.tsx
new file mode 100644
index 000000000..c3931f2eb
--- /dev/null
+++ b/src/components/oauth/management/oauthApps/web.pc.tsx
@@ -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[];
+ 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(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 && (
+
+
+
+ }
+ >
+
+ )}
+ {/* antd model */}
+ {
+ clean()
+ setUpsertId(null);
+ }} onOk={() => {
+ execute()
+ setUpsertId(null);
+ }}>
+ {upsertId && }
+
+ >
+ );
+};
+
+export default OauthProvider;
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthProvider/index.ts b/src/components/oauth/management/oauthProvider/index.ts
new file mode 100644
index 000000000..36ba2122c
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/index.ts
@@ -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"]
+});
diff --git a/src/components/oauth/management/oauthProvider/locales/zh_CN.json b/src/components/oauth/management/oauthProvider/locales/zh_CN.json
new file mode 100644
index 000000000..bcda004bb
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/locales/zh_CN.json
@@ -0,0 +1,7 @@
+{
+ "oauthProviderConfig": "OAuth提供商配置",
+ "confirm": {
+ "deleteTitle": "确认删除",
+ "deleteContent": "您确定要删除此OAuth提供商配置吗?"
+ }
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthProvider/styles.module.less b/src/components/oauth/management/oauthProvider/styles.module.less
new file mode 100644
index 000000000..5fd230c5d
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/styles.module.less
@@ -0,0 +1,12 @@
+.id {
+ font-size: 18px;
+}
+
+.item {
+ font-size: 18px;
+}
+
+.actions {
+ display: flex;
+ gap: 8px;
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthProvider/upsert/index.ts b/src/components/oauth/management/oauthProvider/upsert/index.ts
new file mode 100644
index 000000000..82a9a985f
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/upsert/index.ts
@@ -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() {
+ },
+ }
+});
diff --git a/src/components/oauth/management/oauthProvider/upsert/locales/zh_CN.json b/src/components/oauth/management/oauthProvider/upsert/locales/zh_CN.json
new file mode 100644
index 000000000..68642e34c
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/upsert/locales/zh_CN.json
@@ -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"
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthProvider/upsert/styles.module.less b/src/components/oauth/management/oauthProvider/upsert/styles.module.less
new file mode 100644
index 000000000..b13913541
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/upsert/styles.module.less
@@ -0,0 +1,7 @@
+.id {
+ font-size: 18px;
+}
+
+.item {
+ font-size: 18px;
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx b/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx
new file mode 100644
index 000000000..37e56a485
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/upsert/web.pc.tsx
@@ -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;
+ }
+ >
+) => {
+ const { item } = props.data;
+ const { t, update } = props.methods;
+
+ if (item === undefined) {
+ return {t('noData')}
;
+ }
+
+ return (
+
+
+ {
+ update({ name: v.target.value });
+ }}
+ />
+
+
+
+
+
+
+
+ {
+ update({ logo: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ authorizationEndpoint: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ tokenEndpoint: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ refreshEndpoint: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ userInfoEndpoint: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ revokeEndpoint: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ clientId: v.target.value });
+ }}
+ />
+
+
+
+ {
+ update({ clientSecret: v.target.value });
+ }}
+ />
+
+
+
+
+
+
+ {
+ update({ redirectUri: v.target.value });
+ }}
+ />
+
+
+
+ update({ autoRegister: checked })} />
+
+
+
+ update({ ableState: checked ? "enabled" : "disabled" })} />
+
+
+ {/*
+
+
+
+
+ */}
+
+
+ );
+};
+
+export default Upsert;
\ No newline at end of file
diff --git a/src/components/oauth/management/oauthProvider/web.pc.tsx b/src/components/oauth/management/oauthProvider/web.pc.tsx
new file mode 100644
index 000000000..4cb9e3b51
--- /dev/null
+++ b/src/components/oauth/management/oauthProvider/web.pc.tsx
@@ -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[];
+ 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(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 && (
+
+
+
+ }
+ >
+
+ )}
+ {/* antd model */}
+ {
+ clean()
+ setUpsertId(null);
+ }} onOk={() => {
+ execute()
+ setUpsertId(null);
+ }}>
+ {upsertId && }
+
+ >
+ );
+};
+
+export default OauthProvider;
\ No newline at end of file
diff --git a/src/components/oauth/management/styles.module.less b/src/components/oauth/management/styles.module.less
new file mode 100644
index 000000000..b13913541
--- /dev/null
+++ b/src/components/oauth/management/styles.module.less
@@ -0,0 +1,7 @@
+.id {
+ font-size: 18px;
+}
+
+.item {
+ font-size: 18px;
+}
\ No newline at end of file
diff --git a/src/components/oauth/management/web.pc.tsx b/src/components/oauth/management/web.pc.tsx
new file mode 100644
index 000000000..86ea6ec76
--- /dev/null
+++ b/src/components/oauth/management/web.pc.tsx
@@ -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
+ )
+ },
+ {
+ key: '2',
+ label: t('applications'),
+ children: (
+
+ )
+ }
+ ]}
+ />
+};
+
+export default Management;
\ No newline at end of file
diff --git a/src/components/oauth/styles.module.less b/src/components/oauth/styles.module.less
new file mode 100644
index 000000000..ee279f81e
--- /dev/null
+++ b/src/components/oauth/styles.module.less
@@ -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;
+}
+
diff --git a/src/components/oauth/web.pc.tsx b/src/components/oauth/web.pc.tsx
new file mode 100644
index 000000000..7be1e58d3
--- /dev/null
+++ b/src/components/oauth/web.pc.tsx
@@ -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 (
+
+
+ {loading ? (
+ <>
+
+
{t('oauth.loading.title')}
+
+ {t('oauth.loadingMessage')}
+
+ >
+ ) : hasError ? (
+ <>
+
+
{t('oauth.error.title')}
+
{tErrMsg}
+
+ >
+ ) : (
+ <>
+
+
{t('oauth.success.title')}
+
{t('oauth.successMessage')}
+
+ >
+ )}
+
+
+ );
+};
+
+export default Oauth;
\ No newline at end of file
diff --git a/src/components/system/panel/locales/zh-CN.json b/src/components/system/panel/locales/zh-CN.json
index 02f424206..d8c0ca7d0 100644
--- a/src/components/system/panel/locales/zh-CN.json
+++ b/src/components/system/panel/locales/zh-CN.json
@@ -5,5 +5,6 @@
"style": "样式管理",
"application-list": "应用管理",
"smsTemplate-list": "短信模板管理",
- "login": "登录管理"
+ "login": "登录管理",
+ "oauth": "OAuth管理"
}
\ No newline at end of file
diff --git a/src/components/system/panel/web.pc.tsx b/src/components/system/panel/web.pc.tsx
index db2d34842..bcb758b93 100644
--- a/src/components/system/panel/web.pc.tsx
+++ b/src/components/system/panel/web.pc.tsx
@@ -12,6 +12,7 @@ import { EntityDict } from '../../../oak-app-domain';
import { Config } from '../../../types/Config';
import { Style } from '../../../types/Style';
import Styles from './web.pc.module.less';
+import OAuthManagement from '../../oauth/management';
export default function Render(props: WebComponentProps
),
},
+ {
+ label: (
+
+ {t('oauth')}
+
+ ),
+ key: 'oauth-manage',
+ destroyInactiveTabPane: true,
+ children: (
+
+ ),
+ },
]}
/>