Commits

ont committed 1706a85

(*) login: see details in desctiption...

Here is:
- http-auth-interceptor for handling and replaying 401 requests
- @auth and @jsonify decorators
- User peewee model
- md5( pass + md5(pass) ) hash scheme
- foundation overlay + modal form

  • Participants
  • Parent commits 283feaa

Comments (0)

Files changed (11)

 db/
+tmp/
 .*\.pyc
+.*\.swp
+import bottle
 import models
-from pw2js import pw2js
-from bottle import route, run, static_file, request
+from md5 import md5
+from tools import pw2js, auth, jsonify
+from beaker.middleware import SessionMiddleware
 
-@route('/owns')
+
+@bottle.get('/users/info')
+@auth
+def login():
+    s = bottle.request.environ.get('beaker.session')
+    return {'login' : s['login']}
+
+@bottle.post('/users/login')
+def login():
+    json = bottle.request.json
+    login = bottle.request.json.get('login')
+    passw = bottle.request.json.get('passw')
+
+    passw = md5(passw + md5(passw).hexdigest()).hexdigest()
+
+    try:
+        user = models.User.get( (models.User.login == login) & (models.User.passw == passw) )
+        s = bottle.request.environ.get('beaker.session')
+        s['logged'] = True
+        s['login'] = user.login
+        s.save()
+
+    except:
+        bottle.abort(403, "Wrong login/passw")
+
+    return 'OK'
+
+
+@bottle.route('/owns')
+@auth
+@jsonify
 def owns_list():
     owns = list(models.Own.select().join(models.Card))
-    return {'success' : True, 'data' : pw2js(owns, [
+    return pw2js(owns, [
         #'when',
         ('card',['name','pict','cost','cnum','power','tough',('cset',['name'])]),
-    ])}
+    ])
 
-@route('/owns', method='POST')
+@bottle.route('/owns', method='POST')
+@jsonify
 def owns_add():
-    name = request.json.get('name')
-    cset = request.json.get('cset')
-    cnt  = request.json.get('cnt')
+    json = bottle.request.json
+    name = json.get('name')
+    cset = json.get('cset')
+    cnt  = json.get('cnt')
 
     card = models.Card.search(name, cset).get()
     card.fetch()
         own.save()
         res.append(own)
 
-    return {'success' : True, 'data' : pw2js(res, [
+    return pw2js(res, [
         #'when',
         ('card',['name','pict','cost','cnum','power','tough',('cset',['name'])]),
-    ])}
+    ])
 
 
-@route('/cards')
+@bottle.route('/cards')
+@jsonify
 def cards_list():
-    name = request.query['name']
+    name = bottle.request.query['name']
 
     ## take all cards with names GLOB (case sensitive LIKE) to 'name'
     cs = models.Card.search(name)
 
-    return {'success' : True, 'data' : pw2js(cs, [
+    return pw2js(cs, [
         'name','pict',
         ('cset',['name']),
-    ])}
+    ])
 
 
-@route('/')
-@route('/<root:re:js|css|img>/<path:path>')
+@bottle.route('/')
+@bottle.route('/<root:re:js|css|img>/<path:path>')
 def static( root = '/', path = 'index.html' ):
-    return static_file( path, './' + root )   ## TODO: relative path... (may be problems)
+    return bottle.static_file( path, './' + root )   ## TODO: relative path... (may be problems)
 
-run(host='localhost', port=8080, debug=True, reloader=True)
+
+@bottle.route('/test')
+def test():
+    s = bottle.request.environ.get('beaker.session')
+    s['test'] = s.get('test',0) + 1
+    s.save()
+    return 'Logged: %s [%s]' % (s['logged'], s['test'])
+
+
+################################################################################
+################################################################################
+
+def main():
+    opts = {
+        'session.type': 'file',
+        'session.cookie_expires': 300,
+        'session.data_dir': './tmp',
+        'session.auto': True
+    }
+    app = SessionMiddleware(bottle.app(), opts)
+
+    bottle.run(app=app, host='localhost', port=8080, debug=True, reloader=True)
+
+
+if __name__ == '__main__':
+    main()
     position: relative;
 }
 
-.mtg-cards-grid li > img:last-child {
+.mtg-zoom > img:last-child {
     display: none;
     position: absolute;
     top: 50%;
             transform: translate(-50%,-50%);
 }
 
-.mtg-cards-grid li:hover > img:last-child {
+.mtg-zoom:hover > img:last-child {
     display: block !important;
     max-width: 250px;
     margin: auto;
 .mtg-box {
     position: relative;
 }
+
+.mtg-visible {
+    display: inherit !important;
+    visibility: inherit !important;
+}
+
+.mtg-body {
+    padding-top: 23px;
+}
   <script src="js/peg-0.7.0.min.js"></script>
   <script src="js/lodash.min.js"></script>
   <script src="js/angular.min.js"></script>
-  <script src="js/restangular.min.js"></script>
+  <script src="js/http-auth-interceptor.js"></script>
+  <script src="js/restangular.js"></script>
   <script src="js/directives.js"></script>
   <script src="js/modules.js"></script>
   <script src="js/controllers.js"></script>
   <script src="js/vendor/custom.modernizr.js"></script>
 
 </head>
-<body ng-controller="MainCtrl">
-    <div class="contain-to-grid sticky">
+<body class="mtg-body" ng-controller="MainCtrl">
+    <div ng-class="{'mtg-visible' : dlg_login}" class="reveal-modal-bg"></div>
+    <div ng-controller="LoginCtrl" ng-class="{'mtg-visible' : dlg_login}" class="reveal-modal open">
+        <h2>Login</h2>
+        <div class="row">
+            <div class="small-8">
+                <div class="row">
+                    <div class="small-3 columns">
+                        <label for="email" class="right inline">Email</label>
+                    </div>
+                    <div class="small-9 columns">
+                        <input ng-model="post.login" type="text" id="email" placeholder="Inline Text Input">
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="small-3 columns">
+                        <label for="password" class="right inline">Password</label>
+                    </div>
+                    <div class="small-9 columns">
+                        <input ng-model="post.passw" type="password" id="password" placeholder="Inline Text Input">
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="large-offset-3 small-9 columns">
+                        <a ng-click="login()" class="button success" href="#">Try to login</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <a class="close-reveal-modal">×</a>
+    </div>
+
+    <div class="contain-to-grid sticky fixed">
         <nav class="top-bar">
             <ul class="title-area">
                 <li class="name">
                     <h1><a href="#">MtG deck lib</a></h1>
                 </li>
             </ul>
+            <ul class="right">
+                <li>
+                    <a ng-click="dlg_login=true" class="button">Login</a>
+                </li>
+            </ul>
         </nav>
     </div>
     <hr/>
 
     <div class="row" ng-repeat="(key,grp) in owns_m">
         <div class="large-12 columns">
-                <!--
-                <div ng-show="show_labels()" class="label">
-                    {{m.sort[0]}} = {{key}}
-                </div>
-                -->
             <div ng-switch="dmode" class="panel mtg-box">
-                <div class="label radius mtg-side-info-90">
+                <div ng-show="show_labels()" class="label radius mtg-side-info-90">
                     {{key}}<sup>{{m.sort[0]}}</sup>
                 </div>
                 <ul ng-switch-when="short" class="large-block-grid-3 mtg-cards-grid">
                 <ul ng-switch-when="thumb" class="large-block-grid-6 mtg-cards-grid">
                     <li ng-repeat="own in grp | orderBy:get_sorters()">
                         <span ng-show="own.cnt > 1" class="secondary label mtg-card-cnt">{{own.cnt}}</span>
-                        <img ng-src="{{own.card.pict}}"/>
-                        <img ng-src="{{own.card.pict}}"/> 
+                        <span class="mtg-zoom">
+                            <img ng-src="{{own.card.pict}}"/>
+                            <img ng-src="{{own.card.pict}}"/> 
+                        </span>
                     </li>
                 </ul>
             </div>
   '.js><\/script>')
   </script>
   
+  <!--
   <script src="js/foundation.min.js"></script>
-  <!--
   
   <script src="js/foundation/foundation.js"></script>
   
   -->
   
   <script>
-    $(document).foundation();
+    //$(document).foundation();
   </script>
 </body>
 </html>

File js/controllers.js

-function MainCtrl($scope, $http, Restangular, $mtg) {
+function MainCtrl($scope, $http, Restangular, mtg) {
     var prs = null;
     var owns = Restangular.all('owns');
     var cards = Restangular.all('cards');
 
     $scope.dmode = 'thumb';
+    $scope.dlg_login = false;
 
     $scope.m = {
         'sort' : [null, null, null],
     $scope.get_sorters = function(){
         return [
             function( own ){
-                return $mtg.prop( own.card, $scope.m.sort[1] );
+                return mtg.prop( own.card, $scope.m.sort[1] );
             },
             function( own ){
-                return $mtg.prop( own.card, $scope.m.sort[2] );
+                return mtg.prop( own.card, $scope.m.sort[2] );
             },
             'card.name'
         ];
         return _.size($scope.owns_m) > 1;
     }
 
+    /*
+     * Regroup and refilter owns by new filters.
+     */
     $scope.process = function(){
         var res = _($scope.owns);   // container for sorted and grouped "owns"
 
             });
 
         res = res.groupBy(function(x){
-            return $mtg.prop( x.card, $scope.m.sort[0] );
+            return mtg.prop( x.card, $scope.m.sort[0] );
         });
 
         $scope.owns_m = res.value();  // convert lo-dash to usual array
         console.log(res.value());
     }
+
+    /*
+     * Login form hooks.
+     */
+    $scope.$on('event:auth-loginRequired',  function(){ $scope.dlg_login = true;  });
+    $scope.$on('event:auth-loginConfirmed', function(){ $scope.dlg_login = false; });
+    $scope.$on('event:auth-loginCancelled', function(){ $scope.dlg_login = false; });
 }
 
 function RightNumpadCtrl( $scope )
         $scope.process();
     }
 }
+
+/*
+ * Login form controller wich emits events for MainCtrl via authService.
+ */
+function LoginCtrl( $scope, $http, authService, Restangular)
+{
+    var users = Restangular.all('users');  // build API root for user (info + login API)
+
+    $scope.user = users.one('info').get();
+
+    $scope.login = function(){
+        $scope.user = users.customPOST($scope.post, 'login').then(function(){
+            authService.loginConfirmed();
+        });
+
+        //$http.post('/login', $scope.post).success(function(user){
+        //    authService.loginConfirmed();
+        //});
+    }
+}
+

File js/http-auth-interceptor.js

+/*global angular:true, browser:true */
+
+/**
+ * @license HTTP Auth Interceptor Module for AngularJS
+ * (c) 2012 Witold Szczerba
+ * License: MIT
+ */
+(function () {
+  'use strict';
+
+  angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer'])
+
+  .factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) {
+    return {
+      /**
+       * Call this function to indicate that authentication was successfull and trigger a
+       * retry of all deferred requests.
+       * @param data an optional argument to pass on to $broadcast which may be useful for
+       * example if you need to pass through details of the user that was logged in
+       */
+      loginConfirmed: function(data, configUpdater) {
+        var updater = configUpdater || function(config) {return config;};
+        $rootScope.$broadcast('event:auth-loginConfirmed', data);
+        httpBuffer.retryAll(updater);
+      },
+
+      /**
+       * Call this function to indicate that authentication should not proceed.
+       * All deferred requests will be abandoned or rejected (if reason is provided).
+       * @param data an optional argument to pass on to $broadcast.
+       * @param reason if provided, the requests are rejected; abandoned otherwise.
+       */
+      loginCancelled: function(data, reason) {
+        httpBuffer.rejectAll(reason);
+        $rootScope.$broadcast('event:auth-loginCancelled', data);
+      }
+    };
+  }])
+
+  /**
+   * $http interceptor.
+   * On 401 response (without 'ignoreAuthModule' option) stores the request
+   * and broadcasts 'event:angular-auth-loginRequired'.
+   */
+  .config(['$httpProvider', function($httpProvider) {
+
+    var interceptor = function($rootScope, $q, httpBuffer) {
+      return {
+          'responseError': function(response) {
+            if (response.status === 401 && !response.config.ignoreAuthModule) {
+              var deferred = $q.defer();
+              httpBuffer.append(response.config, deferred);
+              $rootScope.$broadcast('event:auth-loginRequired');
+              return deferred.promise;
+            }
+            // otherwise, default behaviour
+            return $q.reject(response);
+          }
+      }
+    };
+    $httpProvider.interceptors.push(interceptor);
+  }]);
+
+  /**
+   * Private module, a utility, required internally by 'http-auth-interceptor'.
+   */
+  angular.module('http-auth-interceptor-buffer', [])
+
+  .factory('httpBuffer', ['$injector', function($injector) {
+    /** Holds all the requests, so they can be re-requested in future. */
+    var buffer = [];
+
+    /** Service initialized later because of circular dependency problem. */
+    var $http;
+
+    function retryHttpRequest(config, deferred) {
+      function successCallback(response) {
+        deferred.resolve(response);
+      }
+      function errorCallback(response) {
+        deferred.reject(response);
+      }
+      $http = $http || $injector.get('$http');
+      $http(config).then(successCallback, errorCallback);
+    }
+
+    return {
+      /**
+       * Appends HTTP request configuration object with deferred response attached to buffer.
+       */
+      append: function(config, deferred) {
+        buffer.push({
+          config: config,
+          deferred: deferred
+        });
+      },
+
+      /**
+       * Abandon or reject (if reason provided) all the buffered requests.
+       */
+      rejectAll: function(reason) {
+        if (reason) {
+          for (var i = 0; i < buffer.length; ++i) {
+            buffer[i].deferred.reject(reason);
+          }
+        }
+        buffer = [];
+      },
+
+      /**
+       * Retries all the buffered requests clears the buffer.
+       */
+      retryAll: function(updater) {
+        for (var i = 0; i < buffer.length; ++i) {
+          retryHttpRequest(updater(buffer[i].config), buffer[i].deferred);
+        }
+        buffer = [];
+      }
+    };
+  }]);
+})();

File js/modules.js

-var main = angular.module('MainApp', ['widgets', 'restangular', 'services']);
-
-main.config(function(RestangularProvider) {
-    // configuring response exctractor for restangular
-    RestangularProvider.setResponseExtractor(function(response, operation, what, url) {
-        var res = response.data;  // actual data in 'data' section
-        res.meta = response.success;
-        return res;
-    });
-
-    //// add custom method 'search' to cards collection
-    //RestangularProvider.addElementTransformer('cards', false, function(cards) {
-    //        // signature is (name, operation, path, params, headers, elementToPost)
-    //        cards.addRestangularMethod('search', 'post', 'search');
-    //        return cards;
-    //});
-});
+var main = angular.module('MainApp', ['http-auth-interceptor', 'widgets', 'restangular', 'services']);
 
 var services = angular.module('services', []);
 
 /*
  * MtG specific functions in self-containing box.
  */
-services.factory('$mtg', function() {
+services.factory('mtg', function() {
     var getters = {
         'cost' : function( card ){
             var res = 0;
             print self.type, self.power, self.tough, self.cost, self.rule, self.legacy
 
 
+class User(SqliteModel):
+    login = CharField()
+    passw = CharField()
+
+
 class Own(SqliteModel):
+    user = ForeignKeyField(User, related_name='owns')
     card = ForeignKeyField(Card, related_name='owns')
     foil = BooleanField(default=False)
     when = DateTimeField(default=datetime.datetime.now)
 
 
+#Own.create_table()
+#User.create_table()
+
 if not database.get_tables():
     Own.create_table()
     Card.create_table()

File pw2js.py

-import collections
-
-class Jsonify(object):
-    def jsonify(self, obj, fs):
-        res1 = dict([(k, getattr(obj, k)) for k in fs if isinstance(k, str)])
-        res2 = dict([(k[0], self.jsonify(getattr(obj, k[0]),k[1])) for k in fs if isinstance(k, tuple)])
-        res1.update(res2)
-        return res1
-
-    def __call__(self, x, fs):
-        if isinstance(x, collections.Iterable):
-            return [ self.jsonify(v, fs) for v in x ]
-        else:
-            return self.jsonify(x, fs)
-
-pw2js = Jsonify()

File tmp/.hgkeep

Empty file added.
+import json
+import bottle
+import collections
+
+def auth(fn):
+    """ Simple auth decorator based on session.
+    """
+    def wrapper(*args, **kwargs):
+        s = bottle.request.environ.get('beaker.session')
+        if s.get('logged', False):
+            return fn(**kwargs)
+        else:
+            bottle.abort(401, "Sorry")
+
+    return wrapper
+
+
+def jsonify(fn):
+    """ Force JSON answer from function on
+        non dictionary answers.
+    """
+    def wrapper(*args, **kwargs):
+        bottle.response.content_type = 'application/json'
+        return json.dumps(fn(**kwargs))
+
+    return wrapper
+
+
+class Jsonify(object):
+    def jsonify(self, obj, fs):
+        res1 = dict([(k, getattr(obj, k)) for k in fs if isinstance(k, str)])
+        res2 = dict([(k[0], self.jsonify(getattr(obj, k[0]),k[1])) for k in fs if isinstance(k, tuple)])
+        res1.update(res2)
+        return res1
+
+    def __call__(self, x, fs):
+        if isinstance(x, collections.Iterable):
+            return [ self.jsonify(v, fs) for v in x ]
+        else:
+            return self.jsonify(x, fs)
+
+pw2js = Jsonify()