diff --git a/efile_app/efile/authentication.py b/efile_app/efile/authentication.py index e68eb92..43e42d7 100644 --- a/efile_app/efile/authentication.py +++ b/efile_app/efile/authentication.py @@ -96,3 +96,9 @@ def logout(request): session_data = {k: v for k, v in request.session.items() if k in session_keys_to_keep} request.session.clear() request.session.update(session_data) + + @staticmethod + def password_reset(request): + """Calls the efile-proxy's "reset password" API, which + sends an email from Tyler to reset the password. + """ diff --git a/efile_app/efile/forms.py b/efile_app/efile/forms.py index 2567472..785e965 100644 --- a/efile_app/efile/forms.py +++ b/efile_app/efile/forms.py @@ -17,6 +17,10 @@ class EFileLoginForm(forms.Form): ) +class EFilePasswordResetForm(forms.Form): + email = forms.EmailField(widget=forms.EmailInput(attrs={"class": "form-control", "id": "email", "required": True})) + + class EFileRegistrationForm(forms.Form): # Legal Name first_name = forms.CharField( diff --git a/efile_app/efile/static/css/login.css b/efile_app/efile/static/css/login.css index e628912..2124d48 100644 --- a/efile_app/efile/static/css/login.css +++ b/efile_app/efile/static/css/login.css @@ -78,7 +78,9 @@ body { } .btn-sign-in, -.btn-register { +.btn-register, +.btn-password-reset + { background: #1e3a5f; border: none; border-radius: 25px; @@ -87,18 +89,12 @@ body { font-size: 1.1rem; color: white; transition: all 0.3s ease; -} - -.btn-sign-in { - width: 200px; -} - -.btn-register { width: 200px; } .btn-sign-in:hover, -.btn-register:hover { +.btn-register:hover, +.btn-password-reset:hover { background: #1e3a5f; filter: brightness(1.5); transform: translateY(-1px); @@ -165,3 +161,10 @@ body { .login-form.hide { display: none; } + +.alert-error { + --bs-alert-color: var(--bs-danger-text-emphasis); + --bs-alert-bg: var(--bs-danger-bg-subtle); + --bs-alert-border-color: var(--bs-danger-border-subtle); + --bs-alert-link-color: var(--bs-danger-text-emphasis); +} \ No newline at end of file diff --git a/efile_app/efile/templates/efile/login.html b/efile_app/efile/templates/efile/login.html index 43a7aeb..2b30b35 100644 --- a/efile_app/efile/templates/efile/login.html +++ b/efile_app/efile/templates/efile/login.html @@ -96,7 +96,7 @@

{{ config.jurisdiction.name }}

- Forgot Password? + Forgot Password?
diff --git a/efile_app/efile/templates/efile/password_reset.html b/efile_app/efile/templates/efile/password_reset.html new file mode 100644 index 0000000..ee4f389 --- /dev/null +++ b/efile_app/efile/templates/efile/password_reset.html @@ -0,0 +1,104 @@ +{% load static %} + + + + + + {{ config.jurisdiction.name }} - Forgot Password + + + + + +
+ +
+ +
+
+ +

{{ config.jurisdiction.name }}

+
+
+ + + {% if messages %} {% for message in messages %} + + {% endfor %} {% endif %} + + +
+

+ Reset your eFile password +

+ +

+ For security, a link will be sent to your email from "efilingmail.tylertech.cloud". Visit that link + to reset your password. +

+ +
+ {% csrf_token %} +
+ + {{ reset_form.email }} {% if reset_form.email.errors %} +
+ {% for error in reset_form.email.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ +
+
+
+
+
+ + + + + diff --git a/efile_app/efile/tests/test_smoke.py b/efile_app/efile/tests/test_smoke.py index a4c87c4..2a7bdf9 100644 --- a/efile_app/efile/tests/test_smoke.py +++ b/efile_app/efile/tests/test_smoke.py @@ -9,7 +9,7 @@ def test_settings_are_wired(settings): def test_login_page_renders(client): - """Basic smoke test: GET /login/ should render the login page (200).""" + """Basic smoke test: GET "login" should render the login page (200).""" url = reverse("efile_login", kwargs={"jurisdiction": "illinois"}) resp = client.get(url) assert resp.status_code == 200 diff --git a/efile_app/efile/urls.py b/efile_app/efile/urls.py index 83dae40..13ac2c5 100644 --- a/efile_app/efile/urls.py +++ b/efile_app/efile/urls.py @@ -5,7 +5,7 @@ from .views.choose_jurisdiction import choose_jurisdiction from .views.confirmation import filing_confirmation from .views.expert_form import efile_expert_form -from .views.login import efile_login, efile_logout +from .views.login import efile_login, efile_logout, efile_password_reset from .views.options import efile_options from .views.register import efile_register from .views.review import case_review @@ -39,6 +39,7 @@ def jurisdiction_homepage(request, jurisdiction): path("jurisdiction//login/", efile_login, name="efile_login"), path("jurisdiction//logout/", efile_logout, name="efile_logout"), path("jurisdiction//register/", efile_register, name="efile_register"), + path("jurisdiction//password_reset/", efile_password_reset, name="efile_password_reset"), path("jurisdiction//options/", efile_options, name="efile_options"), path("jurisdiction//expert_form/", efile_expert_form, name="expert_form"), path("jurisdiction//upload_first/", efile_upload_first, name="upload_first"), diff --git a/efile_app/efile/utils/proxy_connection.py b/efile_app/efile/utils/proxy_connection.py index 43ab80c..3bed940 100644 --- a/efile_app/efile/utils/proxy_connection.py +++ b/efile_app/efile/utils/proxy_connection.py @@ -80,3 +80,15 @@ def get_headers(): "User-Agent": "LITEfile-Client/1.0", "X-API-Key": getattr(settings, "SUFFOLK_EFILE_API_KEY", None), } + + +def tyler_password_reset(username, jurisdiction): + url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/adminusers/user/password/reset" + try: + api_key = getattr(settings, "SUFFOLK_EFILE_API_KEY", None) + headers = {"X-API-Key": api_key, "User-Agent": f"{jurisdiction.title()}-eFile-Client/1.0"} + + response = requests.post(url, data=username, headers=headers, timeout=10) + return response + except Exception: + logger.exception("Auth password reset failed: %s", url) diff --git a/efile_app/efile/views/login.py b/efile_app/efile/views/login.py index d1ed821..21e565d 100644 --- a/efile_app/efile/views/login.py +++ b/efile_app/efile/views/login.py @@ -11,8 +11,9 @@ from django.shortcuts import redirect, render from efile.utils.config_loader import config_loader +from efile.utils.proxy_connection import tyler_password_reset -from ..forms import EFileLoginForm +from ..forms import EFileLoginForm, EFilePasswordResetForm logger = logging.getLogger(__name__) @@ -46,6 +47,33 @@ def efile_login(request, jurisdiction): return render(request, "efile/login.html", context) +def efile_password_reset(request, jurisdiction): + if jurisdiction not in config_loader.get_available_jurisdictions(): + # TODO(brycew): better prediction of spell correction? (closest juris?) + return redirect("password_reset", jurisdiction="illinois") + + if request.method == "POST" and "reset_submit" in request.POST: + reset_form = EFilePasswordResetForm(request.POST) + if reset_form.is_valid(): + logger.info("Actually acting on the POST") + email = reset_form.cleaned_data["email"] + result = tyler_password_reset(email, jurisdiction) + logger.info("Password reset result: %s", result) + if result.ok: + messages.success(request, "Password reset link sent to email!") + return redirect(f"/jurisdiction/{jurisdiction}/login") + else: + messages.error(request, f"Password reset failed: {result.text}") + else: + messages.error(request, "Please correct the errors below.") + else: + reset_form = EFilePasswordResetForm() + + logger.info("Sending the main page") + context = {"reset_form": reset_form} + return render(request, "efile/password_reset.html", context) + + def efile_logout(request, jurisdiction): """ Custom logout view