[feature] License expired notification trigger added

This commit is contained in:
Georgii Petrov
2024-05-15 15:34:50 +03:00
parent eb8f0a77c8
commit 3d52d3f241
10 changed files with 167 additions and 54 deletions

View File

@ -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": {

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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*() {

View File

@ -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...');

View File

@ -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
};

View File

@ -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) => {

View File

@ -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,