mirror of
https://github.com/nocobase/nocobase
synced 2024-11-14 21:55:55 +00:00
feat: supports subdirectory deployment (#3731)
* feat: supports subdirectory deployment * feat: auto publicPath * fix: buildIndexHtml * fix: format * fix: regexp * fix: test error * fix: nocobase.conf * fix: path * fix: nocobase.conf * fix: bugs * fix: resourcer prefix * fix: cas
This commit is contained in:
parent
126f60c959
commit
b359f9eac6
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
|
89
packages/core/cli/nocobase.conf.tpl
Normal file
89
packages/core/cli/nocobase.conf.tpl
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
21
packages/core/cli/src/commands/create-nginx-conf.js
Normal file
21
packages/core/cli/src/commands/create-nginx-conf.js
Normal file
@ -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);
|
||||
});
|
||||
};
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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<T = any> = [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;
|
||||
}
|
||||
|
@ -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<BrowserRouterProps, 'children'> {
|
||||
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;
|
||||
}
|
||||
|
@ -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.');
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from './APIClient';
|
||||
export { default as getSubAppName } from './getSubAppName';
|
||||
|
||||
export { default as getSubAppName } from './getSubAppName';
|
||||
|
@ -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`,
|
||||
|
@ -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('/');
|
||||
|
@ -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;
|
||||
|
@ -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()}`;
|
||||
}
|
||||
|
@ -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)}`),
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
@ -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(() => {
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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`,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 <Card style={{ marginBottom: 24 }}>{t('Please fill in the iframe URL')}</Card>;
|
||||
|
@ -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 (
|
||||
<Card
|
||||
style={{
|
||||
|
@ -1,20 +1,15 @@
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { css, useApp } from '@nocobase/client';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../locale';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { css } from '@nocobase/client';
|
||||
import { Button } from 'antd';
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
|
||||
export const OpenInNewTab = () => {
|
||||
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 (
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 && (
|
||||
<a target={'_blank'} href={`/apps/${props.value}/admin`} rel="noreferrer">
|
||||
<a target={'_blank'} href={app.getRouteUrl(`/apps/${props.value}/admin`)} rel="noreferrer">
|
||||
{props.value}
|
||||
</a>
|
||||
);
|
||||
|
@ -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: <Link to={app.pluginSettingsManager.getRoutePath('multi-app-manager')}>{t('Manage applications')}</Link>,
|
||||
label: (
|
||||
<Link to={instance.pluginSettingsManager.getRoutePath('multi-app-manager')}>{t('Manage applications')}</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
|
@ -115,7 +115,7 @@ const defaultAppOptionsFactory = (appName: string, mainApp: Application) => {
|
||||
},
|
||||
plugins: ['nocobase'],
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
prefix: process.env.API_BASE_PATH,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
3
storage/.gitignore
vendored
3
storage/.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.pm2
|
||||
tmp
|
||||
app.watch.ts
|
||||
/e2e
|
||||
/e2e
|
||||
nocobase.conf
|
||||
|
Loading…
Reference in New Issue
Block a user