mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-04-07 14:04:35 +08:00
[feature] License expired notification trigger added
This commit is contained in:
@ -43,7 +43,7 @@
|
||||
],
|
||||
"template": {
|
||||
"title": "License",
|
||||
"body": "license expires after %s!!!"
|
||||
"body": "license expires in %s!!!"
|
||||
},
|
||||
"policies": {
|
||||
"repeatInterval": "1d"
|
||||
@ -425,7 +425,8 @@
|
||||
"license" : {
|
||||
"license_file": "",
|
||||
"warning_limit_percents": 70,
|
||||
"packageType": 0
|
||||
"packageType": 0,
|
||||
"startNotifyFrom": "30d"
|
||||
},
|
||||
"FileConverter": {
|
||||
"converter": {
|
||||
|
||||
@ -37,7 +37,7 @@ const constants = require('./constants');
|
||||
const buildDate = '6/29/2016';
|
||||
const oBuildDate = new Date(buildDate);
|
||||
|
||||
exports.readLicense = function*() {
|
||||
exports.readLicense = async function () {
|
||||
const c_LR = constants.LICENSE_RESULT;
|
||||
var now = new Date();
|
||||
var startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));//first day of current month
|
||||
|
||||
@ -35,11 +35,8 @@
|
||||
const config = require('config');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const operationContext = require('./operationContext');
|
||||
|
||||
const cfgConnection = config.get('email.connectionConfiguration');
|
||||
|
||||
const ctx = new operationContext.Context();
|
||||
const connectionDefaultSettings = {
|
||||
pool: true,
|
||||
socketTimeout: 1000 * 60 * 2,
|
||||
@ -50,7 +47,7 @@ const connectionDefaultSettings = {
|
||||
const settings = Object.assign(connectionDefaultSettings, cfgConnection);
|
||||
const smtpTransporters = new Map();
|
||||
|
||||
function createTransporter(host, port, auth, messageCommonParameters = {}) {
|
||||
function createTransporter(ctx, host, port, auth, messageCommonParameters = {}) {
|
||||
const server = {
|
||||
host,
|
||||
port,
|
||||
@ -80,7 +77,7 @@ async function send(host, userLogin, mailObject) {
|
||||
return transporter.sendMail(mailObject);
|
||||
}
|
||||
|
||||
function deleteTransporter(host, userLogin) {
|
||||
function deleteTransporter(ctx, host, userLogin) {
|
||||
const transporter = smtpTransporters.get(`${host}:${userLogin}`);
|
||||
if (!transporter) {
|
||||
ctx.logger.error(`MailService: no transporter exists for host "${host}" and user "${userLogin}"`);
|
||||
@ -96,10 +93,6 @@ function transportersRelease() {
|
||||
smtpTransporters.clear();
|
||||
}
|
||||
|
||||
function isCreated(host, user) {
|
||||
return smtpTransporters
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createTransporter,
|
||||
send,
|
||||
|
||||
@ -56,23 +56,30 @@ class MailTransport extends TransportInterface {
|
||||
host = cfgMailServer.host;
|
||||
port = cfgMailServer.port;
|
||||
auth = cfgMailServer.auth;
|
||||
isTemplateRecipientData = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
mailService.createTransporter(this.host, this.port, this.auth, cfgMailMessageDefaults);
|
||||
this.isTemplateRecipientData = cfgMailMessageDefaults.from === 'from.mail@server.com' || cfgMailMessageDefaults.to === 'to.mail@server.com';
|
||||
}
|
||||
|
||||
async send(ctx, message) {
|
||||
ctx.logger.info('Notification service: MailTransport send %j', message);
|
||||
return mailService.send(this.host, this.auth.user, message);
|
||||
if (this.isTemplateRecipientData) {
|
||||
|
||||
ctx.logger.warn('Notification service: Recipient or sender e-mail address is a template address, message will not be sent.');
|
||||
return;
|
||||
}
|
||||
|
||||
return mailService.send(ctx, this.host, this.auth.user, message);
|
||||
}
|
||||
|
||||
contentGeneration(template, messageParams) {
|
||||
let text = util.format(template.body, ...messageParams);
|
||||
return {
|
||||
subject: template.title,
|
||||
text: text
|
||||
text: util.format(template.body, ...messageParams)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -105,21 +112,23 @@ class Transport {
|
||||
|
||||
async function notify(ctx, notificationType, messageParams) {
|
||||
ctx.logger.debug('Notification service: notify "%s"', notificationType);
|
||||
let tenRule;
|
||||
tenRule = ctx.getCfg('notification.rules.' + notificationType, config.get('notification.rules.' + notificationType));
|
||||
|
||||
const tenRule = ctx.getCfg(`notification.rules.${notificationType}`, config.get(`notification.rules.${notificationType}`));
|
||||
if (tenRule && checkRulePolicies(ctx, notificationType, tenRule)) {
|
||||
await notifyRule(ctx, tenRule, messageParams);
|
||||
}
|
||||
}
|
||||
|
||||
function checkRulePolicies(ctx, notificationType, tenRule) {
|
||||
const {repeatInterval} = tenRule.policies;
|
||||
const { repeatInterval } = tenRule.policies;
|
||||
const intervalMilliseconds = ms(repeatInterval) ?? defaultRepeatInterval;
|
||||
let expired = repeatIntervalsExpired.get(notificationType);
|
||||
const expired = repeatIntervalsExpired.get(notificationType);
|
||||
|
||||
if (!expired || expired <= Date.now()) {
|
||||
repeatIntervalsExpired.set(notificationType, Date.now() + intervalMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.logger.debug(`Notification service: skip rule "%s" due to repeat interval %s`, notificationType, repeatInterval);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ const license = require('./../../Common/sources/license');
|
||||
const constants = require('./../../Common/sources/constants');
|
||||
const commonDefines = require('./../../Common/sources/commondefines');
|
||||
const utils = require('./../../Common/sources/utils');
|
||||
const { readFile } = require('fs/promises');
|
||||
const { readFile, readdir } = require('fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
const cfgTenantsBaseDomain = config.get('tenants.baseDomain');
|
||||
@ -79,6 +79,17 @@ function getTenant(ctx, domain) {
|
||||
}
|
||||
return tenant;
|
||||
}
|
||||
async function getAllTenants(ctx) {
|
||||
try {
|
||||
const entitiesList = await readdir(cfgTenantsBaseDir, { withFileTypes: true });
|
||||
const dirList = entitiesList.filter(direntObj => direntObj.isDirectory()).map(directory => directory.name);
|
||||
|
||||
return dirList;
|
||||
} catch (error) {
|
||||
ctx.logger.error('getAllTenants error: ', error.stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function getTenantByConnection(ctx, conn) {
|
||||
return isMultitenantMode(ctx) ? getTenant(ctx, utils.getDomainByConnection(ctx, conn)) : getDefautTenant();
|
||||
}
|
||||
@ -395,6 +406,7 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) {
|
||||
return [res, oLicense];
|
||||
}
|
||||
|
||||
exports.getAllTenants = getAllTenants;
|
||||
exports.getDefautTenant = getDefautTenant;
|
||||
exports.getTenantByConnection = getTenantByConnection;
|
||||
exports.getTenantByRequest = getTenantByRequest;
|
||||
|
||||
@ -87,6 +87,7 @@ const bytes = require('bytes');
|
||||
const storage = require('./../../Common/sources/storage-base');
|
||||
const constants = require('./../../Common/sources/constants');
|
||||
const utils = require('./../../Common/sources/utils');
|
||||
const utilsDocService = require('./utilsDocService');
|
||||
const commonDefines = require('./../../Common/sources/commondefines');
|
||||
const statsDClient = require('./../../Common/sources/statsdclient');
|
||||
const config = require('config');
|
||||
@ -101,7 +102,7 @@ const wopiClient = require('./wopiClient');
|
||||
const queueService = require('./../../Common/sources/taskqueueRabbitMQ');
|
||||
const operationContext = require('./../../Common/sources/operationContext');
|
||||
const tenantManager = require('./../../Common/sources/tenantManager');
|
||||
const notificationService = require('../../Common/sources/notificationService');
|
||||
const { notificationTypes, ...notificationService } = require('../../Common/sources/notificationService');
|
||||
|
||||
const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage');
|
||||
const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage');
|
||||
@ -3527,7 +3528,7 @@ exports.install = function(server, callbackFunction) {
|
||||
|
||||
if (notificationPrefix) {
|
||||
//todo with yield service could throw error
|
||||
notificationService.notify(ctx, notificationService.notificationTypes.LICENSE_LIMIT, [notificationPrefix]);
|
||||
notificationService.notify(ctx, notificationTypes.LICENSE_LIMIT, [notificationPrefix]);
|
||||
}
|
||||
return licenseType;
|
||||
}
|
||||
@ -3944,8 +3945,46 @@ exports.install = function(server, callbackFunction) {
|
||||
});
|
||||
});
|
||||
};
|
||||
exports.setLicenseInfo = function(data, original ) {
|
||||
exports.setLicenseInfo = async function(globalCtx, data, original) {
|
||||
const asyncIOHandler = async function(asyncOperations, errorMessage) {
|
||||
const settledPromises = await Promise.allSettled(asyncOperations);
|
||||
|
||||
const filtered = settledPromises.filter(promise => {
|
||||
if (promise.status === 'rejected') {
|
||||
globalCtx.logger.error(errorMessage, promise.reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return filtered.map(result => result.value);
|
||||
};
|
||||
|
||||
const tenantsList = await tenantManager.getAllTenants(globalCtx);
|
||||
const cacheInitProcess = tenantsList.map(async tenant => {
|
||||
const ctx = new operationContext.Context();
|
||||
ctx.init(tenant);
|
||||
await ctx.initTenantCache();
|
||||
|
||||
return ctx;
|
||||
});
|
||||
|
||||
const tenantContexts = await asyncIOHandler(cacheInitProcess, 'setLicenseInfo error while initializing context: ');
|
||||
const pendingLicenses = tenantContexts.map(async ctx => {
|
||||
const license = await tenantManager.getTenantLicense(ctx);
|
||||
return [ctx, license];
|
||||
});
|
||||
const licenses = await asyncIOHandler(pendingLicenses, 'setLicenseInfo error while reading license: ');
|
||||
|
||||
tenantManager.setDefLicense(data, original);
|
||||
utilsDocService.notifyLicenseExpiration(globalCtx, data.endDate);
|
||||
for (const licenseInfo of licenses) {
|
||||
const ctx = licenseInfo[0];
|
||||
const endDate = licenseInfo[1].endDate;
|
||||
|
||||
utilsDocService.notifyLicenseExpiration(ctx, endDate);
|
||||
}
|
||||
};
|
||||
exports.healthCheck = function(req, res) {
|
||||
return co(function*() {
|
||||
|
||||
@ -105,19 +105,17 @@ const updatePlugins = (eventType, filename) => {
|
||||
updatePluginsTime = new Date();
|
||||
pluginsLoaded = false;
|
||||
};
|
||||
const readLicense = function*() {
|
||||
[licenseInfo, licenseOriginal] = yield* license.readLicense(cfgLicenseFile);
|
||||
const readLicense = async function () {
|
||||
[licenseInfo, licenseOriginal] = await license.readLicense(cfgLicenseFile);
|
||||
};
|
||||
const updateLicense = () => {
|
||||
return co(function*() {
|
||||
try {
|
||||
yield* readLicense();
|
||||
docsCoServer.setLicenseInfo(licenseInfo, licenseOriginal);
|
||||
operationContext.global.logger.info('End updateLicense');
|
||||
} catch (err) {
|
||||
operationContext.global.logger.error('updateLicense error: %s', err.stack);
|
||||
}
|
||||
});
|
||||
const updateLicense = async () => {
|
||||
try {
|
||||
await readLicense();
|
||||
await docsCoServer.setLicenseInfo(operationContext.global, licenseInfo, licenseOriginal);
|
||||
operationContext.global.logger.info('End updateLicense');
|
||||
} catch (err) {
|
||||
operationContext.global.logger.error('updateLicense error: %s', err.stack);
|
||||
}
|
||||
};
|
||||
|
||||
operationContext.global.logger.warn('Express server starting...');
|
||||
|
||||
@ -32,9 +32,15 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const exifParser = require("exif-parser");
|
||||
const Jimp = require("jimp");
|
||||
const config = require('config');
|
||||
const exifParser = require('exif-parser');
|
||||
const Jimp = require('jimp');
|
||||
const locale = require('windows-locale');
|
||||
const ms = require('ms');
|
||||
|
||||
const { notificationTypes, ...notificationService } = require('../../Common/sources/notificationService');
|
||||
|
||||
const cfgStartNotifyFrom = ms(config.get('license.startNotifyFrom'));
|
||||
|
||||
async function fixImageExifRotation(ctx, buffer) {
|
||||
if (!buffer) {
|
||||
@ -70,7 +76,62 @@ function localeToLCID(lang) {
|
||||
return elem && elem.id;
|
||||
}
|
||||
|
||||
function humanFriendlyExpirationTime(endTime) {
|
||||
const timeWithPostfix = (timeName, value) => `${value} ${timeName}${value > 1 ? 's' : ''}`;
|
||||
const currentTime = new Date();
|
||||
const monthDiff = getMonthDiff(currentTime, endTime);
|
||||
|
||||
if (monthDiff > 0) {
|
||||
return timeWithPostfix('month', monthDiff);
|
||||
}
|
||||
|
||||
const daysDiff = endTime.getUTCDate() - currentTime.getUTCDate();
|
||||
if (daysDiff > 0) {
|
||||
return timeWithPostfix('day', daysDiff);
|
||||
}
|
||||
|
||||
const hoursDiff = endTime.getHours() - currentTime.getHours();
|
||||
const minutesDiff = endTime.getMinutes() - currentTime.getMinutes();
|
||||
|
||||
let timeString = '';
|
||||
if (hoursDiff > 0) {
|
||||
timeString += timeWithPostfix('hour', hoursDiff);
|
||||
}
|
||||
|
||||
if (minutesDiff > 0) {
|
||||
if (timeString.length !== 0) {
|
||||
timeString += ' ';
|
||||
}
|
||||
|
||||
timeString += timeWithPostfix('minute', minutesDiff);
|
||||
}
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify server user about license expiration via configured notification transports.
|
||||
* @param {string} ctx Context.
|
||||
* @param {date} endDate Date of expiration.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function notifyLicenseExpiration(ctx, endDate) {
|
||||
if (!endDate) {
|
||||
ctx.logger.warn('notifyLicenseExpiration(): endDate is not defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDate = new Date();
|
||||
const licenseEndTime = new Date(endDate);
|
||||
|
||||
if (currentDate.getTime() >= licenseEndTime.getTime() - cfgStartNotifyFrom) {
|
||||
const formattedTimeRemaining = humanFriendlyExpirationTime(licenseEndTime);
|
||||
notificationService.notify(ctx, notificationTypes.LICENSE_EXPIRED, [formattedTimeRemaining]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fixImageExifRotation,
|
||||
localeToLCID
|
||||
};
|
||||
localeToLCID,
|
||||
notifyLicenseExpiration
|
||||
};
|
||||
|
||||
@ -47,12 +47,12 @@ if (cluster.isMaster) {
|
||||
const cfgMaxProcessCount = config.get('FileConverter.converter.maxprocesscount');
|
||||
|
||||
var workersCount = 0;
|
||||
const readLicense = function* () {
|
||||
const readLicense = async function () {
|
||||
const numCPUs = os.cpus().length;
|
||||
const availableParallelism = os.availableParallelism?.();
|
||||
operationContext.global.logger.warn('num of CPUs: %d; availableParallelism: %s', numCPUs, availableParallelism);
|
||||
workersCount = Math.ceil((availableParallelism || numCPUs) * cfgMaxProcessCount);
|
||||
let [licenseInfo] = yield* license.readLicense(cfgLicenseFile);
|
||||
let [licenseInfo] = await license.readLicense(cfgLicenseFile);
|
||||
workersCount = Math.min(licenseInfo.count, workersCount);
|
||||
//todo send license to workers for multi-tenancy
|
||||
};
|
||||
@ -73,16 +73,14 @@ if (cluster.isMaster) {
|
||||
}
|
||||
}
|
||||
};
|
||||
const updateLicense = () => {
|
||||
return co(function*() {
|
||||
try {
|
||||
yield* readLicense();
|
||||
operationContext.global.logger.warn('update cluster with %s workers', workersCount);
|
||||
updateWorkers();
|
||||
} catch (err) {
|
||||
operationContext.global.logger.error('updateLicense error: %s', err.stack);
|
||||
}
|
||||
});
|
||||
const updateLicense = async () => {
|
||||
try {
|
||||
await readLicense();
|
||||
operationContext.global.logger.warn('update cluster with %s workers', workersCount);
|
||||
updateWorkers();
|
||||
} catch (err) {
|
||||
operationContext.global.logger.error('updateLicense error: %s', err.stack);
|
||||
}
|
||||
};
|
||||
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
const { describe, test, expect, afterAll } = require('@jest/globals');
|
||||
const nodemailer = require('../../Common/node_modules/nodemailer');
|
||||
|
||||
const operationContext = require('../../Common/sources/operationContext');
|
||||
const mailService = require('../../Common/sources/mailService');
|
||||
|
||||
const ctx = new operationContext.Context();
|
||||
const defaultTestSMTPServer = {
|
||||
host: 'smtp.ethereal.email',
|
||||
port: 587
|
||||
@ -13,7 +16,6 @@ afterAll(function () {
|
||||
})
|
||||
|
||||
describe('Mail service', function () {
|
||||
console.log('!!!!!!!!!!!!!!!!', global.dev);
|
||||
describe('SMTP', function () {
|
||||
const { host, port } = defaultTestSMTPServer;
|
||||
|
||||
@ -22,7 +24,7 @@ describe('Mail service', function () {
|
||||
// Ethereial is a special SMTP sever for mailing tests in collaboration with Nodemailer.
|
||||
const accounts = await Promise.all([nodemailer.createTestAccount(), nodemailer.createTestAccount(), nodemailer.createTestAccount()]);
|
||||
const auth = accounts.map(account => { return { user: account.user, pass: account.pass }});
|
||||
auth.forEach(credential => mailService.createTransporter(host, port, credential, { from: 'some.mail@ethereal.com' }));
|
||||
auth.forEach(credential => mailService.createTransporter(ctx, host, port, credential, { from: 'some.mail@ethereal.com' }));
|
||||
|
||||
for (let i = 0; i < auth.length; i++) {
|
||||
const credentials = auth[i];
|
||||
@ -36,7 +38,7 @@ describe('Mail service', function () {
|
||||
}
|
||||
|
||||
const accountToBeDeleted = auth[1];
|
||||
mailService.deleteTransporter(host, accountToBeDeleted.user);
|
||||
mailService.deleteTransporter(ctx, host, accountToBeDeleted.user);
|
||||
|
||||
const errorPromise = mailService.send(
|
||||
host,
|
||||
|
||||
Reference in New Issue
Block a user