Browse Source

fixes #136: user pages

Davide Alberani 7 years ago
parent
commit
5fc0ff0723

+ 9 - 0
angular_app/js/app.js

@@ -164,6 +164,15 @@ eventManApp.config(['$stateProvider', '$urlRouterProvider',
                 templateUrl: 'users-list.html',
                 controller: 'UsersCtrl'
             })
+            .state('user', {
+                url: '/user',
+                templateUrl: 'user-main.html'
+            })
+            .state('user.edit', {
+                url: '/edit/:id',
+                templateUrl: 'user-edit.html',
+                controller: 'UsersCtrl'
+            })
             .state('login', {
                 url: '/login',
                 templateUrl: 'login.html',

+ 38 - 4
angular_app/js/controllers.js

@@ -273,6 +273,9 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
         /* Stuff to do when a ticket is added, modified or removed locally. */
 
         $scope._localAddTicket = function(ticket, original_person) {
+            if (!$state.is('event.tickets')) {
+                return true;
+            }
             var ret = true;
             if (!$scope.event.persons) {
                 $scope.event.persons = [];
@@ -652,10 +655,21 @@ eventManControllers.controller('PersonDetailsCtrl', ['$scope', '$state', 'Person
 );
 
 
-eventManControllers.controller('UsersCtrl', ['$scope', '$rootScope', '$state', '$log', 'User',
-    function ($scope, $rootScope, $state, $log, User) {
+eventManControllers.controller('UsersCtrl', ['$scope', '$rootScope', '$state', '$log', 'User', '$uibModal',
+    function ($scope, $rootScope, $state, $log, User, $uibModal) {
         $scope.loginData = {};
-        $scope.usersOrderProp = ['username'];
+        $scope.user = {};
+        $scope.updateUserInfo = {};
+        $scope.users = [];
+        $scope.usersOrderProp = 'username';
+        $scope.ticketsOrderProp = 'title';
+
+        $scope.confirm_delete = 'Do you really want to delete this user?';
+        $rootScope.$on('$translateChangeSuccess', function () {
+            $translate('Do you really want to delete this user?').then(function (translation) {
+                $scope.confirm_delete = translation;
+            });
+        });
 
         $scope.updateUsersList = function() {
             if ($state.is('users')) {
@@ -665,8 +679,28 @@ eventManControllers.controller('UsersCtrl', ['$scope', '$rootScope', '$state', '
 
         $scope.updateUsersList();
 
+        if ($state.is('user.edit') && $state.params.id) {
+            $scope.user = User.get({id: $state.params.id}, function() {
+                $scope.updateUserInfo = $scope.user;
+            });
+        }
+
+        $scope.updateUser = function() {
+            User.update($scope.updateUserInfo);
+        };
+
         $scope.deleteUser = function(user_id) {
-            User.delete({id: user_id}, $scope.updateUsersList);
+            var modalInstance = $uibModal.open({
+                scope: $scope,
+                templateUrl: 'modal-confirm-action.html',
+                controller: 'ModalConfirmInstanceCtrl',
+                resolve: {
+                    message: function() { return $scope.confirm_delete; }
+                }
+            });
+            modalInstance.result.then(function() {
+                User.delete({id: user_id}, $scope.updateUsersList);
+            });
         };
 
         $scope.register = function() {

+ 1 - 5
angular_app/js/services.js

@@ -251,11 +251,7 @@ eventManServices.factory('User', ['$resource', '$rootScope',
                 method: 'GET',
                 interceptor : {responseError: $rootScope.errorHandler},
                 transformResponse: function(data, headers) {
-                    data = angular.fromJson(data);
-                    if (data.error) {
-                        return data;
-                    }
-                    return data.user || {};
+                    return angular.fromJson(data);
                 }
             },
 

+ 58 - 0
angular_app/user-edit.html

@@ -0,0 +1,58 @@
+<!-- show details of a User -->
+<div class="container">
+    <h1>{{user.username}}</h1>
+    <div class="panel panel-success table-striped">
+        <div class="panel-heading">{{'Update user information' | translate}}</div>
+        <div class="panel-body">
+            <form method="POST">
+                <div class="input-group input-group-lg top10">
+                    <span class="input-group-addon min150">{{'Email' | translate}}</span>
+                    <input type="email" id="new-email" name="new-email" ng-model="updateUserInfo.email" class="form-control">
+                </div>
+                <div class="input-group input-group-lg top10">
+                    <span class="input-group-addon min150">{{'Old password' | translate}}</span>
+                    <input type="password" id="old-password" name="old-password" ng-model="updateUserInfo.old_password" class="form-control">
+                    <span class="input-group-addon min150">{{'New password' | translate}}</span>
+                    <input type="password" id="new-password" name="new-password" ng-model="updateUserInfo.new_password" class="form-control">
+                </div>
+                <button type="submit" ng-click="updateUser()" class="btn btn-success top10">{{'update' | translate}}</button>
+            </form>
+        </div>
+    </div>
+    <div class="panel panel-primary table-striped top5">
+        <div class="panel-heading">{{'Tickets' | translate}}</div>
+        <div class="panel-body">
+
+            <form class="form-inline">
+                <div class="form-group">
+                    <label for="query-tickets">{{'Search:' | translate}}</label>
+                    <input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Event title' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
+                </div>
+                <div class="form-group">
+                    <label for="tickets-order">{{'Sort by:' | translate}}</label>
+                    <select id="tickets-order" class="form-control" ng-model="ticketsOrderProp">
+                        <option value="title">{{'Title' | translate}}</option>
+                        <option value="-title">{{'Title (descending)' | translate}}</option>
+                    </select>
+                </div>
+            </form>
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th>{{'Event' | translate}}</th>
+                        <th class="text-center">{{'Attended' | translate}}</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr ng-repeat="ticket in (user.tickets || []) | splittedFilter:query | orderBy:ticketsOrderProp">
+                        <td><strong><a ui-sref="event.ticket.edit({id: ticket.event_id, ticket_id: ticket._id})">{{ticket.event_title}}</a></strong></td>
+                        <td class="text-center">
+                            <span class="fa fa-lg {{(ticket.attended) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+

+ 2 - 0
angular_app/user-main.html

@@ -0,0 +1,2 @@
+<!-- main view for User -->
+<div ui-view></div>

+ 81 - 22
eventman_server.py

@@ -124,6 +124,8 @@ class BaseHandler(tornado.web.RequestHandler):
         'true': True
     }
 
+    _re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)')
+
     def write_error(self, status_code, **kwargs):
         """Default error handler."""
         if isinstance(kwargs.get('exc_info', (None, None))[1], BaseException):
@@ -200,6 +202,32 @@ class BaseHandler(tornado.web.RequestHandler):
             return collection_permission(permission)
         return False
 
+    def user_authorized(self, username, password):
+        """Check if a combination of username/password is valid.
+
+        :param username: username or email
+        :type username: str
+        :param password: password
+        :type password: str
+
+        :returns: tuple like (bool_user_is_authorized, dict_user_info)
+        :rtype: dict"""
+        query = [{'username': username}, {'email': username}]
+        res = self.db.query('users', query)
+        if not res:
+            return (False, {})
+        user = res[0]
+        db_password = user.get('password') or ''
+        if not db_password:
+            return (False, {})
+        match = self._re_split_salt.match(db_password)
+        if not match:
+            return (False, {})
+        salt = match.group('salt')
+        if utils.hash_password(password, salt=salt) == db_password:
+            return (True, user)
+        return (False, {})
+
     def build_error(self, message='', status=400):
         """Build and write an error message."""
         self.set_status(status)
@@ -733,6 +761,23 @@ class UsersHandler(CollectionHandler):
     document = 'user'
     collection = 'users'
 
+    def filter_get(self, data):
+        if 'password' in data:
+            del data['password']
+        if '_id' in data:
+            tickets = []
+            events = self.db.query('events', {'persons.created_by': data['_id']})
+            for event in events:
+                event_title = event.get('title') or ''
+                event_id = str(event.get('_id'))
+                evt_tickets = self._filter_results(event.get('persons') or [], {'created_by': data['_id']})
+                for evt_ticket in evt_tickets:
+                    evt_ticket['event_title'] = event_title
+                    evt_ticket['event_id'] = event_id
+                tickets.extend(evt_tickets)
+            data['tickets'] = tickets
+        return data
+
     def filter_get_all(self, data):
         if 'users' not in data:
             return data
@@ -741,6 +786,14 @@ class UsersHandler(CollectionHandler):
                 del user['password']
         return data
 
+    @gen.coroutine
+    @authenticated
+    def get(self, id_=None, resource=None, resource_id=None, acl=True, **kwargs):
+        if id_ is not None:
+            if (self.has_permission('user|read') or str(self.current_user_info.get('_id')) == id_):
+                acl = False
+        super(UsersHandler, self).get(id_, resource, resource_id, acl=acl, **kwargs)
+
     def filter_input_post_all(self, data):
         username = (data.get('username') or '').strip()
         password = (data.get('password') or '').strip()
@@ -753,6 +806,31 @@ class UsersHandler(CollectionHandler):
         return {'username': username, 'password': utils.hash_password(password),
                 'email': email, '_id': self.gen_id()}
 
+    def filter_input_put(self, data):
+        old_pwd = data.get('old_password')
+        new_pwd = data.get('new_password')
+        if old_pwd is not None:
+            del data['old_password']
+        if new_pwd is not None:
+            del data['new_password']
+            authorized, user = self.user_authorized(data['username'], old_pwd)
+            if not (self.has_permission('user|update') or (authorized and self.current_user == data['username'])):
+                raise InputException('not authorized to change password')
+            data['password'] = utils.hash_password(new_pwd)
+        if '_id' in data:
+            # Avoid overriding _id
+            del data['_id']
+        return data
+
+    @gen.coroutine
+    @authenticated
+    def put(self, id_=None, resource=None, resource_id=None, **kwargs):
+        if id_ is None:
+            return self.build_error(status=404, message='unable to access the resource')
+        if not (self.has_permission('user|update') or str(self.current_user_info.get('_id')) == id_):
+            return self.build_error(status=401, message='insufficient permissions: user|update or current user')
+        super(UsersHandler, self).put(id_, resource, resource_id, **kwargs)
+
 
 class EbCSVImportPersonsHandler(BaseHandler):
     """Importer for CSV files exported from eventbrite."""
@@ -870,7 +948,6 @@ class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
 
 class LoginHandler(BaseHandler):
     """Handle user authentication requests."""
-    re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)')
 
     @gen.coroutine
     def get(self, **kwds):
@@ -883,26 +960,6 @@ class LoginHandler(BaseHandler):
             with open(self.angular_app_path + "/login.html", 'r') as fd:
                 self.write(fd.read())
 
-    def _authorize(self, username, password, email=None):
-        """Return True is this username/password is valid."""
-        query = [{'username': username}]
-        if email is not None:
-            query.append({'email': email})
-        res = self.db.query('users', query)
-        if not res:
-            return False
-        user = res[0]
-        db_password = user.get('password') or ''
-        if not db_password:
-            return False
-        match = self.re_split_salt.match(db_password)
-        if not match:
-            return False
-        salt = match.group('salt')
-        if utils.hash_password(password, salt=salt) == db_password:
-            return True
-        return False
-
     @gen.coroutine
     def post(self, *args, **kwargs):
         # authenticate a user
@@ -917,7 +974,9 @@ class LoginHandler(BaseHandler):
             self.set_status(401)
             self.write({'error': True, 'message': 'missing username or password'})
             return
-        if self._authorize(username, password):
+        authorized, user = self.user_authorized(username, password)
+        if authorized and user.get('username'):
+            username = user['username']
             logging.info('successful login for user %s' % username)
             self.set_secure_cookie("user", username)
             self.write({'error': False, 'message': 'successful login'})