diff --git a/docker/nocobase/Dockerfile b/docker/nocobase/Dockerfile index 2728e74862..db96d07fa9 100644 --- a/docker/nocobase/Dockerfile +++ b/docker/nocobase/Dockerfile @@ -30,7 +30,6 @@ RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \ && apt-get update && apt-get install -y nginx RUN rm -rf /etc/nginx/sites-enabled/default -COPY ./nocobase.conf /etc/nginx/sites-enabled/nocobase.conf COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz WORKDIR /app/nocobase diff --git a/docker/nocobase/docker-entrypoint.sh b/docker/nocobase/docker-entrypoint.sh index adb13286f6..62c7918c62 100755 --- a/docker/nocobase/docker-entrypoint.sh +++ b/docker/nocobase/docker-entrypoint.sh @@ -1,9 +1,6 @@ #!/bin/sh set -e -nginx -echo 'nginx started'; - if [ ! -d "/app/nocobase" ]; then mkdir nocobase fi @@ -14,6 +11,13 @@ if [ ! -f "/app/nocobase/package.json" ]; then touch /app/nocobase/node_modules/@nocobase/app/dist/client/index.html fi +cd /app/nocobase && yarn nocobase create-nginx-conf +rm -rf /etc/nginx/sites-enabled/nocobase.conf +ln -s /app/nocobase/storage/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf + +nginx +echo 'nginx started'; + cd /app/nocobase && yarn start --quickstart # Run command with node if the first argument contains a "-" or is not a system command. The last diff --git a/packages/core/app/client/.umirc.ts b/packages/core/app/client/.umirc.ts index 95761bad16..2eeffa9148 100644 --- a/packages/core/app/client/.umirc.ts +++ b/packages/core/app/client/.umirc.ts @@ -19,16 +19,29 @@ indexGenerator.generate(); export default defineConfig({ title: 'Loading...', devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, - favicons: ['/favicon/favicon.ico'], + favicons: [`{{env.APP_PUBLIC_PATH}}favicon/favicon.ico`], metas: [{ name: 'viewport', content: 'initial-scale=0.1' }], links: [ - { rel: 'apple-touch-icon', size: '180x180', ref: '/favicon/apple-touch-icon.png' }, - { rel: 'icon', type: 'image/png', size: '32x32', ref: '/favicon/favicon-32x32.png' }, - { rel: 'icon', type: 'image/png', size: '16x16', ref: '/favicon/favicon-16x16.png' }, - { rel: 'manifest', href: '/favicon/site.webmanifest' }, - { rel: 'stylesheet', href: '/global.css' }, + { rel: 'apple-touch-icon', size: '180x180', ref: `{{env.APP_PUBLIC_PATH}}favicon/apple-touch-icon.png` }, + { rel: 'icon', type: 'image/png', size: '32x32', ref: `{{env.APP_PUBLIC_PATH}}favicon/favicon-32x32.png` }, + { rel: 'icon', type: 'image/png', size: '16x16', ref: `{{env.APP_PUBLIC_PATH}}favicon/favicon-16x16.png` }, + { rel: 'manifest', href: `{{env.APP_PUBLIC_PATH}}favicon/site.webmanifest` }, + { rel: 'stylesheet', href: `{{env.APP_PUBLIC_PATH}}global.css` }, + ], + headScripts: [ + { + src: `{{env.APP_PUBLIC_PATH}}browser-checker.js`, + }, + { + content: ` + window['__webpack_public_path__'] = '{{env.APP_PUBLIC_PATH}}'; + window['__nocobase_api_base_url__'] = '{{env.API_BASE_URL}}'; + window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}'; + window['__nocobase_ws_url__'] = '{{env.WS_URL}}'; + window['__nocobase_ws_path__'] = '{{env.WS_PATH}}'; + `, + }, ], - headScripts: ['/browser-checker.js'], outputPath: path.resolve(__dirname, '../dist/client'), hash: true, alias: { @@ -41,6 +54,7 @@ export default defineConfig({ proxy: { ...umiConfig.proxy, }, + publicPath: 'auto', fastRefresh: false, // 热更新会导致 Context 丢失,不开启 mfsu: false, esbuildMinifyIIFE: true, diff --git a/packages/core/app/client/src/pages/index.tsx b/packages/core/app/client/src/pages/index.tsx index 82c94739e1..ecebfeae1c 100644 --- a/packages/core/app/client/src/pages/index.tsx +++ b/packages/core/app/client/src/pages/index.tsx @@ -4,10 +4,18 @@ import devDynamicImport from '../.plugins/index'; export const app = new Application({ apiClient: { - baseURL: process.env.API_BASE_URL, + // @ts-ignore + baseURL: window['__nocobase_api_base_url__'] || '/api/', }, + // @ts-ignore + publicPath: window['__nocobase_public_path__'] || '/', plugins: [NocoBaseClientPresetPlugin], - ws: true, + ws: { + // @ts-ignore + url: window['__nocobase_ws_url__'] || '', + // @ts-ignore + basename: window['__nocobase_ws_path__'] || '/ws', + }, loadRemotePlugins: true, devDynamicImport, }); diff --git a/packages/core/cli/nocobase.conf.tpl b/packages/core/cli/nocobase.conf.tpl new file mode 100644 index 0000000000..b1a7dcdfd6 --- /dev/null +++ b/packages/core/cli/nocobase.conf.tpl @@ -0,0 +1,89 @@ +log_format apm '"$time_local" client=$remote_addr ' + 'method=$request_method request="$request" ' + 'request_length=$request_length ' + 'status=$status bytes_sent=$bytes_sent ' + 'body_bytes_sent=$body_bytes_sent ' + 'referer=$http_referer ' + 'user_agent="$http_user_agent" ' + 'upstream_addr=$upstream_addr ' + 'upstream_status=$upstream_status ' + 'request_time=$request_time ' + 'upstream_response_time=$upstream_response_time ' + 'upstream_connect_time=$upstream_connect_time ' + 'upstream_header_time=$upstream_header_time'; + +server { + listen 80; + server_name _; + root {{cwd}}/node_modules/@nocobase/app/dist/client; + index index.html; + client_max_body_size 1000M; + access_log /var/log/nginx/nocobase.log apm; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # 不缓存 HTML 文件 + # location ~ \.html$ { + # if_modified_since off; + # expires off; + # etag off; + # } + + # # 缓存 JavaScript 和 CSS 文件 + # location ~* \.(js|css)$ { + # expires 365d; + # add_header Cache-Control "public"; + # } + + location {{publicPath}}storage/uploads/ { + alias {{cwd}}/storage/uploads/; + add_header Cache-Control "public"; + access_log off; + autoindex off; + } + + location {{publicPath}} { + alias {{cwd}}/node_modules/@nocobase/app/dist/client/; + try_files $uri $uri/ /index.html; + add_header Last-Modified $date_gmt; + add_header Cache-Control 'no-store, no-cache'; + if_modified_since off; + expires off; + etag off; + } + + location ^~ {{publicPath}}api/ { + proxy_pass http://127.0.0.1:{{apiPort}}{{publicPath}}api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_connect_timeout 600; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; + } + + location ^~ {{publicPath}}static/plugins/ { + proxy_pass http://127.0.0.1:{{apiPort}}{{publicPath}}static/plugins/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_connect_timeout 600; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; + } + + location {{publicPath}}ws { + proxy_pass http://127.0.0.1:{{apiPort}}{{publicPath}}ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } +} diff --git a/packages/core/cli/src/commands/build.js b/packages/core/cli/src/commands/build.js index 39adbab65b..d08bd5d7c6 100644 --- a/packages/core/cli/src/commands/build.js +++ b/packages/core/cli/src/commands/build.js @@ -1,6 +1,6 @@ const { resolve } = require('path'); const { Command } = require('commander'); -const { run, nodeCheck, isPackageValid } = require('../util'); +const { run, nodeCheck, isPackageValid, buildIndexHtml } = require('../util'); /** * @@ -31,5 +31,6 @@ module.exports = (cli) => { !options.dts ? '--no-dts' : '', options.sourcemap ? '--sourcemap' : '', ]); + buildIndexHtml(true); }); }; diff --git a/packages/core/cli/src/commands/create-nginx-conf.js b/packages/core/cli/src/commands/create-nginx-conf.js new file mode 100644 index 0000000000..ffacb5f92f --- /dev/null +++ b/packages/core/cli/src/commands/create-nginx-conf.js @@ -0,0 +1,21 @@ +const { resolve } = require('path'); +const { Command } = require('commander'); +const { readFileSync, writeFileSync } = require('fs'); + +/** + * + * @param {Command} cli + */ +module.exports = (cli) => { + cli.command('create-nginx-conf').action(async (name, options) => { + const file = resolve(__dirname, '../../nocobase.conf.tpl'); + const data = readFileSync(file, 'utf-8'); + const replaced = data + .replace(/\{\{cwd\}\}/g, '/app/nocobase') + .replace(/\{\{publicPath\}\}/g, process.env.APP_PUBLIC_PATH) + .replace(/\{\{apiPort\}\}/g, process.env.APP_PORT); + + const targetFile = resolve(process.cwd(), 'storage', 'nocobase.conf'); + writeFileSync(targetFile, replaced); + }); +}; diff --git a/packages/core/cli/src/commands/index.js b/packages/core/cli/src/commands/index.js index 2555675cc5..48bfd57ee6 100644 --- a/packages/core/cli/src/commands/index.js +++ b/packages/core/cli/src/commands/index.js @@ -8,6 +8,7 @@ const { isPackageValid, generateAppDir } = require('../util'); module.exports = (cli) => { generateAppDir(); require('./global')(cli); + require('./create-nginx-conf')(cli); require('./build')(cli); require('./tar')(cli); require('./dev')(cli); diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index eeac308717..93dbbd02c2 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -180,6 +180,7 @@ exports.generateAppDir = function generateAppDir() { } else { process.env.APP_PACKAGE_ROOT = appPkgPath; } + buildIndexHtml(); }; exports.genTsConfigPaths = function genTsConfigPaths() { @@ -257,6 +258,30 @@ function parseEnv(name) { } } +function buildIndexHtml(force = false) { + const file = `${process.env.APP_PACKAGE_ROOT}/dist/client/index.html`; + if (!fs.existsSync(file)) { + return; + } + const tpl = `${process.env.APP_PACKAGE_ROOT}/dist/client/index.html.tpl`; + if (force && fs.existsSync(tpl)) { + fs.rmSync(tpl); + } + if (!fs.existsSync(tpl)) { + fs.copyFileSync(file, tpl); + } + const data = fs.readFileSync(tpl, 'utf-8'); + const replacedData = data + .replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH) + .replace(/\{\{env.API_BASE_URL\}\}/g, process.env.API_BASE_URL || process.env.API_BASE_PATH) + .replace(/\{\{env.WS_URL\}\}/g, process.env.WEBSOCKET_URL || '') + .replace(/\{\{env.WS_PATH\}\}/g, process.env.WS_PATH) + .replace('src="/umi.', `src="${process.env.APP_PUBLIC_PATH}umi.`); + fs.writeFileSync(file, replacedData, 'utf-8'); +} + +exports.buildIndexHtml = buildIndexHtml; + exports.initEnv = function initEnv() { const env = { APP_ENV: 'development', @@ -280,7 +305,10 @@ exports.initEnv = function initEnv() { PLAYWRIGHT_AUTH_FILE: resolve(process.cwd(), 'storage/playwright/.auth/admin.json'), CACHE_DEFAULT_STORE: 'memory', CACHE_MEMORY_MAX: 2000, + PLUGIN_STATICS_PATH: '/static/plugins/', LOGGER_BASE_PATH: 'storage/logs', + APP_SERVER_BASE_URL: '', + APP_PUBLIC_PATH: '/', }; if ( @@ -319,4 +347,16 @@ exports.initEnv = function initEnv() { process.env[key] = env[key]; } } + + if (process.env.APP_PUBLIC_PATH) { + const publicPath = process.env.APP_PUBLIC_PATH.replace(/\/$/g, ''); + const keys = ['API_BASE_PATH', 'WS_PATH', 'PLUGIN_STATICS_PATH']; + for (const key of keys) { + process.env[key] = publicPath + process.env[key]; + } + } + + if (process.env.APP_SERVER_BASE_URL && !process.env.API_BASE_URL) { + process.env.API_BASE_URL = process.env.APP_SERVER_BASE_URL + process.env.API_BASE_PATH; + } }; diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 26385c14be..9c8ff70cd1 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -1,4 +1,4 @@ -import { APIClient as APIClientSDK } from '@nocobase/sdk'; +import { APIClient as APIClientSDK, getSubAppName } from '@nocobase/sdk'; import { Result } from 'ahooks/es/useRequest/src/types'; import { notification } from 'antd'; import React from 'react'; @@ -39,9 +39,9 @@ export class APIClient extends APIClientSDK { interceptors() { this.axios.interceptors.request.use((config) => { config.headers['X-With-ACL-Meta'] = true; - const match = location.pathname.match(/^\/apps\/([^/]*)\//); - if (match) { - config.headers['X-App'] = match[1]; + const appName = this.app ? getSubAppName(this.app.getPublicPath()) : null; + if (appName) { + config.headers['X-App'] = appName; } return config; }); diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 2629d334f0..938745e13d 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -9,14 +9,14 @@ import { createRoot } from 'react-dom/client'; import { I18nextProvider } from 'react-i18next'; import { Link, NavLink, Navigate } from 'react-router-dom'; +import { APIClient, APIClientProvider } from '../api-client'; import { CSSVariableProvider } from '../css-variable'; import { AntdAppProvider, GlobalThemeProvider } from '../global-theme'; +import { i18n } from '../i18n'; import { PluginManager, PluginType } from './PluginManager'; import { PluginSettingOptions, PluginSettingsManager } from './PluginSettingsManager'; import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager'; import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient'; -import { APIClient, APIClientProvider } from '../api-client'; -import { i18n } from '../i18n'; import { AppComponent, BlankComponent, defaultAppComponents } from './components'; import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer'; import * as schemaInitializerComponents from './schema-initializer/components'; @@ -25,10 +25,10 @@ import { compose, normalizeContainer } from './utils'; import { defineGlobalDeps } from './utils/globalDeps'; import { getRequireJs } from './utils/requirejs'; -import { type DataSourceManagerOptions, DataSourceManager } from '../data-source/data-source/DataSourceManager'; -import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider'; import { CollectionField } from '../data-source/collection-field/CollectionField'; +import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider'; import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider'; +import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import type { Plugin } from './Plugin'; @@ -44,6 +44,7 @@ export type DevDynamicImport = (packageName: string) => Promise<{ default: typeo export type ComponentAndProps = [ComponentType, T]; export interface ApplicationOptions { name?: string; + publicPath?: string; apiClient?: APIClientOptions | APIClient; ws?: WebSocketClientOptions | boolean; i18n?: i18next; @@ -116,9 +117,10 @@ export class Application { this.addReactRouterComponents(); this.addProviders(options.providers || []); this.ws = new WebSocketClient(options.ws); + this.ws.app = this; this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this); this.addRoutes(); - this.name = this.options.name || getSubAppName() || 'main'; + this.name = this.options.name || getSubAppName(options.publicPath) || 'main'; } private initRequireJs() { @@ -157,6 +159,18 @@ export class Application { }); } + getOptions() { + return this.options; + } + + getPublicPath() { + return this.options.publicPath || '/'; + } + + getRouteUrl(pathname: string) { + return this.options.publicPath.replace(/\/$/g, '') + pathname; + } + getCollectionManager(dataSource?: string) { return this.dataSourceManager.getDataSource(dataSource)?.collectionManager; } diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx index 588f60c4c4..5784463b2d 100644 --- a/packages/core/client/src/application/RouterManager.tsx +++ b/packages/core/client/src/application/RouterManager.tsx @@ -1,4 +1,4 @@ -import { set, get } from 'lodash'; +import { get, set } from 'lodash'; import React, { ComponentType } from 'react'; import { BrowserRouter, @@ -10,8 +10,8 @@ import { RouteObject, useRoutes, } from 'react-router-dom'; -import { BlankComponent, RouterContextCleaner } from './components'; import { Application } from './Application'; +import { BlankComponent, RouterContextCleaner } from './components'; export interface BrowserRouterOptions extends Omit { type?: 'browser'; @@ -98,6 +98,10 @@ export class RouterManager { this.options.type = type; } + getBasename() { + return this.options.basename; + } + setBasename(basename: string) { this.options.basename = basename; } diff --git a/packages/core/client/src/application/WebSocketClient.ts b/packages/core/client/src/application/WebSocketClient.ts index af662402aa..cdadcd6fd3 100644 --- a/packages/core/client/src/application/WebSocketClient.ts +++ b/packages/core/client/src/application/WebSocketClient.ts @@ -1,34 +1,13 @@ import { define, observable } from '@formily/reactive'; import { getSubAppName } from '@nocobase/sdk'; - -export const getWebSocketURL = () => { - if (!process.env.API_BASE_URL) { - return; - } - const subApp = getSubAppName(); - const queryString = subApp ? `?__appName=${subApp}` : ''; - const wsPath = process.env.WS_PATH || '/ws'; - if (process.env.WEBSOCKET_URL) { - const url = new URL(process.env.WEBSOCKET_URL); - if (url.hostname === 'localhost') { - const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; - return `${protocol}://${location.hostname}:${url.port}${wsPath}${queryString}`; - } - return `${process.env.WEBSOCKET_URL}${queryString}`; - } - try { - const url = new URL(process.env.API_BASE_URL); - return `${url.protocol === 'https:' ? 'wss' : 'ws'}://${url.host}${wsPath}${queryString}`; - } catch (error) { - return `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}${wsPath}${queryString}`; - } -}; +import { Application } from './Application'; export type WebSocketClientOptions = { reconnectInterval?: number; reconnectAttempts?: number; pingInterval?: number; url?: string; + basename?: string; protocols?: string | string[]; onServerDown?: any; }; @@ -38,6 +17,7 @@ export class WebSocketClient { protected _reconnectTimes = 0; protected events = []; protected options: WebSocketClientOptions; + app: Application; enabled: boolean; connected = false; serverDown = false; @@ -57,6 +37,34 @@ export class WebSocketClient { }); } + getURL() { + if (!this.app) { + return; + } + const options = this.app.getOptions(); + const apiBaseURL = options?.apiClient?.['baseURL']; + if (!apiBaseURL) { + return; + } + const subApp = getSubAppName(this.app.getPublicPath()); + const queryString = subApp ? `?__appName=${subApp}` : ''; + const wsPath = this.options.basename || '/ws'; + if (this.options.url) { + const url = new URL(this.options.url); + if (url.hostname === 'localhost') { + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + return `${protocol}://${location.hostname}:${url.port}${wsPath}${queryString}`; + } + return `${this.options.url}${queryString}`; + } + try { + const url = new URL(apiBaseURL); + return `${url.protocol === 'https:' ? 'wss' : 'ws'}://${url.host}${wsPath}${queryString}`; + } catch (error) { + return `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}${wsPath}${queryString}`; + } + } + get reconnectAttempts() { return this.options?.reconnectAttempts || 30; } @@ -90,7 +98,7 @@ export class WebSocketClient { return; } this._reconnectTimes++; - const ws = new WebSocket(this.options.url || getWebSocketURL(), this.options.protocols); + const ws = new WebSocket(this.getURL(), this.options.protocols); let pingIntervalTimer: any; ws.onopen = () => { console.log('[nocobase-ws]: connected.'); diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index 61b3e39fee..38f5335618 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -10,6 +10,7 @@ import { useAPIClient } from '../api-client'; import { Application } from '../application'; import { Plugin } from '../application/Plugin'; import { BlockSchemaComponentPlugin } from '../block-provider'; +import { CollectionPlugin } from '../collection-manager'; import { RemoteDocumentTitlePlugin } from '../document-title'; import { PinnedListPlugin } from '../plugin-manager'; import { PMPlugin } from '../pm'; @@ -22,7 +23,6 @@ import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates'; import { SystemSettingsPlugin } from '../system-settings'; import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user'; import { LocalePlugin } from './plugins/LocalePlugin'; -import { CollectionPlugin } from '../collection-manager'; const AppSpin = () => { return ( @@ -36,7 +36,7 @@ const useErrorProps = (app: Application, error: any) => { return {}; } const err = error?.response?.data?.errors?.[0] || error; - const subApp = getSubAppName(); + const subApp = getSubAppName(app.getPublicPath()); switch (err.code) { case 'USER_HAS_NO_ROLES_ERR': return { diff --git a/packages/core/devtools/umiConfig.js b/packages/core/devtools/umiConfig.js index 0f08b7f03a..db24764136 100644 --- a/packages/core/devtools/umiConfig.js +++ b/packages/core/devtools/umiConfig.js @@ -37,6 +37,7 @@ function getUmiConfig() { return memo; }, {}), define: { + 'process.env.APP_PUBLIC_PATH': process.env.APP_PUBLIC_PATH, 'process.env.WS_PATH': process.env.WS_PATH, 'process.env.API_BASE_URL': API_BASE_URL || API_BASE_PATH, 'process.env.APP_ENV': process.env.APP_ENV, diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts index 300644aaeb..297ed29d7c 100644 --- a/packages/core/sdk/src/APIClient.ts +++ b/packages/core/sdk/src/APIClient.ts @@ -48,7 +48,7 @@ export class Auth { if (typeof window === 'undefined') { return; } - const appName = getSubAppName(); + const appName = getSubAppName(this.api['app'] ? this.api['app'].getPublicPath() : '/'); if (!appName) { return; } diff --git a/packages/core/sdk/src/getSubAppName.ts b/packages/core/sdk/src/getSubAppName.ts index ec79fe7b8a..c56d84e402 100644 --- a/packages/core/sdk/src/getSubAppName.ts +++ b/packages/core/sdk/src/getSubAppName.ts @@ -1,9 +1,11 @@ -const getSubAppName = () => { - const match = window.location.pathname.match(/^\/apps\/([^/]*)\/?/); - if (!match) { - return ''; +const getSubAppName = (publicPath = '/') => { + const prefix = `${publicPath}apps/`; + if (!window.location.pathname.startsWith(prefix)) { + return; } - return match[1]; + const pathname = window.location.pathname.substring(prefix.length); + const args = pathname.split('/', 1); + return args[0] || ''; }; export default getSubAppName; diff --git a/packages/core/sdk/src/index.ts b/packages/core/sdk/src/index.ts index 22ae2b1eb7..907a148cbe 100644 --- a/packages/core/sdk/src/index.ts +++ b/packages/core/sdk/src/index.ts @@ -1,3 +1,3 @@ export * from './APIClient'; -export { default as getSubAppName } from './getSubAppName'; +export { default as getSubAppName } from './getSubAppName'; diff --git a/packages/core/server/src/gateway/index.ts b/packages/core/server/src/gateway/index.ts index a8ce1613ca..71ff9dd21e 100644 --- a/packages/core/server/src/gateway/index.ts +++ b/packages/core/server/src/gateway/index.ts @@ -15,7 +15,7 @@ import handler from 'serve-handler'; import { parse } from 'url'; import { AppSupervisor } from '../app-supervisor'; import { ApplicationOptions } from '../application'; -import { PLUGIN_STATICS_PATH, getPackageDirByExposeUrl, getPackageNameByExposeUrl } from '../plugin-manager'; +import { getPackageDirByExposeUrl, getPackageNameByExposeUrl } from '../plugin-manager'; import { applyErrorWithArgs, getErrorWithCode } from './errors'; import { IPCSocketClient } from './ipc-socket-client'; import { IPCSocketServer } from './ipc-socket-server'; @@ -168,14 +168,16 @@ export class Gateway extends EventEmitter { async requestHandler(req: IncomingMessage, res: ServerResponse) { const { pathname } = parse(req.url); + const { PLUGIN_STATICS_PATH, APP_PUBLIC_PATH } = process.env; - if (pathname === '/__umi/api/bundle-status') { + if (pathname.endsWith('/__umi/api/bundle-status')) { res.statusCode = 200; res.end('ok'); return; } - if (pathname.startsWith('/storage/uploads/')) { + if (pathname.startsWith(APP_PUBLIC_PATH + 'storage/uploads/')) { + req.url = req.url.substring(APP_PUBLIC_PATH.length - 1); await compress(req, res); return handler(req, res, { public: resolve(process.cwd()), @@ -203,7 +205,8 @@ export class Gateway extends EventEmitter { }); } - if (!pathname.startsWith('/api')) { + if (!pathname.startsWith(process.env.API_BASE_PATH)) { + req.url = req.url.substring(APP_PUBLIC_PATH.length - 1); await compress(req, res); return handler(req, res, { public: `${process.env.APP_PACKAGE_ROOT}/dist/client`, diff --git a/packages/core/server/src/plugin-manager/clientStaticUtils.ts b/packages/core/server/src/plugin-manager/clientStaticUtils.ts index e7e6b6087f..bb8c17ad1e 100644 --- a/packages/core/server/src/plugin-manager/clientStaticUtils.ts +++ b/packages/core/server/src/plugin-manager/clientStaticUtils.ts @@ -36,7 +36,7 @@ export function getPackageFilePathWithExistCheck(packageName: string, filePath: } export function getExposeUrl(packageName: string, filePath: string) { - return `${PLUGIN_STATICS_PATH}${packageName}/${filePath}`; + return `${process.env.PLUGIN_STATICS_PATH}${packageName}/${filePath}`; } export function getExposeReadmeUrl(packageName: string, lang: string) { @@ -63,7 +63,7 @@ export function getExposeChangelogUrl(packageName: string) { * getPluginNameByClientStaticUrl('/static/plugins/@nocobase/foo/README.md') => '@nocobase/foo' */ export function getPackageNameByExposeUrl(pathname: string) { - pathname = pathname.replace(PLUGIN_STATICS_PATH, ''); + pathname = pathname.replace(process.env.PLUGIN_STATICS_PATH, ''); const pathArr = pathname.split('/'); if (pathname.startsWith('@')) { return pathArr.slice(0, 2).join('/'); diff --git a/packages/core/server/src/plugin-manager/options/resource.ts b/packages/core/server/src/plugin-manager/options/resource.ts index a82895dd6a..8bd130868e 100644 --- a/packages/core/server/src/plugin-manager/options/resource.ts +++ b/packages/core/server/src/plugin-manager/options/resource.ts @@ -5,7 +5,6 @@ import Application from '../../application'; import { getExposeUrl } from '../clientStaticUtils'; import PluginManager from '../plugin-manager'; //@ts-ignore -import { version } from '../../../package.json'; export default { name: 'pm', @@ -131,7 +130,10 @@ export default { try { return { ...item.toJSON(), - url: `${getExposeUrl(item.packageName, PLUGIN_CLIENT_ENTRY_FILE)}?version=${item.version}`, + url: `${process.env.APP_SERVER_BASE_URL}${getExposeUrl( + item.packageName, + PLUGIN_CLIENT_ENTRY_FILE, + )}?version=${item.version}`, }; } catch { return false; diff --git a/packages/plugins/@nocobase/plugin-api-doc/src/client/Document.tsx b/packages/plugins/@nocobase/plugin-api-doc/src/client/Document.tsx index b515fcdfb3..2110468d4b 100644 --- a/packages/plugins/@nocobase/plugin-api-doc/src/client/Document.tsx +++ b/packages/plugins/@nocobase/plugin-api-doc/src/client/Document.tsx @@ -1,4 +1,5 @@ -import { css, useAPIClient, useRequest } from '@nocobase/client'; +import { css, useAPIClient, useApp, useRequest } from '@nocobase/client'; +import { getSubAppName } from '@nocobase/sdk'; import { Select, Space, Spin, Typography } from 'antd'; import React, { useEffect, useRef, useState } from 'react'; import SwaggerUIBundle from 'swagger-ui-dist/swagger-ui-bundle'; @@ -13,11 +14,12 @@ const Documentation = () => { const { t } = useTranslation(); const swaggerUIRef = useRef(); const { data: urls } = useRequest<{ data: { name: string; url: string }[] }>({ url: 'swagger:getUrls' }); + const app = useApp(); const requestInterceptor = (req) => { if (!req.headers['Authorization']) { - const match = location.pathname.match(/^\/apps\/([^/]*)\//); - if (match?.[1]) { - req.headers['X-App'] = match?.[1]; + const appName = getSubAppName(app.getPublicPath()); + if (appName) { + req.headers['X-App'] = appName; } req.headers['Authorization'] = `Bearer ${apiClient.auth.getToken()}`; } diff --git a/packages/plugins/@nocobase/plugin-api-doc/src/server/swagger/index.ts b/packages/plugins/@nocobase/plugin-api-doc/src/server/swagger/index.ts index d4f55f22aa..3f56f4daf3 100644 --- a/packages/plugins/@nocobase/plugin-api-doc/src/server/swagger/index.ts +++ b/packages/plugins/@nocobase/plugin-api-doc/src/server/swagger/index.ts @@ -1,10 +1,10 @@ import APIDocPlugin from '../server'; import baseSwagger from './base-swagger'; +import collectionToSwaggerObject from './collections'; import { SchemaTypeMapping } from './constants'; import { createDefaultActionSwagger, getInterfaceCollection } from './helpers'; import { getPluginsSwagger, getSwaggerDocument, loadSwagger } from './loader'; import { merge } from './merge'; -import collectionToSwaggerObject from './collections'; export class SwaggerManager { private plugin: APIDocPlugin; @@ -81,6 +81,10 @@ export class SwaggerManager { return merge(await this.getBaseSwagger(), await loadSwagger('@nocobase/server')); } + getURL(pathname: string) { + return process.env.API_BASE_PATH + pathname; + } + async getUrls() { const plugins = await getPluginsSwagger(this.db) .then((res) => { @@ -88,7 +92,7 @@ export class SwaggerManager { const schema = res[name]; return { name: schema.info?.title || name, - url: `/api/swagger:get?ns=${encodeURIComponent(`plugins/${name}`)}`, + url: this.getURL(`swagger:get?ns=${encodeURIComponent(`plugins/${name}`)}`), }; }); }) @@ -102,25 +106,25 @@ export class SwaggerManager { return [ { name: 'NocoBase API', - url: '/api/swagger:get', + url: this.getURL('swagger:get'), }, { name: 'NocoBase API - Core', - url: '/api/swagger:get?ns=core', + url: this.getURL('swagger:get?ns=core'), }, { name: 'NocoBase API - All plugins', - url: '/api/swagger:get?ns=plugins', + url: this.getURL('swagger:get?ns=plugins'), }, { name: 'NocoBase API - Custom collections', - url: '/api/swagger:get?ns=collections', + url: this.getURL('swagger:get?ns=collections'), }, ...plugins, ...collections.map((collection) => { return { name: `Collection API - ${collection.title}`, - url: `/api/swagger:get?ns=${encodeURIComponent('collections/' + collection.name)}`, + url: this.getURL(`swagger:get?ns=${encodeURIComponent('collections/' + collection.name)}`), }; }), ]; diff --git a/packages/plugins/@nocobase/plugin-cas/src/client/SigninPage.tsx b/packages/plugins/@nocobase/plugin-cas/src/client/SigninPage.tsx index 0918d46b0f..d38aae3356 100644 --- a/packages/plugins/@nocobase/plugin-cas/src/client/SigninPage.tsx +++ b/packages/plugins/@nocobase/plugin-cas/src/client/SigninPage.tsx @@ -1,19 +1,23 @@ -import React, { useEffect } from 'react'; import { LoginOutlined } from '@ant-design/icons'; -import { Button, Space, message } from 'antd'; -import { useLocation } from 'react-router-dom'; -import { getSubAppName } from '@nocobase/sdk'; +import { useApp } from '@nocobase/client'; import { Authenticator } from '@nocobase/plugin-auth/client'; +import { getSubAppName } from '@nocobase/sdk'; +import { Button, Space, message } from 'antd'; +import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; export const SigninPage = (props: { authenticator: Authenticator }) => { const authenticator = props.authenticator; const location = useLocation(); const params = new URLSearchParams(location.search); const redirect = params.get('redirect'); + const app = useApp(); - const app = getSubAppName() || 'main'; + const appName = getSubAppName(app.getPublicPath()) || 'main'; const login = async () => { - window.location.replace(`/api/cas:login?authenticator=${authenticator.name}&__appName=${app}&redirect=${redirect}`); + window.location.replace( + `/api/cas:login?authenticator=${authenticator.name}&__appName=${appName}&redirect=${redirect}`, + ); }; useEffect(() => { diff --git a/packages/plugins/@nocobase/plugin-cas/src/server/actions/service.ts b/packages/plugins/@nocobase/plugin-cas/src/server/actions/service.ts index 132faf50c9..28db05de41 100644 --- a/packages/plugins/@nocobase/plugin-cas/src/server/actions/service.ts +++ b/packages/plugins/@nocobase/plugin-cas/src/server/actions/service.ts @@ -9,7 +9,7 @@ export const service = async (ctx: Context, next: Next) => { if (appName && appName !== 'main') { const appSupervisor = AppSupervisor.getInstance(); if (appSupervisor?.runningMode !== 'single') { - prefix = `/apps/${appName}`; + prefix = process.env.APP_PUBLIC_PATH + `apps/${appName}`; } } diff --git a/packages/plugins/@nocobase/plugin-cas/src/server/auth.ts b/packages/plugins/@nocobase/plugin-cas/src/server/auth.ts index 3a07dc9470..69c7a0e8cf 100644 --- a/packages/plugins/@nocobase/plugin-cas/src/server/auth.ts +++ b/packages/plugins/@nocobase/plugin-cas/src/server/auth.ts @@ -19,7 +19,7 @@ export class CASAuth extends BaseAuth { const opts = this.options || {}; return { ...opts, - serviceUrl: `${opts.serviceDomain}/api/cas:service`, + serviceUrl: `${opts.serviceDomain}${process.env.API_BASE_PATH}cas:service`, }; } diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts index 748a41b322..c73090bdb9 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts @@ -10,6 +10,9 @@ export class FileModel extends Model { if (!json['thumbnailRule'] && storage?.type === 'ali-oss') { json['thumbnailRule'] = '?x-oss-process=image/auto-orient,1/resize,m_fill,w_94,h_94/quality,q_90'; } + if (storage?.type === 'local' && process.env.APP_PUBLIC_PATH) { + json['url'] = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '') + json.url; + } } return json; } diff --git a/packages/plugins/@nocobase/plugin-iframe-block/src/client/Iframe.tsx b/packages/plugins/@nocobase/plugin-iframe-block/src/client/Iframe.tsx index e60f489fca..36bcb257d7 100644 --- a/packages/plugins/@nocobase/plugin-iframe-block/src/client/Iframe.tsx +++ b/packages/plugins/@nocobase/plugin-iframe-block/src/client/Iframe.tsx @@ -1,5 +1,5 @@ import { observer, useField } from '@formily/react'; -import { useAPIClient } from '@nocobase/client'; +import { useAPIClient, useApp } from '@nocobase/client'; import { Card } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,13 +22,16 @@ export const Iframe: any = observer( const { t } = useTranslation(); const api = useAPIClient(); const field = useField(); + const app = useApp(); const src = React.useMemo(() => { if (mode === 'html') { - return `/api/iframeHtml:getHtml/${htmlId}?token=${api.auth.getToken()}&v=${field.data?.v || ''}`; + const options = app.getOptions(); + const apiBaseURL: string = options?.apiClient?.['baseURL']; + return `${apiBaseURL}iframeHtml:getHtml/${htmlId}?token=${api.auth.getToken()}&v=${field.data?.v || ''}`; } return url; - }, [url, mode, htmlId, field.data?.v]); + }, [app, url, mode, htmlId, field.data?.v]); if ((mode === 'url' && !url) || (mode === 'html' && !htmlId)) { return {t('Please fill in the iframe URL')}; diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/configuration/App.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/configuration/App.tsx index 7d8e829165..2e1b576e88 100644 --- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/configuration/App.tsx +++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/configuration/App.tsx @@ -1,18 +1,14 @@ +import { useApp } from '@nocobase/client'; import { Card, Form, Input } from 'antd'; import React, { useMemo } from 'react'; import { useTranslation } from '../locale'; -import { useLocation } from 'react-router-dom'; export const AppConfiguration = () => { + const app = useApp(); const { t } = useTranslation(); - const location = useLocation(); const targetUrl = useMemo(() => { - let baseUrl = '/mobile'; - if (location.pathname.startsWith('/apps')) { - baseUrl = location.pathname.split('/').slice(0, 3).join('/'); - } - return baseUrl; - }, [location.pathname]); + return app.getRouteUrl('/mobile'); + }, [app]); return ( { - const location = useLocation(); const { t } = useTranslation(); + const app = useApp(); const onOpenInNewTab = () => { - let baseUrl = window.origin; - if (window.location.pathname.startsWith('/apps')) { - baseUrl = window.origin + window.location.pathname.split('/').slice(0, 3).join('/'); - } - window.open(`${baseUrl}${location.pathname}${location.search}`); + window.open(app.getRouteUrl('/mobile')); }; return ( diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppManager.tsx b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppManager.tsx index bd66cf223e..be7ae97ed2 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppManager.tsx +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppManager.tsx @@ -1,4 +1,4 @@ -import { SchemaComponent, useRecord } from '@nocobase/client'; +import { SchemaComponent, useApp, useRecord } from '@nocobase/client'; import { Card } from 'antd'; import React from 'react'; import { schema } from './settings/schemas/applications'; @@ -6,10 +6,11 @@ import { usePluginUtils } from './utils'; const useLink = () => { const record = useRecord(); + const app = useApp(); if (record.options?.standaloneDeployment && record.cname) { return `//${record.cname}`; } - return `/apps/${record.name}/admin/`; + return app.getRouteUrl(`/apps/${record.name}/admin/`); }; const AppVisitor = () => { diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppNameInput.tsx b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppNameInput.tsx index 3cd216ca65..a510267208 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppNameInput.tsx +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/AppNameInput.tsx @@ -1,10 +1,12 @@ import { connect, mapReadPretty } from '@formily/react'; +import { useApp } from '@nocobase/client'; import { Input as AntdInput } from 'antd'; import React from 'react'; const ReadPretty = (props) => { + const app = useApp(); const content = props.value && ( - + {props.value} ); diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx index f4a56fb2aa..d04707df4a 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx @@ -20,10 +20,10 @@ const MultiAppManager = () => { }, ); const { t } = usePluginUtils(); - const app = useApp(); + const instance = useApp(); const items = [ ...(data?.data || []).map((app) => { - let link = `/apps/${app.name}/admin/`; + let link = instance.getRouteUrl(`/apps/${app.name}/admin/`); if (app.options?.standaloneDeployment && app.cname) { link = `//${app.cname}`; } @@ -38,7 +38,9 @@ const MultiAppManager = () => { }), { key: '.manager', - label: {t('Manage applications')}, + label: ( + {t('Manage applications')} + ), }, ]; return ( diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts index 16b9a4067b..cd1c5675ec 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts @@ -115,7 +115,7 @@ const defaultAppOptionsFactory = (appName: string, mainApp: Application) => { }, plugins: ['nocobase'], resourcer: { - prefix: '/api', + prefix: process.env.API_BASE_PATH, }, }; }; diff --git a/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/plugin.ts index 6ff0b353f2..af5121e17a 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/plugin.ts @@ -274,7 +274,7 @@ export class MultiAppShareCollectionPlugin extends Plugin { }), plugins: plugins.includes('nocobase') ? ['nocobase'] : plugins, resourcer: { - prefix: '/api', + prefix: process.env.API_BASE_PATH, }, logger: { ...mainApp.options.logger, diff --git a/packages/plugins/@nocobase/plugin-oidc/src/client/Options.tsx b/packages/plugins/@nocobase/plugin-oidc/src/client/Options.tsx index a84b16979a..9dc2325d25 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/client/Options.tsx +++ b/packages/plugins/@nocobase/plugin-oidc/src/client/Options.tsx @@ -1,9 +1,9 @@ import { CopyOutlined } from '@ant-design/icons'; import { ArrayItems, FormTab } from '@formily/antd-v5'; import { observer } from '@formily/react'; -import { FormItem, Input, SchemaComponent } from '@nocobase/client'; +import { FormItem, Input, SchemaComponent, useApp } from '@nocobase/client'; import { Card, Space, message } from 'antd'; -import React from 'react'; +import React, { useMemo } from 'react'; import { lang, useOidcTranslation } from './locale'; const schema = { @@ -327,9 +327,16 @@ const schema = { const Usage = observer( () => { const { t } = useOidcTranslation(); + const app = useApp(); - const { protocol, host } = window.location; - const url = `${protocol}//${host}/api/oidc:redirect`; + const url = useMemo(() => { + const options = app.getOptions(); + const apiBaseURL: string = options?.apiClient?.['baseURL']; + const { protocol, host } = window.location; + return apiBaseURL.startsWith('http') + ? `${apiBaseURL}oidc:redirect` + : `${protocol}//${host}${apiBaseURL}oidc:redirect`; + }, [app]); const copy = (text: string) => { navigator.clipboard.writeText(text); diff --git a/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts b/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts index 42d6b04f0f..7cdadd8392 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts +++ b/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts @@ -1,6 +1,6 @@ import { Context, Next } from '@nocobase/actions'; -import { OIDCAuth } from '../oidc-auth'; import { AppSupervisor } from '@nocobase/server'; +import { OIDCAuth } from '../oidc-auth'; export const redirect = async (ctx: Context, next: Next) => { const { @@ -14,7 +14,7 @@ export const redirect = async (ctx: Context, next: Next) => { if (appName && appName !== 'main') { const appSupervisor = AppSupervisor.getInstance(); if (appSupervisor?.runningMode !== 'single') { - prefix = `/apps/${appName}`; + prefix = process.env.APP_PUBLIC_PATH + `apps/${appName}`; } } const auth = (await ctx.app.authManager.get(authenticator, ctx)) as OIDCAuth; diff --git a/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts b/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts index 5227717bff..021e5a591e 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts +++ b/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts @@ -1,7 +1,7 @@ import { AuthConfig, BaseAuth } from '@nocobase/auth'; +import { AuthModel } from '@nocobase/plugin-auth'; import { Issuer } from 'openid-client'; import { cookieName } from '../constants'; -import { AuthModel } from '@nocobase/plugin-auth'; export class OIDCAuth extends BaseAuth { constructor(config: AuthConfig) { @@ -17,7 +17,7 @@ export class OIDCAuth extends BaseAuth { const { http, port } = this.getOptions(); const protocol = http ? 'http' : 'https'; const host = port ? `${ctx.hostname}${port ? `:${port}` : ''}` : ctx.host; - return `${protocol}://${host}/api/oidc:redirect`; + return `${protocol}://${host}${process.env.API_BASE_PATH}oidc:redirect`; } getOptions() { diff --git a/packages/plugins/@nocobase/plugin-saml/src/client/Options.tsx b/packages/plugins/@nocobase/plugin-saml/src/client/Options.tsx index 5a938c5742..061a69daf2 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/client/Options.tsx +++ b/packages/plugins/@nocobase/plugin-saml/src/client/Options.tsx @@ -1,11 +1,10 @@ -import React from 'react'; -import { SchemaComponent } from '@nocobase/client'; -import { Card, message } from 'antd'; import { CopyOutlined } from '@ant-design/icons'; import { observer, useForm } from '@formily/react'; -import { useRecord, FormItem, Input } from '@nocobase/client'; -import { lang, useSamlTranslation } from './locale'; +import { FormItem, Input, SchemaComponent, useApp, useRecord } from '@nocobase/client'; import { getSubAppName } from '@nocobase/sdk'; +import { Card, message } from 'antd'; +import React, { useMemo } from 'react'; +import { lang, useSamlTranslation } from './locale'; const schema = { type: 'object', @@ -74,10 +73,19 @@ const Usage = observer( const record = useRecord(); const { t } = useSamlTranslation(); - const app = getSubAppName() || 'main'; + const app = useApp(); const name = form.values.name ?? record.name; - const { protocol, host } = window.location; - const url = `${protocol}//${host}/api/saml:redirect?authenticator=${name}&__appName=${app}`; + + const url = useMemo(() => { + const options = app.getOptions(); + const apiBaseURL: string = options?.apiClient?.['baseURL']; + const { protocol, host } = window.location; + const appName = getSubAppName(app.getPublicPath()) || 'main'; + + return apiBaseURL.startsWith('http') + ? `${apiBaseURL}saml:redirect?authenticator=${name}&__appName=${appName}` + : `${protocol}//${host}${apiBaseURL}saml:redirect?authenticator=${name}&__appName=${appName}`; + }, [app, name]); const copy = (text: string) => { navigator.clipboard.writeText(text); diff --git a/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts b/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts index 0acddaf536..21f225947c 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts +++ b/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts @@ -1,6 +1,6 @@ import { Context, Next } from '@nocobase/actions'; -import { SAMLAuth } from '../saml-auth'; import { AppSupervisor } from '@nocobase/server'; +import { SAMLAuth } from '../saml-auth'; export const redirect = async (ctx: Context, next: Next) => { const { authenticator, __appName: appName } = ctx.action.params || {}; @@ -9,7 +9,7 @@ export const redirect = async (ctx: Context, next: Next) => { if (appName && appName !== 'main') { const appSupervisor = AppSupervisor.getInstance(); if (appSupervisor?.runningMode !== 'single') { - prefix = `/apps/${appName}`; + prefix = process.env.APP_PUBLIC_PATH + `apps/${appName}`; } } const auth = (await ctx.app.authManager.get(authenticator, ctx)) as SAMLAuth; diff --git a/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts b/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts index 10ffee08c1..51fd19eb4f 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts +++ b/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts @@ -24,7 +24,7 @@ export class SAMLAuth extends BaseAuth { const name = this.authenticator.get('name'); const protocol = http ? 'http' : 'https'; return { - callbackUrl: `${protocol}://${ctx.host}/api/saml:redirect?authenticator=${name}&__appName=${ctx.app.name}`, + callbackUrl: `${protocol}://${ctx.host}${process.env.API_BASE_PATH}saml:redirect?authenticator=${name}&__appName=${ctx.app.name}`, entryPoint: ssoUrl, issuer: name, cert: certificate, diff --git a/packages/presets/nocobase/src/client/index.ts b/packages/presets/nocobase/src/client/index.ts index c3c35d81c8..657f2a8a59 100644 --- a/packages/presets/nocobase/src/client/index.ts +++ b/packages/presets/nocobase/src/client/index.ts @@ -1,4 +1,4 @@ -import { NocoBaseBuildInPlugin, Plugin } from '@nocobase/client'; +import { Application, NocoBaseBuildInPlugin, Plugin } from '@nocobase/client'; const getCurrentTimezone = () => { const timezoneOffset = new Date().getTimezoneOffset() / -60; @@ -6,15 +6,17 @@ const getCurrentTimezone = () => { return (timezoneOffset > 0 ? '+' : '-') + timezone; }; -function getBasename() { - const match = location.pathname.match(/^\/apps\/([^/]*)\//); - return match ? match[0] : '/'; +function getBasename(app: Application) { + const publicPath = app.getPublicPath(); + const pattern = `^${publicPath}apps/([^/]*)/`; + const match = location.pathname.match(new RegExp(pattern)); + return match ? match[0] : publicPath; } export class NocoBaseClientPresetPlugin extends Plugin { async afterAdd() { this.router.setType('browser'); - this.router.setBasename(getBasename()); + this.router.setBasename(getBasename(this.app)); this.app.apiClient.axios.interceptors.request.use((config) => { config.headers['X-Hostname'] = window?.location?.hostname; config.headers['X-Timezone'] = getCurrentTimezone(); diff --git a/storage/.gitignore b/storage/.gitignore index 3f29fa2b29..39489c602b 100644 --- a/storage/.gitignore +++ b/storage/.gitignore @@ -1,4 +1,5 @@ .pm2 tmp app.watch.ts -/e2e \ No newline at end of file +/e2e +nocobase.conf