mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[bug] Refactor admin panel routing; Fix bug 77576
This commit is contained in:
@ -3,5 +3,3 @@
|
||||
|
||||
# Backend URL for API calls
|
||||
REACT_APP_BACKEND_URL=http://localhost:9000
|
||||
# Docservice Backend URL(only for dev mode)
|
||||
REACT_APP_DOCSERVICE_URL=http://localhost:8000
|
||||
|
||||
@ -6,29 +6,7 @@ import AuthWrapper from './components/AuthWrapper/AuthWrapper';
|
||||
import ConfigLoader from './components/ConfigLoader/ConfigLoader';
|
||||
import Menu from './components/Menu/Menu';
|
||||
import {menuItems} from './config/menuItems';
|
||||
|
||||
/**
|
||||
* Simple basename computation from URL path.
|
||||
* Basename is everything before the last path segment.
|
||||
* Examples:
|
||||
* - '/statistics' -> basename ''
|
||||
* - '/admin/' -> basename '/admin'
|
||||
* - '/admin/statistics' -> basename '/admin'
|
||||
* - '/admin/su/statistics' -> basename '/admin/su'
|
||||
* @returns {string} basename
|
||||
*/
|
||||
const getBasename = () => {
|
||||
const path = window.location.pathname || '/';
|
||||
if (path === '/') return '';
|
||||
// Treat '/prefix/' as a directory prefix
|
||||
if (path.endsWith('/')) return path.slice(0, -1);
|
||||
// Remove trailing slash (keep root '/') for consistent parsing
|
||||
const normalized = path;
|
||||
const lastSlash = normalized.lastIndexOf('/');
|
||||
// If no parent directory, there is no basename
|
||||
if (lastSlash <= 0) return '';
|
||||
return normalized.slice(0, lastSlash);
|
||||
};
|
||||
import {getBasename} from './utils/paths';
|
||||
|
||||
function App() {
|
||||
const basename = getBasename();
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL ?? '';
|
||||
const API_BASE_PATH = '/api/v1/admin';
|
||||
import {getApiBasePath, getDocServicePath} from '../utils/paths';
|
||||
|
||||
const API_BASE_PATH = getApiBasePath();
|
||||
const DOCSERVICE_URL = getDocServicePath();
|
||||
|
||||
const isNetworkError = error => {
|
||||
if (!error) return false;
|
||||
@ -21,27 +23,27 @@ const safeFetch = async (url, options = {}) => {
|
||||
};
|
||||
|
||||
export const fetchStatistics = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/stat`);
|
||||
const response = await safeFetch(`${API_BASE_PATH}/stat`);
|
||||
if (!response.ok) throw new Error('Failed to fetch statistics');
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchConfiguration = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config`, {credentials: 'include'});
|
||||
const response = await safeFetch(`${API_BASE_PATH}/config`, {credentials: 'include'});
|
||||
if (response.status === 401) throw new Error('UNAUTHORIZED');
|
||||
if (!response.ok) throw new Error('Failed to fetch configuration');
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchConfigurationSchema = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/schema`, {credentials: 'include'});
|
||||
const response = await safeFetch(`${API_BASE_PATH}/config/schema`, {credentials: 'include'});
|
||||
if (response.status === 401) throw new Error('UNAUTHORIZED');
|
||||
if (!response.ok) throw new Error('Failed to fetch configuration schema');
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateConfiguration = async configData => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/config`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
@ -61,7 +63,7 @@ export const updateConfiguration = async configData => {
|
||||
};
|
||||
|
||||
export const fetchCurrentUser = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/me`, {credentials: 'include'});
|
||||
const response = await safeFetch(`${API_BASE_PATH}/me`, {credentials: 'include'});
|
||||
if (!response.ok) throw new Error('Failed to fetch current user');
|
||||
const data = await response.json();
|
||||
if (data && data.authorized === false) {
|
||||
@ -71,13 +73,13 @@ export const fetchCurrentUser = async () => {
|
||||
};
|
||||
|
||||
export const checkSetupRequired = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/setup/required`, {credentials: 'include'});
|
||||
const response = await safeFetch(`${API_BASE_PATH}/setup/required`, {credentials: 'include'});
|
||||
if (!response.ok) throw new Error('Failed to check setup status');
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const setupAdminPassword = async ({bootstrapToken, password}) => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/setup`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/setup`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
@ -97,7 +99,7 @@ export const setupAdminPassword = async ({bootstrapToken, password}) => {
|
||||
};
|
||||
|
||||
export const login = async password => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/login`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/login`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
@ -120,7 +122,7 @@ export const login = async password => {
|
||||
};
|
||||
|
||||
export const changePassword = async ({currentPassword, newPassword}) => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/change-password`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/change-password`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
@ -140,7 +142,7 @@ export const changePassword = async ({currentPassword, newPassword}) => {
|
||||
};
|
||||
|
||||
export const logout = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/logout`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/logout`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include'
|
||||
@ -150,7 +152,7 @@ export const logout = async () => {
|
||||
};
|
||||
|
||||
export const rotateWopiKeys = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/wopi/rotate-keys`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/wopi/rotate-keys`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include'
|
||||
@ -169,8 +171,7 @@ export const rotateWopiKeys = async () => {
|
||||
};
|
||||
|
||||
export const checkHealth = async () => {
|
||||
const url = process.env.NODE_ENV === 'development' ? '/healthcheck-api' : '../healthcheck';
|
||||
const response = await safeFetch(url);
|
||||
const response = await safeFetch(`${DOCSERVICE_URL}/healthcheck`);
|
||||
if (!response.ok) throw new Error('DocService health check failed');
|
||||
const result = await response.text();
|
||||
if (result !== 'true') throw new Error('DocService health check failed');
|
||||
@ -178,7 +179,7 @@ export const checkHealth = async () => {
|
||||
};
|
||||
|
||||
export const resetConfiguration = async () => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/reset`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/config/reset`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include'
|
||||
@ -188,7 +189,7 @@ export const resetConfiguration = async () => {
|
||||
};
|
||||
|
||||
export const generateDocServerToken = async body => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/generate-docserver-token`, {
|
||||
const response = await safeFetch(`${API_BASE_PATH}/generate-docserver-token`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
@ -204,8 +205,7 @@ const callCommandService = async body => {
|
||||
const {token} = await generateDocServerToken(body);
|
||||
body.token = token;
|
||||
|
||||
const url = process.env.REACT_APP_DOCSERVICE_URL ? `${process.env.REACT_APP_DOCSERVICE_URL}/command` : '../command';
|
||||
const response = await safeFetch(url, {
|
||||
const response = await safeFetch(`${DOCSERVICE_URL}/command`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {useState, useEffect, useRef, useCallback} from 'react';
|
||||
import {generateDocServerToken} from '../../api';
|
||||
import {getBasename, getDocServicePath} from '../../utils/paths';
|
||||
|
||||
/**
|
||||
* Preview page component with ONLYOFFICE Document Editor
|
||||
@ -17,12 +18,18 @@ function Preview(props) {
|
||||
*/
|
||||
const initEditor = useCallback(async () => {
|
||||
const userName = user?.email?.split('@')[0] || 'admin';
|
||||
let url = `${getBasename()}/assets/sample.docx`;
|
||||
|
||||
// Add origin only if URL is not absolute (doesn't start with http:// or https://)
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
url = window.location.origin + url;
|
||||
}
|
||||
|
||||
const document = {
|
||||
fileType: 'docx',
|
||||
key: '0' + Math.random(),
|
||||
title: 'Example Document',
|
||||
url: `${window.location.origin}/${window.location.pathname.split('/')[1].includes('example') ? '' : window.location.pathname.split('/')[1] + '/'}assets/sample.docx`,
|
||||
url,
|
||||
permissions: {
|
||||
edit: true,
|
||||
review: true,
|
||||
@ -71,10 +78,7 @@ function Preview(props) {
|
||||
useEffect(() => {
|
||||
// Load ONLYOFFICE API script
|
||||
const script = document.createElement('script');
|
||||
const url = process.env.REACT_APP_DOCSERVICE_URL
|
||||
? `${process.env.REACT_APP_DOCSERVICE_URL}/web-apps/apps/api/documents/api.js`
|
||||
: '../web-apps/apps/api/documents/api.js';
|
||||
script.src = url;
|
||||
script.src = `${getDocServicePath()}/web-apps/apps/api/documents/api.js`;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
initEditor();
|
||||
|
||||
35
AdminPanel/client/src/utils/paths.js
Normal file
35
AdminPanel/client/src/utils/paths.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Get the base path for AdminPanel routes
|
||||
* Supports both regular /admin and vpath /vpath/admin routes
|
||||
* @returns {string} Base path (e.g., '/admin' or '/vpath/admin')
|
||||
*/
|
||||
export const getBasename = () => {
|
||||
const pathname = window.location.pathname;
|
||||
const adminMatch = pathname.match(/^(.*?\/admin)(?:\/|$)/);
|
||||
|
||||
if (adminMatch) {
|
||||
return adminMatch[1]; // Returns /admin or /vpath/admin
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Backend URL for API calls
|
||||
*/
|
||||
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL ?? '';
|
||||
|
||||
/**
|
||||
* Get relative path to DocService resources from AdminPanel
|
||||
* @returns {string} Relative path prefix
|
||||
*/
|
||||
export const getDocServicePath = () => {
|
||||
return `${BACKEND_URL}${getBasename()}/..`;
|
||||
};
|
||||
|
||||
/**
|
||||
* API base path including basename
|
||||
*/
|
||||
export const getApiBasePath = () => {
|
||||
return `${BACKEND_URL}${getBasename()}/api/v1`;
|
||||
};
|
||||
@ -27,21 +27,25 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'build'),
|
||||
publicPath: ''
|
||||
},
|
||||
static: path.join(__dirname, 'public'),
|
||||
port: 3000,
|
||||
open: true,
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
'/healthcheck-api': {
|
||||
target: process.env.REACT_APP_DOCSERVICE_URL,
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/healthcheck-api': '/healthcheck'
|
||||
open: '/admin/',
|
||||
hot: true,
|
||||
historyApiFallback: {
|
||||
index: '/admin/index.html'
|
||||
},
|
||||
devMiddleware: {
|
||||
publicPath: '/admin'
|
||||
},
|
||||
setupMiddlewares: (middlewares, devServer) => {
|
||||
// Redirect /admin -> /admin/ (only exact match without trailing slash)
|
||||
devServer.app.use((req, res, next) => {
|
||||
if (req.path === '/admin' && !req.originalUrl.endsWith('/')) {
|
||||
return res.redirect(302, '/admin/');
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
return middlewares;
|
||||
}
|
||||
},
|
||||
|
||||
@ -85,8 +89,7 @@ module.exports = (env, argv) => {
|
||||
]
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.REACT_APP_BACKEND_URL': JSON.stringify(process.env.REACT_APP_BACKEND_URL),
|
||||
'process.env.REACT_APP_DOCSERVICE_URL': JSON.stringify(process.env.REACT_APP_DOCSERVICE_URL)
|
||||
'process.env.REACT_APP_BACKEND_URL': JSON.stringify(process.env.REACT_APP_BACKEND_URL)
|
||||
})
|
||||
],
|
||||
|
||||
|
||||
136
AdminPanel/server/sources/devProxy.js
Normal file
136
AdminPanel/server/sources/devProxy.js
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2024
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const cors = require('cors');
|
||||
const operationContext = require('../../../Common/sources/operationContext');
|
||||
|
||||
/**
|
||||
* Setup development proxy to DocService
|
||||
* @param {object} app - Express app
|
||||
* @param {object} server - HTTP server
|
||||
* @param {number} targetPort - DocService port
|
||||
*/
|
||||
function setupDevProxy(app, server, targetPort) {
|
||||
/**
|
||||
* Simple proxy middleware for DocService HTTP requests
|
||||
* @param {object} req - Express request
|
||||
* @param {object} res - Express response
|
||||
* @param {Function} next - Express next
|
||||
*/
|
||||
function proxyToDocService(req, res, next) {
|
||||
// Skip requests that start with /admin
|
||||
if (req.path.startsWith('/admin')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const proxyReq = http.request(
|
||||
{
|
||||
hostname: req.hostname || 'localhost',
|
||||
port: targetPort,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: req.headers
|
||||
},
|
||||
proxyRes => {
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
}
|
||||
);
|
||||
|
||||
proxyReq.on('error', err => {
|
||||
operationContext.global.logger.error('Proxy error: %s', err.message);
|
||||
res.status(502).send('Bad Gateway');
|
||||
});
|
||||
|
||||
req.pipe(proxyReq);
|
||||
}
|
||||
|
||||
app.use(proxyToDocService);
|
||||
|
||||
/**
|
||||
* Proxy WebSocket upgrade requests to DocService
|
||||
*/
|
||||
server.on('upgrade', (req, socket, _head) => {
|
||||
// Skip WebSocket requests that start with /admin
|
||||
if (req.url.startsWith('/admin')) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyReq = http.request({
|
||||
hostname: req.headers.host?.split(':')[0] || 'localhost',
|
||||
port: targetPort,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: req.headers
|
||||
});
|
||||
|
||||
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
|
||||
// Forward upgrade response to client
|
||||
socket.write(`HTTP/${proxyRes.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage}\r\n`);
|
||||
Object.entries(proxyRes.headers).forEach(([key, value]) => {
|
||||
socket.write(`${key}: ${value}\r\n`);
|
||||
});
|
||||
socket.write('\r\n');
|
||||
socket.write(proxyHead);
|
||||
|
||||
// Bidirectional pipe
|
||||
proxySocket.pipe(socket);
|
||||
socket.pipe(proxySocket);
|
||||
});
|
||||
|
||||
proxyReq.on('error', err => {
|
||||
operationContext.global.logger.error('WebSocket proxy error: %s', err.message);
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
proxyReq.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CORS middleware for development
|
||||
* @returns {Function} CORS middleware
|
||||
*/
|
||||
function getDevCors() {
|
||||
return cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {setupDevProxy, getDevCors};
|
||||
@ -42,7 +42,6 @@ const runtimeConfigManager = require('../../../Common/sources/runtimeConfigManag
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const infoRouter = require('../../../DocService/sources/routes/info');
|
||||
@ -52,9 +51,11 @@ const adminpanelRouter = require('./routes/adminpanel/router');
|
||||
const wopiRouter = require('./routes/wopi/router');
|
||||
const passwordManager = require('./passwordManager');
|
||||
const bootstrap = require('./bootstrap');
|
||||
const devProxy = require('./devProxy');
|
||||
|
||||
const port = config.get('adminPanel.port');
|
||||
const cfgLicenseFile = config.get('license.license_file');
|
||||
const cfgCoAuthoringPort = config.get('services.CoAuthoring.server.port');
|
||||
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
@ -111,12 +112,10 @@ setInterval(updateLicense, 86400000);
|
||||
}
|
||||
})();
|
||||
|
||||
const corsWithCredentials = cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
});
|
||||
// Development mode setup: CORS + DocService proxy
|
||||
if (process.env.NODE_ENV.startsWith('development-')) {
|
||||
app.use(devProxy.getDevCors());
|
||||
}
|
||||
|
||||
operationContext.global.logger.warn('AdminPanel server starting...');
|
||||
|
||||
@ -129,26 +128,54 @@ function disableCache(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
app.use('/api/v1/admin/config', corsWithCredentials, utils.checkClientIp, disableCache, configRouter);
|
||||
app.use('/api/v1/admin/wopi', corsWithCredentials, utils.checkClientIp, disableCache, wopiRouter);
|
||||
app.use('/api/v1/admin', corsWithCredentials, utils.checkClientIp, disableCache, adminpanelRouter);
|
||||
app.get('/api/v1/admin/stat', corsWithCredentials, utils.checkClientIp, disableCache, async (req, res) => {
|
||||
// API routes under /admin prefix
|
||||
app.use('/admin/api/v1/config', utils.checkClientIp, disableCache, configRouter);
|
||||
app.use('/admin/api/v1/wopi', utils.checkClientIp, disableCache, wopiRouter);
|
||||
app.use('/admin/api/v1', utils.checkClientIp, disableCache, adminpanelRouter);
|
||||
app.get('/admin/api/v1/stat', utils.checkClientIp, disableCache, async (req, res) => {
|
||||
await infoRouter.licenseInfo(req, res);
|
||||
});
|
||||
// Serve AdminPanel client build as static assets
|
||||
const clientBuildPath = path.resolve('client/build');
|
||||
app.use('/', express.static(clientBuildPath));
|
||||
|
||||
// Serve AdminPanel client build as static assets under /admin
|
||||
const clientBuildPath = path.resolve('client/build');
|
||||
|
||||
/**
|
||||
* Custom middleware to handle /admin redirect with relative path
|
||||
* Prevents express.static from doing absolute 302 redirect
|
||||
*/
|
||||
app.use('/admin', (req, res, next) => {
|
||||
// If path is exactly /admin (no trailing slash), redirect relatively
|
||||
if ((req.path === '' || req.path === '/') && !req.originalUrl.endsWith('/')) {
|
||||
// Relative redirect preserves virtual path prefix
|
||||
return res.redirect(302, 'admin/');
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/admin', express.static(clientBuildPath));
|
||||
|
||||
/**
|
||||
* Serves SPA index.html for client-side routing
|
||||
* @param {object} req - Express request
|
||||
* @param {object} res - Express response
|
||||
* @param {Function} next - Express next middleware
|
||||
*/
|
||||
function serveSpaIndex(req, res, next) {
|
||||
if (req.path.startsWith('/api')) return next();
|
||||
if (req.path.startsWith('/admin/api')) return next();
|
||||
|
||||
// Disable caching for SPA index.html to ensure updates work
|
||||
disableCache(req, res, () => {});
|
||||
|
||||
res.sendFile(path.join(clientBuildPath, 'index.html'));
|
||||
}
|
||||
// Client SPA routes fallback
|
||||
app.get('*', serveSpaIndex);
|
||||
|
||||
// Client SPA routes fallback for /admin/*
|
||||
app.get('/admin/*', serveSpaIndex);
|
||||
|
||||
// Development mode: proxy non-/admin requests to DocService (must be last)
|
||||
if (process.env.NODE_ENV.startsWith('development-')) {
|
||||
devProxy.setupDevProxy(app, server, cfgCoAuthoringPort);
|
||||
}
|
||||
|
||||
app.use((err, req, res, _next) => {
|
||||
const ctx = new operationContext.Context();
|
||||
|
||||
@ -92,6 +92,20 @@ const cfgDownloadMaxBytes = config.get('FileConverter.converter.maxDownloadBytes
|
||||
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Enable CORS in development mode for AdminPanel webpack dev server
|
||||
if (process.env.NODE_ENV.startsWith('development-')) {
|
||||
const cors = require('cors');
|
||||
app.use(
|
||||
cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
//path.resolve uses __dirname by default(unexpected path in pkg)
|
||||
app.set('views', path.resolve(process.cwd(), cfgHtmlTemplate));
|
||||
app.set('view engine', 'ejs');
|
||||
@ -162,44 +176,6 @@ docsCoServer.install(server, app, () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Proxy AdminPanel endpoints for testing
|
||||
if (process.env.NODE_ENV.startsWith('development-')) {
|
||||
/**
|
||||
* Simple proxy to localhost:9000 for testing AdminPanel routes
|
||||
* @param {string} pathPrefix - Path to prepend or empty string to strip mount path
|
||||
*/
|
||||
const proxyToAdmin =
|
||||
(pathPrefix = '') =>
|
||||
(req, res) => {
|
||||
const targetPath = pathPrefix + req.url;
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: 9000,
|
||||
path: targetPath,
|
||||
method: req.method,
|
||||
headers: {...req.headers, host: 'localhost:9000'}
|
||||
};
|
||||
|
||||
const proxyReq = http.request(options, proxyRes => {
|
||||
res.status(proxyRes.statusCode);
|
||||
Object.entries(proxyRes.headers).forEach(([key, value]) => res.setHeader(key, value));
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
|
||||
proxyReq.on('error', () => res.sendStatus(502));
|
||||
req.pipe(proxyReq);
|
||||
};
|
||||
|
||||
app.use('/api/v1/admin', proxyToAdmin('/api/v1/admin'));
|
||||
app.all('/admin', (req, res, next) => {
|
||||
if (req.path === '/admin' && !req.path.endsWith('/')) {
|
||||
return res.redirect(302, '/admin/');
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.use('/admin', proxyToAdmin());
|
||||
}
|
||||
|
||||
app.get('/index.html', (req, res) => {
|
||||
return co(function* () {
|
||||
const ctx = new operationContext.Context();
|
||||
|
||||
Reference in New Issue
Block a user