Ignore:
Timestamp:
03/05/14 22:44:48 (11 years ago)
Author:
hendrikvanantwerpen
Message:

Completed migration to API, without CouchDB proxy.

Move to API is now completed. The full API is password protected, a very
limited API is exposed for respondents, which works with secrets that
are passed in URLs.

Serverside the HTTPResult class was introduced, which is similar to
Promises, but specifically for HTTP. It carries a status code and
response and makes it easier to extract parts of async handling in
separate functions.

Fixed a bug in our schema (it seems optional attributes don't exist but
a required list does). Verification of our schema by grunt-tv4 didn't
work yet. Our schema is organized the wrong way (this is fixable),
but the json-schema schema has problems with simple types and $refs.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • Dev/trunk/src/server/app.js

    r481 r487  
    99  , _ = require("underscore")
    1010  , tv4 = require("tv4")
     11  , HTTPResult = require("./util/http-result")
     12  , etags = require("./util/etags")
     13  , cryptoken = require('./util/crypto-token')
    1114  ;
    1215
     
    2225exports.App = function(settings) {
    2326
    24     assertSetting("couchDbURL", settings, _.isString);
    25     var couch = new CouchDB(settings.couchDbURL);
     27    assertSetting("couchServerURL", settings, _.isString);
     28    assertSetting("dbName", settings, _.isString);
     29    var couch = new CouchDB(settings.couchServerURL,settings.dbName);
    2630   
    2731    var schema = require("./config/couchdb-schema.json");
     
    6973    app.use(passport.initialize());
    7074    app.use(passport.session());
     75   
     76    // various middlewares
    7177    function ensureAuthenticated(req,res,next){
    7278        if (!req.user) {
     
    7884    function returnUser(req,res) {
    7985        res.send(200, req.user);
     86    }
     87    function notImplemented(req,res) {
     88        res.send(501,{error:"API not implemented yet."});
    8089    }
    8190
     
    112121        });
    113122
     123    var JSON_MIME = 'application/json';
     124    var CSV_MIME = 'application/json';
     125    function ensureMIME(mimeType) {
     126        return function(req,res,next) {
     127            if (!req.accepts(mimeType)) {
     128                res.send(406);
     129            } else {
     130                res.set({
     131                    'Content-Type': mimeType
     132                });
     133                next();
     134            }
     135        };
     136    }
     137   
     138    function stripAndReturnPrivates(obj) {
     139        var priv = {};
     140        _.each(obj||{},function(val,key){
     141            if (key.substring(0,1) === '_') {
     142                priv[key] = val;
     143                delete obj[key];
     144            }
     145        });
     146        return priv;
     147    }
     148
     149    function identity(obj) { return obj; }
     150    function handleUnknownResponse(status,error){
     151        return new HTTPResult(500,{error: "Unexpected database response",
     152                                   innerStatus: status, innerResponse: error});
     153    }
     154    function handleUnknownError(error){
     155        return new HTTPResult(500, {error: "Unknown error", innerError: error});
     156    }
     157    function handleRowValues(result) {
     158        return _.map(result.rows, function(item) { return item.value; });
     159    }
     160    function handleRowDocs(result) {
     161        return _.map(result.rows, function(item) { return item.doc; });
     162    }
     163
     164    function getDocumentsOfType (type) {
     165        var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
     166        return HTTPResult.fromResponsePromise(couch.get(url).response,
     167                                              handleUnknownError)
     168        .handle({
     169            200: handleRowValues,
     170            404: function() { return {error: "Cannot find collection of type "+type}; },
     171            default: handleUnknownResponse
     172        });
     173    }
     174    function getDocument(id,rev,type) {
     175        var opts = {headers:{}};
     176        if (rev) {
     177            opts.headers['If-Non-Match'] = '"'+rev+'"';
     178        }
     179        return HTTPResult.fromResponsePromise(couch.get(id,opts).response,
     180                                              handleUnknownError)
     181        .handle({
     182            200: function(doc){
     183                if ( doc.type !== type ) {
     184                    return new HTTPResult(404,{error:"Document not found."});
     185                } else {
     186                    var priv = stripAndReturnPrivates(doc);
     187                    if ( priv._rev !== rev ) {
     188                        doc._id = priv._id;
     189                        doc._rev = priv._rev;
     190                        return doc;
     191                    } else {
     192                        return new HTTPResult(304);
     193                    }
     194                }
     195            },
     196            304: identity,
     197            default: handleUnknownResponse
     198        });
     199    }
     200    function putDocument(id,rev,type,doc) {
     201        var priv = stripAndReturnPrivates(doc);
     202        if ( doc.type === type && tv4.validateResult(doc, schema) ) {
     203            var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
     204            return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response,
     205                                                  handleUnknownError)
     206            .handle({
     207                201: function(res){
     208                    doc._id = res.id;
     209                    doc._rev = res.rev;
     210                    return doc;
     211                },
     212                409: function(error) {
     213                    return {error: error.reason};
     214                },
     215                default: handleUnknownResponse
     216            });
     217        } else {
     218            return new HTTPResult(400,{error: "Document failed schema verification."});
     219        }
     220    }
     221    function deleteDocument(id,rev) {
     222        if ( rev ) {
     223            var opts = {headers:{'If-Match':'"'+rev+'"'}};
     224            return HTTPResult.fromResponsePromise(couch.delete(id,opts).response,
     225                                                  handleUnknownError)
     226            .handle({
     227                200: identity,
     228                409: function(error) {
     229                    return {error: error.reason};
     230                },
     231                default: handleUnknownResponse
     232            });
     233        } else {
     234            return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
     235        }
     236    }
     237    function postDocument(type,doc) {
     238        var priv = stripAndReturnPrivates(doc);
     239        if ( doc.type === type && tv4.validateResult(doc, schema) ) {
     240            return HTTPResult.fromResponsePromise(couch.post(doc).response,
     241                                                  handleUnknownError)
     242            .handle({
     243                201: function(response) {
     244                    doc._id = response.id;
     245                    doc._rev = response.rev;
     246                    return doc;
     247                },
     248                default: handleUnknownResponse
     249            });
     250        } else {
     251            return new HTTPResult(400,{error: "Document failed schema verification."});
     252        }
     253    }
     254
    114255    function makeDocsGet(type) {
    115256        return function(req,res) {
    116             var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
    117             couch.get(url).then(function(result){
    118                 var items = _.map(result.rows, function(item) { return item.value; });
    119                 res.send(200, items);
    120             }, function(error){
    121                 res.send(404, {error: "Cannot find collection"});
     257            getDocumentsOfType(type)
     258            .handle(res.send.bind(res));
     259        };
     260    }
     261    function makeDocGet_id(type) {
     262        return function(req,res) {
     263            var id = req.params.id;
     264            var rev = etags.parse(req.header('If-Non-Match'))[0];
     265            getDocument(id,rev,type)
     266            .handle({
     267                200: function(doc){
     268                    res.set({
     269                        'ETag': etags.format([doc._rev])
     270                    }).send(200, doc);
     271                },
     272                default: res.send.bind(res)
    122273            });
    123274        };
    124275    }
    125     function makeDocGet_id(type) {
     276    function makeDocPut_id(type) {
    126277        return function(req,res) {
    127278            var id = req.params.id;
    128             couch.get(id).then(function(result){
    129                 if ( result.type === type ) {
    130                     res.send(200, result);
    131                 } else {
    132                     res.send(404, {error: "Document "+id+" is not a "+type});
    133                 }
    134             }, function(error){
    135                 res.send(404, {error: "Cannot find survey run "+id});
     279            var doc = req.body;
     280            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     281            putDocument(id,rev,type,doc)
     282            .handle({
     283                201: function(doc) {
     284                    res.set({
     285                        'ETag': etags.format([doc._rev])
     286                    }).send(201, doc);
     287                },
     288                default: res.send.bind(res)
    136289            });
    137290        };
    138291    }
    139     function makeDocPut_id(type) {
     292    function makeDocDel_id(type) {
    140293        return function(req,res) {
    141294            var id = req.params.id;
    142295            var doc = req.body;
    143             if ( doc.type === type && tv4.validateResult(doc, schema) ) {
    144                 couch.put(id,doc).then(function(result){
    145                     res.send(200, result);
    146                 }, function(error){
    147                     res.send(500, error);
     296            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     297            deleteDocument(id,rev)
     298            .handle(res.send.bind(res));
     299        };
     300    }
     301    function makeDocPost(type) {
     302        return function(req,res) {
     303            var doc = req.body;
     304            postDocument(type,doc)
     305            .handle({
     306                201: function(doc) {
     307                    res.set({
     308                        'ETag': etags.format([doc._rev])
     309                    }).send(201, doc);
     310                },
     311                default: res.send.bind(res)
     312            });
     313        };
     314    }
     315
     316    // Questions
     317    function getQuestionsWithCode(code) {
     318        var url = '_design/questions/_view/by_code';
     319        var query = {include_docs:true,key:code};
     320        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
     321                                              handleUnknownError)
     322        .handle({
     323            200: handleRowDocs,
     324            default: handleUnknownResponse
     325        });
     326    }
     327    function getQuestionsWithTopic(topic) {
     328        var url = '_design/questions/_view/all_topics';
     329        var query = {reduce:false,include_docs:true,key:topic};
     330        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
     331                                              handleUnknownError)
     332        .handle({
     333            200: handleRowDocs,
     334            default: handleUnknownResponse
     335        });
     336    }
     337    function getQuestionsWithCategoryAndTopic(category,topic) {
     338        var hasTopic = typeof topic !== 'undefined';
     339        var url = '_design/questions/_view/all';
     340        var query = {reduce:false,include_docs:true,
     341                     startkey:hasTopic?[category,topic]:[category],
     342                     endkey:hasTopic?[category,topic]:[category,{}]};
     343        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
     344                                              handleUnknownError)
     345        .handle({
     346            200: handleRowDocs,
     347            default: handleUnknownResponse
     348        });
     349    }
     350    app.get('/api/questions',
     351        ensureAuthenticated,
     352        ensureMIME(JSON_MIME),
     353        function(req,res) {
     354            var hr;
     355            if ( 'category' in req.query ) {
     356                hr = getQuestionsWithCategoryAndTopic(req.query.category,req.query.topic);
     357            } else if ( 'topic' in req.query ) {
     358                hr = getQuestionsWithTopic(req.query.topic);
     359            } else if ( 'code' in req.query ) {
     360                hr = getQuestionsWithCode(req.query.code);
     361            }
     362            hr.handle(res.send.bind(res));
     363        });
     364    app.post('/api/questions',
     365        ensureAuthenticated,
     366        ensureMIME(JSON_MIME),
     367        function (req,res) {
     368            var doc = req.body;
     369            getQuestionsWithCode(doc.code)
     370            .handle({
     371                200: function(others) {
     372                    if ( others.length > 0 ) {
     373                        return new HTTPResult(403,{error:"Other question with this code already exists."});
     374                    } else {
     375                        return postDocument('Question',doc);
     376                    }
     377                }
     378            })
     379            .handle(res.send.bind(res));
     380        });
     381    app.get('/api/questions/:id',
     382        ensureAuthenticated,
     383        ensureMIME(JSON_MIME),
     384        makeDocGet_id('Question'));
     385    app.put('/api/questions/:id',
     386        ensureAuthenticated,
     387        ensureMIME(JSON_MIME),
     388        function (req,res) {
     389            var id = req.params.id;
     390            var doc = req.body;
     391            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     392            getQuestionsWithCode(doc.code)
     393            .handle({
     394                200: function(others) {
     395                    if ( others.length > 0 ) {
     396                        return new HTTPResult(403,{error:"Other question with this code already exists."});
     397                    } else {
     398                        return putDocument(id,rev,'Question',doc);
     399                    }
     400                }
     401            })
     402            .handle(res.send.bind(res));
     403        });
     404    app.delete('/api/questions/:id',
     405        ensureAuthenticated,
     406        ensureMIME(JSON_MIME),
     407        makeDocDel_id('Question'));
     408
     409
     410    // Categories and topics
     411    app.get('/api/categories',
     412        ensureAuthenticated,
     413        ensureMIME(JSON_MIME),
     414        function(req,res) {
     415            var url = '_design/questions/_view/all';
     416            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
     417                                           handleUnknownError)
     418            .handle({
     419                200: function(result) {
     420                    return _.map(result.rows, function(item) {
     421                        return { name:item.key[0], count:item.value };
     422                    });
     423                },
     424                default: handleUnknownResponse
     425            })
     426            .handle(res.send.bind(res));
     427        });
     428    app.get('/api/categories/:category/topics',
     429        ensureAuthenticated,
     430        ensureMIME(JSON_MIME),
     431        function(req,res) {
     432            var category = req.params.category;
     433            var url = '_design/questions/_view/all';
     434            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
     435                                           handleUnknownError)
     436            .handle({
     437                200: function(result) {
     438                    return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
     439                },
     440                default: handleUnknownResponse
     441            })
     442            .handle(res.send.bind(res));
     443        });
     444    app.get('/api/topics',
     445        ensureAuthenticated,
     446        ensureMIME(JSON_MIME),
     447        function(req,res) {
     448            var url = '_design/questions/_view/all_topics';
     449            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
     450                                           handleUnknownError)
     451            .handle({
     452                200: function(result) {
     453                    return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
     454                },
     455                default: handleUnknownResponse
     456            })
     457            .handle(res.send.bind(res));
     458        });
     459
     460    // Surveys
     461    app.get('/api/surveys',
     462        ensureAuthenticated,
     463        ensureMIME(JSON_MIME),
     464        function(req,res) {
     465            var url;
     466            if ( 'drafts' in req.query ) {
     467                url = '_design/surveys/_view/drafts';
     468            } else if ( 'published' in req.query ) {
     469                url = '_design/surveys/_view/published';
     470            } else {
     471                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
     472            }
     473            HTTPResult.fromResponsePromise(couch.get(url).response,
     474                                           handleUnknownError)
     475            .handle({
     476                200: function(result) {
     477                    return _.map(result.rows, function(item) { return item.value; });
     478                },
     479                default: handleUnknownResponse
     480            })
     481            .handle(res.send.bind(res));
     482        });
     483    app.post('/api/surveys',
     484        ensureAuthenticated,
     485        ensureMIME(JSON_MIME),
     486        makeDocPost('Survey'));
     487    app.get('/api/surveys/:id',
     488        ensureAuthenticated,
     489        ensureMIME(JSON_MIME),
     490        makeDocGet_id('Survey'));
     491    app.put('/api/surveys/:id',
     492        ensureAuthenticated,
     493        ensureMIME(JSON_MIME),
     494        makeDocPut_id('Survey'));
     495    app.delete('/api/surveys/:id',
     496        ensureAuthenticated,
     497        ensureMIME(JSON_MIME),
     498        makeDocDel_id('Survey'));
     499
     500    // SurveyRuns
     501    app.get('/api/surveyRuns',
     502        ensureAuthenticated,
     503        ensureMIME(JSON_MIME),
     504        makeDocsGet('SurveyRun'));
     505    app.post('/api/surveyRuns',
     506        ensureAuthenticated,
     507        ensureMIME(JSON_MIME),
     508        function(req,res) {
     509            var doc = req.body;
     510            randomToken()
     511            .handle({
     512                201: function(token) {
     513                    doc.secret = token;
     514                    return postDocument('SurveyRun',doc);
     515                }
     516            })
     517            .handle(res.send.bind(res));
     518        });
     519    app.get('/api/surveyRuns/:id',
     520        ensureAuthenticated,
     521        ensureMIME(JSON_MIME),
     522        makeDocGet_id('SurveyRun'));
     523    app.put('/api/surveyRuns/:id',
     524        ensureAuthenticated,
     525        ensureMIME(JSON_MIME),
     526        function (req,res) {
     527            var id = req.params.id;
     528            var doc = req.body;
     529            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     530            var hr;
     531            if ( typeof doc.secret === 'undefined' ) {
     532                hr = randomToken()
     533                .handle({
     534                    201: function(token) {
     535                        doc.secret = token;
     536                        return putDocument(id,rev,'SurveyRun',doc);
     537                    }
    148538                });
    149539            } else {
    150                 res.send(400,{error: "Document failed schema verification."});
     540                hr = putDocument(id,rev,'SurveyRun',doc);
    151541            }
    152         };
    153     }
    154     function makeDocDel_id(type) {
    155         return function(req,res) {
    156             var id = req.params.id;
    157             var rev = req.query.rev;
    158             couch.delete(id,rev).then(function(result){
    159                 res.send(200, result);
    160             }, function(error){
    161                 res.send(500, error);
     542            hr.handle(res.send.bind(res));
     543        });
     544    app.delete('/api/surveyRuns/:id',
     545        ensureAuthenticated,
     546        ensureMIME(JSON_MIME),
     547        makeDocDel_id('SurveyRun'));
     548    app.get('/api/surveyRuns/:id/responses',
     549        ensureAuthenticated,
     550        ensureMIME(JSON_MIME),
     551        function(req,res) {
     552            var id = req.params.id;
     553            getResponsesBySurveyRunId(id)
     554            .handle(res.send.bind(res));
     555        });
     556    app.get('/api/surveyRuns/:id/responses.csv',
     557        ensureAuthenticated,
     558        ensureMIME(CSV_MIME),
     559        function(req, res) {
     560            var id = req.params.id;
     561            getResponsesBySurveyRunId(id)
     562            .handle({
     563                200: function(responses) {
     564                    var flatResponses = responsesToVariables(responses);
     565                    res.set({
     566                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
     567                    });
     568                    res.status(200);
     569                    writeObjectsToCSVStream(flatResponses, res);
     570                    res.end();
     571                },
     572                default: res.send.bind(res)
    162573            });
    163         };
    164     }
    165     function makeDocPost(type) {
    166         return function(req,res) {
    167             var doc = req.body;
    168             if ( doc.type === type && tv4.validateResult(doc, schema) ) {
    169                 couch.post(req.body).then(function(result){
    170                     res.send(200, result);
    171                 }, function(error){
    172                     res.send(500, error);
     574        });
     575   
     576    // Responses
     577    function getResponsesBySurveyRunId(surveyRunId) {
     578        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
     579        return HTTPResult.fromResponsePromise(couch.get(url).response,
     580                                              handleUnknownError)
     581        .handle({
     582            200: handleRowValues,
     583            default: handleUnknownResponse
     584        });
     585    }
     586    app.get('/api/responses',
     587        ensureAuthenticated,
     588        ensureMIME(JSON_MIME),
     589        function(req,res){
     590            var hr;
     591            if ( 'surveyRunId' in req.query ) {
     592                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
     593            } else {
     594                hr = getDocumentsOfType('Response');
     595            }
     596            hr.handle(res.send.bind(res));
     597        });
     598    app.get('/api/responses/:id',
     599        ensureAuthenticated,
     600        ensureMIME(JSON_MIME),
     601        makeDocGet_id('Response'));
     602    app.post('/api/responses',
     603        ensureAuthenticated,
     604        ensureMIME(JSON_MIME),
     605        function (req,res) {
     606            var doc = req.body;
     607            randomToken()
     608            .handle({
     609                201: function(token) {
     610                    doc.secret = token;
     611                    return postDocument('Response',doc);
     612                }
     613            })
     614            .handle(res.send.bind(res));
     615        });
     616    app.put('/api/responses/:id',
     617        ensureAuthenticated,
     618        ensureMIME(JSON_MIME),
     619        function (req,res) {
     620            var id = req.params.id;
     621            var doc = req.body;
     622            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     623            var hr;
     624            if ( typeof doc.secret === 'undefined' ) {
     625                hr = randomToken()
     626                .handle({
     627                    201: function(token) {
     628                        doc.secret = token;
     629                        return putDocument(id,rev,'Response',doc);
     630                    }
    173631                });
    174632            } else {
    175                 res.send(400,{error: "Document failed schema verification."});
     633                hr = putDocument(id,rev,'Response',doc);
    176634            }
    177         };
    178     }
    179     function notImplemented(req,res) {
    180         res.send(501,{error:"API not implemented yet."});
    181     }
    182 
    183     // Questions
    184     app.get('/api/questions',
    185         ensureAuthenticated,
    186         makeDocsGet('Question'));
    187     app.get('/api/questions/:id',
    188         ensureAuthenticated,
    189         makeDocGet_id('Question'));
    190     app.post('/api/questions',
    191         ensureAuthenticated,
    192         makeDocPost('Question'));
    193     app.put('/api/questions/:id',
    194         ensureAuthenticated,
    195         makeDocPut_id('Question'));
    196     app.delete('/api/questions/:id',
    197         ensureAuthenticated,
    198         makeDocDel_id('Question'));
    199     app.get('/api/questions/categories',
    200         ensureAuthenticated,
    201         notImplemented);
    202 
    203     // Surveys
    204     app.get('/api/surveys',
    205         ensureAuthenticated,
    206         makeDocsGet('Survey'));
    207     app.get('/api/surveys/:id',
    208         ensureAuthenticated,
    209         makeDocGet_id('Survey'));
    210     app.post('/api/surveys',
    211         ensureAuthenticated,
    212         makeDocPost('Survey'));
    213     app.put('/api/surveys/:id',
    214         ensureAuthenticated,
    215         makeDocPut_id('Survey'));
    216     app.delete('/api/surveys/:id',
    217         ensureAuthenticated,
    218         makeDocDel_id('Survey'));
    219 
    220     // SurveyRuns
    221     app.get('/api/surveyRuns',
    222         ensureAuthenticated,
    223         makeDocsGet('SurveyRun'));
    224     app.get('/api/surveyRuns/:id',
    225         ensureAuthenticated,
    226         makeDocGet_id('SurveyRun'));
    227     app.get('/api/surveyRuns/:id/responses.csv',
    228         ensureAuthenticated,
    229         function(req, res) {
    230             var id = req.params.id;
    231             var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id);
    232             couch.get(url).then(function(result){
    233                 var responses = _.map(result.rows, function(item) { return item.value; });
    234                 var flatResponses = responsesToVariables(responses);
    235                 res.set({
    236                     'Content-Type': 'text/csv',
    237                     'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
    238                 });
    239                 res.status(200);
    240                 writeObjectsToCSVStream(flatResponses, res);
    241                 res.end();
    242             }, function(error){
    243                 res.send(404, {error: "Cannot find responses for survey run "+id});
    244             });
    245         });
    246     app.get('/api/surveyRuns/:id/responses',
    247         ensureAuthenticated,
     635            hr.handle(res.send.bind(res));
     636        });
     637    app.delete('/api/responses/:id',
     638        ensureAuthenticated,
     639        ensureMIME(JSON_MIME),
     640        makeDocDel_id('Response'));
     641
     642    //respondents api
     643    function isSurveyRunRunning(surveyRun) {
     644        var now = new Date();
     645        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
     646        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
     647        return afterStart && beforeEnd;
     648    }
     649    app.get('/api/open/responses/:id',
     650        ensureMIME(JSON_MIME),
    248651        function(req,res) {
    249652            var id = req.params.id;
    250             var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id);
    251             couch.get(url).then(function(result){
    252                 var responses = _.map(result.rows, function(item) { return item.value; });
    253                 res.send(200, responses);
    254             }, function(error){
    255                 res.send(404, {error: "Cannot find responses for survey run "+id});
    256             });
    257         });
    258    
    259     // Responses
    260     app.get('/api/responses',
    261         ensureAuthenticated,
    262         makeDocsGet('Response'));
    263     app.get('/api/responses/:id',
    264         ensureAuthenticated,
    265         makeDocGet_id('Response'));
    266     app.post('/api/responses',
    267         ensureAuthenticated,
    268         makeDocPost('Response'));
    269     app.put('/api/responses/:id',
    270         ensureAuthenticated,
    271         makeDocPut_id('Response'));
    272     app.delete('/api/responses/:id',
    273         ensureAuthenticated,
    274         makeDocDel_id('Response'));
     653            var rev = etags.parse(req.header('If-Non-Match'))[0];
     654            var secret = req.query.secret;
     655            getDocument(id,rev,'Response')
     656            .handle({
     657                200: function(response) {
     658                    if ( response.secret === secret ) {
     659                        return getDocument(response.surveyRunId,[],'SurveyRun')
     660                        .handle({
     661                            200: function(surveyRun) {
     662                                if ( !isSurveyRunRunning(surveyRun) ) {
     663                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
     664                                } else {
     665                                    response._surveyRun = surveyRun;
     666                                    return response;
     667                                }
     668                            }
     669                        });
     670                    } else {
     671                        return new HTTPResult(403,{error: "Wrong secret for response"});
     672                    }
     673                }
     674            })
     675            .handle(res.send.bind(res));
     676        });
     677    app.post('/api/open/responses',
     678        ensureMIME(JSON_MIME),
     679        function(req,res) {
     680            var secret = req.query.secret;
     681            var response = req.body;
     682            delete response._surveyRun;
     683            getDocument(response.surveyRunId,[],'SurveyRun')
     684            .handle({
     685                200: function(surveyRun) {
     686                    if ( surveyRun.secret !== secret ) {
     687                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
     688                    } else if ( !isSurveyRunRunning(surveyRun) ) {
     689                        return new HTTPResult(404,{error:"Survey is not running anymore."});
     690                    } else if ( surveyRun.mode === 'closed' ) {
     691                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
     692                    } else {
     693                        return randomToken()
     694                        .handle({
     695                            201: function(token) {
     696                                response.secret = token;
     697                                return postDocument('Response',response)
     698                                .handle({
     699                                    201: function(doc){
     700                                        doc._surveyRun = surveyRun;
     701                                        return doc;
     702                                    }
     703                                });
     704                            }
     705                        });
     706                    }
     707                }
     708            })
     709            .handle(res.send.bind(res));
     710        });
     711    app.put('/api/open/responses/:id',
     712        ensureMIME(JSON_MIME),
     713        function(req,res){
     714            var id = req.params.id;
     715            var doc = req.body;
     716            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     717            var secret = req.query.secret || doc.secret;
     718            delete doc._surveyRun;
     719            getDocument(id,[],'Response')
     720            .handle({
     721                200: function(prev) {
     722                    if ( prev.secret !== secret ) {
     723                        return new HTTPResult(403,{error: "Secrets are not the same."});
     724                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
     725                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
     726                    } else {
     727                        doc.secret = secret; // restore in case it got lost or was changed
     728                        return getDocument(doc.surveyRunId,[],'SurveyRun')
     729                        .handle({
     730                            200: function(surveyRun) {
     731                                if ( !isSurveyRunRunning(surveyRun) ) {
     732                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
     733                                } else {
     734                                    return putDocument(id,rev,'Response',doc)
     735                                    .handle({
     736                                        200: function(doc) {
     737                                            doc._surveyRun = surveyRun;
     738                                            return new HTTPResult(201,doc);
     739                                        }
     740                                    });
     741                                }
     742                            }
     743                        });
     744                    }
     745                }
     746            })
     747            .handle(res.send.bind(res));
     748        });
     749    app.delete('/api/open/responses/:id',
     750        ensureMIME(JSON_MIME),
     751        function(req,res){
     752            var id = req.params.id;
     753            var doc = req.body;
     754            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
     755            var secret = req.query.secret || doc.secret;
     756            delete doc._surveyRun;
     757            getDocument(id,[],'Response')
     758            .handle({
     759                200: function(prev) {
     760                    if ( prev.secret !== secret ) {
     761                        return new HTTPResult(403,{error: "Secrets are not the same."});
     762                    } else {
     763                        return deleteDocument(id,rev,doc);
     764                    }
     765                }
     766            })
     767            .handle(res.send.bind(res));
     768        });
     769
     770    // uuids
     771    app.get('/api/uuids',
     772        ensureAuthenticated,
     773        ensureMIME(JSON_MIME),
     774        function(req,res){
     775            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
     776            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
     777                                           handleUnknownError)
     778            .handle({
     779                200: function(res) {
     780                    return res.uuids;
     781                },
     782                default: handleUnknownResponse
     783            })
     784            .handle(res.send.bind(res));
     785        });
    275786
    276787    return app;
    277 
    278788}
    279789
     
    341851    });
    342852}
     853
     854function randomToken() {
     855    var result = new HTTPResult();
     856    cryptoken(8)
     857    .then(function(token){
     858        result.set(201,token);
     859    }, function(ex){
     860        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
     861    });
     862    return result;
     863}
Note: See TracChangeset for help on using the changeset viewer.