/**
* @module api-v1-controller
*/
const surveyModel = require( '../models/survey-model' );
const instanceModel = require( '../models/instance-model' );
const account = require( '../models/account-model' );
const auth = require( 'basic-auth' );
const express = require( 'express' );
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
return 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
return 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 );
}
}