mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
Merge pull request 'fix/admin-panel-bugs-5' (#76) from fix/admin-panel-bugs-5 into release/v9.1.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/76
This commit is contained in:
87
AdminPanel/client/src/components/Note/Note.js
Normal file
87
AdminPanel/client/src/components/Note/Note.js
Normal file
@ -0,0 +1,87 @@
|
||||
import styles from './Note.module.scss';
|
||||
|
||||
/**
|
||||
* Note component for displaying different types of messages
|
||||
* @param {Object} props - Component properties
|
||||
* @param {('note'|'warning'|'tip'|'important')} props.type - Type of note to display
|
||||
* @param {React.ReactNode} props.children - Content to display in the note
|
||||
* @returns {JSX.Element} Note component
|
||||
*/
|
||||
function Note({type = 'note', children}) {
|
||||
const typeConfig = {
|
||||
note: {
|
||||
title: 'Note',
|
||||
className: styles.note,
|
||||
icon: (
|
||||
<svg className={styles.icon} width='24' height='24' viewBox='0 0 24 24' fill='none'>
|
||||
<circle cx='12' cy='12' r='10' stroke='#FF6F3D' strokeWidth='2' />
|
||||
<path d='M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z' fill='#FF6F3D' />
|
||||
<path
|
||||
d='M12 10C12.5523 10 13 10.4477 13 11V17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17V11C11 10.4477 11.4477 10 12 10Z'
|
||||
fill='#FF6F3D'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
warning: {
|
||||
title: 'Warning',
|
||||
className: styles.warning,
|
||||
icon: (
|
||||
<svg className={styles.icon} width='24' height='24' viewBox='0 0 24 24' fill='none'>
|
||||
<path
|
||||
d='M10.5 4C11.1667 3 12.8333 3 13.5 4L21 17C21.6667 18 21 19.5 19.6667 19.5H5.33333C4 19.5 3.33333 18 4 17L10.5 4Z'
|
||||
stroke='#CB0000'
|
||||
strokeWidth='2'
|
||||
/>
|
||||
<path
|
||||
d='M12 8C12.5523 8 13 8.44772 13 9V13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13V9C11 8.44772 11.4477 8 12 8Z'
|
||||
fill='#CB0000'
|
||||
/>
|
||||
<circle cx='12' cy='16.5' r='1' fill='#CB0000' />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
tip: {
|
||||
title: 'Tip',
|
||||
className: styles.tip,
|
||||
icon: (
|
||||
<svg className={styles.icon} width='24' height='24' viewBox='0 0 24 24' fill='none'>
|
||||
<circle cx='12' cy='12' r='10' stroke='#007B14' strokeWidth='2' />
|
||||
<path d='M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z' fill='#007B14' />
|
||||
<path
|
||||
d='M12 10C12.5523 10 13 10.4477 13 11V17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17V11C11 10.4477 11.4477 10 12 10Z'
|
||||
fill='#007B14'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
important: {
|
||||
title: 'Important',
|
||||
className: styles.important,
|
||||
icon: (
|
||||
<svg className={styles.icon} width='24' height='24' viewBox='0 0 24 24' fill='none'>
|
||||
<rect x='2' y='2' width='20' height='20' rx='2' stroke='#262BA5' strokeWidth='2' />
|
||||
<path d='M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z' fill='#262BA5' />
|
||||
<path
|
||||
d='M12 10C12.5523 10 13 10.4477 13 11V17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17V11C11 10.4477 11.4477 10 12 10Z'
|
||||
fill='#262BA5'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
const config = typeConfig[type] || typeConfig.note;
|
||||
|
||||
return (
|
||||
<div className={`${styles.noteContainer} ${config.className}`}>
|
||||
<div className={styles.header}>
|
||||
{config.icon}
|
||||
<span className={styles.title}>{config.title}</span>
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Note;
|
||||
101
AdminPanel/client/src/components/Note/Note.module.scss
Normal file
101
AdminPanel/client/src/components/Note/Note.module.scss
Normal file
@ -0,0 +1,101 @@
|
||||
/* Base note container */
|
||||
.noteContainer {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 2px;
|
||||
flex: none;
|
||||
align-self: stretch;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* Header with icon and title */
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* Icon styling */
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* Title text */
|
||||
.title {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: -0.02em;
|
||||
flex: none;
|
||||
order: 1;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* Content text */
|
||||
.content {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 150%;
|
||||
color: #333333;
|
||||
flex: none;
|
||||
order: 1;
|
||||
align-self: stretch;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* Note variant - Orange */
|
||||
.note {
|
||||
border-left: 2px solid #ff6f3d;
|
||||
|
||||
.title {
|
||||
color: #ff6f3d;
|
||||
}
|
||||
}
|
||||
|
||||
/* Warning variant - Red */
|
||||
.warning {
|
||||
border-left: 2px solid #cb0000;
|
||||
border-radius: 2px 0 0 2px;
|
||||
|
||||
.title {
|
||||
color: #cb0000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tip variant - Green */
|
||||
.tip {
|
||||
border-left: 2px solid #007b14;
|
||||
|
||||
.title {
|
||||
color: #007b14;
|
||||
}
|
||||
}
|
||||
|
||||
/* Important variant - Blue */
|
||||
.important {
|
||||
border-left: 2px solid #262ba5;
|
||||
|
||||
.title {
|
||||
color: #262ba5;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
.forgottenPage {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.pageHeader {
|
||||
|
||||
@ -11,6 +11,7 @@ import ToggleSwitch from '../../components/ToggleSwitch/ToggleSwitch';
|
||||
import Input from '../../components/Input/Input';
|
||||
import Checkbox from '../../components/Checkbox/Checkbox';
|
||||
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
|
||||
import Note from '../../components/Note/Note';
|
||||
import styles from './WOPISettings.module.scss';
|
||||
|
||||
function WOPISettings() {
|
||||
@ -148,6 +149,9 @@ function WOPISettings() {
|
||||
<div className={styles.sectionDescription}>
|
||||
Rotate WOPI encryption keys. Current keys will be moved to "Old" and new keys will be generated.
|
||||
</div>
|
||||
<div className={styles.noteWrapper}>
|
||||
<Note type='warning'>Do not rotate keys more than once per 24 hours; storage may not refresh in time and authentication can fail.</Note>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
label='Current Public Key'
|
||||
|
||||
@ -37,6 +37,10 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.noteWrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
|
||||
@ -7,6 +7,7 @@ const runtimeConfigManager = require('../../../../../Common/sources/runtimeConfi
|
||||
const {getScopedConfig, validateScoped, getScopedSchema} = require('./config.service');
|
||||
const {validateJWT} = require('../../middleware/auth');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const utils = require('../../../../../Common/sources/utils');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(cookieParser());
|
||||
@ -66,10 +67,10 @@ router.patch('/', validateJWT, rawFileParser, async (req, res) => {
|
||||
} else {
|
||||
await runtimeConfigManager.saveConfig(ctx, validationResult.value);
|
||||
}
|
||||
|
||||
const filteredConfig = getScopedConfig(ctx);
|
||||
const newConfig = await runtimeConfigManager.getConfig(ctx);
|
||||
|
||||
res.status(200).json(newConfig);
|
||||
res.status(200).json(utils.deepMergeObjects(filteredConfig, newConfig));
|
||||
} catch (error) {
|
||||
ctx.logger.error('Configuration save error: %s', error.stack);
|
||||
res.status(500).json({error: 'Internal server error', details: error.message});
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const utils = require('../../../../../Common/sources/utils');
|
||||
@ -41,6 +42,11 @@ const {validateJWT} = require('../../middleware/auth');
|
||||
const {getConfig} = require('../../../../../Common/sources/runtimeConfigManager');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const cfgWopiPublicKey = config.get('wopi.publicKey');
|
||||
const cfgWopiModulus = config.get('wopi.modulus');
|
||||
const cfgWopiPrivateKey = config.get('wopi.privateKey');
|
||||
const cfgWopiExponent = config.get('wopi.exponent');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(cookieParser());
|
||||
|
||||
@ -154,21 +160,26 @@ function generateWopiKeys() {
|
||||
router.post('/rotate-keys', validateJWT, express.json(), async (req, res) => {
|
||||
const ctx = req.ctx;
|
||||
try {
|
||||
ctx.initTenantCache();
|
||||
ctx.logger.info('WOPI key rotation start');
|
||||
|
||||
const currentConfig = await getConfig(ctx);
|
||||
const wopiConfig = currentConfig.wopi || {};
|
||||
|
||||
const newWopiConfig = generateWopiKeys();
|
||||
|
||||
const hasEmptyKeys = !wopiConfig.publicKey && !wopiConfig.modulus && !wopiConfig.privateKey;
|
||||
const publicKey = ctx.getCfg('wopi.publicKey', cfgWopiPublicKey);
|
||||
const modulus = ctx.getCfg('wopi.modulus', cfgWopiModulus);
|
||||
const privateKey = ctx.getCfg('wopi.privateKey', cfgWopiPrivateKey);
|
||||
const exponent = ctx.getCfg('wopi.exponent', cfgWopiExponent);
|
||||
|
||||
const hasEmptyKeys = !(publicKey && modulus && privateKey && exponent);
|
||||
|
||||
const configUpdate = {
|
||||
wopi: {
|
||||
publicKeyOld: hasEmptyKeys ? newWopiConfig.publicKey : wopiConfig.publicKey,
|
||||
modulusOld: hasEmptyKeys ? newWopiConfig.modulus : wopiConfig.modulus,
|
||||
exponentOld: hasEmptyKeys ? newWopiConfig.exponent : wopiConfig.exponent,
|
||||
privateKeyOld: hasEmptyKeys ? newWopiConfig.privateKey : wopiConfig.privateKey,
|
||||
publicKeyOld: hasEmptyKeys ? newWopiConfig.publicKey : publicKey,
|
||||
modulusOld: hasEmptyKeys ? newWopiConfig.modulus : modulus,
|
||||
exponentOld: hasEmptyKeys ? newWopiConfig.exponent : exponent,
|
||||
privateKeyOld: hasEmptyKeys ? newWopiConfig.privateKey : privateKey,
|
||||
publicKey: newWopiConfig.publicKey,
|
||||
modulus: newWopiConfig.modulus,
|
||||
exponent: newWopiConfig.exponent,
|
||||
|
||||
@ -126,8 +126,9 @@ function disableCache(req, res, 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, infoRouter.licenseInfo);
|
||||
|
||||
app.get('/api/v1/admin/stat', corsWithCredentials, 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));
|
||||
|
||||
@ -38,6 +38,8 @@ const mailService = require('./mailService');
|
||||
|
||||
const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage');
|
||||
const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage');
|
||||
const cfgSmtpServerConfiguration = config.get('email.smtpServerConfiguration');
|
||||
const cfgContactDefaults = config.get('email.contactDefaults');
|
||||
const editorStatStorage = require('./../../DocService/sources/' + (cfgEditorStatStorage || cfgEditorDataStorage));
|
||||
|
||||
const editorStat = editorStatStorage.EditorStat ? new editorStatStorage.EditorStat() : new editorStatStorage();
|
||||
@ -57,11 +59,11 @@ class MailTransport extends TransportInterface {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
const mailServerConfig = ctx.getCfg('email.smtpServerConfiguration');
|
||||
const mailServerConfig = ctx.getCfg('email.smtpServerConfiguration', cfgSmtpServerConfiguration);
|
||||
this.host = mailServerConfig.host;
|
||||
this.port = mailServerConfig.port;
|
||||
this.auth = mailServerConfig.auth;
|
||||
const cfgMailMessageDefaults = ctx.getCfg('email.contactDefaults');
|
||||
const cfgMailMessageDefaults = ctx.getCfg('email.contactDefaults', cfgContactDefaults);
|
||||
|
||||
mailService.createTransporter(ctx, this.host, this.port, this.auth, cfgMailMessageDefaults);
|
||||
}
|
||||
|
||||
@ -4695,6 +4695,15 @@ exports.shutdown = function (req, res) {
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Get active connections array
|
||||
* @returns {Array} Active connections
|
||||
*/
|
||||
function getConnections() {
|
||||
return connections;
|
||||
}
|
||||
|
||||
exports.getConnections = getConnections;
|
||||
exports.getEditorConnectionsCount = function (req, res) {
|
||||
const ctx = new operationContext.Context();
|
||||
let count = 0;
|
||||
|
||||
@ -353,10 +353,13 @@ async function getPluginSettingsForInterface(ctx) {
|
||||
pluginSettings = undefined;
|
||||
}
|
||||
}
|
||||
//remove keys from providers
|
||||
if (pluginSettings && pluginSettings.providers) {
|
||||
//remove keys from providers - create deep copy to avoid modifying cached config
|
||||
if (pluginSettings?.providers) {
|
||||
for (const key in pluginSettings.providers) {
|
||||
pluginSettings.providers[key].key = '';
|
||||
if (pluginSettings.providers[key]?.key) {
|
||||
pluginSettings.providers[key] = JSON.parse(JSON.stringify(pluginSettings.providers[key]));
|
||||
pluginSettings.providers[key].key = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return pluginSettings;
|
||||
|
||||
@ -54,8 +54,9 @@ function getLicenseNowUtc() {
|
||||
* License info endpoint handler
|
||||
* @param {import('express').Request} req Express request
|
||||
* @param {import('express').Response} res Express response
|
||||
* @param {Function} getConnections Function to get active connections
|
||||
*/
|
||||
async function licenseInfo(req, res) {
|
||||
async function licenseInfo(req, res, getConnections = null) {
|
||||
let isError = false;
|
||||
const serverDate = new Date();
|
||||
// Security risk of high-precision time
|
||||
@ -174,7 +175,8 @@ async function licenseInfo(req, res) {
|
||||
const nowUTC = getLicenseNowUtc();
|
||||
let execRes;
|
||||
execRes = await editorStat.getPresenceUniqueUser(ctx, nowUTC);
|
||||
output.quota.edit.connectionsCount = await editorStat.getEditorConnectionsCount(ctx, {});
|
||||
const connections = getConnections ? getConnections() : null;
|
||||
output.quota.edit.connectionsCount = await editorStat.getEditorConnectionsCount(ctx, connections);
|
||||
output.quota.edit.usersCount.unique = execRes.length;
|
||||
execRes.forEach(elem => {
|
||||
if (elem.anonym) {
|
||||
@ -183,7 +185,7 @@ async function licenseInfo(req, res) {
|
||||
});
|
||||
|
||||
execRes = await editorStat.getPresenceUniqueViewUser(ctx, nowUTC);
|
||||
output.quota.view.connectionsCount = await editorStat.getLiveViewerConnectionsCount(ctx, {});
|
||||
output.quota.view.connectionsCount = await editorStat.getLiveViewerConnectionsCount(ctx, connections);
|
||||
output.quota.view.usersCount.unique = execRes.length;
|
||||
execRes.forEach(elem => {
|
||||
if (elem.anonym) {
|
||||
@ -218,24 +220,29 @@ async function licenseInfo(req, res) {
|
||||
isError = true;
|
||||
ctx.logger.error('licenseInfo error %s', err.stack);
|
||||
} finally {
|
||||
if (!isError) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify(output));
|
||||
} else {
|
||||
res.sendStatus(400);
|
||||
if (!res.headersSent) {
|
||||
if (!isError) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify(output));
|
||||
} else {
|
||||
res.sendStatus(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shared Info router
|
||||
* @param {Function} getConnections Optional function to get active connections
|
||||
* @returns {import('express').Router} Router instance
|
||||
*/
|
||||
function createInfoRouter() {
|
||||
function createInfoRouter(getConnections = null) {
|
||||
const router = express.Router();
|
||||
|
||||
// License info endpoint with CORS and client IP check
|
||||
router.get('/info.json', cors(), utils.checkClientIp, licenseInfo);
|
||||
router.get('/info.json', cors(), utils.checkClientIp, async (req, res) => {
|
||||
await licenseInfo(req, res, getConnections);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@ -295,7 +295,7 @@ docsCoServer.install(server, app, () => {
|
||||
converterService.builder(req, res);
|
||||
});
|
||||
// Shared Info router (provides /info.json)
|
||||
app.use('/info', infoRouter());
|
||||
app.use('/info', infoRouter(docsCoServer.getConnections));
|
||||
app.put('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
|
||||
app.delete('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
|
||||
app.get('/internal/connections/edit', docsCoServer.getEditorConnectionsCount);
|
||||
|
||||
Reference in New Issue
Block a user