Skip to content

Commit

Permalink
Merge pull request #266 from AberystwythSystemsBiology/feature/forgetpw
Browse files Browse the repository at this point in the history
  • Loading branch information
KeironO authored Oct 13, 2023
2 parents 4872b72 + 6f146bd commit 0920fb9
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 74 deletions.
Binary file modified services/web/.DS_Store
Binary file not shown.
69 changes: 39 additions & 30 deletions services/web/app/admin/forms/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ class StaticForm(FlaskForm):
return StaticForm()


class ForgetPasswordForm(FlaskForm):
email = StringField("Email Address", validators=[DataRequired(), Email()])
# password = PasswordField("Password", validators=[DataRequired(), Length(min=6)])
submit = SubmitField("Reset Password")


def AdminUserAccountEditForm(sites=[], data={}) -> FlaskForm:
if "account_type" in data:
data["account_type"] = AccountType(data["account_type"]).name
Expand Down Expand Up @@ -190,16 +196,17 @@ class Meta:
# )

# -- Consent
consent_template_choices = SelectMultipleField(
"Consent template choices",
choices=[],
coerce=int,
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
)
consent_template_selected = TextAreaField(
"Current choice",
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
)
if False:
consent_template_choices = SelectMultipleField(
"Consent template choices",
choices=[],
coerce=int,
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
)
consent_template_selected = TextAreaField(
"Current choice",
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
)
consent_template_default = SelectField(
"Default working consent template",
choices=[],
Expand Down Expand Up @@ -227,16 +234,17 @@ class Meta:
)

# -- sample acquisition protocols
collection_protocol_choices = SelectMultipleField(
"Sample acquisition protocol choices",
choices=[],
coerce=int,
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
)
collection_protocol_selected = TextAreaField(
"Current choice",
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
)
if False:
collection_protocol_choices = SelectMultipleField(
"Sample acquisition protocol choices",
choices=[],
coerce=int,
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
)
collection_protocol_selected = TextAreaField(
"Current choice",
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
)
collection_protocol_default = SelectField(
"Default sample acquisition protocol",
choices=[],
Expand All @@ -245,16 +253,17 @@ class Meta:
)

# -- sample processing protocols
processing_protocol_choices = SelectMultipleField(
"Sample processing protocol choices",
choices=[],
coerce=int,
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
)
processing_protocol_selected = TextAreaField(
"Current choice",
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
)
if False:
processing_protocol_choices = SelectMultipleField(
"Sample processing protocol choices",
choices=[],
coerce=int,
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
)
processing_protocol_selected = TextAreaField(
"Current choice",
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
)
processing_protocol_default = SelectField(
"Default sample processing protocol",
choices=[],
Expand Down
89 changes: 72 additions & 17 deletions services/web/app/admin/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from ...misc import get_internal_api_header

from ...auth.forms import UserAccountRegistrationForm, UserAccountEditForm
from ..forms import AdminUserAccountEditForm
from ..forms import AdminUserAccountEditForm, ForgetPasswordForm
from ..forms.auth import AccountLockPasswordForm
from ...sample.enums import (
SampleBaseType,
Expand All @@ -29,7 +29,8 @@
FluidContainer,
CellContainer,
)
from ...database import TemporaryStore
from ...protocol.enums import ProtocolType
from ...database import TemporaryStore, db, UserAccount

from flask import render_template, url_for, redirect, abort, flash, current_app, request
from flask_login import current_user, login_required
Expand Down Expand Up @@ -249,14 +250,17 @@ def populate_settings(
def flatten_settings(name, settings_val, choices=[], setting={}):
name_choices = name + "_choices"
name_default = name + "_default"
name_selected = name + "_selected"
name_selected = name + "_selected" # display text for selected choices

try:
setting[name_choices] = settings_val["choices"]

if setting[name_choices] is None or len(setting[name_choices]) == 0:
setting[name_choices] = [s[0] for s in choices]
# setting[name_choices] = [s[0] for s in choices]
setting[name_choices] = []
except:
setting[name_choices] = [s[0] for s in choices]
# setting[name_choices] = [s[0] for s in choices]
setting[name_choices] = []

try:
setting[name_default] = settings_val["default"]
Expand All @@ -266,9 +270,12 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
except:
setting[name_default] = None

setting[name_selected] = "\n".join(
[s[1] for s in choices if s[0] in setting[name_choices]]
)
if len(setting[name_choices]) > 0:
setting[name_selected] = "\n".join(
[s[1] for s in choices if s[0] in setting[name_choices]]
)
else:
setting[name_selected] = ""

return setting

Expand All @@ -289,11 +296,12 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
]

settings = []

for access_type in settings_data:
setting = {}
for k in item_list:
setting.update({k + "_choices": [], k + "_default": None})
setting.update({k + "_selected": []})
setting.update({k + "_selected": "None"})

if access_type == "data_entry":
setting["access_level"] = 1
Expand All @@ -312,12 +320,12 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
setting["site_selected"] = "\n".join(
[s[1] for s in sites if s[0] in setting["site_choices"]]
)

# print("setting: ", setting)
# -- Consent templates
if "consent_template" in item_list:
try:
# settings_val = account_data["settings"][access_type]["consent_template"]
settings_val = settings_data[access_type]["consent_template"]
settings_val["choices"] = None # disable setting choices
setting = flatten_settings(
name="consent_template",
settings_val=settings_val,
Expand All @@ -344,6 +352,7 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
if "collection_protocol" in item_list:
try:
settings_val = settings_data[access_type]["protocol"]["ACQ"]
settings_val["choices"] = None # disable setting choices
setting = flatten_settings(
name="collection_protocol",
settings_val=settings_val,
Expand All @@ -357,6 +366,7 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
if "processing_protocol" in item_list:
try:
settings_val = settings_data[access_type]["protocol"]["SAP"]
settings_val["choices"] = None # disable setting choices
setting = flatten_settings(
name="processing_protocol",
settings_val=settings_val,
Expand Down Expand Up @@ -488,7 +498,7 @@ def jsonise_settings(form, account_data):
settings["data_entry"].update(
{
"consent_template": {
"choices": setting.consent_template_choices.data,
# "choices": setting.consent_template_choices.data,
"default": setting.consent_template_default.data,
},
"protocol": {
Expand All @@ -497,11 +507,11 @@ def jsonise_settings(form, account_data):
"default": setting.study_protocol_default.data,
},
"ACQ": {
"choices": setting.collection_protocol_choices.data,
# "choices": setting.collection_protocol_choices.data,
"default": setting.collection_protocol_default.data,
},
"SAP": {
"choices": setting.processing_protocol_choices.data,
# "choices": setting.processing_protocol_choices.data,
"default": setting.processing_protocol_default.data,
},
},
Expand Down Expand Up @@ -599,6 +609,7 @@ def admin_edit_settings(id, use_template=None):
study_protocols = []
if protocols_response.status_code == 200:
study_protocols = protocols_response.json()["content"]["choices"]
# print("stu choices: ", study_protocols)

protocols_response = requests.get(
url_for("api.protocol_query_tokenuser", default_type="ACQ", _external=True),
Expand Down Expand Up @@ -644,6 +655,7 @@ def admin_edit_settings(id, use_template=None):
None,
sites,
consent_templates,
study_protocols,
collection_protocols,
processing_protocols,
)
Expand All @@ -659,6 +671,7 @@ def admin_edit_settings(id, use_template=None):
None,
sites,
consent_templates,
study_protocols,
collection_protocols,
processing_protocols,
)
Expand All @@ -680,16 +693,17 @@ def admin_edit_settings(id, use_template=None):
for setting in form.settings.entries:
setting.site_choices.choices = sites
# setting.site_default.choices = sites
setting.consent_template_choices.choices = consent_templates

# setting.consent_template_choices.choices = consent_templates
setting.consent_template_default.choices = consent_templates

setting.study_protocol_choices.choices = study_protocols
setting.study_protocol_default.choices = study_protocols

setting.collection_protocol_choices.choices = collection_protocols
# setting.collection_protocol_choices.choices = collection_protocols
setting.collection_protocol_default.choices = collection_protocols

setting.processing_protocol_choices.choices = processing_protocols
# setting.processing_protocol_choices.choices = processing_protocols
setting.processing_protocol_default.choices = processing_protocols

if (
Expand Down Expand Up @@ -741,3 +755,44 @@ def admin_edit_settings(id, use_template=None):
)
else:
return abort(response.status_code)


@admin.route("auth/forget_password", methods=["GET", "POST"])
def auth_forget_password():
form = ForgetPasswordForm()
if form.validate_on_submit():
# get password reset token
token_email = requests.post(
url_for("api.auth_forget_password", _external=True),
headers={
"FlaskApp": current_app.config.get("SECRET_KEY"),
"Email": form.email.data,
},
json={"email": form.email.data},
)

if token_email.status_code == 200:
token = token_email.json()["content"]["token"]
confirm_url = url_for(
"auth.change_password_external", token=token, _external=True
)
template = render_template(
"admin/auth/email/password_reset.html", reset_url=confirm_url
)
subject = "LIMBUS: Password Reset Email"
msg = Message(
subject,
recipients=[form.email.data],
html=template,
sender=current_app.config["MAIL_USERNAME"],
)

# Send password reset email
mail.send(msg)
flash("Password reset email has been sent!")
return redirect(url_for("auth.login"))

else:
flash(token_email.json()["message"])

return render_template("admin/auth/forget_password.html", form=form)
39 changes: 39 additions & 0 deletions services/web/app/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,45 @@ def auth_password_reset(tokenuser: UserAccount):
}


@api.route("/auth/user/password/forget", methods=["GET", "POST"])
def auth_forget_password():
values = request.get_json()

if not values:
return no_values_response()

try:
result = password_reset_form_schema.load(values)
except ValidationError as err:
return validation_error_response(err)

user = UserAccount.query.filter_by(email=values["email"]).first()
if user is None:
return validation_error_response(
{"message": "Incorrect email or this email hasn't been registered!"}
)

else:
uaprt = UserAccountPasswordResetToken.query.filter_by(user_id=user.id).first()

new_token = str(uuid4())

if uaprt == None:
uaprt = UserAccountPasswordResetToken(user_id=user.id)
uaprt.token = new_token
else:
uaprt.token = new_token
uaprt.update({"editor_id": user.id})

try:
db.session.add(uaprt)
db.session.commit()
# return success_with_content_response(basic_user_account_schema.dump(user))
return success_with_content_response({"token": new_token})
except Exception as err:
return transaction_error_response(err)


@api.route("/auth/user/new_token", methods=["GET"])
@token_required
def auth_new_token(tokenuser: UserAccount):
Expand Down
13 changes: 10 additions & 3 deletions services/web/app/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@
from ast import Pass
from flask import redirect, render_template, url_for, flash, abort, request
from flask_login import login_required, login_user, logout_user, current_user

import requests
from sqlalchemy import func

from . import auth

from .forms import LoginForm, PasswordChangeForm, UserAccountEditForm
from .forms import (
LoginForm,
PasswordChangeForm,
UserAccountEditForm,
) # , ForgetPasswordForm
from .models import UserAccount, UserAccountToken, UserAccountPasswordResetToken

from ..database import db
Expand Down Expand Up @@ -137,10 +142,12 @@ def change_password_external(token: str):
)

if uaprt == None:
flash("This token is invalid. Please contact your system administrator")
flash(
"This token is invalid. "
) # Please contact your system administrator")
elif datetime.now() > (uaprt.updated_on + timedelta(hours=24)):
flash(
"This token is older than 24 hours old. Please contact your system administrator"
"This token is older than 24 hours old. " # Please contact your system administrator"
)
else:
user = (
Expand Down
Loading

0 comments on commit 0920fb9

Please sign in to comment.