app/src/routes/api/v1/subscription.router.js
const Router = require('koa-router');
const UrlService = require('services/urlService');
const logger = require('logger');
const moment = require('moment');
const mongoose = require('mongoose');
const request = require('request-promise-native');
const Subscription = require('models/subscription');
const SubscriptionService = require('services/subscriptionService');
const SubscriptionSerializer = require('serializers/subscriptionSerializer');
const UpdateService = require('services/updateService');
const DatasetService = require('services/datasetService');
const StatisticsService = require('services/statisticsService');
const mailService = require('services/mailService');
const MockService = require('services/mockService');
const GenericError = require('errors/genericError');
const AlertQueue = require('queues/alert.queue');
const config = require('config');
const redis = require('redis');
const lodashGet = require('lodash/get');
const { USER_ROLES } = require('app.constants');
const router = new Router({
prefix: '/subscriptions'
});
const CHANNEL = config.get('apiGateway.subscriptionAlertsChannelName');
const redisClient = redis.createClient({ url: config.get('redis.url') });
const serializeObjToQuery = (obj) => Object.keys(obj).reduce((a, k) => {
a.push(`${k}=${encodeURIComponent(obj[k])}`);
return a;
}, []).join('&');
class SubscriptionsRouter {
static getUser(ctx) {
const { query, body } = ctx.request;
let user;
try {
user = { ...(query.loggedUser ? JSON.parse(query.loggedUser) : {}), ...ctx.request.body.loggedUser };
} catch (error) {
ctx.throw(400, 'Invalid user token');
return null;
}
if (body.fields && body.fields.loggedUser) {
user = Object.assign(user, JSON.parse(body.fields.loggedUser));
}
return user;
}
static async getSubscription(ctx) {
logger.debug(JSON.parse(ctx.request.query.loggedUser));
const user = JSON.parse(ctx.request.query.loggedUser);
const { id } = ctx.params;
try {
ctx.body = user.role === 'ADMIN'
? SubscriptionSerializer.serialize(await SubscriptionService.getSubscriptionById(id))
: await SubscriptionService.getSubscriptionForUser(id, user.id);
} catch (err) {
logger.error(err);
}
}
static async getSubscriptionData(ctx) {
logger.debug(JSON.parse(ctx.request.query.loggedUser));
const user = JSON.parse(ctx.request.query.loggedUser);
const { id } = ctx.params;
try {
const subscription = await SubscriptionService.getSubscriptionForUser(id, user.id);
ctx.body = { data: await DatasetService.processSubscriptionData(subscription.data.id) };
} catch (err) {
logger.error(err);
ctx.throw(404, 'Subscription not found');
}
}
static async getSubscriptions(ctx) {
const user = JSON.parse(ctx.request.query.loggedUser);
logger.info('Getting subscription for user ', user.id);
try {
ctx.body = await SubscriptionService.getSubscriptionsForUser(user.id, ctx.query.application || 'gfw', ctx.query.env || 'production');
} catch (err) {
logger.error(err);
}
}
static validateSubscription(subs) {
if ((!subs.datasets || subs.datasets.length === 0) && (!subs.datasetsQuery || subs.datasetsQuery.length === 0)) {
return 'Datasets or datasetsQuery required';
}
if (!subs.language) {
return 'Language required';
}
if (!subs.resource) {
return 'Resource required';
}
if (!subs.params) {
return 'Params required';
}
return null;
}
static async createSubscription(ctx) {
logger.info('Creating subscription with body', ctx.request.body);
const message = SubscriptionsRouter.validateSubscription(ctx.request.body);
if (message) {
ctx.throw(400, message);
return;
}
ctx.body = await SubscriptionService.createSubscription(ctx.request.body);
}
static async confirmSubscription(ctx) {
logger.info('Confirming subscription by id %s', ctx.params.id);
const subscription = await SubscriptionService.confirmSubscription(
ctx.params.id
);
if (ctx.query.application && ctx.query.application === 'rw') {
ctx.redirect(UrlService.flagshipUrlRW('/myrw/areas', subscription.data.attributes.env));
} else {
ctx.redirect(UrlService.flagshipUrl('/my-gfw/subscriptions?subscription_confirmed=true'));
}
}
static async sendConfirmation(ctx) {
logger.info('Resending confirmation email for subscription with id %s', ctx.params.id);
const user = JSON.parse(ctx.request.query.loggedUser);
const { id } = ctx.params;
const subscription = await Subscription.where({
_id: id,
userId: user.id
}).findOne();
try {
SubscriptionService.sendConfirmation(subscription);
logger.info(`Redirect to: ${config.get('gfw.flagshipUrl')}/my-gfw/subscriptions`);
// Allows redirect=false flag to be provided, but defaults to applying the redirect
if (ctx.query.redirect !== 'false') {
ctx.redirect(`${config.get('gfw.flagshipUrl')}/my-gfw/subscriptions`);
return;
}
ctx.body = subscription;
} catch (err) {
logger.error(err);
}
}
static async updateSubscription(ctx) {
logger.info('Update subscription by id %s', ctx.params.id);
const message = SubscriptionsRouter.validateSubscription(ctx.request.body);
if (message) {
ctx.throw(400, message);
return;
}
ctx.body = await SubscriptionService.updateSubscription(ctx.params.id, ctx.request.body);
}
static async unsubscribeSubscription(ctx) {
logger.info('Unsubscribing subscription by id %s', ctx.params.id);
const subscription = await SubscriptionService.deleteSubscriptionById(
ctx.params.id
);
if (!subscription) {
logger.error('Subscription not found');
ctx.throw(404, 'Subscription not found');
return;
}
if (ctx.query.redirect) {
ctx.redirect(UrlService.flagshipUrl(
'/my-gfw/subscriptions?unsubscription_confirmed=true'
));
return;
}
ctx.body = subscription;
}
static async deleteSubscription(ctx) {
logger.info('Deleting subscription by id %s', ctx.params.id);
const subscription = await SubscriptionService.deleteSubscriptionById(
ctx.params.id, JSON.parse(ctx.request.query.loggedUser).id
);
if (!subscription) {
logger.error('Subscription not found');
ctx.throw(404, 'Subscription not found');
return;
}
ctx.body = subscription;
}
static async notifyUpdates(ctx) {
const { dataset } = ctx.params;
logger.info(`Notify '${dataset}' was updated`);
const result = await UpdateService.checkUpdated(dataset);
logger.info(`Checking if '${dataset}' was updated`);
if (result.updated) {
redisClient.publish(CHANNEL, JSON.stringify({
layer_slug: dataset,
begin_date: new Date(result.beginDate),
end_date: new Date(result.endDate)
}));
ctx.body = `Dataset:${dataset} was updated`;
} else {
logger.info(`${dataset} was not updated`);
ctx.body = `Dataset:${dataset} wasn't updated`;
}
}
static async statistics(ctx) {
logger.info('Obtaining statistics');
ctx.assert(ctx.query.start, 400, 'Start date required');
ctx.assert(ctx.query.end, 400, 'End date required');
ctx.body = await StatisticsService.getStatistics(new Date(ctx.query.start), new Date(ctx.query.end), ctx.query.application);
}
static async statisticsGroup(ctx) {
logger.info('Obtaining statistics group');
ctx.assert(ctx.query.start, 400, 'Start date required');
ctx.assert(ctx.query.end, 400, 'End date required');
ctx.assert(ctx.query.application, 400, 'Application required');
ctx.body = await StatisticsService.infoGroupSubscriptions(new Date(ctx.query.start), new Date(ctx.query.end), ctx.query.application);
}
static async checkHook(ctx) {
logger.info('Checking hook');
const info = ctx.request.body;
const slug = info.slug ? info.slug : 'viirs-active-fires';
const mock = MockService.getMock(slug);
if (info.type === 'EMAIL') {
mailService.sendMail('forest-fires-notification-viirs-en', mock, [{ email: info.content }]);
} else {
try {
await request({
uri: info.content,
method: 'POST',
body: mock,
json: true
});
} catch (e) {
throw new GenericError(400, `${e.message}`);
}
}
ctx.body = 'ok';
}
static async statisticsByUser(ctx) {
logger.info('Obtaining statistics by user');
ctx.assert(ctx.query.start, 400, 'Start date required');
ctx.assert(ctx.query.end, 400, 'End date required');
ctx.assert(ctx.query.application, 400, 'Application required');
ctx.body = await StatisticsService.infoByUserSubscriptions(new Date(ctx.query.start), new Date(ctx.query.end), ctx.query.application);
}
static async findByIds(ctx) {
const ids = lodashGet(ctx.request, 'body.ids', null);
if (ids === null) {
throw new GenericError(400, 'Ids not provided.');
}
logger.info(`[SubscriptionsRouter] Getting all subscriptions with ids`, ids);
ctx.body = await SubscriptionService.getSubscriptionsByIds(ids);
}
static async findUserSubscriptions(ctx) {
logger.info(`[SubscriptionsRouter] Getting all subscriptions for user with id`, ctx.params.userId);
ctx.body = await SubscriptionService.getSubscriptionsForUser(ctx.params.userId, ctx.query.application, ctx.query.env);
}
static async findAllSubscriptions(ctx) {
logger.info(`[SubscriptionsRouter] Getting ALL subscriptions`);
const page = ctx.query.page && ctx.query.page.number ? parseInt(ctx.query.page.number, 10) : 1;
const limit = ctx.query.page && ctx.query.page.size ? parseInt(ctx.query.page.size, 10) : 10;
if (limit > 100) {
throw new GenericError(400, 'Invalid page size (>100).');
}
const updatedAtSince = ctx.query.updatedAtSince ? ctx.query.updatedAtSince : null;
const updatedAtUntil = ctx.query.updatedAtUntil ? ctx.query.updatedAtUntil : null;
const clonedQuery = { ...ctx.query };
delete clonedQuery['page[size]'];
delete clonedQuery['page[number]'];
delete clonedQuery.page;
delete clonedQuery.loggedUser;
const serializedQuery = serializeObjToQuery(clonedQuery) ? `?${serializeObjToQuery(clonedQuery)}&` : '?';
const apiVersion = ctx.mountPath.split('/')[ctx.mountPath.split('/').length - 1];
const link = `${ctx.request.protocol}://${ctx.request.host}/${apiVersion}${ctx.request.path}${serializedQuery}`;
const subscriptions = await SubscriptionService.getAllSubscriptions(
link,
ctx.request.query.application,
ctx.request.query.env,
page,
limit,
updatedAtSince,
updatedAtUntil,
);
logger.info(`[SubscriptionsRouter] Subscriptions loaded, returning`);
ctx.body = subscriptions;
}
static async testEmailAlert(ctx) {
logger.info(`[EmailAlertsRouter] Starting test email alerts.`);
const {
alert, email, subId, fromDate, toDate
} = ctx.request.body;
if (!subId) {
ctx.throw(400, 'Subscription id is required.');
return;
}
if (!['glad-alerts', 'viirs-active-fires', 'monthly-summary'].includes(alert)) {
ctx.throw(400, 'The alert provided is not supported for testing.');
return;
}
try {
await AlertQueue.processMessage(null, JSON.stringify({
layer_slug: alert,
begin_date: fromDate ? moment(fromDate).toDate() : moment().subtract('2', 'w').toDate(),
end_date: toDate ? moment(toDate).toDate() : moment().subtract('1', 'w').toDate(),
email,
subId,
}));
ctx.body = { success: true };
} catch (e) {
ctx.body = { success: false, message: e.message };
}
}
}
const isAdmin = async (ctx, next) => {
const loggedUser = SubscriptionsRouter.getUser(ctx);
if (!loggedUser || USER_ROLES.indexOf(loggedUser.role) === -1) {
ctx.throw(401, 'Not authorized');
return;
}
if (loggedUser.role !== 'ADMIN') {
ctx.throw(403, 'Not authorized');
return;
}
if (!loggedUser.extraUserData || !loggedUser.extraUserData.apps || loggedUser.extraUserData.apps.indexOf('gfw') === -1) {
ctx.throw(403, 'Not authorized');
return;
}
await next();
};
const subscriptionExists = (isForUser) => async (ctx, next) => {
const { id } = ctx.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
ctx.throw(400, 'ID is not valid');
}
const user = SubscriptionsRouter.getUser(ctx);
const subscription = (isForUser && user.id !== 'microservice' && user.role !== 'ADMIN')
? await Subscription.findOne({ _id: id, userId: user.id })
: await Subscription.findById(id);
if (!subscription) {
ctx.throw(404, 'Subscription not found');
return;
}
await next();
};
const isMicroservice = (ctx) => {
const loggedUser = SubscriptionsRouter.getUser(ctx);
return loggedUser.id === 'microservice';
};
const hasLoggedUser = (ctx) => {
const loggedUser = SubscriptionsRouter.getUser(ctx);
return loggedUser && USER_ROLES.indexOf(loggedUser.role) !== -1;
};
const hasValidLoggedUser = (ctx) => {
const loggedUser = SubscriptionsRouter.getUser(ctx);
return typeof loggedUser === 'object' && loggedUser.role;
};
const validateLoggedUserAuth = async (ctx, next) => {
if (!hasLoggedUser(ctx)) {
ctx.throw(401, 'Not authorized');
return;
}
if (!hasValidLoggedUser(ctx)) {
ctx.throw(401, 'Not valid loggedUser, it should be json a valid object string in query');
return;
}
await next();
};
const validateMicroserviceAuth = async (ctx, next) => {
if (!isMicroservice(ctx)) {
ctx.throw(401, 'Not authorized');
return;
}
await next();
};
const validateLoggedUserOrMicroserviceAuth = async (ctx, next) => {
if (!(hasLoggedUser(ctx) || hasValidLoggedUser(ctx)) && !isMicroservice(ctx)) {
ctx.throw(401, 'Not authorized');
return;
}
await next();
};
router.post('/', SubscriptionsRouter.createSubscription);
router.get('/', validateLoggedUserAuth, SubscriptionsRouter.getSubscriptions);
router.get('/find-all', validateMicroserviceAuth, SubscriptionsRouter.findAllSubscriptions);
router.get('/statistics', isAdmin, SubscriptionsRouter.statistics);
router.get('/statistics-group', isAdmin, SubscriptionsRouter.statisticsGroup);
router.get('/statistics-by-user', isAdmin, SubscriptionsRouter.statisticsByUser);
router.get('/:id', validateLoggedUserAuth, subscriptionExists(true), SubscriptionsRouter.getSubscription); // not done
router.get('/:id/data', validateLoggedUserAuth, subscriptionExists(true), SubscriptionsRouter.getSubscriptionData);
router.get('/:id/confirm', subscriptionExists(), SubscriptionsRouter.confirmSubscription);
router.get('/:id/send_confirmation', validateLoggedUserAuth, subscriptionExists(true), SubscriptionsRouter.sendConfirmation);
router.get('/:id/unsubscribe', subscriptionExists(), SubscriptionsRouter.unsubscribeSubscription);
router.patch('/:id', validateLoggedUserOrMicroserviceAuth, subscriptionExists(true), SubscriptionsRouter.updateSubscription);
router.delete('/:id', validateLoggedUserOrMicroserviceAuth, subscriptionExists(true), SubscriptionsRouter.deleteSubscription);
router.post('/notify-updates/:dataset', SubscriptionsRouter.notifyUpdates);
router.post('/test-email-alerts', isAdmin, SubscriptionsRouter.testEmailAlert);
router.post('/check-hook', SubscriptionsRouter.checkHook);
router.get('/user/:userId', validateMicroserviceAuth, SubscriptionsRouter.findUserSubscriptions);
router.post('/find-by-ids', validateMicroserviceAuth, SubscriptionsRouter.findByIds);
module.exports = router;