app/controllers/api-v1-controller.js

/**
 * @module api-v1-controller
 */

const auth = require('basic-auth');
const express = require('express');
const surveyModel = require('../models/survey-model');
const instanceModel = require('../models/instance-model');
const account = require('../models/account-model');

const router = express.Router();
const quotaErrorMessage = 'Forbidden. No quota left';
// var debug = require( 'debug' )( 'api-controller-v1' );

module.exports = (app) => {
    app.use(`${app.get('base path')}/api/v1`, router);
    // old enketo-legacy URL structure for migration-friendliness
    app.use(`${app.get('base path')}/api_v1`, router);
};

router
    .get('/', (req, res) => {
        res.redirect('http://apidocs.enketo.org/v1');
    })
    .all('*', authCheck)
    .all('*', _setQuotaUsed)
    .all('/*/iframe', _setIframe)
    .all('/survey/preview*', (req, res, next) => {
        req.webformType = 'preview';
        next();
    })
    .all('/survey/all*', (req, res, next) => {
        req.webformType = 'all';
        next();
    })
    .all('/instance*', (req, res, next) => {
        req.webformType = 'edit';
        next();
    })
    .all('*', _setReturnQueryParam)
    .get('/survey', getExistingSurvey)
    .get('/survey/iframe', getExistingSurvey)
    .post('/survey', getNewOrExistingSurvey)
    .post('/survey/iframe', getNewOrExistingSurvey)
    .delete('/survey', deactivateSurvey)
    .get('/survey/preview', getExistingSurvey)
    .get('/survey/preview/iframe', getExistingSurvey)
    .post('/survey/preview', getNewOrExistingSurvey)
    .post('/survey/preview/iframe', getNewOrExistingSurvey)
    .get('/survey/all', getExistingSurvey)
    .post('/survey/all', getNewOrExistingSurvey)
    .get('/surveys/number', getNumber)
    .post('/surveys/number', getNumber)
    .get('/surveys/list', getList)
    .post('/surveys/list', getList)
    .post('/instance', cacheInstance)
    .post('/instance/iframe', cacheInstance)
    .delete('/instance', removeInstance)
    .all('*', (req, res, next) => {
        const error = new Error('Not allowed');
        error.status = 405;
        next(error);
    });

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function authCheck(req, res, next) {
    // check authentication and account
    let error;
    const creds = auth(req);
    const key = creds ? creds.name : undefined;
    const server = req.body.server_url || req.query.server_url;

    // set content-type to json to provide appropriate json Error responses
    res.set('Content-Type', 'application/json');

    account
        .get(server)
        .then((account) => {
            if (!key || key !== account.key) {
                error = new Error('Not Allowed. Invalid API key.');
                error.status = 401;
                res.status(error.status).set(
                    'WWW-Authenticate',
                    'Basic realm="Enter valid API key as user name"'
                );
                next(error);
            } else {
                req.account = account;
                next();
            }
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function getExistingSurvey(req, res, next) {
    if (req.account.quota < req.account.quotaUsed) {
        return _render(403, quotaErrorMessage, res);
    }

    return surveyModel
        .getId({
            openRosaServer: req.query.server_url,
            openRosaId: req.query.form_id,
        })
        .then((id) => {
            if (id) {
                _render(200, _generateWebformUrls(id, req), res);
            } else {
                _render(404, 'Survey not found', res);
            }
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function getNewOrExistingSurvey(req, res, next) {
    const survey = {
        openRosaServer: req.body.server_url || req.query.server_url,
        openRosaId: req.body.form_id || req.query.form_id,
    };

    if (req.account.quota < req.account.quotaUsed) {
        return _render(403, quotaErrorMessage, res);
    }

    return surveyModel
        .getId(survey)
        .then((id) =>
            // will return existing && active surveys
            id ? surveyModel.get(id) : null
        )
        .catch((error) => {
            if (error.status === 404) {
                return null;
            }
            throw error;
        })
        .then((storedSurvey) => {
            if (!storedSurvey && req.account.quota <= req.account.quotaUsed) {
                return _render(403, quotaErrorMessage, res);
            }
            const status = storedSurvey ? 200 : 201;

            // even if id was found still call .set() method to update any properties
            return surveyModel.set(survey).then((id) => {
                if (id) {
                    _render(status, _generateWebformUrls(id, req), res);
                } else {
                    _render(404, 'Survey not found', res);
                }
            });
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function deactivateSurvey(req, res, next) {
    return surveyModel
        .update({
            openRosaServer: req.body.server_url,
            openRosaId: req.body.form_id,
            active: false,
        })
        .then((id) => {
            if (id) {
                _render(204, null, res);
            } else {
                _render(404, 'Survey not found', res);
            }
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function getNumber(req, res, next) {
    return surveyModel
        .getNumber(req.body.server_url || req.query.server_url)
        .then((number) => {
            if (number) {
                _render(
                    200,
                    {
                        code: 200,
                        number,
                    },
                    res
                );
            } else {
                // this cannot be reached I think
                _render(404, 'No surveys found', res);
            }
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function getList(req, res, next) {
    let obj;

    return surveyModel
        .getList(req.body.server_url || req.query.server_url)
        .then((list) => {
            list = list.map((survey) => {
                obj = _generateWebformUrls(survey.enketoId, req);
                obj.form_id = survey.openRosaId;
                obj.server_url = survey.openRosaServer;

                return obj;
            });
            _render(
                200,
                {
                    code: 200,
                    forms: list,
                },
                res
            );
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function cacheInstance(req, res, next) {
    let survey;
    let enketoId;

    if (req.account.quota < req.account.quotaUsed) {
        return _render(403, quotaErrorMessage, res);
    }

    survey = {
        openRosaServer: req.body.server_url,
        openRosaId: req.body.form_id,
        instance: req.body.instance,
        instanceId: req.body.instance_id,
        returnUrl: req.body.return_url,
    };

    return surveyModel
        .getId(survey)
        .then((id) =>
            // will return existing && active surveys
            id ? surveyModel.get(id) : null
        )
        .catch((error) => {
            if (error.status === 404) {
                return null;
            }
            throw error;
        })
        .then((storedSurvey) => {
            if (!storedSurvey) {
                if (req.account.quota <= req.account.quotaUsed) {
                    return _render(403, quotaErrorMessage, res);
                }

                // Create a new enketo ID.
                return surveyModel.set(survey);
            }

            // Do not update properties if ID was found to avoid overwriting theme.
            return storedSurvey.enketoId;
        })
        .then((id) => {
            enketoId = id;

            return instanceModel.set(survey);
        })
        .then(() => {
            _render(201, _generateWebformUrls(enketoId, req), res);
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function removeInstance(req, res, next) {
    return instanceModel
        .remove({
            openRosaServer: req.body.server_url,
            openRosaId: req.body.form_id,
            instanceId: req.body.instance_id,
        })
        .then((instanceId) => {
            if (instanceId) {
                _render(204, null, res);
            } else {
                _render(404, 'Record not found', res);
            }
        })
        .catch(next);
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function _setQuotaUsed(req, res, next) {
    if (!req.app.get('account lib')) {
        // Pretend quota used = 0 if not running SaaS.
        req.account.quotaUsed = 0;
        next();
    } else {
        // For SaaS service:
        surveyModel
            .getNumber(req.account.linkedServer)
            .then((number) => {
                req.account.quotaUsed = number;
                next();
            })
            .catch(next);
    }
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function _setIframe(req, res, next) {
    req.iframe = true;
    next();
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function _setReturnQueryParam(req, res, next) {
    const returnUrl = req.body.return_url || req.query.return_url;
    if (
        returnUrl &&
        (req.webformType === 'edit' || req.webformType === 'single')
    ) {
        req.returnQueryParam = `return_url=${encodeURIComponent(
            decodeURIComponent(returnUrl)
        )}`;
    }
    next();
}

/**
 * @param {Array<string>} [params] - List of parameters.
 */
function _generateQueryString(params = []) {
    const paramsJoined = params
        .filter((part) => part && part.length > 0)
        .join('&');

    return paramsJoined ? `?${paramsJoined}` : '';
}

/**
 * @param { string } id - Form id.
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 */
function _generateWebformUrls(id, req) {
    let queryString;
    const obj = {};
    const IFRAMEPATH = 'i/';
    const iframePart = req.iframe ? IFRAMEPATH : '';
    const protocol = req.headers['x-forwarded-proto'] || req.protocol;
    const baseUrl = `${protocol}://${req.headers.host}${req.app.get(
        'base path'
    )}/`;
    const offline = req.app.get('offline enabled');

    req.webformType = req.webformType || 'default';

    switch (req.webformType) {
        case 'preview':
            obj.preview_url = `${baseUrl}preview/${iframePart}${id}`;
            break;
        case 'edit':
            queryString = _generateQueryString([
                `instance_id=${req.body.instance_id}`,
                req.returnQueryParam,
            ]);
            obj.edit_url = `${baseUrl}edit/${iframePart}${id}${queryString}`;
            break;
        case 'all':
            // non-iframe views
            obj.url = offline ? `${baseUrl}x/${id}` : baseUrl + id;
            obj.preview_url = `${baseUrl}preview/${id}`;
            // iframe views
            obj.iframe_url = baseUrl + IFRAMEPATH + id;
            obj.preview_iframe_url = `${baseUrl}preview/${IFRAMEPATH}${id}`;
            // enketo-legacy
            obj.subdomain = '';
            break;
        default:
            if (iframePart) {
                obj.url = offline
                    ? `${baseUrl}x/${id}`
                    : baseUrl + iframePart + id;
            } else {
                obj.url = offline ? `${baseUrl}x/${id}` : baseUrl + id;
            }
            break;
    }

    return obj;
}

/**
 * @param { number } status - HTTP status code
 * @param {object|string} body - response body
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 */
function _render(status, body, res) {
    if (status === 204) {
        // send 204 response without a body
        res.status(status).end();
    } else {
        if (typeof body === 'string') {
            body = {
                message: body,
            };
        }
        body.code = status;
        res.status(status).json(body);
    }
}