gfw-api/gfw-area

View on GitHub
app/src/routes/api/v2/area.router.js

Summary

Maintainability
F
1 wk
Test Coverage
A
91%
const Router = require('koa-router');
const logger = require('logger');
const config = require('config');
const moment = require('moment');
const AreaSerializerV2 = require('serializers/area.serializerV2');
const AreaModel = require('models/area.modelV2');
const AreaValidatorV2 = require('validators/area.validatorV2');
const TeamService = require('services/team.service');
const SubscriptionService = require('services/subscription.service');
const mongoose = require('mongoose');
const MailService = require('services/mail.service');
const gladAlertTypes = require('models/glad-alert-types');
const UserService = require('../../../services/user.service');

const shouldUseAllFilter = (ctx) => ctx.state.loggedUser.role === 'ADMIN' && ctx.query.all && ctx.query.all.trim().toLowerCase() === 'true';

const getHostForPaginationLink = (ctx) => {
    if ('x-rw-domain' in ctx.request.header) {
        return ctx.request.header['x-rw-domain'];
    }
    if ('referer' in ctx.request.header) {
        const url = new URL(ctx.request.header.referer);
        return url.host;
    }
    return ctx.request.host;
};

function getFilters(ctx) {
    const filter = shouldUseAllFilter(ctx) ? {} : { userId: ctx.state.loggedUser.id };

    const { query } = ctx;
    if (query.application) {
        filter.application = query.application.split(',').map((el) => el.trim());
    }

    if (query.status) {
        filter.status = query.status.trim();
    }

    if (query.public) {
        filter.public = query.public.trim().toLowerCase() === 'true';
    }

    const env = query.env ? query.env : 'production';

    if (env === 'all') {
        logger.debug(`Applying all env filter`);
    } else {
        filter.env = { $in: env.split(',').map((elem) => elem.trim()) };
    }

    return filter;
}

function getFilteredSort(sort) {
    const sortParams = sort.split(',');
    const filteredSort = {};
    const areaAttributes = Object.keys(AreaModel.schema.obj);
    sortParams.forEach((param) => {
        let sign = param.substr(0, 1);
        let signlessParam = param.substr(1);
        if (sign !== '-' && sign !== '+') {
            signlessParam = param;
            sign = '+';
        }
        if (areaAttributes.indexOf(signlessParam) >= 0) {
            filteredSort[signlessParam] = parseInt(sign + 1, 10);
        }
    });
    return filteredSort;
}

function getEmailParametersFromArea(area) {
    const { id, name } = area;
    const emailTags = area.tags && area.tags.join(', ');

    return {
        id,
        name,
        tags: emailTags,
        image_url: area.image,
        location: name,
        subscriptions_url: `${config.get('gfw.flagshipUrl')}my-gfw?lang=${area.language}`,
        dashboard_link: `${config.get('gfw.flagshipUrl')}dashboards/aoi/${id}?lang=${area.language}`,
        map_link: `${config.get('gfw.flagshipUrl')}map/aoi/${id}?lang=${area.language}`,
    };
}

const serializeObjToQuery = (obj) => Object.keys(obj).reduce((a, k) => {
    a.push(`${k}=${encodeURIComponent(obj[k])}`);
    return a;
}, []).join('&');

const SUPPORTED_LANG_CODES = ['en', 'fr', 'es_MX', 'pt_BR', 'zh', 'id'];
const DEFAULT_LANG_CODE = 'en';

class AreaRouterV2 {

    static async getAll(ctx) {
        logger.info('[AREAS-V2-ROUTER] Obtaining all v2 areas of the user ', ctx.state.loggedUser.id);
        const { query, request } = ctx;
        const sort = query.sort || '_id';

        const filter = getFilters(ctx);

        logger.info(`[AREAS-V2-ROUTER] Going to find areas`);
        const page = query['page[number]'] ? parseInt(query['page[number]'], 10) : 1;
        const limit = query['page[size]'] ? parseInt(query['page[size]'], 10) : 300;

        const clonedQuery = { ...ctx.query };
        delete clonedQuery['page[size]'];
        delete clonedQuery['page[number]'];
        const serializedQuery = serializeObjToQuery(clonedQuery) ? `?${serializeObjToQuery(clonedQuery)}&` : '?';

        const apiVersion = ctx.mountPath.split('/')[ctx.mountPath.split('/').length - 1];
        const link = `${request.protocol}://${getHostForPaginationLink(ctx)}/${apiVersion}${request.path}${serializedQuery}`;
        const filteredSort = getFilteredSort(sort);

        const areas = await AreaModel.paginate(filter, { page, limit, sort: filteredSort });

        await Promise.all(areas.docs.map((el) => SubscriptionService.mergeSubscriptionSpecificProps(el, ctx.request.headers['x-api-key'])));
        ctx.body = AreaSerializerV2.serialize(areas, link);
    }

    static async get(ctx) {
        logger.info(`Obtaining v2 area with areaId ${ctx.params.id}`);

        if (!mongoose.Types.ObjectId.isValid(ctx.params.id)) {
            ctx.throw(404, 'Area not found');
        }

        // 1. Check for area in areas
        let area = await AreaModel.findById(ctx.params.id);
        const areaExists = area !== null;

        // 3. if area doesn't exist
        if (!areaExists) {
            // get from subscriptions and return subscription mapped to have area props keys
            const [subscription] = await SubscriptionService.findByIds([ctx.params.id], ctx.request.headers['x-api-key']);

            // if it doesn't exist, send rude message
            if (!subscription) {
                ctx.throw(404, 'Area not found');
                return;
            }

            area = await SubscriptionService.getAreaFromSubscription({
                ...subscription.attributes,
                id: subscription.id
            });

            // 2. If area exists
            // if it has subscription get subscription also and merge props
            // if it doesn't have subscription just return the area
        } else if (area.subscriptionId) {
            area = await SubscriptionService.mergeSubscriptionSpecificProps(area, ctx.request.headers['x-api-key']);
        }

        const user = ctx.state.loggedUser || null;
        if (area.public === false && (!user || (user && area.userId !== user.id && user.role !== 'ADMIN'))) {
            ctx.throw(401, 'Area private');
            return;
        }

        const shouldHideAreaInfo = !user || (user && area.userId !== user.id && user.role !== 'ADMIN');
        if (shouldHideAreaInfo) {
            area.tags = null;
            area.userId = null;
            area.monthlySummary = null;
            area.deforestationAlerts = null;
            area.fireAlerts = null;
            area.name = null;
            area.webhookUrl = null;
            area.email = null;
            area.language = null;
        }

        if (areaExists && shouldHideAreaInfo) {
            area.subscriptionId = null;
        }

        area = await SubscriptionService.mergeSubscriptionSpecificProps(area, ctx.request.headers['x-api-key']);
        ctx.body = AreaSerializerV2.serialize(area);
    }

    static async save(ctx) {
        logger.info('Saving v2 area', ctx.request.body);
        const userId = ctx.state.loggedUser.id;
        let isSaved = false;

        let image = '';
        if (ctx.request.files && ctx.request.files.image && ctx.request.files.image.s3Url) {
            image = ctx.request.files.image.s3Url;
        }

        // Check geostore exists already with status=saved
        const geostore = (ctx.request.body && ctx.request.body.geostore) || null;
        logger.info(`Checking if data created already for geostore ${geostore}`);
        if (geostore) {
            const existsAreaForGeostore = await AreaModel.existsSavedAreaForGeostore(geostore);
            if (existsAreaForGeostore) {
                isSaved = true;
            }
        }

        // Check geostoreDataApi exists already with status=saved
        const geostoreDataApi = (ctx.request.body && ctx.request.body.geostoreDataApi) || null;
        logger.info(`Checking if data created already for geostoreDataApi ${geostoreDataApi}`);
        if (geostoreDataApi) {
            const existsAreaForGeostoreDataApi = await AreaModel.existsSavedAreaForGeostoreDataApi(geostoreDataApi);
            if (existsAreaForGeostoreDataApi) {
                isSaved = true;
            }
        }

        let datasets = [];
        if (ctx.request.body.datasets) {
            datasets = JSON.parse(ctx.request.body.datasets);
        }
        const use = {};
        if (ctx.request.body.use) {
            use.id = ctx.request.body.use ? ctx.request.body.use.id : null;
            use.name = ctx.request.body.use ? ctx.request.body.use.name : null;
        }
        const iso = {};
        if (ctx.request.body.iso) {
            iso.country = ctx.request.body.iso ? ctx.request.body.iso.country : null;
            iso.region = ctx.request.body.iso ? ctx.request.body.iso.region : null;
            if (iso.country || iso.region) {
                isSaved = true;
            }
        }
        const admin = {};
        if (ctx.request.body.admin) {
            admin.adm0 = ctx.request.body.admin ? ctx.request.body.admin.adm0 : null;
            admin.adm1 = ctx.request.body.admin ? ctx.request.body.admin.adm1 : null;
            admin.adm2 = ctx.request.body.admin ? ctx.request.body.admin.adm2 : null;
            if (admin.adm0) {
                isSaved = true;
            }
        }
        let wdpaid = null;
        if (ctx.request.body.wdpaid) {
            wdpaid = ctx.request.body.wdpaid;
            if (wdpaid) {
                isSaved = true;
            }
        }
        let tags = [];
        if (ctx.request.body.tags) {
            tags = ctx.request.body.tags;
        }
        let publicStatus = false;
        if (ctx.request.body.public) {
            publicStatus = ctx.request.body.public;
        }
        let fireAlertSub = false;
        if (ctx.request.body.fireAlerts) {
            fireAlertSub = ctx.request.body.fireAlerts;
        }
        let webhookUrl = '';
        if (ctx.request.body.webhookUrl) {
            webhookUrl = ctx.request.body.webhookUrl;
        }
        let summarySub = false;
        if (ctx.request.body.monthlySummary) {
            summarySub = ctx.request.body.monthlySummary;
        }
        let email = '';
        if (ctx.request.body.email) {
            email = ctx.request.body.email;
        }

        if (ctx.request.body.deforestationAlertsType && !Object.values(gladAlertTypes).includes(ctx.request.body.deforestationAlertsType)) {
            ctx.throw(400, 'Invalid GLAD alert type');
            return;
        }

        logger.info(`Building areaData`);

        const areaData = {
            name: ctx.request.body.name,
            application: ctx.request.body.application || 'gfw',
            geostore: ctx.request.body.geostore,
            geostoreDataApi: ctx.request.body.geostoreDataApi,
            wdpaid,
            userId: userId || ctx.state.loggedUser.id,
            use,
            env: ctx.request.body.env || 'production',
            iso,
            admin,
            datasets,
            image,
            tags,
            status: isSaved ? 'saved' : 'pending',
            public: publicStatus,
            fireAlerts: fireAlertSub,
            deforestationAlerts: ctx.request.body.deforestationAlerts || false,
            ...(ctx.request.body.deforestationAlertsType && { deforestationAlertsType: ctx.request.body.deforestationAlertsType }),
            webhookUrl,
            monthlySummary: summarySub,
            language: SUPPORTED_LANG_CODES.includes(ctx.request.body.language) ? ctx.request.body.language : DEFAULT_LANG_CODE,
            email
        };
        logger.info(`Creating v2 area with the following data: ${JSON.stringify(areaData)}`);

        let area = await new AreaModel(areaData).save();

        // If no datasets to register, no need to create a subscription
        if (area.fireAlerts || area.deforestationAlerts || area.monthlySummary) {
            const subscriptionId = await SubscriptionService.createSubscriptionFromArea(area, ctx.request.headers['x-api-key']);
            if (subscriptionId) {
                // Update the subscription id in the area and save again
                area.subscriptionId = subscriptionId;
                area = await area.save();
            }
        }

        area = await SubscriptionService.mergeSubscriptionSpecificProps(area, ctx.request.headers['x-api-key']);
        ctx.body = AreaSerializerV2.serialize(area);

        if (email) {
            const { application, status, language } = area;
            const lang = language || 'en';
            await MailService.sendMail(
                status === 'pending' ? `dashboard-pending-${lang}` : `dashboard-complete-${lang}`,
                getEmailParametersFromArea(area),
                [{ address: area.email }],
                application
            );
        }
    }

    static async update(ctx) {
        let previousArea = await AreaModel.findById(ctx.params.id);
        let area = await AreaModel.findById(ctx.params.id);
        if (!area) {
            // Try to find subscription with same ID
            const [subscription] = await SubscriptionService.findByIds([ctx.params.id], ctx.request.headers['x-api-key']);
            if (!subscription) {
                ctx.throw(404, 'Area not found');
                return;
            }

            // Create a new area from the subscription
            area = await SubscriptionService.getAreaFromSubscription({
                ...subscription.attributes,
                id: subscription.id,
                subscriptionId: subscription.id,
            }, { _id: subscription.id });

            area = await area.save();

            // Set also the subscription id in the previousArea
            previousArea = { subscriptionId: subscription.id };
        }

        const { files, body } = ctx.request;
        if (body.application || !area.application) {
            area.application = body.application || 'gfw';
        }
        if (body.name) {
            area.name = body.name;
        }

        let isSaved = false;

        if (body.geostore) {
            // check if it exists in db with status=saved
            const { geostore } = body;
            logger.info(`Checking if data created already for geostore ${geostore}`);
            if (await AreaModel.existsSavedAreaForGeostore(geostore)) isSaved = true;
            area.geostore = body.geostore;

            // Update status to saved if geostore already exists with status=saved
            area.status = isSaved ? 'saved' : 'pending';
            logger.info(`Updating area with id ${ctx.params.id} to status ${isSaved ? 'saved' : 'pending'}`);
        } else if (body.geostore === null) {
            area.geostore = null;
        }

        if (Object.keys(body).includes('geostoreDataApi')) {
            const { geostoreDataApi } = body;
            area.geostoreDataApi = geostoreDataApi;

            // check if it exists in db with status=saved
            logger.info(`Checking if data created already for geostoreDataApi ${geostoreDataApi}`);
            if (geostoreDataApi) {
                const existsAreaForGeostoreDataApi = await AreaModel.existsSavedAreaForGeostoreDataApi(geostoreDataApi);
                if (existsAreaForGeostoreDataApi) {
                    isSaved = true;
                }
            }

            // Update status to saved if geostoreDataApi already exists with status=saved
            area.status = isSaved ? 'saved' : 'pending';
            logger.info(`Updating area with id ${ctx.params.id} to status ${isSaved ? 'saved' : 'pending'}`);
        }

        if (body.wdpaid) {
            area.wdpaid = body.wdpaid;
        }
        const use = {};
        if (body.use) {
            use.id = body.use ? body.use.id : null;
            use.name = body.use ? body.use.name : null;
        }
        area.use = use;
        const iso = {};
        if (body.iso) {
            iso.country = body.iso ? body.iso.country : null;
            iso.region = body.iso ? body.iso.region : null;
        }
        area.iso = iso;
        const admin = {};
        if (body.admin) {
            admin.adm0 = body.admin ? body.admin.adm0 : null;
            admin.adm1 = body.admin ? body.admin.adm1 : null;
            admin.adm2 = body.admin ? body.admin.adm2 : null;
        }
        area.admin = admin;
        if (body.datasets) {
            area.datasets = JSON.parse(body.datasets);
        }
        if (body.tags) {
            area.tags = body.tags;
        }
        if (body.public) {
            area.public = body.public;
        }
        const updateKeys = body && Object.keys(body);
        area.public = updateKeys.includes('public') ? body.public : area.public;
        area.webhookUrl = updateKeys.includes('webhookUrl') ? body.webhookUrl : area.webhookUrl;
        area.env = updateKeys.includes('env') ? body.env : area.env;
        area.fireAlerts = updateKeys.includes('fireAlerts') ? body.fireAlerts : area.fireAlerts;
        area.deforestationAlerts = updateKeys.includes('deforestationAlerts') ? body.deforestationAlerts : area.deforestationAlerts;
        area.deforestationAlertsType = updateKeys.includes('deforestationAlertsType') ? body.deforestationAlertsType : area.deforestationAlertsType;
        if (updateKeys.includes('deforestationAlertsType')) {
            if (!Object.values(gladAlertTypes).includes(body.deforestationAlertsType)) {
                ctx.throw(400, 'Invalid GLAD alert type');
                return;
            }
            area.deforestationAlertsType = body.deforestationAlertsType;
        }
        area.monthlySummary = updateKeys.includes('monthlySummary') ? body.monthlySummary : area.monthlySummary;
        area.subscriptionId = updateKeys.includes('subscriptionId') ? body.subscriptionId : area.subscriptionId;
        area.email = updateKeys.includes('email') ? body.email : area.email;
        area.status = updateKeys.includes('status') && ctx.state.loggedUser.role === 'ADMIN' ? body.status : area.status;
        if (updateKeys.includes('language')) {
            area.language = SUPPORTED_LANG_CODES.includes(body.language) ? body.language : DEFAULT_LANG_CODE;
        }
        if (files && files.image && files.image.s3Url) {
            area.image = files.image.s3Url;
        }
        if (typeof body.templateId !== 'undefined') {
            area.templateId = body.templateId;
        }
        area.updatedAt = Date.now();
        await area.save();

        // Update associated subscription after updating the area

        // 1. The area already exists and has subscriptions preference in the request data
        if (area.fireAlerts || area.deforestationAlerts || area.monthlySummary) {
            const subscriptionId = previousArea.subscriptionId
                ? await SubscriptionService.updateSubscriptionFromArea(area, ctx.request.headers['x-api-key'])
                : await SubscriptionService.createSubscriptionFromArea(area, ctx.request.headers['x-api-key']);

            if (subscriptionId) {
                area.subscriptionId = subscriptionId;
                area = await area.save();
            }

            // 2. The area already exists and doesn’t have subscription preferences in the data
        } else if (previousArea.subscriptionId) {
            await SubscriptionService.deleteSubscription(area.subscriptionId, ctx.request.headers['x-api-key']);
            area.subscriptionId = '';
            area = await area.save();
        }

        area = await SubscriptionService.mergeSubscriptionSpecificProps(area, ctx.request.headers['x-api-key']);
        ctx.body = AreaSerializerV2.serialize(area);

        if (area.email && area.status === 'saved') {
            const { email, application } = area;
            const lang = area.language || 'en';
            await MailService.sendMail(
                `subscription-preference-change-${lang}`,
                getEmailParametersFromArea(area),
                [{ address: email }],
                application
            );
        }
    }

    static async delete(ctx) {
        logger.info(`Deleting area with id ${ctx.params.id}`);
        const areaToDelete = await AreaModel.findById(ctx.params.id);
        if (areaToDelete) {
            if (areaToDelete.subscriptionId) {
                // Try to find subscription and delete subscription
                const [subscription] = await SubscriptionService.findByIds([areaToDelete.subscriptionId], ctx.request.headers['x-api-key']);
                if (subscription) {
                    await SubscriptionService.deleteSubscription(areaToDelete.subscriptionId, ctx.request.headers['x-api-key']);
                }
            }

            // Then delete area
            await AreaModel.deleteOne({ _id: ctx.params.id });
        } else {
            // Try to find subscription and delete subscription
            const [subscription] = await SubscriptionService.findByIds([ctx.params.id], ctx.request.headers['x-api-key']);
            if (subscription) {
                await SubscriptionService.deleteSubscription(ctx.params.id, ctx.request.headers['x-api-key']);
            }
        }
        logger.info(`Area ${ctx.params.id} deleted successfully`);

        const userId = ctx.state.loggedUser.id;
        let team = null;
        try {
            team = await TeamService.getTeamByUserId(userId, ctx.request.headers['x-api-key']);
        } catch (e) {
            logger.error(e);
        }
        if (team && team.areas.includes(ctx.params.id)) {
            const areas = team.areas.filter((area) => area !== ctx.params.id);
            try {
                await TeamService.patchTeamById(team.id, { areas }, ctx.request.headers['x-api-key']);
                logger.info('Team patched successful.');

            } catch (e) {
                logger.error(e);
            }
        }
        ctx.body = '';
        ctx.statusCode = 204;
    }

    static async getByUserId(ctx) {
        logger.info(`Finding areas of user with id ${ctx.params.userId}`);
        const userAreas = await AreaModel.find({ userId: { $eq: ctx.params.userId } }).exec();

        ctx.body = AreaSerializerV2.serialize(userAreas);
    }

    static async deleteByUserId(ctx) {
        logger.info(`Deleting areas of user with id ${ctx.params.userId}`);
        const userIdToDelete = ctx.params.userId;

        try {
            await UserService.getUserById(userIdToDelete, ctx.request.headers['x-api-key']);
        } catch (error) {
            ctx.throw(404, `User ${userIdToDelete} does not exist`);
        }

        const userAreas = await AreaModel.find({ userId: { $eq: userIdToDelete } }).exec();

        for (let i = 0, { length } = userAreas; i < length; i++) {
            const currentArea = userAreas[i];
            const currentAreaId = currentArea._id.toString();

            logger.debug('[AreasService]: Deleting subscriptions of area');
            if (currentArea.subscriptionId) {
                const [subscription] = await SubscriptionService.findByIds([currentArea.subscriptionId], ctx.request.headers['x-api-key']);
                if (subscription) {
                    await SubscriptionService.deleteSubscription(currentArea.subscriptionId, ctx.request.headers['x-api-key']);
                }
            }

            logger.debug('[AreasService]: Deleting areas from teams');
            let team = null;
            try {
                team = await TeamService.getTeamByUserId(userIdToDelete, ctx.request.headers['x-api-key']);
            } catch (e) {
                logger.error(e);
            }

            if (team && team.areas.includes(currentAreaId)) {
                const areas = team.areas.filter((area) => area !== currentAreaId);
                try {
                    await TeamService.patchTeamById(team.id, { areas }, ctx.request.headers['x-api-key']);
                } catch (e) {
                    logger.error(e);
                }
            }
            await currentArea.remove();
        }

        ctx.body = AreaSerializerV2.serialize(userAreas);
    }

    static async updateByGeostore(ctx) {
        const geostores = ctx.request.body.geostores || [];
        const updateParams = ctx.request.body.update_params || {};
        logger.info('Updating geostores: ', geostores);
        logger.info('Updating with params: ', updateParams);

        try {
            updateParams.updatedAt = Date.now();
            const response = await AreaModel.updateMany(
                { geostore: { $in: geostores } },
                { $set: updateParams }
            );

            logger.info(`Updated ${response.nModified} out of ${response.n}.`);
            const areas = await AreaModel.find({ geostore: { $in: geostores } });
            ctx.body = AreaSerializerV2.serialize(areas);

            const areasToNotify = areas.filter((a) => a.status === 'saved');
            await Promise.all(areasToNotify.map((area) => {
                const { email, application } = area;
                const lang = area.language || 'en';
                if (!email) {
                    return new Promise((resolve) => { resolve(); });
                }

                return MailService.sendMail(
                    `dashboard-complete-${lang}`,
                    getEmailParametersFromArea(area),
                    [{ address: email }],
                    application
                );
            }));
        } catch (err) {
            ctx.throw(400, err.message);
        }
    }

    static async sync(ctx) {
        try {
            // Default interval is the last 2 days
            let startDate = moment().subtract('2', 'd').hour(0).minute(0);
            let endDate = moment().hour(0).minute(0);

            if (ctx.query.startDate && moment(ctx.query.startDate).isValid()) {
                startDate = moment(ctx.query.startDate);
            }

            if (ctx.query.endDate && moment(ctx.query.endDate).isValid()) {
                endDate = moment(ctx.query.endDate);
            }

            const dryRun = ctx.query.dryRun === 'true';

            logger.info(`[AREAS V2 ROUTER - SYNC] Starting sync from ${startDate.toISOString()} until ${endDate.toISOString()} (dry run: ${dryRun})`);

            let syncedAreas = 0;
            let createdAreas = 0;
            let totalSubscriptions = 0;
            let page = 1;
            let hasMoreSubscriptions = true;
            const affectedAreaIds = [];
            while (hasMoreSubscriptions) {
                const response = await SubscriptionService.getAllSubscriptions(
                    page,
                    100,
                    startDate.toISOString(),
                    endDate.toISOString(),
                    ctx.request.headers['x-api-key']
                );
                const subscriptions = response.data;
                const { links } = response;
                logger.info(`[AREAS V2 ROUTER - SYNC] Found page ${page} with ${subscriptions.length} subscriptions.`);
                totalSubscriptions += subscriptions.length;

                // eslint-disable-next-line no-loop-func
                await Promise.all(subscriptions.map(async (sub) => {
                    const area = await AreaModel.findOne({ subscriptionId: sub.id });
                    logger.info(`[AREAS V2 ROUTER - SYNC] Executing sync for subscription with ID: ${sub.id}`);
                    const areaToSave = area
                        ? await SubscriptionService.mergeSubscriptionOverArea(area, {
                            ...sub.attributes,
                            id: sub.id
                        })
                        : await SubscriptionService.getAreaFromSubscription({
                            ...sub.attributes,
                            id: sub.id
                        });

                    try {
                        if (!dryRun) {
                            // check area geostore for status=saved
                            if (areaToSave && areaToSave.geostore) {
                                // check if it exists in db with status=saved
                                const { geostore } = areaToSave;
                                const query = {
                                    $and: [
                                        { status: 'saved' },
                                        { geostore }
                                    ]
                                };
                                logger.info(`Checking if data created already for geostore ${geostore}`);
                                const savedAreas = await AreaModel.find(query);
                                if (savedAreas && savedAreas.length > 0) {
                                    areaToSave.status = 'saved';
                                }
                            }
                            await areaToSave.save();
                        }

                        if (area) {
                            syncedAreas += 1;
                        } else {
                            createdAreas += 1;
                        }

                        affectedAreaIds.push(areaToSave._id);
                    } catch (e) {
                        logger.error(`[AREAS V2 ROUTER - SYNC] Error saving area for subscription with ID: ${sub.id}`);
                    }
                }));

                logger.info(`[AREAS V2 ROUTER - SYNC] Synced ${syncedAreas} so far.`);
                logger.info(`[AREAS V2 ROUTER - SYNC] Created ${createdAreas} so far.`);

                page++;
                hasMoreSubscriptions = links.self !== links.last;
            }

            logger.info(`[AREAS V2 ROUTER - SYNC] Analyzed a total of ${totalSubscriptions} subscriptions, ${syncedAreas} synced areas and ${createdAreas} created areas.`);
            logger.info(`[AREAS V2 ROUTER - SYNC] Affected area ids: ${affectedAreaIds}`);
            ctx.body = {
                data: {
                    syncedAreas,
                    createdAreas,
                    totalSubscriptions,
                    affectedAreaIds,
                }
            };
        } catch (err) {
            ctx.throw(400, err.message);
        }
    }

}

async function loggedUserToState(ctx, next) {
    if (ctx.query && ctx.query.loggedUser) {
        ctx.state.loggedUser = JSON.parse(ctx.query.loggedUser);
        delete ctx.query.loggedUser;
    } else if (ctx.request.body && ctx.request.body.loggedUser) {
        if (typeof ctx.request.body.loggedUser === 'object') {
            ctx.state.loggedUser = ctx.request.body.loggedUser;
        } else {
            ctx.state.loggedUser = JSON.parse(ctx.request.body.loggedUser);
        }
        delete ctx.request.body.loggedUser;
    } else if (ctx.request.body.fields && ctx.request.body.fields.loggedUser) {
        ctx.state.loggedUser = JSON.parse(ctx.request.body.fields.loggedUser);
        delete ctx.request.body.loggedUser;
    }
    await next();
}

async function ensureUserIsLogged(ctx, next) {
    if (!ctx.state.loggedUser) {
        ctx.throw(401, 'Unauthorized');
        return;
    }
    await next();
}

async function checkPermission(ctx, next) {
    ctx.assert(ctx.params.id, 400, 'Id required');
    const area = await AreaModel.findById(ctx.params.id);
    if (area && area.userId !== ctx.state.loggedUser.id && area.userId !== ctx.request.body.userId && ctx.state.loggedUser.role !== 'ADMIN') {
        ctx.throw(403, 'Not authorized');
        return;
    }
    await next();
}

async function unwrapJSONStrings(ctx, next) {
    if (ctx.request.body.use && typeof ctx.request.body.use === 'string' && ctx.request.body.use.length > 0) {
        try {
            ctx.request.body.use = JSON.parse(ctx.request.body.use);
        } catch (e) {
            // not a JSON, ignore and move on
        }
    }
    if (ctx.request.body.iso && typeof ctx.request.body.iso === 'string' && ctx.request.body.iso.length > 0) {
        try {
            ctx.request.body.iso = JSON.parse(ctx.request.body.iso);
        } catch (e) {
            // not a JSON, ignore and move on
        }
    }

    await next();
}

const ensureAdminUser = async (ctx, next) => {
    if (ctx.state.loggedUser.role !== 'ADMIN') {
        ctx.throw(401, 'Not authorized');
        return;
    }

    await next();
};

const isOwnerAdminOrMicroservice = async (ctx, next) => {
    logger.debug(`[AreaRouter] Checking access to resources by ownership, ADMIN role or microservice token`);

    const user = ctx.state.loggedUser;
    const userFromParam = ctx.params.userId;

    if (user.id === 'microservice' || user.role === 'ADMIN') {
        await next();
        return;
    }

    if (userFromParam === user.id) {
        await next();
        return;
    }

    ctx.throw(403, 'Forbidden');
};

const router = new Router({ prefix: '/area' });

router.get('/', loggedUserToState, ensureUserIsLogged, AreaRouterV2.getAll);
router.post('/', loggedUserToState, ensureUserIsLogged, unwrapJSONStrings, AreaValidatorV2.create, AreaRouterV2.save);
router.patch('/:id', loggedUserToState, ensureUserIsLogged, checkPermission, unwrapJSONStrings, AreaValidatorV2.update, AreaRouterV2.update);
router.get('/:id', loggedUserToState, AreaRouterV2.get);
router.get('/by-user/:userId', loggedUserToState, ensureUserIsLogged, isOwnerAdminOrMicroservice, AreaRouterV2.getByUserId);
router.delete('/by-user/:userId', loggedUserToState, ensureUserIsLogged, isOwnerAdminOrMicroservice, AreaRouterV2.deleteByUserId);
router.delete('/:id', loggedUserToState, ensureUserIsLogged, checkPermission, AreaRouterV2.delete);
router.post('/update', loggedUserToState, ensureUserIsLogged, ensureAdminUser, AreaValidatorV2.updateByGeostore, AreaRouterV2.updateByGeostore);
router.post('/sync', loggedUserToState, ensureUserIsLogged, ensureAdminUser, AreaRouterV2.sync);

module.exports = router;