From bd5a4ba277b7ee7bb6516c94f044a913cdbcc892 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 25 Mar 2018 00:12:04 -0400 Subject: [PATCH] [#83] Profile: update page and able to update password - Rename connection button - Remove user_password and email_password - Manage only 1 password and no more salt from client --- src/web/handlers.py | 143 ++++++++++++++++-- src/web/partials/_base.html | 3 +- src/web/partials/admin/_base.html | 2 +- src/web/partials/login.html | 8 +- src/web/partials/profile.html | 27 +++- src/web/py_class/db.py | 43 +++--- .../js/tl_module/profile_ctrl/profile_ctrl.js | 75 +++++++++ src/web/web.py | 6 + 8 files changed, 266 insertions(+), 41 deletions(-) create mode 100644 src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js diff --git a/src/web/handlers.py b/src/web/handlers.py index b3259fdb..dad65e9a 100644 --- a/src/web/handlers.py +++ b/src/web/handlers.py @@ -101,9 +101,9 @@ def post(self): return # Login - if self.get_argument("username_or_email", ""): + if self.get_argument("username_or_email", default=""): - username_or_email = self.get_argument("username_or_email", "") + username_or_email = self.get_argument("username_or_email", default="") if not username_or_email: print("Email or Username is empty.", file=sys.stderr) self.redirect("/login?invalid=username_or_email") @@ -127,8 +127,8 @@ def post(self): return # Sign Up - elif self.get_argument("username"): - name = self.get_argument("username") + elif self.get_argument("username", default=""): + name = self.get_argument("username", default="") if not name: print("Username is empty from %s" % self.request.remote_ip, file=sys.stderr) self.redirect("/login?invalid=username") @@ -136,19 +136,15 @@ def post(self): email = self.get_argument("email", default=None) - password_mail = self.get_argument("pwconfirm") - if not password_mail: - print("Password is empty from %s" % self.request.remote_ip, file=sys.stderr) - self.redirect("/login?invalid=password") - return - - if self._db.create_user(name, email, password, password_mail): + if self._db.create_user(name, email=email, password=password): self.redirect("/login") return else: self.redirect("/login?invalid=signup") return + self.redirect("/login") + class GoogleOAuth2LoginHandler(base_handler.BaseHandler, tornado.auth.GoogleOAuth2Mixin): @tornado.gen.coroutine @@ -489,6 +485,131 @@ def get(self): self.finish() +class ProfileCmdUpdatePasswordHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + def post(self): + if self._global_arg["disable_login"]: + # Not Found + self.set_status(404) + self.send_error(404) + raise tornado.web.Finish() + + # Be sure the user is connected + current_user = self.get_current_user() + if not current_user: + print("Cannot send user command if not connect. %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + self.prepare_json() + + # Validate password is not empty + old_password = self.get_argument("old_password") + new_password = self.get_argument("new_password") + if not old_password or not new_password: + print("Password is empty from %s" % self.request.remote_ip, file=sys.stderr) + data = {"error": "Password is empty."} + self.write(data) + self.finish() + return + + # Validate old_password is good before update with the new_password + success_password = self._db.compare_password(old_password, self.current_user.get("password")) + if not success_password: + print("Wrong password from ip %s." % self.request.remote_ip) + data = {"error": "Wrong password."} + self.write(data) + self.finish() + return + + # Validate the password is a new one + success_password = self._db.compare_password(new_password, self.current_user.get("password")) + if success_password: + print("Same password from ip %s." % self.request.remote_ip) + data = {"status": "Same password."} + self.write(data) + self.finish() + return + + # Update password + current_user["password"] = self._db.generate_password(new_password) + self._db.update_user(current_user) + + # TODO Need to validate insertion + data = {"status": "Password updated."} + self.write(data) + self.finish() + + +class ProfileCmdAddNewPasswordHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + def post(self): + if self._global_arg["disable_login"]: + # Not Found + self.set_status(404) + self.send_error(404) + raise tornado.web.Finish() + + # Be sure the user is connected + current_user = self.get_current_user() + if not current_user: + print("Cannot send user command if not connect. %s" % self.request.remote_ip, file=sys.stderr) + # Forbidden + self.set_status(403) + self.send_error(403) + raise tornado.web.Finish() + + # Validate if can add a new password + if current_user["password"]: + # Already contain a password + print("User password is not empty from %s" % self.request.remote_ip, file=sys.stderr) + data = {"error": "User password is not empty."} + self.write(data) + self.finish() + return + + self.prepare_json() + + # Validate password is not empty + password = self.get_argument("password") + if not password: + print("Password is empty from %s" % self.request.remote_ip, file=sys.stderr) + data = {"error": "Password is empty."} + self.write(data) + self.finish() + return + + # Update password + current_user["password"] = self._db.generate_password(password) + self._db.update_user(current_user) + + # TODO Need to validate insertion + data = {"status": "Password added."} + self.write(data) + self.finish() + + +class ProfileCmdInfoHandler(jsonhandler.JsonHandler): + @tornado.web.asynchronous + @tornado.web.authenticated + def get(self): + # TODO not sure it's secure + user = self.current_user + return_user = { + "email": user.get("email"), + "name": user.get("name"), + "password": bool(user.get("password")), + "user_id": user.get("user_id"), + "google_id": bool(user.get("google_id")), + "facebook_id": bool(user.get("facebook_id")), + "twitter_id": bool(user.get("twitter_id")), + "permission": user.get("permission"), + } + self.write(return_user) + self.finish() + + class StatSeasonPass(jsonhandler.JsonHandler): @tornado.web.asynchronous def get(self): diff --git a/src/web/partials/_base.html b/src/web/partials/_base.html index 254a751d..b7eea911 100644 --- a/src/web/partials/_base.html +++ b/src/web/partials/_base.html @@ -95,7 +95,7 @@
  • {{current_user.get("name")}}
  • Déconnexion
  • {% elif not disable_login %} -
  • Connexion ou Inscription
  • +
  • Connexion
  • {% end %} @@ -188,6 +188,7 @@

    + diff --git a/src/web/partials/admin/_base.html b/src/web/partials/admin/_base.html index 0dfa0d7c..a0ee2d01 100644 --- a/src/web/partials/admin/_base.html +++ b/src/web/partials/admin/_base.html @@ -91,7 +91,7 @@
  • {{current_user.get("name")}}
  • Déconnexion
  • {% elif not disable_login %} -
  • Connexion ou Inscription
  • +
  • Connexion
  • {% end %} diff --git a/src/web/partials/login.html b/src/web/partials/login.html index 9840b1b6..7d470cf1 100644 --- a/src/web/partials/login.html +++ b/src/web/partials/login.html @@ -74,7 +74,9 @@

    Connexion ou + onclick="if(username_or_email.value && loginForm.password.value) { + password.value=hashSha256(password.value); + };"> Se connecter @@ -161,7 +163,9 @@

    Créer un compte + onclick="password.value=hashSha256(password.value); + // TODO empty pwconfirm + pwconfirm.value=password.value;">Créer un compte diff --git a/src/web/partials/profile.html b/src/web/partials/profile.html index bff45953..a1ca6111 100644 --- a/src/web/partials/profile.html +++ b/src/web/partials/profile.html @@ -3,11 +3,30 @@ {% block content %} {% if user %} -
    Profil de {{user.get("name")}}. (ID: {{ user.get("user_id") }})
    -
    -
    Personnage +
    +

    Profil de {{! model_profile.info.name }}

    +

    Fiche de personnage

    + Accéder à sa fiche + +

    Information personnel

    +

    Nom: {{! model_profile.info.name }}

    +

    Email: {{! model_profile.info.email }}

    +

    ID: {{! model_profile.info.user_id }}

    +
    + Modifier son mot de passe.
    + + + Sauvegarder +
    +
    + Ajouter un mot de passe.
    + + + Ajouter le mot de passe. +
    +
    {% else %} -
    Veuillez sélectionner un profil.
    +
    Profil inexistant, veuillez-vous créer un compte.
    {% end %} {% end %} \ No newline at end of file diff --git a/src/web/py_class/db.py b/src/web/py_class/db.py index 84613bed..7352ade3 100644 --- a/src/web/py_class/db.py +++ b/src/web/py_class/db.py @@ -24,8 +24,20 @@ def __init__(self, parser): self._query_user = tinydb.Query() - def create_user(self, name, email=None, password_name=None, password_mail=None, google_id=None, facebook_id=None, - twitter_id=None, permission="Joueur"): + @staticmethod + def generate_password(password): + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + @staticmethod + def compare_password(user_password, hash_password): + if not user_password or not hash_password: + return False + return bcrypt.checkpw(user_password.encode('utf-8'), hash_password.encode('utf-8')) + + def create_user(self, name, email=None, password=None, google_id=None, facebook_id=None, twitter_id=None, + permission="Joueur"): + + # Validate no duplicate user if self._db_user.contains(self._query_user.name == name): print("Cannot create user %s, already exist." % name, file=sys.stderr) return @@ -38,18 +50,10 @@ def create_user(self, name, email=None, password_name=None, password_mail=None, while self._db_user.contains(self._query_user.user_id == user_id): user_id = uuid.uuid4().hex - if password_name: - secure_pass_name = bcrypt.hashpw(password_name.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') - else: - secure_pass_name = None - if password_mail: - secure_pass_mail = bcrypt.hashpw(password_mail.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') - else: - secure_pass_mail = None + secure_pass = self.generate_password(password) if password else None - data = {"email": email, "name": name, "password_name": secure_pass_name, "password_mail": secure_pass_mail, - "user_id": user_id, "google_id": google_id, "facebook_id": facebook_id, "twitter_id": twitter_id, - "permission": permission} + data = {"email": email, "name": name, "password": secure_pass, "user_id": user_id, "google_id": google_id, + "facebook_id": facebook_id, "twitter_id": twitter_id, "permission": permission} eid = self._db_user.insert(data) return self._db_user.get(eid=eid) @@ -67,8 +71,8 @@ def get_user(self, name=None, email=None, password=None, id_type="user", user_id _user = self._db_user.get(self._query_user.name == name) if _user: # Validate password - ddb_password = _user.get("password_name") - if password and ddb_password and bcrypt.checkpw(password.encode('utf-8'), ddb_password.encode('utf-8')): + ddb_password = _user.get("password") + if password and ddb_password and self.compare_password(password, ddb_password): return _user # If no name provided, lookup user by email @@ -77,9 +81,8 @@ def get_user(self, name=None, email=None, password=None, id_type="user", user_id if _user: if not force_email_no_password: # Validate password - ddb_password = _user.get("password_mail") - if password and ddb_password and bcrypt.checkpw(password.encode('utf-8'), - ddb_password.encode('utf-8')): + ddb_password = _user.get("password") + if password and ddb_password and self.compare_password(password, ddb_password): return _user else: return _user @@ -107,10 +110,6 @@ def get_user(self, name=None, email=None, password=None, id_type="user", user_id # print("Missing user name, email or id to get user.", file=sys.stderr) return - if not _user: - # print("User not found", file=sys.stderr) - return - def user_exist(self, email=None, user_id=None, name=None): """Returns True if all the arguments given are found""" return not (email and not self._db_user.get(self._query_user.email == email)) and not ( diff --git a/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js b/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js new file mode 100644 index 00000000..8dae0a9a --- /dev/null +++ b/src/web/resources/js/tl_module/profile_ctrl/profile_ctrl.js @@ -0,0 +1,75 @@ +// Formulaire de Traitre-Lame +"use strict"; + +characterApp.controller("profile_ctrl", ["$scope", "$q", "$http", "$window", /*"$timeout",*/ function ($scope, $q, $http, $window) { + $scope.model_profile = { + info: {} + }; + + $scope.dct_profile_password = { + "old_password": "", + "new_password": "" + }; + + $scope.dct_profile_new_password = { + "password": "", + "check_password": "" + }; + + // Get profile info + $scope.update_profile = function (e) { + $http({ + method: "get", + url: "/cmd/profile/get_info", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + $scope.model_profile.info = response.data; + }); + + }; + $scope.update_profile(); + + $scope.save_password = function (e) { + if ($scope.dct_profile_password.old_password != "" && $scope.dct_profile_password.new_password) { + var data = { + "old_password": hashSha256($scope.dct_profile_password.old_password), + "new_password": hashSha256($scope.dct_profile_password.new_password) + }; + // send command to server + $http({ + method: "post", + url: "/cmd/profile/update_password", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + data: data, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + console.info(response.data); + $scope.dct_profile_password.old_password = ""; + $scope.dct_profile_password.new_password = ""; + }); + } + }; + + $scope.add_new_password = function (e) { + if ($scope.dct_profile_new_password.password != "" && $scope.dct_profile_new_password.password == $scope.dct_profile_new_password.check_password) { + var data = { + "password": hashSha256($scope.dct_profile_new_password.password) + }; + // send command to server + $http({ + method: "post", + url: "/cmd/profile/add_new_password", + headers: {"Content-Type": "application/json; charset=UTF-8"}, + data: data, + timeout: 5000 + }).then(function (response/*, status, headers, config*/) { + console.info(response.data); + $scope.dct_profile_new_password.password = ""; + $scope.dct_profile_new_password.check_password = ""; + // Update profile to change view + $scope.update_profile(); + }); + } + }; +}]); diff --git a/src/web/web.py b/src/web/web.py index 47c9d68c..5d00f7aa 100644 --- a/src/web/web.py +++ b/src/web/web.py @@ -119,6 +119,12 @@ def main(parse_arg): tornado.web.url(r"/cmd/lore/?", handlers.LoreHandler, name='cmd_lore', kwargs=settings), tornado.web.url(r"/cmd/stat/total_season_pass/?", handlers.StatSeasonPass, name='cmd_stat_total_season_pass', kwargs=settings), + tornado.web.url(r"/cmd/profile/update_password/?", handlers.ProfileCmdUpdatePasswordHandler, + name='cmd_profile_update_password', kwargs=settings), + tornado.web.url(r"/cmd/profile/add_new_password/?", handlers.ProfileCmdAddNewPasswordHandler, + name='cmd_profile_add_new_password', kwargs=settings), + tornado.web.url(r"/cmd/profile/get_info/?", handlers.ProfileCmdInfoHandler, + name='cmd_profile_get_info', kwargs=settings), # auto ssl tornado.web.url(r"/.well-known/acme-challenge.*", handlers.AutoSSLHandler, name="auto_ssl")