Skip to content

Commit 0920fb9

Browse files
authored
Merge pull request #266 from AberystwythSystemsBiology/feature/forgetpw
2 parents 4872b72 + 6f146bd commit 0920fb9

File tree

14 files changed

+233
-74
lines changed

14 files changed

+233
-74
lines changed

services/web/.DS_Store

0 Bytes
Binary file not shown.

services/web/app/admin/forms/auth.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class StaticForm(FlaskForm):
9797
return StaticForm()
9898

9999

100+
class ForgetPasswordForm(FlaskForm):
101+
email = StringField("Email Address", validators=[DataRequired(), Email()])
102+
# password = PasswordField("Password", validators=[DataRequired(), Length(min=6)])
103+
submit = SubmitField("Reset Password")
104+
105+
100106
def AdminUserAccountEditForm(sites=[], data={}) -> FlaskForm:
101107
if "account_type" in data:
102108
data["account_type"] = AccountType(data["account_type"]).name
@@ -190,16 +196,17 @@ class Meta:
190196
# )
191197

192198
# -- Consent
193-
consent_template_choices = SelectMultipleField(
194-
"Consent template choices",
195-
choices=[],
196-
coerce=int,
197-
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
198-
)
199-
consent_template_selected = TextAreaField(
200-
"Current choice",
201-
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
202-
)
199+
if False:
200+
consent_template_choices = SelectMultipleField(
201+
"Consent template choices",
202+
choices=[],
203+
coerce=int,
204+
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
205+
)
206+
consent_template_selected = TextAreaField(
207+
"Current choice",
208+
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
209+
)
203210
consent_template_default = SelectField(
204211
"Default working consent template",
205212
choices=[],
@@ -227,16 +234,17 @@ class Meta:
227234
)
228235

229236
# -- sample acquisition protocols
230-
collection_protocol_choices = SelectMultipleField(
231-
"Sample acquisition protocol choices",
232-
choices=[],
233-
coerce=int,
234-
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
235-
)
236-
collection_protocol_selected = TextAreaField(
237-
"Current choice",
238-
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
239-
)
237+
if False:
238+
collection_protocol_choices = SelectMultipleField(
239+
"Sample acquisition protocol choices",
240+
choices=[],
241+
coerce=int,
242+
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
243+
)
244+
collection_protocol_selected = TextAreaField(
245+
"Current choice",
246+
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
247+
)
240248
collection_protocol_default = SelectField(
241249
"Default sample acquisition protocol",
242250
choices=[],
@@ -245,16 +253,17 @@ class Meta:
245253
)
246254

247255
# -- sample processing protocols
248-
processing_protocol_choices = SelectMultipleField(
249-
"Sample processing protocol choices",
250-
choices=[],
251-
coerce=int,
252-
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
253-
)
254-
processing_protocol_selected = TextAreaField(
255-
"Current choice",
256-
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
257-
)
256+
if False:
257+
processing_protocol_choices = SelectMultipleField(
258+
"Sample processing protocol choices",
259+
choices=[],
260+
coerce=int,
261+
render_kw={"size": "1", "class": "selectpicker form-control wd=0.6"},
262+
)
263+
processing_protocol_selected = TextAreaField(
264+
"Current choice",
265+
render_kw={"readonly": True, "rows": 5, "class": "form-control bd-light"},
266+
)
258267
processing_protocol_default = SelectField(
259268
"Default sample processing protocol",
260269
choices=[],

services/web/app/admin/routes/auth.py

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ...misc import get_internal_api_header
1919

2020
from ...auth.forms import UserAccountRegistrationForm, UserAccountEditForm
21-
from ..forms import AdminUserAccountEditForm
21+
from ..forms import AdminUserAccountEditForm, ForgetPasswordForm
2222
from ..forms.auth import AccountLockPasswordForm
2323
from ...sample.enums import (
2424
SampleBaseType,
@@ -29,7 +29,8 @@
2929
FluidContainer,
3030
CellContainer,
3131
)
32-
from ...database import TemporaryStore
32+
from ...protocol.enums import ProtocolType
33+
from ...database import TemporaryStore, db, UserAccount
3334

3435
from flask import render_template, url_for, redirect, abort, flash, current_app, request
3536
from flask_login import current_user, login_required
@@ -249,14 +250,17 @@ def populate_settings(
249250
def flatten_settings(name, settings_val, choices=[], setting={}):
250251
name_choices = name + "_choices"
251252
name_default = name + "_default"
252-
name_selected = name + "_selected"
253+
name_selected = name + "_selected" # display text for selected choices
254+
253255
try:
254256
setting[name_choices] = settings_val["choices"]
255257

256258
if setting[name_choices] is None or len(setting[name_choices]) == 0:
257-
setting[name_choices] = [s[0] for s in choices]
259+
# setting[name_choices] = [s[0] for s in choices]
260+
setting[name_choices] = []
258261
except:
259-
setting[name_choices] = [s[0] for s in choices]
262+
# setting[name_choices] = [s[0] for s in choices]
263+
setting[name_choices] = []
260264

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

269-
setting[name_selected] = "\n".join(
270-
[s[1] for s in choices if s[0] in setting[name_choices]]
271-
)
273+
if len(setting[name_choices]) > 0:
274+
setting[name_selected] = "\n".join(
275+
[s[1] for s in choices if s[0] in setting[name_choices]]
276+
)
277+
else:
278+
setting[name_selected] = ""
272279

273280
return setting
274281

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

291298
settings = []
299+
292300
for access_type in settings_data:
293301
setting = {}
294302
for k in item_list:
295303
setting.update({k + "_choices": [], k + "_default": None})
296-
setting.update({k + "_selected": []})
304+
setting.update({k + "_selected": "None"})
297305

298306
if access_type == "data_entry":
299307
setting["access_level"] = 1
@@ -312,12 +320,12 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
312320
setting["site_selected"] = "\n".join(
313321
[s[1] for s in sites if s[0] in setting["site_choices"]]
314322
)
315-
323+
# print("setting: ", setting)
316324
# -- Consent templates
317325
if "consent_template" in item_list:
318326
try:
319-
# settings_val = account_data["settings"][access_type]["consent_template"]
320327
settings_val = settings_data[access_type]["consent_template"]
328+
settings_val["choices"] = None # disable setting choices
321329
setting = flatten_settings(
322330
name="consent_template",
323331
settings_val=settings_val,
@@ -344,6 +352,7 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
344352
if "collection_protocol" in item_list:
345353
try:
346354
settings_val = settings_data[access_type]["protocol"]["ACQ"]
355+
settings_val["choices"] = None # disable setting choices
347356
setting = flatten_settings(
348357
name="collection_protocol",
349358
settings_val=settings_val,
@@ -357,6 +366,7 @@ def flatten_settings(name, settings_val, choices=[], setting={}):
357366
if "processing_protocol" in item_list:
358367
try:
359368
settings_val = settings_data[access_type]["protocol"]["SAP"]
369+
settings_val["choices"] = None # disable setting choices
360370
setting = flatten_settings(
361371
name="processing_protocol",
362372
settings_val=settings_val,
@@ -488,7 +498,7 @@ def jsonise_settings(form, account_data):
488498
settings["data_entry"].update(
489499
{
490500
"consent_template": {
491-
"choices": setting.consent_template_choices.data,
501+
# "choices": setting.consent_template_choices.data,
492502
"default": setting.consent_template_default.data,
493503
},
494504
"protocol": {
@@ -497,11 +507,11 @@ def jsonise_settings(form, account_data):
497507
"default": setting.study_protocol_default.data,
498508
},
499509
"ACQ": {
500-
"choices": setting.collection_protocol_choices.data,
510+
# "choices": setting.collection_protocol_choices.data,
501511
"default": setting.collection_protocol_default.data,
502512
},
503513
"SAP": {
504-
"choices": setting.processing_protocol_choices.data,
514+
# "choices": setting.processing_protocol_choices.data,
505515
"default": setting.processing_protocol_default.data,
506516
},
507517
},
@@ -599,6 +609,7 @@ def admin_edit_settings(id, use_template=None):
599609
study_protocols = []
600610
if protocols_response.status_code == 200:
601611
study_protocols = protocols_response.json()["content"]["choices"]
612+
# print("stu choices: ", study_protocols)
602613

603614
protocols_response = requests.get(
604615
url_for("api.protocol_query_tokenuser", default_type="ACQ", _external=True),
@@ -644,6 +655,7 @@ def admin_edit_settings(id, use_template=None):
644655
None,
645656
sites,
646657
consent_templates,
658+
study_protocols,
647659
collection_protocols,
648660
processing_protocols,
649661
)
@@ -659,6 +671,7 @@ def admin_edit_settings(id, use_template=None):
659671
None,
660672
sites,
661673
consent_templates,
674+
study_protocols,
662675
collection_protocols,
663676
processing_protocols,
664677
)
@@ -680,16 +693,17 @@ def admin_edit_settings(id, use_template=None):
680693
for setting in form.settings.entries:
681694
setting.site_choices.choices = sites
682695
# setting.site_default.choices = sites
683-
setting.consent_template_choices.choices = consent_templates
696+
697+
# setting.consent_template_choices.choices = consent_templates
684698
setting.consent_template_default.choices = consent_templates
685699

686700
setting.study_protocol_choices.choices = study_protocols
687701
setting.study_protocol_default.choices = study_protocols
688702

689-
setting.collection_protocol_choices.choices = collection_protocols
703+
# setting.collection_protocol_choices.choices = collection_protocols
690704
setting.collection_protocol_default.choices = collection_protocols
691705

692-
setting.processing_protocol_choices.choices = processing_protocols
706+
# setting.processing_protocol_choices.choices = processing_protocols
693707
setting.processing_protocol_default.choices = processing_protocols
694708

695709
if (
@@ -741,3 +755,44 @@ def admin_edit_settings(id, use_template=None):
741755
)
742756
else:
743757
return abort(response.status_code)
758+
759+
760+
@admin.route("auth/forget_password", methods=["GET", "POST"])
761+
def auth_forget_password():
762+
form = ForgetPasswordForm()
763+
if form.validate_on_submit():
764+
# get password reset token
765+
token_email = requests.post(
766+
url_for("api.auth_forget_password", _external=True),
767+
headers={
768+
"FlaskApp": current_app.config.get("SECRET_KEY"),
769+
"Email": form.email.data,
770+
},
771+
json={"email": form.email.data},
772+
)
773+
774+
if token_email.status_code == 200:
775+
token = token_email.json()["content"]["token"]
776+
confirm_url = url_for(
777+
"auth.change_password_external", token=token, _external=True
778+
)
779+
template = render_template(
780+
"admin/auth/email/password_reset.html", reset_url=confirm_url
781+
)
782+
subject = "LIMBUS: Password Reset Email"
783+
msg = Message(
784+
subject,
785+
recipients=[form.email.data],
786+
html=template,
787+
sender=current_app.config["MAIL_USERNAME"],
788+
)
789+
790+
# Send password reset email
791+
mail.send(msg)
792+
flash("Password reset email has been sent!")
793+
return redirect(url_for("auth.login"))
794+
795+
else:
796+
flash(token_email.json()["message"])
797+
798+
return render_template("admin/auth/forget_password.html", form=form)

services/web/app/auth/api.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,45 @@ def auth_password_reset(tokenuser: UserAccount):
144144
}
145145

146146

147+
@api.route("/auth/user/password/forget", methods=["GET", "POST"])
148+
def auth_forget_password():
149+
values = request.get_json()
150+
151+
if not values:
152+
return no_values_response()
153+
154+
try:
155+
result = password_reset_form_schema.load(values)
156+
except ValidationError as err:
157+
return validation_error_response(err)
158+
159+
user = UserAccount.query.filter_by(email=values["email"]).first()
160+
if user is None:
161+
return validation_error_response(
162+
{"message": "Incorrect email or this email hasn't been registered!"}
163+
)
164+
165+
else:
166+
uaprt = UserAccountPasswordResetToken.query.filter_by(user_id=user.id).first()
167+
168+
new_token = str(uuid4())
169+
170+
if uaprt == None:
171+
uaprt = UserAccountPasswordResetToken(user_id=user.id)
172+
uaprt.token = new_token
173+
else:
174+
uaprt.token = new_token
175+
uaprt.update({"editor_id": user.id})
176+
177+
try:
178+
db.session.add(uaprt)
179+
db.session.commit()
180+
# return success_with_content_response(basic_user_account_schema.dump(user))
181+
return success_with_content_response({"token": new_token})
182+
except Exception as err:
183+
return transaction_error_response(err)
184+
185+
147186
@api.route("/auth/user/new_token", methods=["GET"])
148187
@token_required
149188
def auth_new_token(tokenuser: UserAccount):

services/web/app/auth/routes.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
from ast import Pass
1717
from flask import redirect, render_template, url_for, flash, abort, request
1818
from flask_login import login_required, login_user, logout_user, current_user
19+
1920
import requests
2021
from sqlalchemy import func
2122

2223
from . import auth
2324

24-
from .forms import LoginForm, PasswordChangeForm, UserAccountEditForm
25+
from .forms import (
26+
LoginForm,
27+
PasswordChangeForm,
28+
UserAccountEditForm,
29+
) # , ForgetPasswordForm
2530
from .models import UserAccount, UserAccountToken, UserAccountPasswordResetToken
2631

2732
from ..database import db
@@ -137,10 +142,12 @@ def change_password_external(token: str):
137142
)
138143

139144
if uaprt == None:
140-
flash("This token is invalid. Please contact your system administrator")
145+
flash(
146+
"This token is invalid. "
147+
) # Please contact your system administrator")
141148
elif datetime.now() > (uaprt.updated_on + timedelta(hours=24)):
142149
flash(
143-
"This token is older than 24 hours old. Please contact your system administrator"
150+
"This token is older than 24 hours old. " # Please contact your system administrator"
144151
)
145152
else:
146153
user = (

0 commit comments

Comments
 (0)