From 3c0cafbcd3b4131af4d9ee4a8103d710dcc16732 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Tue, 20 Aug 2024 16:50:02 -0300 Subject: [PATCH 01/26] Add code management - BE --- .../core/dispatchers.py | 4 ++ .../0002_userresetpasswordcodemessages.py | 39 ++++++++++++++ .../core/models.py | 49 +++++++++++++++++ .../core/serializers.py | 27 ++++++++++ .../core/signals.py | 25 +++++++++ ...rd_reset.html => password_reset_code.html} | 13 +++-- .../core/tests.py | 11 ++-- .../core/urls.py | 9 +++- .../core/views.py | 53 +++++++++---------- .../utils/misc.py | 13 +++++ .../utils/tests.py | 8 --- 11 files changed, 205 insertions(+), 46 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/dispatchers.py create mode 100644 {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py rename {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/{password_reset.html => password_reset_code.html} (78%) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/dispatchers.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/dispatchers.py new file mode 100644 index 000000000..81f7e9ed7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/dispatchers.py @@ -0,0 +1,4 @@ +from django import dispatch + +# Custom signals dispatchers +new_reset_password_code_created_ds = dispatch.Signal() diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py new file mode 100644 index 000000000..01b27b27e --- /dev/null +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.15 on 2024-08-16 21:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import {{ cookiecutter.project_slug }}.core.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="UserResetPasswordCodeMessages", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_edited", models.DateTimeField(auto_now=True)), + ("code", models.CharField(max_length=255)), + ("is_used", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="reset_password_codes", to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "ordering": ("-created",), + }, + managers=[ + ("objects", {{ cookiecutter.project_slug }}.core.models.UserResetPasswordCodeMessagesManager()), + ], + ), + ] diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py index e803538e2..e78584512 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py @@ -1,13 +1,17 @@ import logging from django.conf import settings +from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, BaseUserManager from django.contrib.auth.tokens import default_token_generator from django.db import models +from django.utils.timezone import now, timedelta from {{ cookiecutter.project_slug }}.common.models import AbstractBaseModel from {{ cookiecutter.project_slug }}.utils.sites import get_site_url +from .dispatchers import new_reset_password_code_created_ds + logger = logging.getLogger(__name__) @@ -89,3 +93,48 @@ def reset_password_context(self): class Meta: ordering = ["email"] + + +class UserResetPasswordCodeMessagesQuerySet(models.QuerySet): + def for_user(self, user) -> "models.QuerySet[UserResetPasswordCodeMessages]": + if not user or user.is_anonymous: + return self.none() + elif user.is_staff or user.is_superuser: + return self.all() + else: + return self.filter(user=user) + + +class UserResetPasswordCodeMessagesManager(models.Manager): + use_in_migrations = True + + def get_queryset(self) -> "models.QuerySet[UserResetPasswordCodeMessages]": + return UserResetPasswordCodeMessagesQuerySet(self.model, using=self.db) + + def for_user(self, user): + return self.get_queryset().for_user(user) + + def create_code(self, user, code, **kwargs): + hashed_code = make_password(str(code)) + obj = self.model(user=user, code=hashed_code) + obj.save(using=self._db) + new_reset_password_code_created_ds.send(sender="reset_passoword_code_generator", code=code, instance=obj, created=True) + + return obj + + +class UserResetPasswordCodeMessages(AbstractBaseModel): + user = models.ForeignKey("core.User", related_name="reset_password_codes", on_delete=models.CASCADE) + code = models.CharField(max_length=255) + is_used = models.BooleanField(default=False) + objects = UserResetPasswordCodeMessagesManager() + + def __str__(self): + return f"reset password code for {self.user.email}" + + @property + def is_valid(self): + return not (self.is_used | (self.created > now() + timedelta(minutes=settings.RESET_PASSWORD_CODE_VALIDITY_MINUTES))) + + class Meta: + ordering = ("-created",) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/serializers.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/serializers.py index 723f88479..54a9d22e4 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/serializers.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/serializers.py @@ -1,7 +1,13 @@ +from datetime import timedelta + +from django.conf import settings from django.contrib.auth import login +from django.contrib.auth.hashers import check_password from django.contrib.auth.password_validation import validate_password +from django.utils import timezone from rest_framework import serializers from rest_framework.authtoken.models import Token +from rest_framework.validators import ValidationError from .models import User @@ -69,3 +75,24 @@ def validate(self, data): def create(self, validated_data): return User.objects.create_user(**validated_data) + + +class ResetPasswordSerializer(serializers.Serializer): + code = serializers.CharField(allow_blank=False, required=True) + password = serializers.CharField(allow_blank=False, required=True) + + def validate_code(self, value): + code_from_db = ( + self.context.get("user") + .reset_password_codes.filter(created__gte=(timezone.now() - timedelta(minutes=settings.RESET_PASSWORD_CODE_VALIDITY_MINUTES))) + .first() + ) + if not code_from_db or not code_from_db.is_valid or not check_password(str(value), code_from_db.code): + raise ValidationError(detail=["Invalid/Expired code"]) + self.context["code_from_db"] = code_from_db + return value + + def validate(self, data): + password = data.get("password") + validate_password(password) + return data diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py index 88dbf1250..b36162649 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py @@ -1,11 +1,36 @@ +import logging + from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token +from django.conf import settings +from django.template.loader import render_to_string from {{ cookiecutter.project_slug }}.core.models import User +from {{ cookiecutter.project_slug }}.utils.emails import send_html_email + +from .dispatchers import new_reset_password_code_created_ds + +# Logger +logger = logging.getLogger(__name__) @receiver(post_save, sender=User) def create_auth_token_add_permissions(sender, instance, created, **kwargs): if created: Token.objects.create(user=instance) + +@receiver(new_reset_password_code_created_ds) +def generate_reset_password_code(sender, code=None, instance=None, created=None, **kwargs): + if created: + try: + reset_context = {"code": code, "user": instance.user} + send_html_email( + "Password reset for {{ cookiecutter.project_name }}", + "registration/password_reset_code.html", + settings.DEFAULT_FROM_EMAIL, + [instance.user.email], + context=reset_context, + ) + except Exception as e: + logger.error(f"Failed to send message to user with id {str(instance.user.id)}, due to {e}") diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset.html b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html similarity index 78% rename from {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset.html rename to {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html index 127a212e2..a18651535 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset.html +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html @@ -27,9 +27,16 @@ It happens to the best of us. The good news is you can change it right now. {% endblock main_content %} - -{% block button_link %}{{ site_url }}/password/reset/confirm/{{ user.id }}/{{ token }}{% endblock %} -{% block button_text %}RESET YOUR PASSWORD {% endblock %} +Hello! +{% blocktrans %}You are receiving this email because you requested to reset your password for your account. +
+
+Please enter the following code on the app: {{ code }}{% endblocktrans %} + +
+
+{% trans " Your username (in case you forgot) is: " %} {{ user.get_username }} +{% blocktrans %} {% block footer %} diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py index fb054748a..e88db2188 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py @@ -9,7 +9,7 @@ from .models import User from .serializers import UserLoginSerializer -from .views import PreviewTemplateView, request_reset_link +from .views import PreviewTemplateView, request_reset_code @pytest.mark.django_db @@ -119,14 +119,13 @@ def test_password_reset(caplog, api_client, sample_user): # fake our API call to the view that generates an email for the user to reset their password rf = RequestFactory() post_request = rf.post("api/password/reset/", {"email": sample_user.email}) - request_reset_link(post_request) + request_reset_code(post_request) - # Grab from the logs the actual URL link we would send to the user - password_reset_creds = caplog.text.split("password/reset/confirm/")[1].split('"')[0] - password_reset_url = f"/api/password/reset/confirm/{password_reset_creds}/" + code = caplog.text.split()[-1].replace('"', "") + password_reset_url = f"/api/password/reset/confirm/{sample_user.email}/" # Verify the link works for reseting the password - response = api_client.post(password_reset_url, data={"password": "new_password"}, format="json") + response = api_client.post(password_reset_url, data={"password": "new_password", "code": code}, format="json") assert response.status_code == status.HTTP_200_OK # New Password should now work for authentication diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/urls.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/urls.py index 34fa802b7..46adf4c04 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/urls.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/urls.py @@ -15,8 +15,13 @@ path("api/", include(router.urls)), path("api/login/", core_views.UserLoginView.as_view()), path(r"api/logout/", rest_auth_views.LogoutView.as_view()), - path(r"api/password/reset/confirm///", core_views.reset_password, name="password_reset_confirm"), - path(r"api/password/reset/", core_views.request_reset_link), + path(r"api/password/reset/", core_views.request_reset_code), + path( + r"api/password/reset/confirm//", + core_views.reset_password, + # This URL must be named, because django.contrib.auth calls it via a reverse-lookup + name="password_reset_confirm", + ), path(r"api/password/change/", rest_auth_views.PasswordChangeView.as_view()), path(r"api/template_preview/", core_views.PreviewTemplateView.as_view()), ] diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index b6365da38..0b1e65b38 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -19,11 +19,17 @@ from rest_framework.response import Response from {{ cookiecutter.project_slug }}.utils.emails import send_html_email +from {{ cookiecutter.project_slug }}.utils.misc import random_pin_generator from .forms import PreviewTemplateForm -from .models import User from .permissions import HasUserPermissions -from .serializers import UserLoginSerializer, UserRegistrationSerializer, UserSerializer +from .models import User, UserResetPasswordCodeMessages +from .serializers import ( + ResetPasswordSerializer, + UserLoginSerializer, + UserRegistrationSerializer, + UserSerializer +) logger = logging.getLogger(__name__) @@ -92,22 +98,13 @@ def destroy(self, request, *args, **kwargs): @api_view(["post"]) -@permission_classes([]) -@authentication_classes([]) -def request_reset_link(request, *args, **kwargs): +@permission_classes([permissions.AllowAny]) +def request_reset_code(request, *args, **kwargs): email = request.data.get("email") user = User.objects.filter(email=email).first() if not user: - return Response(status=status.HTTP_204_NO_CONTENT) - reset_context = user.reset_password_context() - - send_html_email( - "Password reset for {{ cookiecutter.project_name }}", - "registration/password_reset.html", - settings.DEFAULT_FROM_EMAIL, - [user.email], - context=reset_context, - ) + raise ValidationError(detail={"non_field_errors": ["User not found with that email"]}) + UserResetPasswordCodeMessages.objects.create_code(user=user, code=random_pin_generator(count=7)) return Response(status=status.HTTP_204_NO_CONTENT) @@ -115,20 +112,22 @@ def request_reset_link(request, *args, **kwargs): @api_view(["post"]) @permission_classes([permissions.AllowAny]) def reset_password(request, *args, **kwargs): - user_id = kwargs.get("uid") - token = kwargs.get("token") - user = User.objects.filter(pk=user_id).first() - if not user or not token: - raise ValidationError(detail={"non-field-error": "Invalid or expired token"}) - is_valid = default_token_generator.check_token(user, token) - if not is_valid: - raise ValidationError(detail={"non-field-error": "Invalid or expired token"}) - logger.info(f"Resetting password for user {user_id}") + email = kwargs.get("email") + user = User.objects.filter(email=email).first() + if not user: + raise ValidationError(detail={"non_field_errors": ["User not found with that email"]}) + serializer = ResetPasswordSerializer(data=request.data, context={"user": user}) + serializer.is_valid(raise_exception=True) + + code_from_db = serializer.context.get("code_from_db") + code_from_db.is_used = True + code_from_db.save() + user.set_password(request.data.get("password")) user.save() - # COMMENT THIS WHEN USING THE PASSWORD RESET FLOW ON WEB ONLY FOR MOBILE - PARI BAKER - response_data = UserLoginSerializer.login(user, request) - return Response(response_data, status=status.HTTP_200_OK) + + u = UserLoginSerializer.login(user, request) + return Response(status=status.HTTP_200_OK, data=u) class PreviewTemplateView(views.APIView): diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/misc.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/misc.py index 19ed7f8ad..82cbca033 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/misc.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/misc.py @@ -1,3 +1,5 @@ +import random + from django.utils import timezone @@ -23,3 +25,14 @@ def as_choices(iterable): [A, B ...] is mapped to [(A, A), (B, B) ...] """ return tuple((elem, elem) for elem in iterable) + + +def random_pin_generator(count=4): + """ + Generates a random count digit pin code + """ + num = "" + for _ in range(1, count + 1): + n = random.randint(0, 9) + num += str(n) + return num diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/tests.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/tests.py index 427cd5ad7..761edd0da 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/tests.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/tests.py @@ -1,6 +1,5 @@ import pytest -from {{ cookiecutter.project_slug }}.utils.emails import get_html_body from {{ cookiecutter.project_slug }}.utils.sites import get_site_url @@ -31,10 +30,3 @@ def test_get_site_url_negative(settings, custom_settings): settings.__setattr__(key, custom_settings[key]) with pytest.raises(Exception): get_site_url() - - -@pytest.mark.use_requests -def test_password_reset_email_link(user): - context = user.reset_password_context() - html_body = get_html_body("registration/password_reset.html", context) - assert f"{context['site_url']}/password/reset/confirm/{context['user'].id}/{context['token']}" in html_body From fd073ff70b20c47276eaa775ccd20c6b4fed737a Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Tue, 20 Aug 2024 17:37:59 -0300 Subject: [PATCH 02/26] Add setting --- .../server/{{cookiecutter.project_slug}}/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py index f4b6f713e..ee6db4951 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/settings.py @@ -403,3 +403,6 @@ def filter(self, record): SPECTACULAR_SETTINGS = { "COMPONENT_SPLIT_REQUEST": True, # Needed for file upload to work } + +# Reset password expiration time in minutes +RESET_PASSWORD_CODE_VALIDITY_MINUTES = config("RESET_PASSWORD_CODE_VALIDITY_MINUTES", default=5, cast=int) From bf304352cfdf50fbd173aae02c4aaf1ebcbbd3fd Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 09:29:53 -0300 Subject: [PATCH 03/26] Fix linter --- .../server/{{cookiecutter.project_slug}}/core/signals.py | 2 +- .../server/{{cookiecutter.project_slug}}/core/views.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py index b36162649..85712612a 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py @@ -4,7 +4,6 @@ from django.dispatch import receiver from rest_framework.authtoken.models import Token from django.conf import settings -from django.template.loader import render_to_string from {{ cookiecutter.project_slug }}.core.models import User from {{ cookiecutter.project_slug }}.utils.emails import send_html_email @@ -20,6 +19,7 @@ def create_auth_token_add_permissions(sender, instance, created, **kwargs): if created: Token.objects.create(user=instance) + @receiver(new_reset_password_code_created_ds) def generate_reset_password_code(sender, code=None, instance=None, created=None, **kwargs): if created: diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index 0b1e65b38..f3d4b4f01 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -4,7 +4,6 @@ from django.apps import apps from django.conf import settings from django.contrib.auth import authenticate -from django.contrib.auth.tokens import default_token_generator from django.db import transaction from django.http import Http404 from django.shortcuts import render From e244db82b2e4ce36cd97b411a397a4393570521d Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 09:35:40 -0300 Subject: [PATCH 04/26] Fix email --- .../core/templates/registration/password_reset_code.html | 1 + 1 file changed, 1 insertion(+) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html index a18651535..29278a9b4 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html @@ -1,6 +1,7 @@ {%- raw -%} {% extends '_action-email-base.html' %} {% load tz %} +{% load i18n %} {% comment %} Password Reset From 58f096f72be75671a6f227104e549768f09fbfc4 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 09:43:45 -0300 Subject: [PATCH 05/26] Update email template --- .../core/templates/registration/password_reset_code.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html index 29278a9b4..3600ef6fd 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html @@ -29,15 +29,17 @@ {% endblock main_content %} Hello! -{% blocktrans %}You are receiving this email because you requested to reset your password for your account. +{% blocktrans %}You are receiving this email because you requested to reset your password for your account.{% endblocktrans %}

-Please enter the following code on the app: {{ code }}{% endblocktrans %} +{% blocktrans %}Please enter the following code on the app: {{ code }}{% endblocktrans %}

{% trans " Your username (in case you forgot) is: " %} {{ user.get_username }} -{% blocktrans %} + +
+
{% block footer %} From d399601f040f3670b75e8fef707e93fadac6050d Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 09:59:37 -0300 Subject: [PATCH 06/26] Update email util --- .../server/{{cookiecutter.project_slug}}/utils/emails.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py index a2e3c8767..46514b431 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py @@ -19,6 +19,9 @@ def log_email_details(html_text): if "href" in line: url_path = line.split('href="')[1].split('"')[0] logger.info(f"Sending email with containing URL: '{url_path}'") + if "code" in line: + code = line.split("")[1].split("")[0] + logger.info(f"Sending email with containing code: '{code}'") def get_html_body(template, context): From 2671097bc1511c9340024c332193def688449c54 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 10:07:59 -0300 Subject: [PATCH 07/26] Test --- .../server/{{cookiecutter.project_slug}}/core/tests.py | 1 - .../server/{{cookiecutter.project_slug}}/utils/emails.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py index e88db2188..13d2b24c0 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/tests.py @@ -123,7 +123,6 @@ def test_password_reset(caplog, api_client, sample_user): code = caplog.text.split()[-1].replace('"', "") password_reset_url = f"/api/password/reset/confirm/{sample_user.email}/" - # Verify the link works for reseting the password response = api_client.post(password_reset_url, data={"password": "new_password", "code": code}, format="json") assert response.status_code == status.HTTP_200_OK diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py index 46514b431..9d2d35eb9 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py @@ -16,9 +16,9 @@ def log_email_details(html_text): # Only true when running tests or in lower environments that don't email # Log URLs more clearing to help with debugging and automated tests for line in html_text.split("\n"): - if "href" in line: - url_path = line.split('href="')[1].split('"')[0] - logger.info(f"Sending email with containing URL: '{url_path}'") + # if "href" in line: + # url_path = line.split('href="')[1].split('"')[0] + # logger.info(f"Sending email with containing URL: '{url_path}'") if "code" in line: code = line.split("")[1].split("")[0] logger.info(f"Sending email with containing code: '{code}'") From 77c5019b399c7886a2fa52fe839b278d904a1ec5 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 10:22:40 -0300 Subject: [PATCH 08/26] Log lines --- .../server/{{cookiecutter.project_slug}}/utils/emails.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py index 9d2d35eb9..431144884 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py @@ -19,6 +19,8 @@ def log_email_details(html_text): # if "href" in line: # url_path = line.split('href="')[1].split('"')[0] # logger.info(f"Sending email with containing URL: '{url_path}'") + print("LINEEEEEE") + print(line) if "code" in line: code = line.split("")[1].split("")[0] logger.info(f"Sending email with containing code: '{code}'") From 6c34a053e7669061d3ccb2b5dd9168f5fb32efb5 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 10:27:32 -0300 Subject: [PATCH 09/26] Fix mail main content --- .../core/templates/registration/password_reset_code.html | 2 +- .../server/{{cookiecutter.project_slug}}/utils/emails.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html index 3600ef6fd..c83c7ecf6 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/templates/registration/password_reset_code.html @@ -27,7 +27,6 @@ It happens to the best of us. The good news is you can change it right now. -{% endblock main_content %} Hello! {% blocktrans %}You are receiving this email because you requested to reset your password for your account.{% endblocktrans %}
@@ -40,6 +39,7 @@

+{% endblock main_content %} {% block footer %} diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py index 431144884..9d2d35eb9 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/utils/emails.py @@ -19,8 +19,6 @@ def log_email_details(html_text): # if "href" in line: # url_path = line.split('href="')[1].split('"')[0] # logger.info(f"Sending email with containing URL: '{url_path}'") - print("LINEEEEEE") - print(line) if "code" in line: code = line.split("")[1].split("")[0] logger.info(f"Sending email with containing code: '{code}'") From b1fa02b1cfdb2b990b916f2cc760a96b03d28dcb Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 10:32:27 -0300 Subject: [PATCH 10/26] Fix view linters --- .../server/{{cookiecutter.project_slug}}/core/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index f3d4b4f01..61c9b5c6d 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -11,7 +11,6 @@ from rest_framework import generics, mixins, permissions, status, views, viewsets from rest_framework.decorators import ( api_view, - authentication_classes, permission_classes, ) from rest_framework.exceptions import ValidationError @@ -27,7 +26,7 @@ ResetPasswordSerializer, UserLoginSerializer, UserRegistrationSerializer, - UserSerializer + UserSerializer, ) logger = logging.getLogger(__name__) From 14f0bb80dd7d04dff65e165bcd813b3494e6c711 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Wed, 21 Aug 2024 10:56:41 -0300 Subject: [PATCH 11/26] Fix isort --- .../server/{{cookiecutter.project_slug}}/core/signals.py | 2 +- .../server/{{cookiecutter.project_slug}}/core/views.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py index 85712612a..e67f4dfd5 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py @@ -1,9 +1,9 @@ import logging +from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token -from django.conf import settings from {{ cookiecutter.project_slug }}.core.models import User from {{ cookiecutter.project_slug }}.utils.emails import send_html_email diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index 61c9b5c6d..a77610fae 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -9,10 +9,7 @@ from django.shortcuts import render from django.template import TemplateDoesNotExist from rest_framework import generics, mixins, permissions, status, views, viewsets -from rest_framework.decorators import ( - api_view, - permission_classes, -) +from rest_framework.decorators import api_view, permission_classes from rest_framework.exceptions import ValidationError from rest_framework.response import Response From 56a50c08a9dcf005ac6fe4f30525bab1ce90dd09 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 30 Aug 2024 09:10:57 -0300 Subject: [PATCH 12/26] Add throttles --- .../{{cookiecutter.project_slug}}/core/throttles.py | 11 +++++++++++ .../{{cookiecutter.project_slug}}/core/views.py | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 {{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/throttles.py diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/throttles.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/throttles.py new file mode 100644 index 000000000..0f9718019 --- /dev/null +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/throttles.py @@ -0,0 +1,11 @@ +from rest_framework.throttling import AnonRateThrottle + + +class ResetPasswordRequestLimit(AnonRateThrottle): + rate = "5/hour" + scope = "reset_password_request_code" + + +class ResetPasswordConfirmLimit(AnonRateThrottle): + rate = "5/hour" + scope = "reset_password_confirm" diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index a77610fae..5efc39d07 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -9,7 +9,7 @@ from django.shortcuts import render from django.template import TemplateDoesNotExist from rest_framework import generics, mixins, permissions, status, views, viewsets -from rest_framework.decorators import api_view, permission_classes +from rest_framework.decorators import api_view, permission_classes, throttle_classes from rest_framework.exceptions import ValidationError from rest_framework.response import Response @@ -25,6 +25,7 @@ UserRegistrationSerializer, UserSerializer, ) +from .throttles import ResetPasswordConfirmLimit, ResetPasswordRequestLimit logger = logging.getLogger(__name__) @@ -94,6 +95,7 @@ def destroy(self, request, *args, **kwargs): @api_view(["post"]) @permission_classes([permissions.AllowAny]) +@throttle_classes([ResetPasswordRequestLimit]) def request_reset_code(request, *args, **kwargs): email = request.data.get("email") user = User.objects.filter(email=email).first() @@ -106,6 +108,7 @@ def request_reset_code(request, *args, **kwargs): @api_view(["post"]) @permission_classes([permissions.AllowAny]) +@throttle_classes([ResetPasswordConfirmLimit]) def reset_password(request, *args, **kwargs): email = kwargs.get("email") user = User.objects.filter(email=email).first() From 18116c7691d66a18429e8c1d36118ec0c53dfe25 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 30 Aug 2024 16:44:21 -0300 Subject: [PATCH 13/26] Update reset password fe in React --- .../src/pages/request-password-reset.tsx | 81 +++++++------------ .../web/react/src/pages/reset-password.tsx | 23 ++++-- .../web/react/src/services/user/api.ts | 6 +- .../web/react/src/services/user/forms.ts | 16 ++-- .../clients/web/react/src/utils/routes.tsx | 2 +- 5 files changed, 54 insertions(+), 74 deletions(-) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx index 31ef49e1c..281219095 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/request-password-reset.tsx @@ -16,7 +16,6 @@ import { getErrorMessages } from 'src/utils/errors' export const RequestPasswordResetInner = () => { const [errorMessage, setErrorMessage] = useState() - const [resetLinkSent, setResetLinkSent] = useState(false) const { createFormFieldChangeHandler, form } = useTnForm() const navigate = useNavigate() @@ -24,7 +23,7 @@ export const RequestPasswordResetInner = () => { mutationFn: userApi.csc.requestPasswordReset, onSuccess: (data) => { setErrorMessage(undefined) - setResetLinkSent(true) + navigate('/password/reset/confirm/' + form.email.value) }, onError(e: any) { const errors = getErrorMessages(e) @@ -42,58 +41,32 @@ export const RequestPasswordResetInner = () => { return (
- {resetLinkSent ? ( - <> -

- Your request has been submitted. If there is an account associated with the email - provided, you should receive an email momentarily with instructions to reset your - password. -

-

- If you do not see the email in your main folder soon, please make sure to check your - spam folder. -

-
- -
- - ) : ( - <> -
{ - e.preventDefault() - }} - className="flex flex-col gap-2" - > - createFormFieldChangeHandler(form.email)(e.target.value)} - value={form.email.value ?? ''} - data-cy="email" - id="id" - label="Email address" - /> - -
- {errorMessage} -
- - - - )} +
{ + e.preventDefault() + }} + className="flex flex-col gap-2" + > + createFormFieldChangeHandler(form.email)(e.target.value)} + value={form.email.value ?? ''} + data-cy="email" + id="id" + label="Email address" + /> + +
+ {errorMessage} +
+ +
) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx index 1f27a30d3..d59ca36ad 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx @@ -12,8 +12,7 @@ import { ResetPasswordForm, TResetPasswordForm, userApi } from 'src/services/use export const ResetPasswordInner = () => { const { form, createFormFieldChangeHandler, overrideForm } = useTnForm() - const { userId, token } = useParams() - console.log(userId, token) + const { userEmail } = useParams() const [error, setError] = useState('') const [success, setSuccess] = useState(false) @@ -32,10 +31,10 @@ export const ResetPasswordInner = () => { }) useEffect(() => { - if (token && userId) { - overrideForm(ResetPasswordForm.create({ token: token, uid: userId }) as TResetPasswordForm) + if (userEmail) { + overrideForm(ResetPasswordForm.create({ email: userEmail }) as TResetPasswordForm) } - }, [overrideForm, token, userId]) + }, [overrideForm, userEmail]) const onSubmit = (e: FormEvent) => { e.preventDefault() @@ -43,8 +42,8 @@ export const ResetPasswordInner = () => { if (form.isValid) { confirmResetPassword({ - userId: form.value.uid!, - token: form.value.token!, + email: form.value.email!, + code: form.value.code!, password: form.value.password!, }) } @@ -63,10 +62,18 @@ export const ResetPasswordInner = () => { ) } return ( - +
+ createFormFieldChangeHandler(form.code)(e.target.value)} + value={form.code.value ?? ''} + data-cy="code" + id="code" + /> + createFormFieldChangeHandler(form.password)(e.target.value)} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/api.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/api.ts index 7a5d5bacc..c3224e4f3 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/api.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/api.ts @@ -39,11 +39,11 @@ const requestPasswordReset = createCustomServiceCall({ }) const resetPassword = createCustomServiceCall({ - inputShape: { userId: z.string(), token: z.string(), password: z.string() }, + inputShape: { email: z.string(), code: z.string(), password: z.string() }, outputShape: userShape, cb: async ({ client, input, utils }) => { - const { token, user_id, ...rest } = utils.toApi(input) - const res = await client.post(`/password/reset/confirm/${user_id}/${token}/`, rest) + const { email, ...rest } = utils.toApi(input) + const res = await client.post(`/password/reset/confirm/${email}/`, rest) return utils.fromApi(res.data) }, }) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts index eeddae331..3bca38c36 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/services/user/forms.ts @@ -89,24 +89,24 @@ export class EmailForgotPasswordForm extends Form { export type TEmailForgotPasswordForm = EmailForgotPasswordForm & EmailForgotPasswordInput export type ResetPasswordInput = { - uid: IFormField - token: IFormField + email: IFormField + code: IFormField password: IFormField confirmPassword: IFormField } export class ResetPasswordForm extends Form { - static uid = new FormField({ - label: 'UID', - placeholder: 'uid', + static email = new FormField({ + label: 'Email', + placeholder: 'email', type: 'text', validators: [new RequiredValidator({ message: 'Please enter a valid uid' })], }) - static token = new FormField({ - placeholder: 'Verification Token', + static code = new FormField({ + placeholder: 'Verification Code', type: 'text', validators: [ - new MinLengthValidator({ message: 'Please enter a valid 5 digit code', minLength: 5 }), + new MinLengthValidator({ message: 'Please enter a valid 7 digit code', minLength: 7 }), ], }) static password = FormField.create({ diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx index 855c23dec..ed903b38e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/utils/routes.tsx @@ -21,7 +21,7 @@ const AuthRoutes = () => { } /> } /> } /> - } /> + } /> ) } From d11d21776925d44e599220d1c63bc9bd22b76fc9 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 30 Aug 2024 17:06:51 -0300 Subject: [PATCH 14/26] Add missing import --- .../clients/web/react/src/pages/reset-password.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx index d59ca36ad..cf3e67dca 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/reset-password.tsx @@ -9,6 +9,7 @@ import { Button } from 'src/components/button' import { ErrorMessage, ErrorsList } from 'src/components/errors' import { PasswordInput } from 'src/components/password-input' import { ResetPasswordForm, TResetPasswordForm, userApi } from 'src/services/user' +import { Input } from 'src/components/input' export const ResetPasswordInner = () => { const { form, createFormFieldChangeHandler, overrideForm } = useTnForm() From b8a43486d08ddde9e16f62a788693070c1f000ff Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Mon, 2 Sep 2024 17:28:51 -0300 Subject: [PATCH 15/26] Add mobile screens --- .../react-native/src/screens/auth/auth.tsx | 2 + .../src/screens/auth/forgot-password.tsx | 64 +++++++++++++++++ .../react-native/src/screens/auth/index.ts | 4 +- .../src/screens/auth/reset-password.tsx | 71 +++++++++++++++++++ .../mobile/react-native/src/screens/routes.ts | 18 ++++- .../react-native/src/services/user/api.ts | 4 +- .../react-native/src/services/user/forms.ts | 2 +- 7 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/auth.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/auth.tsx index 93f92fe7b..c6998706c 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/auth.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/auth.tsx @@ -10,6 +10,7 @@ import { Main } from '@screens/main' import { Login } from '@screens/auth/login' import { SignUp } from '@screens/auth/sign-up' import { Bounceable } from 'rn-bounceable' +import { ForgotPassword } from './forgot-password' const Tab = createMaterialTopTabNavigator() @@ -17,6 +18,7 @@ const tabs = [ { name: 'home', label: 'Home', component: Main }, { name: 'login', label: 'Login', component: Login }, { name: 'signup', label: 'Signup', component: SignUp }, + { name: 'forgot-password', label: 'Forgot Password', component: ForgotPassword }, ] const TopTab = ({ navigation, state }: MaterialTopTabBarProps) => { diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx new file mode 100644 index 000000000..a5640b0f2 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx @@ -0,0 +1,64 @@ +import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' +import { BounceableWind } from '@components/styled' +import { Text } from '@components/text' +import { TextFormField } from '@components/text-form-field' +import { + ForgotPasswordInput, + ForgotPasswordForm, + TForgotPasswordForm, + userApi, +} from '@services/user' +import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' +import { ScrollView, View } from 'react-native' +import { getNavio } from '../routes' + +const ForgotPasswordInner = () => { + const { form, overrideForm } = useTnForm() + const handleSubmit = async () => { + //TODO: + if (!form.isValid) { + const newForm = form.replicate() as TForgotPasswordForm + newForm.validate() + overrideForm(newForm) + } else { + try { + // HACK FOR TN-Forms + await userApi.csc.requestPasswordResetCode(form.value as { email: string }) + getNavio().stacks.push('ResetPasswordStack') + } catch (e) { + console.log(e) + } + } + } + return ( + + + + Reset Password + + + + + + + + Reset Password + + + + + + ) +} + +export const ForgotPassword = () => { + return ( + formClass={ForgotPasswordForm}> + + + ) +} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts index 505af62c4..1ce241f21 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/index.ts @@ -1,2 +1,4 @@ export { Login } from './login' -export { SignUp } from './sign-up' \ No newline at end of file +export { SignUp } from './sign-up' +export { ForgotPassword } from './forgot-password' +export { ResetPassword } from './reset-password' diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx new file mode 100644 index 000000000..062107db2 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx @@ -0,0 +1,71 @@ +import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' +import { BounceableWind } from '@components/styled' +import { Text } from '@components/text' +import { TextFormField } from '@components/text-form-field' +import { ResetPasswordForm, ResetPasswordInput, TResetPasswordForm, userApi } from '@services/user' +import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' +import { ScrollView, View } from 'react-native' +import { getNavio } from '../routes' +import { useAuth } from '@stores/auth' + +const ResetPasswordInner = () => { + const { form, overrideForm } = useTnForm() + const { changeToken, changeUserId } = useAuth.use.actions() + const handleSubmit = async () => { + //TODO: + if (!form.isValid) { + const newForm = form.replicate() as TResetPasswordForm + newForm.validate() + overrideForm(newForm) + } else { + try { + // HACK FOR TN-Forms + const res = await userApi.csc.resetPassword( + form.value as { email: string; code: string; password: string }, + ) + if (!res?.token) { + throw 'Missing token from response' + } + changeUserId(res.id) + changeToken(res.token) + getNavio().stacks.push('MainStack') + } catch (e) { + console.log(e) + } + } + } + return ( + + + + Reset Password + + + + + + + + + + + Reset Password + + + + + + ) +} + +export const ResetPassword = () => { + return ( + formClass={ResetPasswordForm}> + + + ) +} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts index bd6a8eedc..4d60b7e2b 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/routes.ts @@ -2,7 +2,7 @@ import { Platform } from 'react-native' import { Navio } from 'rn-navio' import { NativeStackNavigationOptions } from '@react-navigation/native-stack' import { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs' -import { Login, SignUp } from '@screens/auth' +import { ForgotPassword, Login, ResetPassword, SignUp } from '@screens/auth' import { Main } from '@screens/main' import { Auth } from '@screens/auth/auth' import { DashboardScreen } from '@screens/dashboard' @@ -30,11 +30,25 @@ export const tabDefaultOptions = (): BottomTabNavigationOptions => ({ }) // NAVIO export const navio = Navio.build({ - screens: { Auth, Login, SignUp, Main, DashboardScreen, ComponentsPreview, Settings, ContactUs, EditProfile }, + screens: { + Auth, + Login, + SignUp, + Main, + DashboardScreen, + ComponentsPreview, + Settings, + ContactUs, + EditProfile, + ForgotPassword, + ResetPassword, + }, stacks: { AuthStack: ['Auth'], MainStack: ['DashboardScreen'], SettingsStack: ['Settings', 'ContactUs', 'EditProfile'], + ForgotPasswordStack: ['ForgotPassword'], + ResetPasswordStack: ['ResetPassword'], /** * Set me as the root to see the components preview */ diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts index 5481e9267..18d788f76 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts @@ -15,7 +15,7 @@ const login = createCustomServiceCall({ const requestPasswordResetCode = createCustomServiceCall({ inputShape: forgotPasswordShape, cb: async ({ client, input }) => { - await client.get(`/password/reset/code/${input.email}/`) + await client.post(`/password/reset/`, utils.toApi(input)) }, }) @@ -25,7 +25,7 @@ const resetPassword = createCustomServiceCall({ cb: async ({ client, input, utils }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { email, ...rest } = utils.toApi(input) - const res = await client.post(`/password/reset/code/confirm/${input.email}/`, rest) + const res = await client.post(`/password/reset/confirm/${input.email}/`, rest) return utils.fromApi(res.data) }, }) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/forms.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/forms.ts index 69f04a217..7d4992d5b 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/forms.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/forms.ts @@ -106,7 +106,7 @@ import Form, { placeholder: 'Verification Code', type: 'number', validators: [ - new MinLengthValidator({ message: 'Please enter a valid 5 digit code', minLength: 5 }), + new MinLengthValidator({ message: 'Please enter a valid 7 digit code', minLength: 7 }), ], }) static password = FormField.create({ From 1ba0d4e42384f4a7e0109f2ccc5f96d90f9f2d2e Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Mon, 2 Sep 2024 17:35:14 -0300 Subject: [PATCH 16/26] Add missings --- .../mobile/react-native/src/components/text.tsx | 12 ++++++++++++ .../mobile/react-native/src/services/user/api.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx new file mode 100644 index 000000000..33bb070c3 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react' +import { Text as TextRN, TextProps } from 'react-native' +import { fontFamilyWeightMap, FontWeightStyle } from '@utils/fonts' + +export const Text: FC< + Omit & { variant?: FontWeightStyle; textClassName?: string } +> = ({ variant = 'regular', textClassName = '', ...props }) => { + const style = { + fontFamily: fontFamilyWeightMap[variant], + } + return +} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts index 18d788f76..e5c323c58 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/services/user/api.ts @@ -14,7 +14,7 @@ const login = createCustomServiceCall({ const requestPasswordResetCode = createCustomServiceCall({ inputShape: forgotPasswordShape, - cb: async ({ client, input }) => { + cb: async ({ client, input, utils }) => { await client.post(`/password/reset/`, utils.toApi(input)) }, }) From e57f78446e8e0d62aadd9c5a72e7551b4f4dba85 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Mon, 2 Sep 2024 17:40:01 -0300 Subject: [PATCH 17/26] Add missing fonts --- .../mobile/react-native/src/utils/fonts.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts index 784881052..d0b80844a 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts @@ -14,3 +14,30 @@ export const customFonts = { [`${baseFamily}-MediumItalic` as const]: require(`../../assets/fonts/${baseFamily}-MediumItalic.${fontFormat}`), [`${baseFamily}-Regular` as const]: require(`../../assets/fonts/${baseFamily}-Regular.${fontFormat}`), } + +type FontFamily = keyof typeof customFonts + +export type FontWeightStyle = + | 'light' + | 'italic-light' + | 'regular' + | 'italic' + | 'medium' + | 'italic-medium' + | 'black' + | 'italic-black' + | 'bold' + | 'italic-bold' + +export const fontFamilyWeightMap: Record = { + light: `${baseFamily}-Light`, + 'italic-light': `${baseFamily}-LightItalic`, + regular: `${baseFamily}-Regular`, + italic: `${baseFamily}-Italic`, + medium: `${baseFamily}-Medium`, + 'italic-medium': `${baseFamily}-MediumItalic`, + black: `${baseFamily}-Black`, + 'italic-black': `${baseFamily}-BlackItalic`, + bold: `${baseFamily}-Bold`, + 'italic-bold': `${baseFamily}-BoldItalic`, +} From a63e1e18c5db2bc1763662975eff504093ff57bf Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 29 Nov 2024 17:54:52 -0300 Subject: [PATCH 18/26] Vue - Add reset password with code --- .../clients/web/vue3/src/composables/Users.ts | 14 +++++++------ .../clients/web/vue3/src/router/index.js | 2 +- .../web/vue3/src/services/users/api.ts | 2 +- .../web/vue3/src/services/users/forms.ts | 20 +++++++++---------- .../web/vue3/src/services/users/models.ts | 4 ++-- .../web/vue3/src/views/ResetPassword.vue | 17 ++++++++++++---- 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts index c2d1310a3..20f92d231 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/composables/Users.ts @@ -26,9 +26,9 @@ export function useUsers() { const loading = ref(false) const { errorAlert, successAlert } = useAlert() - const getCodeUidFromRoute = () => { - const { uid, token } = router.currentRoute.value.params - return { uid, token } + const getEmailFromRoute = () => { + const { email } = router.currentRoute.value.params + return { email } } const { data: user, mutate: login } = useMutation({ @@ -58,15 +58,17 @@ export function useUsers() { const { mutate: requestPasswordReset } = useMutation({ mutationFn: async (email: string) => { await userApi.csc.requestPasswordReset({ email }) + return email }, onError: (error: Error) => { loading.value = false console.log(error) }, - onSuccess: () => { + onSuccess: (email: string) => { loading.value = false - successAlert('Password reset link sent to your email') + successAlert('Password reset code to your email') qc.invalidateQueries({ queryKey: ['user'] }) + router.push({ name: 'ResetPassword', params: { email } }) }, }) @@ -115,6 +117,6 @@ export function useUsers() { user, register, registerForm, - getCodeUidFromRoute, + getEmailFromRoute, } } diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/router/index.js b/{{cookiecutter.project_slug}}/clients/web/vue3/src/router/index.js index 5cfd08232..2728f9b9c 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/router/index.js +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/router/index.js @@ -31,7 +31,7 @@ const routes = [ beforeEnter: requireNoAuth, }, { - path: '/password/reset/confirm/:uid/:token', + path: '/password/reset/confirm/:email', name: 'ResetPassword', component: () => import(/* webpackChunkName: "confirmreset" */ '../views/ResetPassword.vue'), beforeEnter: requireNoAuth, diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts index 572005dca..e0506dad0 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/api.ts @@ -29,7 +29,7 @@ const resetPassword = createCustomServiceCall({ outputShape: userShape, cb: async ({ client, input, utils }) => { const { password } = utils.toApi(input) - const res = await client.post(`/password/reset/confirm/${input.uid}/${input.token}/`, { + const res = await client.post(`/password/reset/confirm/${input.email}/`, { password, }) return utils.fromApi(res.data) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/forms.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/forms.ts index bd4504703..54b82d1ae 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/forms.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/forms.ts @@ -90,24 +90,24 @@ export class EmailForgotPasswordForm extends Form { export type TEmailForgotPasswordForm = EmailForgotPasswordForm & EmailForgotPasswordInput export type ResetPasswordInput = { - uid: IFormField - token: IFormField + email: IFormField + code: IFormField password: IFormField confirmPassword: IFormField } export class ResetPasswordForm extends Form { - static uid = new FormField({ - label: 'UID', - placeholder: 'uid', - type: 'text', - validators: [new RequiredValidator({ message: 'Please enter a valid uid' })], + static email = FormField.create({ + label: 'Email', + placeholder: 'Email', + type: 'email', + validators: [new EmailValidator({ message: 'Please enter a valid email' })], }) - static token = new FormField({ - placeholder: 'Verification Token', + static code = new FormField({ + placeholder: 'Verification Code', type: 'text', validators: [ - new MinLengthValidator({ message: 'Please enter a valid 5 digit code', minLength: 5 }), + new MinLengthValidator({ message: 'Please enter a valid code', minLength: 5 }), ], }) static password = FormField.create({ diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts index ebc1bfa82..957472997 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/services/users/models.ts @@ -41,8 +41,8 @@ export const loginShape = { export type LoginShape = GetInferredFromRaw export const resetPasswordShape = { - uid: z.string().uuid(), - token: z.string(), + email: z.string().email(), + code: z.string(), password: z.string(), } diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/ResetPassword.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/ResetPassword.vue index 811fda344..60b0bb6d3 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/ResetPassword.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/ResetPassword.vue @@ -8,6 +8,16 @@
+ + Date: Fri, 29 Nov 2024 18:12:17 -0300 Subject: [PATCH 19/26] Fix linter --- .../server/{{cookiecutter.project_slug}}/core/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index 5efc39d07..135caddb2 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -17,8 +17,8 @@ from {{ cookiecutter.project_slug }}.utils.misc import random_pin_generator from .forms import PreviewTemplateForm -from .permissions import HasUserPermissions from .models import User, UserResetPasswordCodeMessages +from .permissions import HasUserPermissions from .serializers import ( ResetPasswordSerializer, UserLoginSerializer, From 6213e439fad930ee4cb513d86c512315f7831cbc Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 12:26:12 -0300 Subject: [PATCH 20/26] Remove text file --- .../mobile/react-native/src/components/text.tsx | 12 ------------ .../src/screens/auth/forgot-password.tsx | 7 +++---- .../react-native/src/screens/auth/reset-password.tsx | 7 +++---- 3 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 {{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx deleted file mode 100644 index 33bb070c3..000000000 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/components/text.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { FC } from 'react' -import { Text as TextRN, TextProps } from 'react-native' -import { fontFamilyWeightMap, FontWeightStyle } from '@utils/fonts' - -export const Text: FC< - Omit & { variant?: FontWeightStyle; textClassName?: string } -> = ({ variant = 'regular', textClassName = '', ...props }) => { - const style = { - fontFamily: fontFamilyWeightMap[variant], - } - return -} diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx index a5640b0f2..d4b396bfd 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/forgot-password.tsx @@ -1,6 +1,5 @@ import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' import { BounceableWind } from '@components/styled' -import { Text } from '@components/text' import { TextFormField } from '@components/text-form-field' import { ForgotPasswordInput, @@ -9,7 +8,7 @@ import { userApi, } from '@services/user' import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' -import { ScrollView, View } from 'react-native' +import { ScrollView, Text, View } from 'react-native' import { getNavio } from '../routes' const ForgotPasswordInner = () => { @@ -33,7 +32,7 @@ const ForgotPasswordInner = () => { return ( - + Reset Password @@ -45,7 +44,7 @@ const ForgotPasswordInner = () => { disabled={!form.isValid} > - + Reset Password diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx index 062107db2..3dce0cb61 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/screens/auth/reset-password.tsx @@ -1,10 +1,9 @@ import { MultiPlatformSafeAreaView } from '@components/multi-platform-safe-area-view' import { BounceableWind } from '@components/styled' -import { Text } from '@components/text' import { TextFormField } from '@components/text-form-field' import { ResetPasswordForm, ResetPasswordInput, TResetPasswordForm, userApi } from '@services/user' import { FormProvider, useTnForm } from '@thinknimble/tn-forms-react' -import { ScrollView, View } from 'react-native' +import { ScrollView, Text, View } from 'react-native' import { getNavio } from '../routes' import { useAuth } from '@stores/auth' @@ -37,7 +36,7 @@ const ResetPasswordInner = () => { return ( - + Reset Password @@ -52,7 +51,7 @@ const ResetPasswordInner = () => { disabled={!form.isValid} > - + Reset Password From 5e092316db8337148b7ab860d69c2f1d3f6d5560 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 15:17:46 -0300 Subject: [PATCH 21/26] Remove old fonts --- .../mobile/react-native/src/utils/fonts.ts | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts index d0b80844a..a78ee7cfe 100644 --- a/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts +++ b/{{cookiecutter.project_slug}}/clients/mobile/react-native/src/utils/fonts.ts @@ -13,31 +13,4 @@ export const customFonts = { [`${baseFamily}-Medium` as const]: require(`../../assets/fonts/${baseFamily}-Medium.${fontFormat}`), [`${baseFamily}-MediumItalic` as const]: require(`../../assets/fonts/${baseFamily}-MediumItalic.${fontFormat}`), [`${baseFamily}-Regular` as const]: require(`../../assets/fonts/${baseFamily}-Regular.${fontFormat}`), -} - -type FontFamily = keyof typeof customFonts - -export type FontWeightStyle = - | 'light' - | 'italic-light' - | 'regular' - | 'italic' - | 'medium' - | 'italic-medium' - | 'black' - | 'italic-black' - | 'bold' - | 'italic-bold' - -export const fontFamilyWeightMap: Record = { - light: `${baseFamily}-Light`, - 'italic-light': `${baseFamily}-LightItalic`, - regular: `${baseFamily}-Regular`, - italic: `${baseFamily}-Italic`, - medium: `${baseFamily}-Medium`, - 'italic-medium': `${baseFamily}-MediumItalic`, - black: `${baseFamily}-Black`, - 'italic-black': `${baseFamily}-BlackItalic`, - bold: `${baseFamily}-Bold`, - 'italic-bold': `${baseFamily}-BoldItalic`, -} +} \ No newline at end of file From 4013418f66d7f98e5c99387e79285e1c10b0f295 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 17:27:41 -0300 Subject: [PATCH 22/26] Remove is_used flag --- .../migrations/0002_userresetpasswordcodemessages.py | 1 - .../{{cookiecutter.project_slug}}/core/models.py | 11 +---------- .../{{cookiecutter.project_slug}}/core/views.py | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py index 01b27b27e..a90f3d83d 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/migrations/0002_userresetpasswordcodemessages.py @@ -20,7 +20,6 @@ class Migration(migrations.Migration): ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ("created", models.DateTimeField(auto_now_add=True)), ("last_edited", models.DateTimeField(auto_now=True)), - ("code", models.CharField(max_length=255)), ("is_used", models.BooleanField(default=False)), ( "user", diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py index e78584512..ff10718ba 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py @@ -83,14 +83,6 @@ def full_name(self): def __str__(self): return f"{self.full_name} <{self.email}>" - def reset_password_context(self): - return { - "user": self, - "site_url": get_site_url(), - "support_email": settings.STAFF_EMAIL, - "token": default_token_generator.make_token(self), - } - class Meta: ordering = ["email"] @@ -126,7 +118,6 @@ def create_code(self, user, code, **kwargs): class UserResetPasswordCodeMessages(AbstractBaseModel): user = models.ForeignKey("core.User", related_name="reset_password_codes", on_delete=models.CASCADE) code = models.CharField(max_length=255) - is_used = models.BooleanField(default=False) objects = UserResetPasswordCodeMessagesManager() def __str__(self): @@ -134,7 +125,7 @@ def __str__(self): @property def is_valid(self): - return not (self.is_used | (self.created > now() + timedelta(minutes=settings.RESET_PASSWORD_CODE_VALIDITY_MINUTES))) + return (self.created > now() + timedelta(minutes=settings.RESET_PASSWORD_CODE_VALIDITY_MINUTES)) class Meta: ordering = ("-created",) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index 135caddb2..13faf775b 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -118,8 +118,7 @@ def reset_password(request, *args, **kwargs): serializer.is_valid(raise_exception=True) code_from_db = serializer.context.get("code_from_db") - code_from_db.is_used = True - code_from_db.save() + code_from_db.delete() user.set_password(request.data.get("password")) user.save() From 5e961f04faa9d7b0924942ff1b48360813235d84 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 17:28:29 -0300 Subject: [PATCH 23/26] Remove comment --- .../server/{{cookiecutter.project_slug}}/core/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py index e67f4dfd5..0a88cd593 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/signals.py @@ -10,7 +10,6 @@ from .dispatchers import new_reset_password_code_created_ds -# Logger logger = logging.getLogger(__name__) From 2bc6c497c788e0f7eac6bf7ef81d2ffc3c92ea4e Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 17:46:33 -0300 Subject: [PATCH 24/26] Fix code validation --- .../server/{{cookiecutter.project_slug}}/core/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py index ff10718ba..3760b2d08 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py @@ -125,7 +125,8 @@ def __str__(self): @property def is_valid(self): - return (self.created > now() + timedelta(minutes=settings.RESET_PASSWORD_CODE_VALIDITY_MINUTES)) + valid_until = self.created + timedelta(minutes=settings.RESET_PASSWORD_CODE_VALIDITY_MINUTES) + return now() <= valid_until class Meta: ordering = ("-created",) From 09d5775287493c0229e7745e1aa8cb8ad339bc5c Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 17:51:24 -0300 Subject: [PATCH 25/26] Remove unused code --- .../server/{{cookiecutter.project_slug}}/core/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py index 3760b2d08..64f9375bf 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/models.py @@ -3,12 +3,10 @@ from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, BaseUserManager -from django.contrib.auth.tokens import default_token_generator from django.db import models from django.utils.timezone import now, timedelta from {{ cookiecutter.project_slug }}.common.models import AbstractBaseModel -from {{ cookiecutter.project_slug }}.utils.sites import get_site_url from .dispatchers import new_reset_password_code_created_ds From 0fc4a7117fd10d4ee43d706f200809a98c3947c6 Mon Sep 17 00:00:00 2001 From: Elias Biagioni Date: Fri, 6 Dec 2024 17:59:58 -0300 Subject: [PATCH 26/26] Update error messages --- .../server/{{cookiecutter.project_slug}}/core/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py index 13faf775b..660266804 100755 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/views.py @@ -100,7 +100,7 @@ def request_reset_code(request, *args, **kwargs): email = request.data.get("email") user = User.objects.filter(email=email).first() if not user: - raise ValidationError(detail={"non_field_errors": ["User not found with that email"]}) + raise ValidationError(detail={"non_field_errors": ["There was an error resetting your password. Try again later"]}) UserResetPasswordCodeMessages.objects.create_code(user=user, code=random_pin_generator(count=7)) return Response(status=status.HTTP_204_NO_CONTENT) @@ -113,7 +113,7 @@ def reset_password(request, *args, **kwargs): email = kwargs.get("email") user = User.objects.filter(email=email).first() if not user: - raise ValidationError(detail={"non_field_errors": ["User not found with that email"]}) + raise ValidationError(detail={"non_field_errors": ["There was an error resetting your password. Try again later"]}) serializer = ResetPasswordSerializer(data=request.data, context={"user": user}) serializer.is_valid(raise_exception=True)