[feature] Move info.json request to common router

This commit is contained in:
Sergey Konovalov
2025-09-05 02:24:30 +03:00
parent d17cc7448a
commit c189f8bd42
7 changed files with 258 additions and 202 deletions

View File

@ -6,7 +6,7 @@ import Button from '../../components/Button';
import styles from './styles.module.css';
export default function Login() {
const [tenantName, setTenantName] = useState('');
const [tenantName, setTenantName] = useState('localhost');
const [secret, setSecret] = useState('');
const [error, setError] = useState('');
const dispatch = useDispatch();

View File

@ -22,5 +22,11 @@
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"ms": "^2.1.3"
},
"pkg": {
"scripts": [
"../../DocService/sources/editorDataMemory.js",
"../../DocService/sources/editorDataRedis.js"
]
}
}

View File

@ -38,12 +38,12 @@ const operationContext = require('../../../Common/sources/operationContext');
const tenantManager = require('../../../Common/sources/tenantManager');
const license = require('../../../Common/sources/license');
const utils = require('../../../Common/sources/utils');
const commonDefines = require('../../../Common/sources/commondefines');
const express = require('express');
const http = require('http');
const cors = require('cors');
const path = require('path');
const infoRouter = require('../../../DocService/sources/routes/info');
const configRouter = require('./routes/config/router');
const adminpanelRouter = require('./routes/adminpanel/router');
@ -78,38 +78,10 @@ const corsWithCredentials = cors({
operationContext.global.logger.warn('AdminPanel server starting...');
app.get('/info/info.json', cors(), utils.checkClientIp, async (req, res) => {
const serverDate = new Date();
serverDate.setMilliseconds(0);
const output = {
connectionsStat: {},
licenseInfo: {},
serverInfo: {
buildVersion: commonDefines.buildVersion,
buildNumber: commonDefines.buildNumber,
date: serverDate.toISOString()
},
quota: {
edit: {connectionsCount: 0, usersCount: {unique: 0, anonymous: 0}},
view: {connectionsCount: 0, usersCount: {unique: 0, anonymous: 0}},
byMonth: []
}
};
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
await ctx.initTenantCache();
const [licenseInfo] = await tenantManager.getTenantLicense(ctx);
output.licenseInfo = licenseInfo || {};
} catch (e) {
ctx.logger && ctx.logger.warn('info.json error: %s', e.stack);
} finally {
res.json(output);
}
});
app.use('/info/config', corsWithCredentials, utils.checkClientIp, configRouter);
app.use('/info/adminpanel', corsWithCredentials, utils.checkClientIp, adminpanelRouter);
// Shared Info router (provides /info.json)
app.use('/info', infoRouter());
// todo config or _dirname. Serve AdminPanel client build as static assets
const clientBuildPath = path.resolve('client/build');

View File

@ -4416,174 +4416,6 @@ exports.healthCheck = function (req, res) {
}
});
};
exports.licenseInfo = function (req, res) {
return co(function* () {
let isError = false;
const serverDate = new Date();
//security risk of high-precision time
serverDate.setMilliseconds(0);
const output = {
connectionsStat: {},
licenseInfo: {},
serverInfo: {
buildVersion: commonDefines.buildVersion,
buildNumber: commonDefines.buildNumber,
date: serverDate.toISOString()
},
quota: {
edit: {
connectionsCount: 0,
usersCount: {
unique: 0,
anonymous: 0
}
},
view: {
connectionsCount: 0,
usersCount: {
unique: 0,
anonymous: 0
}
},
byMonth: []
}
};
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
yield ctx.initTenantCache();
ctx.logger.debug('licenseInfo start');
const [licenseInfo] = yield tenantManager.getTenantLicense(ctx);
Object.assign(output.licenseInfo, licenseInfo);
const precisionSum = {};
for (let i = 0; i < PRECISION.length; ++i) {
precisionSum[PRECISION[i].name] = {
edit: {min: Number.MAX_VALUE, sum: 0, count: 0, intervalsInPresision: PRECISION[i].val / expDocumentsStep, max: 0},
liveview: {min: Number.MAX_VALUE, sum: 0, count: 0, intervalsInPresision: PRECISION[i].val / expDocumentsStep, max: 0},
view: {min: Number.MAX_VALUE, sum: 0, count: 0, intervalsInPresision: PRECISION[i].val / expDocumentsStep, max: 0}
};
output.connectionsStat[PRECISION[i].name] = {
edit: {min: 0, avr: 0, max: 0},
liveview: {min: 0, avr: 0, max: 0},
view: {min: 0, avr: 0, max: 0}
};
}
const redisRes = yield editorStat.getEditorConnections(ctx);
const now = Date.now();
if (redisRes.length > 0) {
const expDocumentsStep95 = expDocumentsStep * 0.95;
let precisionIndex = 0;
for (let i = redisRes.length - 1; i >= 0; i--) {
const elem = redisRes[i];
let edit = elem.edit || 0;
let view = elem.view || 0;
let liveview = elem.liveview || 0;
//for cluster
while (i > 0 && elem.time - redisRes[i - 1].time < expDocumentsStep95) {
edit += elem.edit || 0;
view += elem.view || 0;
liveview += elem.liveview || 0;
i--;
}
for (let j = precisionIndex; j < PRECISION.length; ++j) {
if (now - elem.time < PRECISION[j].val) {
const precision = precisionSum[PRECISION[j].name];
precision.edit.min = Math.min(precision.edit.min, edit);
precision.edit.max = Math.max(precision.edit.max, edit);
precision.edit.sum += edit;
precision.edit.count++;
precision.view.min = Math.min(precision.view.min, view);
precision.view.max = Math.max(precision.view.max, view);
precision.view.sum += view;
precision.view.count++;
precision.liveview.min = Math.min(precision.liveview.min, liveview);
precision.liveview.max = Math.max(precision.liveview.max, liveview);
precision.liveview.sum += liveview;
precision.liveview.count++;
} else {
precisionIndex = j + 1;
}
}
}
for (const i in precisionSum) {
const precision = precisionSum[i];
const precisionOut = output.connectionsStat[i];
if (precision.edit.count > 0) {
precisionOut.edit.avr = Math.round(precision.edit.sum / precision.edit.intervalsInPresision);
precisionOut.edit.min = precision.edit.min;
precisionOut.edit.max = precision.edit.max;
}
if (precision.liveview.count > 0) {
precisionOut.liveview.avr = Math.round(precision.liveview.sum / precision.liveview.intervalsInPresision);
precisionOut.liveview.min = precision.liveview.min;
precisionOut.liveview.max = precision.liveview.max;
}
if (precision.view.count > 0) {
precisionOut.view.avr = Math.round(precision.view.sum / precision.view.intervalsInPresision);
precisionOut.view.min = precision.view.min;
precisionOut.view.max = precision.view.max;
}
}
}
const nowUTC = getLicenseNowUtc();
let execRes;
execRes = yield editorStat.getPresenceUniqueUser(ctx, nowUTC);
output.quota.edit.connectionsCount = yield editorStat.getEditorConnectionsCount(ctx, connections);
output.quota.edit.usersCount.unique = execRes.length;
execRes.forEach(elem => {
if (elem.anonym) {
output.quota.edit.usersCount.anonymous++;
}
});
execRes = yield editorStat.getPresenceUniqueViewUser(ctx, nowUTC);
output.quota.view.connectionsCount = yield editorStat.getLiveViewerConnectionsCount(ctx, connections);
output.quota.view.usersCount.unique = execRes.length;
execRes.forEach(elem => {
if (elem.anonym) {
output.quota.view.usersCount.anonymous++;
}
});
const byMonth = yield editorStat.getPresenceUniqueUsersOfMonth(ctx);
const byMonthView = yield editorStat.getPresenceUniqueViewUsersOfMonth(ctx);
const byMonthMerged = [];
for (const i in byMonth) {
if (Object.hasOwn(byMonth, i)) {
byMonthMerged[i] = {date: i, users: byMonth[i], usersView: {}};
}
}
for (const i in byMonthView) {
if (Object.hasOwn(byMonthView, i)) {
if (Object.hasOwn(byMonthMerged, i)) {
byMonthMerged[i].usersView = byMonthView[i];
} else {
byMonthMerged[i] = {date: i, users: {}, usersView: byMonthView[i]};
}
}
}
output.quota.byMonth = Object.values(byMonthMerged);
output.quota.byMonth.sort((a, b) => {
return a.date.localeCompare(b.date);
});
ctx.logger.debug('licenseInfo end');
} catch (err) {
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);
}
}
});
};
function validateInputParams(ctx, authRes, command) {
const commandsWithoutKey = ['version', 'license', 'getForgottenList'];
const isValidWithoutKey = commandsWithoutKey.includes(command.c);

View File

@ -0,0 +1,244 @@
'use strict';
const express = require('express');
const cors = require('cors');
const ms = require('ms');
const config = require('config');
const cron = require('cron');
const utils = require('../../../Common/sources/utils');
const commonDefines = require('../../../Common/sources/commondefines');
const operationContext = require('../../../Common/sources/operationContext');
const tenantManager = require('../../../Common/sources/tenantManager');
// Configuration values
const cfgExpDocumentsCron = config.get('services.CoAuthoring.expire.documentsCron');
const cfgEditorStatStorage =
config.get('services.CoAuthoring.server.editorStatStorage') || config.get('services.CoAuthoring.server.editorDataStorage');
// Initialize editor stat storage
const editorStatStorage = require(`../${cfgEditorStatStorage}`);
const editorStat = new editorStatStorage.EditorStat();
console.error(`../${cfgEditorStatStorage}`);
console.error(editorStat);
// Constants
const PRECISION = [
{name: 'hour', val: ms('1h')},
{name: 'day', val: ms('1d')},
{name: 'week', val: ms('7d')},
{name: 'month', val: ms('30d')},
{name: 'year', val: ms('365d')}
];
/**
* Get the time step in milliseconds between cron job executions
* @param {string} cronTime - Cron time expression
* @returns {number} Time difference in milliseconds between consecutive executions
*/
function getCronStep(cronTime) {
const cronJob = new cron.CronJob(cronTime, () => {});
const dates = cronJob.nextDates(2);
return dates[1] - dates[0];
}
const expDocumentsStep = getCronStep(cfgExpDocumentsCron);
/**
* Get current UTC timestamp for license calculations
* @returns {number} UTC timestamp in seconds
*/
function getLicenseNowUtc() {
const now = new Date();
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()) / 1000;
}
/**
* License info endpoint handler
* @param {import('express').Request} req Express request
* @param {import('express').Response} res Express response
*/
async function licenseInfo(req, res) {
let isError = false;
const serverDate = new Date();
// Security risk of high-precision time
serverDate.setMilliseconds(0);
const output = {
connectionsStat: {},
licenseInfo: {},
serverInfo: {
buildVersion: commonDefines.buildVersion,
buildNumber: commonDefines.buildNumber,
date: serverDate.toISOString()
},
quota: {
edit: {
connectionsCount: 0,
usersCount: {
unique: 0,
anonymous: 0
}
},
view: {
connectionsCount: 0,
usersCount: {
unique: 0,
anonymous: 0
}
},
byMonth: []
}
};
const ctx = new operationContext.Context();
try {
ctx.initFromRequest(req);
await ctx.initTenantCache();
ctx.logger.debug('licenseInfo start');
const tenantLicense = await tenantManager.getTenantLicense(ctx);
if (tenantLicense && Array.isArray(tenantLicense) && tenantLicense.length > 0) {
const [licenseInfo] = tenantLicense;
Object.assign(output.licenseInfo, licenseInfo);
}
const precisionSum = {};
for (let i = 0; i < PRECISION.length; ++i) {
precisionSum[PRECISION[i].name] = {
edit: {min: Number.MAX_VALUE, sum: 0, count: 0, intervalsInPresision: PRECISION[i].val / expDocumentsStep, max: 0},
liveview: {min: Number.MAX_VALUE, sum: 0, count: 0, intervalsInPresision: PRECISION[i].val / expDocumentsStep, max: 0},
view: {min: Number.MAX_VALUE, sum: 0, count: 0, intervalsInPresision: PRECISION[i].val / expDocumentsStep, max: 0}
};
output.connectionsStat[PRECISION[i].name] = {
edit: {min: 0, avr: 0, max: 0},
liveview: {min: 0, avr: 0, max: 0},
view: {min: 0, avr: 0, max: 0}
};
}
const redisRes = await editorStat.getEditorConnections(ctx);
const now = Date.now();
if (redisRes.length > 0) {
const expDocumentsStep95 = expDocumentsStep * 0.95;
let precisionIndex = 0;
for (let i = redisRes.length - 1; i >= 0; i--) {
const elem = redisRes[i];
let edit = elem.edit || 0;
let view = elem.view || 0;
let liveview = elem.liveview || 0;
// For cluster
while (i > 0 && elem.time - redisRes[i - 1].time < expDocumentsStep95) {
edit += elem.edit || 0;
view += elem.view || 0;
liveview += elem.liveview || 0;
i--;
}
for (let j = precisionIndex; j < PRECISION.length; ++j) {
if (now - elem.time < PRECISION[j].val) {
const precision = precisionSum[PRECISION[j].name];
precision.edit.min = Math.min(precision.edit.min, edit);
precision.edit.max = Math.max(precision.edit.max, edit);
precision.edit.sum += edit;
precision.edit.count++;
precision.view.min = Math.min(precision.view.min, view);
precision.view.max = Math.max(precision.view.max, view);
precision.view.sum += view;
precision.view.count++;
precision.liveview.min = Math.min(precision.liveview.min, liveview);
precision.liveview.max = Math.max(precision.liveview.max, liveview);
precision.liveview.sum += liveview;
precision.liveview.count++;
} else {
precisionIndex = j + 1;
}
}
}
for (const i in precisionSum) {
const precision = precisionSum[i];
const precisionOut = output.connectionsStat[i];
if (precision.edit.count > 0) {
precisionOut.edit.avr = Math.round(precision.edit.sum / precision.edit.intervalsInPresision);
precisionOut.edit.min = precision.edit.min;
precisionOut.edit.max = precision.edit.max;
}
if (precision.liveview.count > 0) {
precisionOut.liveview.avr = Math.round(precision.liveview.sum / precision.liveview.intervalsInPresision);
precisionOut.liveview.min = precision.liveview.min;
precisionOut.liveview.max = precision.liveview.max;
}
if (precision.view.count > 0) {
precisionOut.view.avr = Math.round(precision.view.sum / precision.view.intervalsInPresision);
precisionOut.view.min = precision.view.min;
precisionOut.view.max = precision.view.max;
}
}
}
const nowUTC = getLicenseNowUtc();
let execRes;
execRes = await editorStat.getPresenceUniqueUser(ctx, nowUTC);
output.quota.edit.connectionsCount = await editorStat.getEditorConnectionsCount(ctx, {});
output.quota.edit.usersCount.unique = execRes.length;
execRes.forEach(elem => {
if (elem.anonym) {
output.quota.edit.usersCount.anonymous++;
}
});
execRes = await editorStat.getPresenceUniqueViewUser(ctx, nowUTC);
output.quota.view.connectionsCount = await editorStat.getLiveViewerConnectionsCount(ctx, {});
output.quota.view.usersCount.unique = execRes.length;
execRes.forEach(elem => {
if (elem.anonym) {
output.quota.view.usersCount.anonymous++;
}
});
const byMonth = await editorStat.getPresenceUniqueUsersOfMonth(ctx);
const byMonthView = await editorStat.getPresenceUniqueViewUsersOfMonth(ctx);
const byMonthMerged = [];
for (const i in byMonth) {
if (Object.hasOwn(byMonth, i)) {
byMonthMerged[i] = {date: i, users: byMonth[i], usersView: {}};
}
}
for (const i in byMonthView) {
if (Object.hasOwn(byMonthView, i)) {
if (Object.hasOwn(byMonthMerged, i)) {
byMonthMerged[i].usersView = byMonthView[i];
} else {
byMonthMerged[i] = {date: i, users: {}, usersView: byMonthView[i]};
}
}
}
output.quota.byMonth = Object.values(byMonthMerged);
output.quota.byMonth.sort((a, b) => {
return a.date.localeCompare(b.date);
});
ctx.logger.debug('licenseInfo end');
} catch (err) {
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);
}
}
}
/**
* Create shared Info router
* @returns {import('express').Router} Router instance
*/
function createInfoRouter() {
const router = express.Router();
// License info endpoint with CORS and client IP check
router.get('/info.json', cors(), utils.checkClientIp, licenseInfo);
return router;
}
module.exports = createInfoRouter;

View File

@ -59,6 +59,7 @@ const tenantManager = require('./../../Common/sources/tenantManager');
const staticRouter = require('./routes/static');
const configRouter = require('./routes/config');
const adminpanelRouter = require('./routes/adminpanel/router');
const infoRouter = require('./routes/info');
const ms = require('ms');
const aiProxyHandler = require('./ai/aiProxyHandler');
const cors = require('cors');
@ -263,11 +264,12 @@ docsCoServer.install(server, () => {
app.post('/docbuilder', utils.checkClientIp, rawFileParser, (req, res) => {
converterService.builder(req, res);
});
app.get('/info/info.json', cors(), utils.checkClientIp, docsCoServer.licenseInfo);
app.use('/info/config', corsWithCredentials, utils.checkClientIp, configRouter);
app.use('/info/adminpanel', corsWithCredentials, utils.checkClientIp, adminpanelRouter);
app.get('/info/plugin/settings', utils.checkClientIp, aiProxyHandler.requestSettings);
app.post('/info/plugin/models', utils.checkClientIp, rawFileParser, aiProxyHandler.requestModels);
// Shared Info router (provides /info.json)
app.use('/info', infoRouter());
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);

View File

@ -46,7 +46,7 @@
"install:FileConverter": "npm ci --prefix ./FileConverter",
"install:Metrics": "npm ci --prefix ./Metrics",
"install:AdminPanel/server": "npm ci --prefix ./AdminPanel/server",
"install:AdminPanel/client": "npm ci --prefix ./AdminPanel/client",
"install:AdminPanel/client": "npm ci --prefix ./AdminPanel/client && npm --prefix ./AdminPanel/client run build",
"3d-party-lic-json:Common": "license-report --output=json --package=./Common/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
"3d-party-lic-json:DocService": "license-report --output=json --package=./DocService/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
"3d-party-lic-json:FileConverter": "license-report --output=json --package=./FileConverter/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",