Skip to content

Commit

Permalink
fix(boards): send mail to new board members
Browse files Browse the repository at this point in the history
  • Loading branch information
WikiRik committed Dec 6, 2024
1 parent 5b07762 commit 28d8bf8
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 1 deletion.
48 changes: 47 additions & 1 deletion lib/boards.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { Sequelize, sequelize } = require('./sequelize');
const core = require('./core');
const mailer = require('./mailer');
const config = require('../config');
const constants = require('./constants');

exports.createBoard = async (req, res) => {
if (!req.permissions.manage_boards[req.body.body_id] && !req.permissions.manage_boards.global) {
Expand All @@ -28,7 +29,7 @@ exports.createBoard = async (req, res) => {
}

await sequelize.transaction(async (t) => {
await Board.create(req.body, { transaction: t });
const createdBoard = await Board.create(req.body, { transaction: t });

await mailer.sendMail({
to: config.new_board_notifications,
Expand All @@ -40,6 +41,8 @@ exports.createBoard = async (req, res) => {
positions
}
});

await this.sendNewBoardEmail(createdBoard.id, true);
});

return res.json({
Expand Down Expand Up @@ -200,3 +203,46 @@ exports.deleteBoard = async (req, res) => {
data: req.board
});
};

exports.sendNewBoardEmail = async (id, newBoard) => {
const board = await Board.findByPk(id);

if (!board) {
return;
}

if (!newBoard && moment(board.startDate).isAfter(moment())) {
return;
}

if (newBoard && moment(board.endDate).isBefore(moment())) {
return;
}

// Getting access and refresh token.
const authRequest = await core.makeRequest({
url: config.core.url + ':' + config.core.port + '/login',
method: 'POST',
body: {
username: config.core.user.login,
password: config.core.user.password
}
});

if (typeof authRequest !== 'object') {
throw new Error('Malformed response when fetching auth request: ' + authRequest);
}

if (!authRequest.success) {
throw new Error('Error fetching auth request: ' + JSON.stringify(authRequest));
}

const memberIds = [board.president, board.secretary, board.treasurer, board.other_members.map((member) => member.user_id)];
const mails = (await core.getMails(memberIds.join(','), authRequest.access_token)).data;

await mailer.sendMail({
to: mails.map((member) => member.notification_email),
subject: constants.MAIL_SUBJECTS.NEW_BOARD_EMAIL,
template: 'network_board_welcome.html',
});
};
5 changes: 5 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
MAIL_SUBJECTS: {
NEW_BOARD_EMAIL: 'MyAEGEE: Congratulations on the start of your board term!'
}
};
9 changes: 9 additions & 0 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,12 @@ module.exports.fetchUser = async (user, token) => {
name: userRequest.data.first_name + ' ' + userRequest.data.last_name
};
};

module.exports.getMails = async (ids, token) => {
const mails = await makeRequest({
url: config.core.url + ':' + config.core.port + '/members_email?query=' + ids,
token
});

return mails.data;
};
135 changes: 135 additions & 0 deletions lib/cron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const scheduler = require('node-schedule');
const moment = require('moment');

const logger = require('./logger');
const { Board } = require('../models');
const boards = require('./boards');

const JobCallbacks = {
NEW_BOARD_EMAIL: async ({ id }) => {
const board = await Board.findByPk(id);

if (!board) {
logger.warn({ id }, 'Sending new board email: Board is not found.');
return;
}

await boards.sendNewBoardEmail(id);
logger.info({ board }, 'Sending new board email: Successfully sent new board email');
}
};

class JobManager {
constructor() {
this.jobs = {};
this.currentJob = 0;

this.JOB_TYPES = {
NEW_BOARD_EMAIL: {
key: 'NEW_BOARD_EMAIL',
description: 'New board email',
callback: JobCallbacks.NEW_BOARD_EMAIL
}
};
}

addJob(jobType, time, params) {
const {
description,
callback,
key
} = jobType;

if (moment().isAfter(time)) {
logger.warn({
description,
scheduled_on: moment(time).format('YYYY-MM-DD HH:mm:SS'),
params
}, 'Job is not added: is in the past, not scheduling.');
return;
}

const id = ++this.currentJob;

scheduler.scheduleJob(time, () => this.executeJob(id));

this.jobs[id] = {
key,
description,
time,
params,
id,
callback
};
logger.info({
id,
description,
scheduled_on: moment(time).format('YYYY-MM-DD HH:mm:SS'),
params
}, 'Added a job');
return id;
}

async executeJob(id) {
const job = this.jobs[id];
if (!job) {
logger.warn({ id }, 'Job is not found.');
return;
}

logger.info({ job }, 'Executing job');
await job.callback(job.params);
logger.info({ job }, 'Executed job');
delete this.jobs[id];
}

cancelJob(id) {
const job = this.jobs[id];
if (!job) {
logger.warn({ id }, 'Job is not found.');
return;
}

logger.info({ job }, 'Cancelling job');
scheduler.cancelJob(job.job);
delete this.jobs[id];
}

// eslint-disable-next-line class-methods-use-this
async registerAllDeadlines() {
const allBoards = await Board.findAll({});
logger.info({ count: allBoards.length }, 'Registering start dates for boards...');
for (const board of allBoards) {
// Triggering model update to run hooks to set start dates.
board.changed('id', true);
await board.save();
}
}

clearJobs(key, params) {
const ids = Object.keys(this.jobs);
for (const id of ids) {
const job = this.jobs[id];

if (job.key !== key.key) {
continue;
}

if (params.id !== job.params.id) {
continue;
}

this.cancelJob(id);
}
}

clearAll() {
const ids = Object.keys(this.jobs);
for (const id of ids) {
this.cancelJob(id);
}
}
}

const manager = new JobManager();
module.exports = manager;
26 changes: 26 additions & 0 deletions lib/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const logger = require('./logger');
const config = require('../config');
const Bugsnag = require('./bugsnag');
const packageInfo = require('../package.json');
const cron = require('./cron');

exports.authenticateUser = async (req, res, next) => {
const token = req.header('x-auth-token');
Expand Down Expand Up @@ -67,6 +68,19 @@ exports.authenticateUser = async (req, res, next) => {
return next();
};

exports.ensureAuthorized = async (req, res, next) => {
// If any of the services returned HTTP 401, then we are not authorized.
if (
req.userRequest.statusCode === 401
|| req.permissionsRequest.statusCode === 401
|| (req.approveRequest && req.approveRequest.statusCode === 401)
) {
return errors.makeUnauthorizedError(res, 'Error fetching data: user is not authenticated.');
}

return next();
};

/* istanbul ignore next */
exports.healthcheck = (req, res) => {
return res.json({
Expand All @@ -79,6 +93,18 @@ exports.healthcheck = (req, res) => {
});
};

/* istanbul ignore next */
exports.getTasksList = (req, res) => {
if (!req.permissions.see_background_tasks) {
return errors.makeForbiddenError(res, 'You cannot see background tasks.');
}

return res.json({
success: true,
data: cron.jobs
});
};

/* eslint-disable no-unused-vars */
exports.notFound = (req, res, next) => errors.makeNotFoundError(res, 'No such API endpoint: ' + req.method + ' ' + req.originalUrl);

Expand Down
4 changes: 4 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const mailComponent = require('./mail_component');
const metrics = require('./metrics');
const endpointsMetrics = require('./endpoints_metrics');
const db = require('./sequelize');
const cron = require('./cron');

const server = express();
server.use(bodyParser.json());
Expand All @@ -34,6 +35,8 @@ GeneralRouter.get('/healthcheck', middlewares.healthcheck);
GeneralRouter.get('/metrics', metrics.getMetrics);
GeneralRouter.get('/metrics/requests', endpointsMetrics.getEndpointMetrics);
GeneralRouter.use(middlewares.authenticateUser);
GeneralRouter.use(middlewares.ensureAuthorized);
GeneralRouter.get('/tasks', middlewares.getTasksList);

GeneralRouter.get('/boards', boards.listAllBoards);
GeneralRouter.get('/boards/recents', boards.listMostRecentBoardsElected);
Expand Down Expand Up @@ -68,6 +71,7 @@ async function startServer() {
app = localApp;
log.info({ host: 'http://localhost:' + config.port }, 'Up and running, listening');
await db.authenticate();
await cron.registerAllDeadlines();
return res();
});
/* istanbul ignore next */
Expand Down
20 changes: 20 additions & 0 deletions models/Board.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,24 @@ const Board = sequelize.define('board', {
updatedAt: 'updated_at'
});

Board.afterUpdate((board) => {
// Yeah, nasty, but prevents us from circular dependencies issues. Been there, done that.
// eslint-disable-next-line global-require
const cron = require('../lib/cron');

// Clearing the times for sending new board emails and setting them again on afterSave() (just in case).
// Only needed on update.
cron.clearJobs(cron.JOB_TYPES.NEW_BOARD_EMAIL, { id: board.id });
});

Board.afterSave((board) => {
// Yeah, nasty, but prevents us from circular dependencies issues. Been there, done that.
// eslint-disable-next-line global-require
const cron = require('../lib/cron');

// Schedule a deadline for sending the new board emails. If it's in the past, cron
// will catch it.
cron.addJob(cron.JOB_TYPES.NEW_BOARD_EMAIL, board.start_date, { id: board.id });
});

module.exports = Board;
Loading

0 comments on commit 28d8bf8

Please sign in to comment.