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: ( + + ), + }, ]} />