From 2ae6011a1b73602b865d0b2b5c1a368baef76d13 Mon Sep 17 00:00:00 2001 From: Trey Date: Sat, 24 Feb 2024 13:37:43 +0000 Subject: [PATCH] Improved resend email feature --- ...alter_verificationcodes_expiry_and_more.py | 29 +++++++++ backend/views/core/auth/login.py | 10 ++- backend/views/core/auth/urls.py | 4 +- backend/views/core/auth/verify.py | 20 ++++-- backend/views/core/other/errors.py | 11 ++-- frontend/templates/base/auth.html | 63 ++++++++++--------- settings/helpers.py | 11 ++++ 7 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 backend/migrations/0021_alter_verificationcodes_expiry_and_more.py diff --git a/backend/migrations/0021_alter_verificationcodes_expiry_and_more.py b/backend/migrations/0021_alter_verificationcodes_expiry_and_more.py new file mode 100644 index 00000000..bb47cd9d --- /dev/null +++ b/backend/migrations/0021_alter_verificationcodes_expiry_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.2 on 2024-02-23 19:00 + +import datetime + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend", "0020_alter_verificationcodes_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="verificationcodes", + name="expiry", + field=models.DateTimeField( + default=datetime.datetime( + 2024, 2, 23, 22, 0, 25, 744643, tzinfo=datetime.timezone.utc + ) + ), + ), + migrations.AlterField( + model_name="verificationcodes", + name="token", + field=models.TextField(default="XBNKTM", editable=False), + ), + ] diff --git a/backend/views/core/auth/login.py b/backend/views/core/auth/login.py index b6f10d02..f8534f85 100644 --- a/backend/views/core/auth/login.py +++ b/backend/views/core/auth/login.py @@ -1,8 +1,10 @@ +import django_ratelimit from django.contrib.auth import authenticate, login, logout from django.http import HttpRequest from django.shortcuts import render from django.urls import resolve, Resolver404, reverse from django.views.decorators.csrf import csrf_exempt +from django_ratelimit.decorators import ratelimit from backend.decorators import * from backend.models import LoginLog @@ -15,6 +17,9 @@ @csrf_exempt @not_authenticated +@ratelimit(key="post:email", method=django_ratelimit.UNSAFE, rate="5/m") +@ratelimit(key="post:email", method=django_ratelimit.UNSAFE, rate="10/5m") +@ratelimit(key="ip", method=django_ratelimit.UNSAFE, rate="5/m") def login_page(request): if request.user.is_authenticated: return redirect("dashboard") @@ -32,8 +37,11 @@ def login_page(request): if not user.is_active: if user.awaiting_email_verification: messages.error(request, f""" + Your account is awaiting email verification - click here to send a new verification email . diff --git a/backend/views/core/auth/urls.py b/backend/views/core/auth/urls.py index f99259ef..fe53c0a0 100644 --- a/backend/views/core/auth/urls.py +++ b/backend/views/core/auth/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from . import login, create_account,verify +from . import login, create_account, verify from .passwords import view as passwords_view, generate as passwords_generate, set as passwords_set urlpatterns = [ @@ -27,7 +27,7 @@ name="login create_account verify", ), path( - "create_account/verify/resend//", + "create_account/verify/resend/", verify.resend_verification_code, name="login create_account verify resend", ), diff --git a/backend/views/core/auth/verify.py b/backend/views/core/auth/verify.py index 9a48b679..ce58a867 100644 --- a/backend/views/core/auth/verify.py +++ b/backend/views/core/auth/verify.py @@ -3,6 +3,8 @@ from django.shortcuts import redirect from django.urls import reverse from django.utils import timezone +from django.views.decorators.http import require_POST +from django_ratelimit.decorators import ratelimit from backend.models import VerificationCodes, User, TracebackError from settings import settings @@ -42,7 +44,17 @@ def create_account_verify(request, uuid, token): return redirect("auth:login") -def resend_verification_code(request, uid): +@ratelimit(group="resend_verification_code", key="ip", rate="1/m") +@ratelimit(group="resend_verification_code", key="ip", rate="3/25m") +@ratelimit(group="resend_verification_code", key="ip", rate="10/6h") +@ratelimit(group="resend_verification_code", key="post:email", rate="1/m") +@ratelimit(group="resend_verification_code", key="post:email", rate="3/25m") +@require_POST +def resend_verification_code(request): + email = request.POST.get("email") + if not email: + messages.error(request, "Invalid resend verification request") + return redirect("auth:login") if not ARE_EMAILS_ENABLED: messages.error(request, "Emails are currently disabled.") TracebackError.objects.create( @@ -50,7 +62,7 @@ def resend_verification_code(request, uid): ) return redirect("auth:login create_account") try: - user = User.objects.get(pk=uid) + user = User.objects.get(email=email) except User.DoesNotExist: messages.error(request, "Invalid resend verification request") return redirect("auth:create_account") @@ -62,8 +74,8 @@ def resend_verification_code(request, uid): "auth:login create_account verify", kwargs={"uuid": magic_link.uuid, "token": token_plain} ) - send_email(destination=request.user.email, subject="Verify your email", message=f""" - Hi {request.user.first_name if request.user.first_name else "User"}, + send_email(destination=email, subject="Verify your email", message=f""" + Hi {user.first_name if user.first_name else "User"}, Verification for your email has been requested to link this email to your MyFinances account. If this wasn't you, you can simply ignore this email. diff --git a/backend/views/core/other/errors.py b/backend/views/core/other/errors.py index 37b853ca..2fa025fc 100644 --- a/backend/views/core/other/errors.py +++ b/backend/views/core/other/errors.py @@ -34,10 +34,13 @@ def e_403(request: HttpRequest, exception=None): request, "Woah, slow down there. You've been temporarily blocked from this page due to extreme requests.", ) - traceback.print_exc() - exec_error = traceback.format_exc() - if len(exec_error) < 4999: - TracebackError(error=exec_error).save() + user_ip = request.META.get("REMOTE_ADDR") + user_id = f"User #{request.user.id}" if request.user.is_authenticated else "Not logged in" + action = f"{user_ip} | Ratelimited | {user_id}" + auditlog = AuditLog(action=action) + if request.user.is_authenticated: + auditlog.user = request.user + auditlog.save() return redirect("auth:login") else: messages.error( diff --git a/frontend/templates/base/auth.html b/frontend/templates/base/auth.html index ade7e1e8..dde0ff8e 100644 --- a/frontend/templates/base/auth.html +++ b/frontend/templates/base/auth.html @@ -1,37 +1,42 @@ {% load static %} - {% include 'base/_head.html' %} - -
-
-
-
-
-
-

Dashboard

-
- -
-

What do you get to manage?

-

✓ Client Lists

-

✓ Invoices

-

✓ Receipt Storage

-

✓ Financial Reports

-
+{% include 'base/_head.html' %} + +
+
+
+
+
+
+

Dashboard

+
+
-
-
- {# {% component "messages_list" %}#} - {% include "base/toasts.html" %} -

- {% block title %} - {% endblock title %} -

- {% block content %} - {% endblock content %} +

What do you get to manage?

+

✓ Client Lists

+

✓ Invoices

+

✓ Receipt Storage

+

✓ Financial Reports

+
+ {# {% component "messages_list" %}#} + {% include "base/toasts.html" %} +

+ {% block title %} + {% endblock title %} +

+ {% block content %} + {% endblock content %} +
- +
+
+ + diff --git a/settings/helpers.py b/settings/helpers.py index 2c8a9b81..03569d4c 100644 --- a/settings/helpers.py +++ b/settings/helpers.py @@ -3,6 +3,7 @@ import boto3 import environ +from django_ratelimit.core import get_usage from mypy_boto3_sesv2.client import SESV2Client ### NEEDS REFACTOR @@ -24,6 +25,16 @@ def get_var(key, default=None, required=False): return value +def increment_rate_limit(request, group): + """ + Alias of is_ratelimited that just increments the rate limit for the given group. + + Returns the new usage count. + """ + usage = get_usage(request, group, increment=True) + return usage.get("count", 0) + + EMAIL_CLIENT: SESV2Client = boto3.client( "sesv2", region_name="eu-west-2",