[Feature] Password Reset Flow (#88)

Fixes #83

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#88
This commit is contained in:
2025-09-06 19:53:36 +12:00
parent a16035677c
commit 1a6608287d
10 changed files with 226 additions and 223 deletions

View File

@ -141,7 +141,9 @@ USE_TZ = True
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# EMAIL if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
else:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_HOST = 'mail.gandi.net' EMAIL_HOST = 'mail.gandi.net'
@ -342,8 +344,12 @@ FLAGS = {
'APPLICABILITY_DOMAIN': APPLICABILITY_DOMAIN_ENABLED, 'APPLICABILITY_DOMAIN': APPLICABILITY_DOMAIN_ENABLED,
} }
# path of the URL are checked via "startswith"
# -> /password_reset/done is covered as well
LOGIN_EXEMPT_URLS = [ LOGIN_EXEMPT_URLS = [
'/api/legacy/', '/api/legacy/',
'/o/token/', '/o/token/',
'/o/userinfo/', '/o/userinfo/',
'/password_reset/',
'/reset/'
] ]

View File

@ -1,21 +1,36 @@
from django.urls import path, re_path from django.urls import path, re_path
from django.contrib.auth import views as auth_views
from . import views as v from . import views as v
# from sesame.views import LoginView
UUID = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' UUID = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
urlpatterns = [ urlpatterns = [
# Sesame
# path("login/", v.EmailLoginView.as_view(), name="email_login"),
# path("login/auth/", LoginView.as_view(), name="login"),
# Home # Home
re_path(r'^$', v.index, name='index'), re_path(r'^$', v.index, name='index'),
# Login
re_path(r'^login', v.login, name='login'), re_path(r'^login', v.login, name='login'),
re_path(r'^logout', v.logout, name='logout'), re_path(r'^logout', v.logout, name='logout'),
# Built In views
path('password_reset/', auth_views.PasswordResetView.as_view(
template_name='static/password_reset_form.html'
), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(
template_name='static/password_reset_done.html'
), name='password_reset_done'),
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(
template_name='static/password_reset_confirm.html'
), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(
template_name='static/password_reset_complete.html'
), name='password_reset_complete'),
# Top level urls # Top level urls
re_path(r'^package$', v.packages, name='packages'), re_path(r'^package$', v.packages, name='packages'),
re_path(r'^compound$', v.compounds, name='compounds'), re_path(r'^compound$', v.compounds, name='compounds'),
@ -78,5 +93,6 @@ urlpatterns = [
re_path(r'^depict$', v.depict, name='depict'), re_path(r'^depict$', v.depict, name='depict'),
# OAuth Stuff
path("o/userinfo/", v.userinfo, name="oauth_userinfo"), path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
] ]

View File

@ -48,7 +48,7 @@ def login(request):
if request.method == 'GET': if request.method == 'GET':
context['title'] = 'enviPath' context['title'] = 'enviPath'
context['next'] = request.GET.get('next', '') context['next'] = request.GET.get('next', '')
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
elif request.method == 'POST': elif request.method == 'POST':
is_login = bool(request.POST.get('login', False)) is_login = bool(request.POST.get('login', False))
@ -67,17 +67,17 @@ def login(request):
if not temp_user.is_active: if not temp_user.is_active:
context['message'] = "User account is not activated yet!" context['message'] = "User account is not activated yet!"
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
email = temp_user.email email = temp_user.email
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
context['message'] = "Login failed!" context['message'] = "Login failed!"
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
try: try:
user = authenticate(username=email, password=password) user = authenticate(username=email, password=password)
except Exception as e: except Exception as e:
context['message'] = "Login failed!" context['message'] = "Login failed!"
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
if user is not None: if user is not None:
login(request, user) login(request, user)
@ -88,7 +88,7 @@ def login(request):
return redirect(s.SERVER_URL) return redirect(s.SERVER_URL)
else: else:
context['message'] = "Login failed!" context['message'] = "Login failed!"
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
elif is_register: elif is_register:
username = request.POST.get('username') username = request.POST.get('username')
@ -98,19 +98,19 @@ def login(request):
if password != rpassword or password == '': if password != rpassword or password == '':
context['message'] = "Registration failed, provided passwords differ!" context['message'] = "Registration failed, provided passwords differ!"
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
try: try:
u = UserManager.create_user(username, email, password) u = UserManager.create_user(username, email, password)
except Exception: except Exception:
context['message'] = "Registration failed! Couldn't create User Account." context['message'] = "Registration failed! Couldn't create User Account."
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
if s.ADMIN_APPROVAL_REQUIRED: if s.ADMIN_APPROVAL_REQUIRED:
context['message'] = "Your account has been created! An admin will activate it soon!" context['message'] = "Your account has been created! An admin will activate it soon!"
else: else:
context['message'] = "Account has been created! You'll receive a mail to activate your account shortly." context['message'] = "Account has been created! You'll receive a mail to activate your account shortly."
return render(request, 'login.html', context) return render(request, 'static/login.html', context)
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:

View File

@ -1,203 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>enviPath - Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 3.3.7 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<style>
body, html {
margin: 0;
height: 100%;
overflow: hidden;
}
.bg-blur {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('{% static "/images/enviPy-screenshot.png" %}') no-repeat center center/cover;
filter: blur(8px);
z-index: -1;
}
.center-button {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.center-message {
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
</style>
</head>
<body>
<!-- Blurred Background -->
<div class="bg-blur"></div>
<div class="bg-dim"></div>
<!-- Trigger Button -->
<div class="center-button">
<button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#signupmodal">Login / Sign Up</button>
</div>
<br>
<div class="center-message">
{% if message %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-success" role="alert">
Kia ora! We are running our closed beta tests at the moment. It would be great to get your help as tester,
you
can apply to become tester by registering for this page, just hit the button below. More information on the
beta
test is available in our <a href="https://community.envipath.org/t/apply-to-join-our-closed-beta/95">
community
form</a>
</div>
{% endif %}
</div>
<!-- Bootstrap Modal -->
<div class="modal fade bs-modal-sm" id="signupmodal" tabindex="-1" role="dialog"
aria-labelledby="mySmallModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<br>
<div class="bs-example bs-example-tabs">
<ul id="myTab" class="nav nav-tabs">
<li class="active">
<a href="#signin" data-toggle="tab">Sign In</a>
</li>
<li class="">
<a href="#signup" data-toggle="tab">Register</a>
</li>
<li class="">
<a href="#why" data-toggle="tab">Why?</a>
</li>
</ul>
</div>
<div class="modal-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="signin">
<form class="form-horizontal" method="post" action="{% url 'login' %}">
{% csrf_token %}
<fieldset>
<input type="hidden" name="login" id="login" value="true"/>
<div class="control-group">
<label class="control-label" for="username">Username:</label>
<div class="controls">
<input required id="username" name="username" type="text"
class="form-control"
placeholder="username" autocomplete="username">
</div>
<label class="control-label" for="passwordinput">Password:</label>
<div class="controls">
<input required id="passwordinput" name="password" class="form-control"
type="password" placeholder="********"
autocomplete="current-password">
</div>
</div>
<!-- Button -->
<div class="control-group">
<label class="control-label" for="signin"></label>
<div class="controls">
<button id="signin" name="signin" class="btn btn-success">Sign In
</button>
</div>
</div>
<input type="hidden" name="next" value="{{ next }}" />
</fieldset>
</form>
</div>
<!-- Why tab -->
<div class="tab-pane fade in" id="why">
<p>After you register, you have more permissions on
this site, e.g., can create your own
packages, submit data for review, and set access
permissions to your data.</p>
<p></p>
<p>
<br> Please
contact <a href="mailto:admin@envipath.org">admin@envipath.org</a>
if you have any questions.</p>
</div>
<!-- Register -->
<div class="tab-pane fade"
id="signup">
<div id="passwordGuideline" class="alert alert-info">
The password must contain 8 to 30 characters<br>
The following characters are allowed:
- Upper and lower case characters<br>
- Digits<br>
- Special characters _, -, +<br>
</div>
<form id="signup-action" class="form-horizontal" action="{% url 'login' %}" method="post">
{% csrf_token %}
<input type="hidden" name="register" id="register" value="true"/>
<label class="control-label" for="userid">Username:</label>
<input id="userid" name="username" class="form-control" type="text"
placeholder="user" required autocomplete="username">
<label class="control-label" for="email">Email:</label>
<input id="email" name="email" class="form-control" type="email"
placeholder="user@envipath.org" required>
<label class="control-label" for="password">Password:</label>
<input id="password" name="password" class="form-control" type="password"
placeholder="********" required autocomplete="new-password">
<label class="control-label" for="rpassword">Repeat Password:</label>
<input id="rpassword" name="rpassword" class="form-control" type="password"
placeholder="********" required autocomplete="new-password">
<div class="control-group">
<label class="control-label" for="confirmsignup"></label>
<div class="controls">
<button id="confirmsignup" name="confirmsignup" class="btn btn-success">Sign
Up
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<center>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</center>
</div>
</div>
</div>
</div>
<!-- Bootstrap 3.3.7 JS + jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,54 @@
{% extends "static/static_base.html" %}
{% block content %}
{% if message %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-success" role="alert">
Kia ora! We are running our closed beta tests at the moment. It would be great to get your help as tester,
you
can apply to become tester by registering for this page, just hit the button below. More information on the
beta
test is available in our <a href="https://community.envipath.org/t/apply-to-join-our-closed-beta/95">
community
form</a>
</div>
{% endif %}
<div class="modal-dialog" style="margin:30px auto; z-index:9999;">
<div class="modal-content">
<div class="modal-body">
<form class="form-horizontal" method="post" action="{% url 'login' %}">
{% csrf_token %}
<fieldset>
<input type="hidden" name="login" id="login" value="true"/>
<div class="control-group">
<label class="control-label" for="username">Username</label>
<div class="controls">
<input required id="username" name="username" type="text"
class="form-control" placeholder="username" autocomplete="username">
</div>
<label class="control-label" for="passwordinput">Password:</label>
<div class="controls">
<input required id="passwordinput" name="password" class="form-control"
type="password" placeholder="********" autocomplete="current-password">
</div>
<div class="form-group text-center" style="margin-top:15px;">
<a href="{% url 'password_reset' %}">Forgot your password?</a>
</div>
</div>
</fieldset>
<div class="control-group">
<label class="control-label" for="signin"></label>
<div class="controls">
<button id="signin" name="signin" class="btn btn-success">Sign In
</button>
</div>
</div>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "static/static_base.html" %}
{% block content %}
<p>Your password has been reset successfully. <a href="{% url 'login' %}">Login</a></p>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "static/static_base.html" %}
{% block content %}
<div class="modal-dialog" style="margin:30px auto; z-index:9999;">
<div class="modal-content">
<div class="modal-body">
<h2>Enter new password</h2>
<form method="post">
{% csrf_token %}
<p>
<label for="id_new_password1">New password:</label>
<input type="password" class="form-control" name="new_password1" autocomplete="new-password"
required=""
aria-describedby="id_new_password1_helptext" id="id_new_password1">
<span class="helptext" id="id_new_password1_helptext"></span></p>
{{ form.new_password1.help_text|safe }}
<p>
<label for="id_new_password2">New password confirmation:</label>
<input type="password" class="form-control" name="new_password2" autocomplete="new-password"
required=""
aria-describedby="id_new_password2_helptext" id="id_new_password2">
{{ form.new_password2.help_text|safe }}
</p>
<button class="btn btn-primary" type="submit">Reset Password</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "static/static_base.html" %}
{% block content %}
<div class="alert alert-success" role="alert">
An email has been sent with instructions to reset your password.
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "static/static_base.html" %}
{% block content %}
<div class="modal-dialog" style="margin:30px auto; z-index:9999;">
<div class="modal-content">
<div class="modal-body">
<form method="post">
{% csrf_token %}
<label class="control-label" for="username">Email:</label>
<input type="email" name="email" class="form-control" maxlength="254"
required="" id="id_email">
<div class="control-group">
<label class="control-label" for="signin"></label>
<div class="controls">
<button id="signin" name="signin" type="submit" class="btn btn-success">Send
</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>enviPath - Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 3.3.7 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<style>
body, html {
margin: 0;
height: 100%;
overflow: hidden;
}
.bg-blur {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('{% static "/images/enviPy-screenshot.png" %}') no-repeat center center/cover;
filter: blur(8px);
z-index: -1;
}
.center-button {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.static-content {
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
</style>
</head>
<body>
<!-- Blurred Background -->
<div class="bg-blur"></div>
<div class="bg-dim"></div>
<div class="static-content">
{% block content %}
{% endblock content %}
</div>
<!-- Bootstrap 3.3.7 JS + jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>