Changeset 468


Ignore:
Timestamp:
06/26/13 21:17:41 (12 years ago)
Author:
hendrikvanantwerpen
Message:

Improved authentication

  • Authentication cannot be cancelled, removing a lot of weird race conditions.
  • We offer a request provider that has automatic retry in case of authentication failures.
  • Reduced dependency on LoginDialog? by having it act independent based on events. Other modules can just depend on 'session'.
Location:
Dev/trunk
Files:
1 added
1 deleted
11 edited
3 moved

Legend:

Unmodified
Added
Removed
  • Dev/trunk/docs/TODO.md

    r463 r468  
    22====
    33
     4 * Drop separate development directory for more complicated build process.
     5   - Generate files in-place (maybe even include in commit, although I hate that.)
     6     Automatically update gitignore for generated files?
     7   - Add header to files to make sure they are not edited as source files by accident.
     8   - A smarter copy might also fix this, maybe we should implement our own copy.
     9   - Can we do amd dependency checks?
     10   - We should also lint the generated javascript, because coffee
     11     doesn't catch accidental globals
    412 * Have a generic page-wide note (rounded corner, close button thingy)
    513   for notification (maybe completely replace the toaster?)
     
    1220     these must match the types returned by the actual widgets. Fix
    1321     this.
     22   - Would be nicer if only newly dropped questions are in edit mode
     23     and the ones that exist on load in view mode.
    1424 * Codes
    1525   - Details need to be figured out. Now we have a code in one place,
     
    2434     was disabled because the questions didn't pass anymore.
    2535   - use it server side (and maybe client side as well?)
     36 * Editing
     37   - Warn if leaving a page that has edits.
    2638 * Validate documents on the server before saving them.
    2739 * Implement authentication and later authorization.
  • Dev/trunk/src/client/index.html

    r466 r468  
    3030        </div>
    3131        <div id="toaster" data-dojo-type="qed-client/app/Notifications"></div>
    32         <div id="loginDialog" data-dojo-type="qed-client/ui/LoginDialog"></div>
     32        <div data-dojo-type="qed-client/ui/LoginDialogWrapper"></div>
    3333    </body>
    3434</html>
  • Dev/trunk/src/client/qed-client/index.js

    r467 r468  
    77    "./store",
    88    "./store/request",
     9    "./ui/LoginDialogWrapper",
    910    "./ui/MainMenu",
    1011    "dojo/_base/array",
     
    1213    "./stddeps",
    1314    "dojo/domReady!"
    14 ], function(Content, Page, Router, routes, session, store, request, MainMenu, array, parser) {
     15], function(Content, Page, Router, routes, session, store,
     16            request, LoginDialogWrapper, MainMenu, array, parser) {
    1517
    1618    parser.parse();
     
    2123        Router.register(route);
    2224    });
     25    Router.startup();
    2326
    24     session.restore().then(function(){
    25         Router.startup();
    26     });
    27    
     27    session.restore();
     28
    2829});
  • Dev/trunk/src/client/qed-client/request.js

    r464 r468  
    11define([
     2    "./session",
    23    "dojo/Deferred",
     4    "dojo/on",
    35    "dojo/request"
    4 ], function(Deferred, request) {
     6], function(session, Deferred, on, request) {
    57
    6     var authenticated = true;
     8    var user = session.get();
    79    var queue = [];
    810   
     11    on(session, 'change', function(newUser) {
     12        user = newUser;
     13        if ( user ) {
     14            retry();
     15        }
     16    });
     17   
     18    function retry() {
     19        if (queue.length > 0) {
     20            var item = queue.shift();
     21            console.log("Retry",item.url);
     22            real_request(item);
     23        }
     24    }
     25   
     26    function real_request(item) {
     27        var req = request(item.url,item.options);
     28
     29        // forward successfull response
     30        req.then(function(body){
     31            item.dfd.resolve(body);
     32        });
     33
     34        // handle unauthenticated and queued requests
     35        req.response.then(function(response){
     36            retry();
     37        }, function(error) {
     38            if ( error.response.status === 401 ) {
     39                queue.unshift(item);
     40                session.restore();
     41            } else {
     42                item.dfd.reject(error); // this should be error body
     43                                        // not, the request?
     44                retry();
     45            }
     46        });
     47    }
     48   
    949    var _request = function(url, options) {
    10         var dfd = new Deferred();
    11         if ( authenticated ) {
    12             var req = request(url,options);
     50        var item = {
     51            url: url,
     52            options: options,
     53            dfd: new Deferred()
     54        };
     55        // only do the request directly if we are authenticated and
     56        // there are no earlier requests queued.
     57        if ( user && queue.length === 0 ) {
     58            console.log("Request",url);
     59            real_request(item);
     60        } else {
     61            console.log("Push",url);
     62            queue.push(item);
     63        }
     64        return item.dfd.promise;
     65    };
    1366
    14             // forward successfull response
    15             req.then(function(data){
    16                 dfd.resolve(data);
    17             });
    18            
    19             req.response.then(function(response){
    20                 // if we are recovering, launch other requests
    21             }, function(error) {
    22                 if ( error.response.status === 401 ) {
    23                     queue.push({
    24                         url: url,
    25                         options: options,
    26                         dfd: dfd
    27                     });
    28                     console.log("Retry goes here :)");
    29                 } else {
    30                     dfd.reject(error);
    31                 }
    32             });
    33         } else {
    34             queue.push({
    35                 url: url,
    36                 options: options,
    37                 dfd: dfd
    38             });
    39         }
    40         return dfd.promise;
    41     };
    4267    return _request;
    4368});
  • Dev/trunk/src/client/qed-client/session.coffee

    r466 r468  
    77    Session = declare [Evented],
    88        info: null
     9
    910        get: () ->
    1011            @info
     12
    1113        restore: () ->
    1214            request '/api/login',
     
    1618                @_set res
    1719            , () =>
    18                 @_set null
     20                throw (@_set null)
     21
    1922        login: (username, password) ->
    2023            request '/api/login',
     
    2730                @_set res
    2831            , () =>
    29                 @_set null
     32                throw (@_set null)
     33
    3034        logout: () ->
    3135            request '/api/logout',
    3236                method: "POST"
    3337                handleAs: "json"
    34             .then ((res) =>
    35                 console.log "Logged out"
    36                 @_set null), () =>
    37                 console.log "Error logging out"
     38            .then (res) =>
     39                @_set null
     40            , () =>
     41                @_set null
    3842       
    3943        _set: (newInfo) ->
    40             @info = newInfo
    41             @emit 'change', @info
    42             @info
     44            if (newInfo isnt @info)
     45                @info = newInfo
     46                @emit 'change', @info
     47                @info
    4348
    4449    new Session()
  • Dev/trunk/src/client/qed-client/stddeps.js

    r466 r468  
    3333    './model/widgets/SurveyWidget',
    3434
    35     './ui/LoginDialog',
     35    './ui/LoginDialogWrapper',
    3636    './ui/MainMenu',
    3737    './ui/MenuBarLink',
  • Dev/trunk/src/client/qed-client/store.js

    r466 r468  
    55    'dojox/json/schema',
    66    './model/schema',
     7    './request',
    78    './store/CouchStore'
    8 ],function(stamp,Cache,Memory,jsonSchema,schema,CouchStore){
     9],function(stamp,Cache,Memory,jsonSchema,schema,request,CouchStore){
    910   
    1011    var couchStore = new CouchStore({
    11         target: 'api/data/' /*,
     12        target: 'api/data/',
     13        request: request /*,
    1214        validate: function(object) {
    1315            var result = jsonSchema.validate(object,schema);
  • Dev/trunk/src/client/qed-client/store/CouchStore.js

    r466 r468  
    2828        _responseIdProperty: "id",
    2929        _responseRevProperty: "rev",
     30        request: request,
    3031        constructor: function(options){
    3132            declare.safeMixin(this, options);
     
    3940        info: function(){
    4041            var dfd = new Deferred();
    41             request(this.target, {
     42            this.request(this.target, {
    4243                method: "GET",
    4344                handleAs: "json",
     
    5859        get: function(id){
    5960            var dfd = new Deferred();
    60             request(this.target + encodeURIComponent(id), {
     61            this.request(this.target + encodeURIComponent(id), {
    6162                method: "GET",
    6263                handleAs: "json",
     
    103104            var id = options.id ? options.id : this.getIdentity(object);
    104105            var hasId = typeof id !== "undefined";
    105             request(hasId ? this.target + encodeURIComponent(id) : this.target, {
     106            this.request(hasId ? this.target + encodeURIComponent(id) : this.target, {
    106107                method: hasId ? "PUT" : "POST",
    107108                data: json.toJson(object),
     
    129130        remove: function(id,rev){
    130131            var dfd = new Deferred();
    131             request(this.target + encodeURIComponent(id), {
     132            this.request(this.target + encodeURIComponent(id), {
    132133                method: "DELETE",
    133134                headers: {
     
    243244            }
    244245           
    245             request(this.target + query, {
     246            this.request(this.target + query, {
    246247                method: "GET",
    247248                handleAs: "json",
  • Dev/trunk/src/client/qed-client/ui/LoginDialogWrapper.coffee

    r466 r468  
    11define [
    22    "../session",
     3    "dijit/_WidgetBase",
     4    "dijit/_TemplatedMixin",
    35    "dijit/_WidgetsInTemplateMixin",
    4     "dijit/Dialog",
    5     "dijit/form/ValidationTextBox",
     6    "dijit/registry",
    67    "dojo/_base/declare",
    78    "dojo/_base/event",
    8     "dojo/dom-construct",
    9     "dojo/text!./templates/LoginDialog.html"
    10 ], (session, _WidgetsInTemplateMixin, Dialog,
    11     ValidationTextBox, declare, event,
    12     domConstruct, template) ->
    13     declare [Dialog,_WidgetsInTemplateMixin],
     9    "dojo/_base/lang",
     10    "dojo/on",
     11    "dojo/text!./templates/LoginDialogWrapper.html"
     12], (session, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin,
     13    registry, declare, event, lang, _on, template) ->
     14    declare [_WidgetBase,_TemplatedMixin,_WidgetsInTemplateMixin],
    1415        templateString: template
    15         title: "Login to QED"
    16         show: () ->
    17             @loginForm.reset()
     16        startup: () ->
     17            if @_started then return
    1818            @inherited arguments
     19            _on session, 'change', (lang.hitch @, @onUserChange)
     20            @onUserChange session.get()
    1921        onLogin: (evt) ->
    20             if @loginForm.validate
     22            if @loginForm.validate()
    2123                value = @loginForm.get 'value'
    2224                session.login value.username,
    2325                              value.password
    24                 .then (() => @hide()),
    25                       () => alert "Login failed!"
    26             @hide()
     26                .then () =>
     27                    @loginDialog.hide()
     28                , () =>
     29                    alert "Login failed!"
    2730            event.stop evt if evt
    2831            false
    29         onCancel: (evt) ->
    30             @hide()
    31             event.stop evt if evt
    32             false
     32        onUserChange: (user) ->
     33            if user
     34                @loginDialog.hide()
     35            else
     36                @loginDialog.show()
  • Dev/trunk/src/client/qed-client/ui/SessionMenu.coffee

    r466 r468  
    11define [
    22    "../session",
    3     "dijit/DropDownMenu",
    4     "dijit/registry",
     3    "./LoginDialogWrapper",
    54    "dijit/MenuBarItem",
    65    "dojo/_base/declare",
    76    "dojo/_base/event",
    87    "dojo/on"
    9 ], (session, DropDownMenu, registry, MenuBarItem, declare, event, _on) ->
     8], (session, LoginDialogWrapper, MenuBarItem, declare, event, _on) ->
    109    declare [MenuBarItem],
    1110        info: null
    1211        postCreate: () ->
    13             @loginDialog = registry.byId 'loginDialog'
    14             console.error "Cannot find loginDialog" if not @loginDialog
    1512            @set 'label', "Login"
    1613        startup: () ->
     
    2926                session.logout()
    3027            else
    31                 @loginDialog.show()
     28                LoginDialogWrapper.show()
    3229            event.stop evt if evt
    3330            false
  • Dev/trunk/src/package.json

    r464 r468  
    99    "passport-local": "~0.1.6",
    1010    "q": "~0.9.6",
    11     "q-io": "~1.9.1"
     11    "request": "~2.21.0"
    1212  },
    1313  "engines": {
  • Dev/trunk/src/server/config/config-couchdb.js

    r466 r468  
    1 var q = require('q');
    2 var request = require('../util/request');
    3 var _ = require('underscore');
    4 var util = require('util');
     1var q = require('q'),
     2    request = require('../util/q-request'),
     3    _ = require('underscore'),
     4    util = require('util');
    55
    66var designDocs = require('./couchdb-design-docs');
     
    3434                'accept': 'application/json'
    3535            },
    36             body: content
     36            body: JSON.stringify(content)
    3737        };
    38         return request(url,options);
     38        //console.log('req',url,options);
     39        return request(url,options).then(function(res){
     40            return JSON.parse(res);
     41        }, function(err){
     42            return JSON.parse(err);
     43        });
    3944    }
    4045
     
    9095            }
    9196        }));
    92     }).then(function(){
     97    }).then(function(results){
    9398        console.log("Done!");
    9499    },function(err){
  • Dev/trunk/src/server/heroku.js

    r464 r468  
    33
    44var configCouch = require('./config/config-couchdb');
     5
     6console.log("Running on",couchDbURL);
    57
    68configCouch(couchDbURL)
  • Dev/trunk/src/server/util/q-request.js

    r466 r468  
    1 var http = require('q-io/http'),
     1var q = require('q'),
     2    request = require('request'),
    23    url = require('url'),
    34    _ = require('underscore');
    45
    5 module.exports = function(urlOrObject, options) {
     6module.exports = function(url, options) {
    67
     8    var dfd = q.defer();
     9    dfd.response = q.defer();
     10   
    711    options = options
    812        ? _.clone(options)
    913        : {};
    10     options.url = _.isString(urlOrObject)
    11         ? url.parse(urlOrObject)
    12         : _.clone(urlOrObject);
     14    options.uri = url || options.uri;
     15   
     16    request(options,function(err,res,body){
     17        if ( err ) {
     18            dfd.response.reject(err);
     19        } else {
     20            if ( res.statusCode >= 200 && res.statusCode < 300 ) {
     21                dfd.resolve(body);
     22            } else {
     23                dfd.reject(body);
     24            }
     25        }
     26        dfd.response.resolve(res);
     27    });
    1328
    14     // wrap content into q-io thingy
    15     if ( options.body ) {
    16         options.body = {
    17             forEach: function(callback) {
    18                 callback(JSON.stringify(options.body));
    19             }
    20         };
    21     };
     29    return dfd.promise;
    2230   
    23     // add auth header since q-io doesn't support this
    24     if ( options.url.auth ) {
    25         options.headers.authorization = 'Basic '+(new Buffer(options.url.auth).toString("base64"));
    26     }
    27    
    28     // make request and collect results
    29     return http.request(options)
    30     .then(function(res){
    31         return res.body.read().then(function(content){
    32             return content.length > 0 ? JSON.parse(content) : null;
    33         });
    34     },function(res){
    35         return res.body.read().then(function(error){
    36             return error.length > 0 ? JSON.parse(error) : null;
    37         });
    38     });
    3931};
Note: See TracChangeset for help on using the changeset viewer.