From ddfe46e34c7b88323d472d67dd04eba27673d4b6 Mon Sep 17 00:00:00 2001 From: James Wang Date: Sat, 16 Jan 2016 22:36:30 +0000 Subject: [PATCH 1/6] Implement password reset --- ctf.nginx | 6 +-- server/api/decorators.py | 2 +- server/api/models.py | 3 +- server/api/user.py | 81 ++++++++++++++++++++++++++++------- server/config.py | 1 + web/js/easyctf.js | 69 ++++++++++++++++++++++++++---- web/pages/forgot.html | 92 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 226 insertions(+), 28 deletions(-) create mode 100644 web/pages/forgot.html diff --git a/ctf.nginx b/ctf.nginx index 7d71a7d..ad45bfa 100644 --- a/ctf.nginx +++ b/ctf.nginx @@ -14,7 +14,7 @@ server { # } # Put all the pages here so Angular doesn't fail. - location ~^/(about|chat|help|learn|login|profile|register|scoreboard|settings|team)$ { + location ~^/(about|chat|help|learn|login|profile|register|scoreboard|settings|team|forgot)$ { default_type text/html; try_files /index.html /index.html; } @@ -22,7 +22,7 @@ server { default_type text/html; try_files /index.html /index.html; } - location ~^/(profile|team)/(.*)$ { + location ~^/(profile|team|forgot)/(.*)$ { default_type text/html; try_files /index.html /index.html; } @@ -34,4 +34,4 @@ server { proxy_pass http://localhost:8000; proxy_redirect off; } -} \ No newline at end of file +} diff --git a/server/api/decorators.py b/server/api/decorators.py index 9474ef1..385eeb8 100644 --- a/server/api/decorators.py +++ b/server/api/decorators.py @@ -44,7 +44,7 @@ def api_wrapper(f): token = utils.generate_string() response.set_cookie("csrf_token", token) session["csrf_token"] = token - + return response return wrapper diff --git a/server/api/models.py b/server/api/models.py index 62a537f..c3fe684 100644 --- a/server/api/models.py +++ b/server/api/models.py @@ -18,6 +18,7 @@ class Users(db.Model): utype = db.Column(db.Integer) tid = db.Column(db.Integer) registertime = db.Column(db.Integer) + reset_token = db.Column(db.String(64)) def __init__(self, name, username, email, password, utype=1): self.name = name @@ -166,4 +167,4 @@ class TeamInvitations(db.Model): def __init__(self, rtype, frid, toid): self.rtype = rtype self.frid = frid - self.toid = toid \ No newline at end of file + self.toid = toid diff --git a/server/api/user.py b/server/api/user.py index 119cf35..6f67e8b 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -19,6 +19,55 @@ import utils blueprint = Blueprint("user", __name__) +@blueprint.route("/forgot", methods=["POST"]) +@blueprint.route("/forgot/", methods=["GET", "POST"]) +@api_wrapper +def user_forgot_password(token=None): + params = utils.flat_multi(request.form) + if token is not None: + user = get_user(reset_token=token).first() + if user is None: + return { "success": 0, "message": "Invalid reset token"} + + # We are viewing the actual reset form + if request.method == "GET": + return { "success": 1, "message": ""} + + # Submission of actual reset form + if request.method == "POST": + password = params.get("password") + confirm_password = params.get("confirm_password") + if password != confirm_password: + return { "success": 0, "message": "Passwords do not match." } + else: + user.password = utils.hash_password(password) + user.reset_token = None + current_session = db.session.object_session(user) + current_session.add(user) + current_session.commit() + return { "success": 1, "message": "Success!" } + else: + email = params.get("email") + + user = get_user(email=email).first() + if user is None: + return { "success": 0, "message": "User with that email does not exist." } + + token = utils.generate_string(length=64) + user.reset_token = token + current_session = db.session.object_session(user) + current_session.add(user) + current_session.commit() + + reset_link = "%s/forgot/%s" % ("127.0.0.1:8000", token) + subject = "EasyCTF password reset" + body = """%s,\n\nA request to reset your EasyCT password has been made. If you did not request this password reset, you may safely ignore this email and delete it.\n\nYou may reset your password by clicking this link or pasting it to your browser.\n\n%s\n\nThis link can only be used once, and will lead you to a page where you can reset your password.\n\nGood luck!\n\n- The EasyCTF Team""" % (user.username, reset_link) + response = utils.send_email(email, subject, body).json() + if "Queued" in response["message"]: + return { "success": 1, "message": "Email sent to %s" % email } + else: + return { "success": 0, "message": response["message"] } + @blueprint.route("/register", methods=["POST"]) @api_wrapper def user_register(): @@ -150,21 +199,23 @@ UserSchema = Schema({ "notify": str }, extra=True) -def get_user(username=None, username_lower=None, email=None, uid=None): - match = {} - if username != None: - match.update({ "username": username }) - elif username_lower != None: - match.update({ "username_lower": username_lower }) - elif uid != None: - match.update({ "uid": uid }) - elif email != None: - match.update({ "email": email }) - elif is_logged_in(): - match.update({ "username": session["username"] }) - with app.app_context(): - result = Users.query.filter_by(**match) - return result +def get_user(username=None, username_lower=None, email=None, uid=None, reset_token=None): + match = {} + if username != None: + match.update({ "username": username }) + elif username_lower != None: + match.update({ "username_lower": username_lower }) + elif uid != None: + match.update({ "uid": uid }) + elif email != None: + match.update({ "email": email }) + elif is_logged_in(): + match.update({ "username": session["username"] }) + elif reset_token != None: + match.update({ "reset_token": reset_token }) + with app.app_context(): + result = Users.query.filter_by(**match) + return result def login_user(username, password): user = get_user(username_lower=username.lower()).first() diff --git a/server/config.py b/server/config.py index 5778f43..9f47195 100644 --- a/server/config.py +++ b/server/config.py @@ -19,6 +19,7 @@ UPLOAD_FOLDER = os.path.normpath("../web/files") CTF_BEGIN = 0 # To be used later CTF_END = 0 # To be used later + MG_HOST = "" MG_API_KEY = "" ADMIN_EMAIL = "" diff --git a/web/js/easyctf.js b/web/js/easyctf.js index 89190d3..30e4499 100644 --- a/web/js/easyctf.js +++ b/web/js/easyctf.js @@ -48,6 +48,14 @@ app.config(function($routeProvider, $locationProvider) { templateUrl: "pages/settings.html", controller: "mainController" }) + .when("/forgot", { + templateUrl: "pages/forgot.html", + controller: "resetController" + }) + .when("/forgot/:token", { + templateUrl: "pages/forgot.html", + controller: "resetController" + }) .when("/team", { templateUrl: "pages/team.html", controller: "teamController" @@ -120,13 +128,23 @@ app.controller("teamController", ["$controller", "$scope", "$http", "$routeParam } else { $controller("loginController", { $scope: $scope }); } - $.get("/api/team/info", data, function(result) { - if (result["success"] == 1) { - $scope.team = result["team"]; - } - $scope.$apply(); - $(".timeago").timeago(); - }); +}]); + +app.controller("resetController", ["$controller", "$scope", "$http", "$routeParams", function($controller, $scope, $http, $routeParams) { + var data = { }; + $scope.token = false; + data["csrf_token"] = $.cookie("csrf_token"); + if ("token" in $routeParams) { + $scope.token = true; + token = $routeParams["token"]; + $.get("/api/user/forgot/" + token, data, function(data) { + $scope.body = data["message"]; + $scope.success = data["success"] + $scope.$apply(); + }); + } else { + $controller("mainController", { $scope: $scope }); + } }]); app.controller("adminController", ["$controller", "$scope", "$http", function($controller, $scope, $http) { @@ -208,6 +226,41 @@ var register_form = function() { }); }; +// password reset +var request_reset_form = function() { + var data = $("#request_reset_form").serializeObject(); + data["csrf_token"] = $.cookie("csrf_token"); + $.post("/api/user/forgot", data, function(result) { + if (result["success"] == 1) { + display_message("reset_msg", "success", result["message"]); + } else { + display_message("reset_msg", "danger", result["message"]); + } + }).fail(function(jqXHR, status, error) { + var result = JSON.parse(jqXHR["responseText"]); + display_message("reset_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + }); +} + +var reset_form = function() { + var data = $("#reset_form").serializeObject(); + data["csrf_token"] = $.cookie("csrf_token"); + var url = window.location.href; + var token = url.substr(url.lastIndexOf("/")+1); + $.post("/api/user/forgot/" + token, data, function(result) { + if (result["success"] == 1) { + display_message("reset_msg", "success", result["message"], function() { + location.href = "/login"; + }); + } else { + display_message("reset_msg", "danger", result["message"]); + } + }).fail(function(jqXHR, status, error) { + var result = JSON.parse(jqXHR["responseText"]); + display_message("reset_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + }); +} + // login page var login_form = function() { @@ -250,4 +303,4 @@ var add_member = function() { location.reload(true); } }); -}; \ No newline at end of file +}; diff --git a/web/pages/forgot.html b/web/pages/forgot.html new file mode 100644 index 0000000..01f210f --- /dev/null +++ b/web/pages/forgot.html @@ -0,0 +1,92 @@ +
+

 

+
+
+ {{ body }} +
+
+ +
+
+
+
+

Reset Password

+
+
+
+
+
+
+
+
+

Reset your password here

+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Reset Password

+
+
+
+
+
+
+
+
+

Enter the email you used to sign up with, and we'll send you an an email with a link to reset your password.

+
+ +
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ +
From dcc557fd899f74f2fc2dfb1a8f0abe4c93b96e5a Mon Sep 17 00:00:00 2001 From: James Wang Date: Sat, 16 Jan 2016 23:13:41 +0000 Subject: [PATCH 2/6] Make use of api_call --- web/js/easyctf.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/js/easyctf.js b/web/js/easyctf.js index 30e4499..da90c05 100644 --- a/web/js/easyctf.js +++ b/web/js/easyctf.js @@ -229,8 +229,7 @@ var register_form = function() { // password reset var request_reset_form = function() { var data = $("#request_reset_form").serializeObject(); - data["csrf_token"] = $.cookie("csrf_token"); - $.post("/api/user/forgot", data, function(result) { + api_call("POST", "/api/user/forgot", data, function(result) { if (result["success"] == 1) { display_message("reset_msg", "success", result["message"]); } else { @@ -247,7 +246,7 @@ var reset_form = function() { data["csrf_token"] = $.cookie("csrf_token"); var url = window.location.href; var token = url.substr(url.lastIndexOf("/")+1); - $.post("/api/user/forgot/" + token, data, function(result) { + api_call("POST", "/api/user/forgot/" + token, data, function(result) { if (result["success"] == 1) { display_message("reset_msg", "success", result["message"], function() { location.href = "/login"; From 796fac7f859000d2d5502b3cd1351387b38a9e8f Mon Sep 17 00:00:00 2001 From: James Wang Date: Sun, 17 Jan 2016 00:15:37 +0000 Subject: [PATCH 3/6] Temporarily disable button to prevent multiple submissions --- server/api/team.py | 4 +-- web/js/easyctf.js | 59 ++++++++++++++++++++++++++++++++++++-------- web/pages/login.html | 2 +- web/pages/team.html | 2 +- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/server/api/team.py b/server/api/team.py index ffcfdab..6c93ffc 100644 --- a/server/api/team.py +++ b/server/api/team.py @@ -34,7 +34,7 @@ def team_create(): db.session.commit() Users.query.filter_by(uid=_user.uid).update({ "tid": team.tid }) db.session.commit() - + return { "success": 1, "message": "Success!" } @blueprint.route("/invite", methods=["POST"]) @@ -149,4 +149,4 @@ def get_team(tid=None, teamname=None, teamname_lower=None, owner=None): match.update({ "tid": _user.tid }) with app.app_context(): result = Teams.query.filter_by(**match) - return result \ No newline at end of file + return result diff --git a/web/js/easyctf.js b/web/js/easyctf.js index da90c05..3a01a38 100644 --- a/web/js/easyctf.js +++ b/web/js/easyctf.js @@ -214,30 +214,42 @@ $.fn.serializeObject = function() { var register_form = function() { var input = "#register_form input"; var data = $("#register_form").serializeObject(); + var button = $("#register_form").find(":submit"); + button.prop("disabled", true); api_call("POST", "/api/user/register", data, function(result) { if (result["success"] == 1) { location.href = "/profile"; } else { - display_message("register_msg", "danger", result["message"]); + display_message("register_msg", "danger", result["message"], function() { + button.removeAttr("disabled"); + }); } }).fail(function(jqXHR, status, error) { var result = JSON.parse(jqXHR["responseText"]); - display_message("register_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + display_message("register_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"], function() { + button.removeAttr("disabled"); + }); }); }; // password reset var request_reset_form = function() { var data = $("#request_reset_form").serializeObject(); + var button = $("#request_reset_form").find(":submit"); + button.prop("disabled", true); api_call("POST", "/api/user/forgot", data, function(result) { if (result["success"] == 1) { display_message("reset_msg", "success", result["message"]); } else { - display_message("reset_msg", "danger", result["message"]); + display_message("reset_msg", "danger", result["message"], function() { + button.removeAttr("disabled"); + }); } }).fail(function(jqXHR, status, error) { var result = JSON.parse(jqXHR["responseText"]); - display_message("reset_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + display_message("reset_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"], function() { + button.removeAttr("disabled"); + }); }); } @@ -246,17 +258,23 @@ var reset_form = function() { data["csrf_token"] = $.cookie("csrf_token"); var url = window.location.href; var token = url.substr(url.lastIndexOf("/")+1); + var button = $("#reset_form").find(":submit"); + button.prop("disabled", true); api_call("POST", "/api/user/forgot/" + token, data, function(result) { if (result["success"] == 1) { display_message("reset_msg", "success", result["message"], function() { location.href = "/login"; }); } else { - display_message("reset_msg", "danger", result["message"]); + display_message("reset_msg", "danger", result["message"], function() { + button.removeAttr("disabled"); + }); } }).fail(function(jqXHR, status, error) { var result = JSON.parse(jqXHR["responseText"]); - display_message("reset_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + display_message("reset_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"], function() { + button.removeAttr("disabled"); + }); }); } @@ -265,15 +283,21 @@ var reset_form = function() { var login_form = function() { var input = "#login_form input"; var data = $("#login_form").serializeObject(); + var button = $("#login_form").find(":submit"); + button.prop("disabled", true); api_call("POST", "/api/user/login", data, function(result) { if (result["success"] == 1) { location.href = "/profile"; } else { - display_message("login_msg", "danger", result["message"]); + display_message("login_msg", "danger", result["message"], function() { + button.removeAttr("disabled"); + }); } }).fail(function(jqXHR, status, error) { var result = JSON.parse(jqXHR["responseText"]); - display_message("login_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + display_message("login_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"], function() { + button.removeAttr("disabled"); + }); }); }; @@ -282,24 +306,39 @@ var login_form = function() { var create_team = function() { var input = "#create_team input"; var data = $("#create_team").serializeObject(); + var button = $("#create_team").find(":submit"); + button.prop("disabled", true); api_call("POST", "/api/team/create", data, function(result) { if (result["success"] == 1) { location.reload(true); } else { - display_message("create_team_msg", "danger", result["message"]); + display_message("create_team_msg", "danger", result["message"], function() { + button.removeAttr("disabled"); + }); } }).fail(function(jqXHR, status, error) { var result = JSON.parse(jqXHR["responseText"]); - display_message("create_team_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"]); + display_message("create_team_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"], function() { + button.removeAttr("disabled"); + }); }); }; var add_member = function() { var input = "#add_member input"; var data = $("#add_member").serializeObject(); + var button = $("#add_member").find(":submit"); + button.prop("disabled", true); api_call("POST", "/api/team/invite", data, function(result) { if (result["success"] == 1) { location.reload(true); + } else { + button.removeAtr("disabled"); } + }).fail(function(jqXHR, status, error) { + var result = JSON.parse(jqXHR["responseText"]); + display_message("create_team_msg", "danger", "Error " + jqXHR["status"] + ": " + result["message"], function() { + button.removeAttr("disabled"); + }); }); }; diff --git a/web/pages/login.html b/web/pages/login.html index 8a9939e..16f254b 100644 --- a/web/pages/login.html +++ b/web/pages/login.html @@ -51,4 +51,4 @@ - \ No newline at end of file + diff --git a/web/pages/team.html b/web/pages/team.html index 5f0b3d9..9e9942f 100644 --- a/web/pages/team.html +++ b/web/pages/team.html @@ -157,4 +157,4 @@ } }); $("[data-toggle=tooltip]").tooltip(); - \ No newline at end of file + From f38063a2d533a7cf22620cf1263c4e8122664a8b Mon Sep 17 00:00:00 2001 From: James Wang Date: Sun, 17 Jan 2016 00:58:29 +0000 Subject: [PATCH 4/6] Add team id to session --- server/api/team.py | 3 ++- server/api/user.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/api/team.py b/server/api/team.py index 6c93ffc..fdbdd34 100644 --- a/server/api/team.py +++ b/server/api/team.py @@ -1,4 +1,4 @@ -from flask import Blueprint, request +from flask import Blueprint, request, session from flask import current_app as app from voluptuous import Schema, Length, Required @@ -35,6 +35,7 @@ def team_create(): Users.query.filter_by(uid=_user.uid).update({ "tid": team.tid }) db.session.commit() + session["tid"] = team.tid return { "success": 1, "message": "Success!" } @blueprint.route("/invite", methods=["POST"]) diff --git a/server/api/user.py b/server/api/user.py index 6f67e8b..2eff859 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -237,6 +237,8 @@ def login_user(username, password): session["sid"] = token.sid session["username"] = token.username session["admin"] = user.admin == True + if user.tid is not None and user.tid >= 0: + session["tid"] = user.tid return True From 6d0219782b93e74421159fabf198cc7734eec4b9 Mon Sep 17 00:00:00 2001 From: James Wang Date: Sun, 17 Jan 2016 02:06:02 +0000 Subject: [PATCH 5/6] Add function to delete teams --- server/api/team.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/server/api/team.py b/server/api/team.py index fdbdd34..fab7e66 100644 --- a/server/api/team.py +++ b/server/api/team.py @@ -21,7 +21,7 @@ blueprint = Blueprint("team", __name__) def team_create(): params = utils.flat_multi(request.form) _user = user.get_user().first() - if _user.tid is not None or _user.tid >= 0 or get_team(owner=_user.uid).first() is not None: + if (_user.tid is not None and _user.tid >= 0) or get_team(owner=_user.uid).first() is not None: raise WebException("You're already in a team!") verify_to_schema(TeamSchema, params) @@ -29,15 +29,36 @@ def team_create(): school = params.get("school") team = Teams(teamname, school, _user.uid, _user.utype != 1) + tid = team.tid with app.app_context(): db.session.add(team) db.session.commit() Users.query.filter_by(uid=_user.uid).update({ "tid": team.tid }) db.session.commit() - session["tid"] = team.tid + session["tid"] = tid return { "success": 1, "message": "Success!" } +@blueprint.route("/delete", methods=["POST"]) +@api_wrapper +@login_required +def team_delete(): + username = session["username"] + tid = session["tid"] + team = Teams.query.filter_by(tid=tid).first() + usr = Users.query.filter_by(username=username).first() + owner = team.owner + if usr.uid == owner or usr.admin: + usr.tid = -1 + with app.app_context(): + db.session.add(usr) + db.session.delete(team) + db.session.commit() + session.pop("tid") + return { "success": 1, "message": "Success!" } + else: + raise WebException("Not authorized.") + @blueprint.route("/invite", methods=["POST"]) @api_wrapper @login_required From f1d476b71353b0f2eb557e9a877d74d20a6300ac Mon Sep 17 00:00:00 2001 From: James Wang Date: Sun, 17 Jan 2016 02:19:02 +0000 Subject: [PATCH 6/6] Minor refactor --- server/api/decorators.py | 2 +- server/api/problem.py | 12 ++++++------ server/api/user.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/api/decorators.py b/server/api/decorators.py index 385eeb8..07fd3f8 100644 --- a/server/api/decorators.py +++ b/server/api/decorators.py @@ -40,7 +40,7 @@ def api_wrapper(f): response = make_response(result) # Setting CSRF token - if "token" not in session: + if "csrf_token" not in session: token = utils.generate_string() response.set_cookie("csrf_token", token) session["csrf_token"] = token diff --git a/server/api/problem.py b/server/api/problem.py index 88c436f..bbea4d3 100644 --- a/server/api/problem.py +++ b/server/api/problem.py @@ -7,7 +7,7 @@ from flask import current_app as app from werkzeug import secure_filename from models import db, Files, Problems, Solves, Teams -from decorators import admins_only, api_wrapper, login_required +from decorators import admins_only, api_wrapper, login_required, WebException blueprint = Blueprint("problem", __name__) @@ -24,7 +24,7 @@ def problem_add(): name_exists = Problems.query.filter_by(name=name).first() if name_exists: - return { "success": 0, "message": "Problem name already taken." } + raise WebException("Problem name already taken.") problem = Problems(name, category, description, hint, flag, value) db.session.add(problem) db.session.commit() @@ -57,7 +57,7 @@ def problem_delete(): Problems.query.filter_by(pid=pid).delete() db.session.commit() return { "success": 1, "message": "Success!" } - return { "success": 0, "message": "Problem does not exist!" } + raise WebException("Problem does not exist!") @blueprint.route("/update", methods=["POST"]) @admins_only @@ -86,7 +86,7 @@ def problem_update(): db.session.commit() return { "success": 1, "message": "Success!" } - return { "success": 0, "message": "Problem does not exist!" } + raise WebException("Problem does not exist!") @blueprint.route("/submit", methods=["POST"]) @api_wrapper @@ -113,10 +113,10 @@ def problem_submit(): else: logger.log("submissions.log", logger.WARNING, "%s has incorrectly submitted %s to %s" % (team.name, flag, problem.name)) - return { "success": 0, "message": "Incorrect." } + raise WebException("Incorrect.") else: - return { "success": 0, "message": "Problem does not exist!" } + raise WebException("Problem does not exist!") @blueprint.route("/data", methods=["POST"]) #@api_wrapper # Disable atm due to json serialization issues: will fix diff --git a/server/api/user.py b/server/api/user.py index 2eff859..4b57ede 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -27,7 +27,7 @@ def user_forgot_password(token=None): if token is not None: user = get_user(reset_token=token).first() if user is None: - return { "success": 0, "message": "Invalid reset token"} + raise WebException("Invalid reset token.") # We are viewing the actual reset form if request.method == "GET": @@ -38,7 +38,7 @@ def user_forgot_password(token=None): password = params.get("password") confirm_password = params.get("confirm_password") if password != confirm_password: - return { "success": 0, "message": "Passwords do not match." } + raise WebException("Passwords do not match.") else: user.password = utils.hash_password(password) user.reset_token = None @@ -51,7 +51,7 @@ def user_forgot_password(token=None): user = get_user(email=email).first() if user is None: - return { "success": 0, "message": "User with that email does not exist." } + raise WebException("User with that email does not exist.") token = utils.generate_string(length=64) user.reset_token = token @@ -66,7 +66,7 @@ def user_forgot_password(token=None): if "Queued" in response["message"]: return { "success": 1, "message": "Email sent to %s" % email } else: - return { "success": 0, "message": response["message"] } + raise WebException(response["message"]) @blueprint.route("/register", methods=["POST"]) @api_wrapper