fixes #136: user pages
This commit is contained in:
parent
1ec1bca6fe
commit
5fc0ff0723
6 changed files with 189 additions and 31 deletions
9
angular_app/js/app.js
vendored
9
angular_app/js/app.js
vendored
|
@ -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',
|
||||
|
|
42
angular_app/js/controllers.js
vendored
42
angular_app/js/controllers.js
vendored
|
@ -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() {
|
||||
|
|
6
angular_app/js/services.js
vendored
6
angular_app/js/services.js
vendored
|
@ -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
angular_app/user-edit.html
Normal file
58
angular_app/user-edit.html
Normal file
|
@ -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
angular_app/user-main.html
Normal file
2
angular_app/user-main.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<!-- main view for User -->
|
||||
<div ui-view></div>
|
|
@ -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'})
|
||||
|
|
Loading…
Reference in a new issue