feat: reset password working

This commit is contained in:
2025-01-16 23:32:03 +01:00
parent 6c60df2da0
commit 80bc3797ff
20 changed files with 932 additions and 416 deletions
+5
View File
@@ -191,6 +191,11 @@ SPECTACULAR_SETTINGS = {
"SERVE_INCLUDE_SCHEMA": False,
}
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="memory://localhost:8000//")
CELERY_TIME_ZONE = TIME_ZONE
CELERY_ALWAYS_EAGER = DEBUG
+8
View File
@@ -57,3 +57,11 @@ REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "")
ITEMS_PER_PAGE = env.int("ITEMS_PER_PAGE", 20)
SHOP_COUNTRY = env.str("SHOP_COUNTRY", "ESPAÑA")
NPM_BIN_PATH = which("npm")
EMAIL_BACKEND = env.str(
"EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend"
)
EMAIL_HOST = env.str("EMAIL_HOST", "")
EMAIL_HOST_USER = env.str("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD", "")
EMAIL_PORT = env.int("EMAIL_PORT", 587)
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,8 @@
# Generated by Django 5.1.4 on 2025-01-16 17:00
import django.db.models.deletion
from decimal import Decimal
import django.db.models.deletion
from django.db import migrations, models
+1 -1
View File
@@ -14,6 +14,7 @@ from django.utils.text import gettext_lazy as _
from shop.exceptions import RedsysPaymentException, RedsysValidationException
from shop.models import (
Cart,
CartItem,
Order,
OrderLine,
Payment,
@@ -21,7 +22,6 @@ from shop.models import (
ProductBatch,
ProductPrice,
ShippingMethod,
CartItem,
)
from shop.settings import ERROR_CODES
from shop.signals import clear_cart
+1 -1
View File
@@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from shop.models import Order
from shop.utils import validate_payment_for_order, add_payment_to_order
from shop.utils import add_payment_to_order, validate_payment_for_order
@csrf_exempt
+1 -1
View File
@@ -1,5 +1,5 @@
from django import forms
from django.contrib.auth.forms import SetPasswordMixin, BaseUserCreationForm
from django.contrib.auth.forms import BaseUserCreationForm, SetPasswordMixin
from django.contrib.auth.models import User
from django.forms import EmailField
+38
View File
@@ -0,0 +1,38 @@
from django import forms
from django.contrib.auth.forms import PasswordResetForm as BasePasswordResetForm
from django.contrib.auth.forms import SetPasswordForm as BaseSetPasswordForm
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
class PasswordResetForm(BasePasswordResetForm):
pass
def validate_token(token):
is_valid = default_token_generator.check_token(token)
if not is_valid:
raise ValidationError(_("El token no es válido o ha expirado"))
class SetPasswordForm(BaseSetPasswordForm):
token = forms.CharField(
required=True,
widget=forms.HiddenInput(),
validators=[],
)
def validate_token(self):
is_valid = default_token_generator.check_token(self.user, self.data.get('token'))
if not is_valid:
raise ValidationError(_("El token no es válido o ha expirado"))
def is_valid(self):
try:
self.validate_token()
except ValidationError as e:
self.add_error(self.token, str(e))
return False
return super().is_valid()
+14
View File
@@ -0,0 +1,14 @@
{% load i18n %}{% autoescape off %}
{% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %}
{% translate "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'web:reset_password_confirm' uidb64=uid token=token %}
{% endblock %}
{% translate 'Your username, in case youve forgotten:' %} {{ user.get_username }}
{% translate "Thanks for using our site!" %}
{% blocktranslate %}The {{ site_name }} team{% endblocktranslate %}
{% endautoescape %}
+1
View File
@@ -0,0 +1 @@
Hola madafaka
+22 -7
View File
@@ -5,7 +5,8 @@
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="{% url 'web:index' %}" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<a href="{% url 'web:index' %}"
class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="{{ logo }}" alt="logo">
{{ title }}
</a>
@@ -28,23 +29,37 @@
{{ field }}
{% else %}
<div class="my-2 flex items-center">
{{ field }}
<label for="{{ field.id_for_label }}"
class="inline-block text-sm font-medium text-gray-900 dark:text-white mr-2">
class="inline-block text-sm font-medium text-gray-900 dark:text-white ml-2">
{{ field.label }}
</label>
{{ field }}
</div>
{% endif %}
{% endfor %}
<div class="flex justify-center">
<p class="text-sm font-light text-gray-500 my-4 dark:text-gray-400">
{% translate '¿Aún no tienes cuenta?' %}
<a href="{% url 'web:register' %}"
class="font-medium text-primary-600 hover:underline dark:text-primary-500">{% translate 'Regístrate aquí' %}</a>
</p>
</div>
<button
class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
{% translate 'Iniciar sesión' %}
</button>
<p class="text-sm font-light text-gray-500 mt-4 dark:text-gray-400">
{% translate '¿Aún no tienes cuenta?' %}
<a href="{% url 'web:register' %}" class="font-medium text-primary-600 hover:underline dark:text-primary-500">{% translate 'Regístrate aquí' %}</a>
</p>
<div class="flex justify-center">
<p class="text-sm font-light text-gray-500 mt-4 dark:text-gray-400">
<a href="{% url 'web:reset_password' %}"
class="font-medium text-primary-600 hover:underline dark:text-primary-500">{% translate '¿Has olvidado tu contraseña?' %}</a>
</p>
</div>
</form>
</div>
</div>
+37 -33
View File
@@ -1,42 +1,46 @@
{% extends 'base/base.html' %}
{% extends 'theme/base.html' %}
{% load i18n %}
{% block content %}
{% block main %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
<a href="{% url 'web:index' %}" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="{{ logo }}" alt="logo">
{{ title }}
</a>
<div class="w-full p-6 bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md dark:bg-gray-800 dark:border-gray-700 sm:p-8">
<h2 class="mb-1 text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Change Password
</h2>
<form class="mt-4 space-y-4 lg:mt-5 md:space-y-5" action="#">
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
<div
class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
{% translate 'Restablecer contraseña' %}
</h1>
<form action="{% url 'web:reset_password' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div class="my-2 flex items-center">
<label for="{{ field.id_for_label }}"
class="inline-block text-sm font-medium text-gray-900 dark:text-white mr-2">
{{ field.label }}
</label>
{{ field }}
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-blue-500" required="">
</div>
<div>
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
<input type="confirm-password" name="confirm-password" id="confirm-password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-blue-500" required="">
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="newsletter" aria-describedby="newsletter" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
</div>
<div class="ml-3 text-sm">
<label for="newsletter" class="font-light text-gray-500 dark:text-gray-300">I accept the <a class="font-medium text-primary-600 hover:underline dark:text-primary-500" href="#">Terms and Conditions</a></label>
</div>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Reset passwod</button>
{% endfor %}
<button
class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
{% translate 'Restablecer contraseña' %}
</button>
<p class="text-sm font-light text-gray-500 mt-4 dark:text-gray-400">
{% translate '¿Aún no tienes cuenta?' %}
<a href="{% url 'web:register' %}" class="font-medium text-primary-600 hover:underline dark:text-primary-500">{% translate 'Regístrate aquí' %}</a>
</p>
</form>
</div>
</div>
</div>
</section>
</div>
</section>
{% endblock %}
@@ -0,0 +1,50 @@
{% extends 'theme/base.html' %}
{% load i18n %}
{% block main %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="{% url 'web:index' %}" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="{{ logo }}" alt="logo">
{{ title }}
</a>
<div
class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
{% translate 'Restablecer contraseña' %}
</h1>
{% if valid_token %}
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="my-2 flex items-center">
<label for="{{ field.id_for_label }}"
class="inline-block text-sm font-medium text-gray-900 dark:text-white mr-2">
{{ field.label }}
</label>
{{ field }}
</div>
{% endfor %}
<button
class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
{% translate 'Restablecer contraseña' %}
</button>
<p class="text-sm font-light text-gray-500 mt-4 dark:text-gray-400">
{% translate '¿Aún no tienes cuenta?' %}
<a href="{% url 'web:register' %}" class="font-medium text-primary-600 hover:underline dark:text-primary-500">{% translate 'Regístrate aquí' %}</a>
</p>
</form>
{% else %}
<span>El enlace no es válido o ha expirado</span>
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
@@ -0,0 +1,31 @@
{% extends 'theme/base.html' %}
{% load i18n %}
{% block main %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="{% url 'web:index' %}"
class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="{{ logo }}" alt="logo">
{{ title }}
</a>
<div
class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
{% translate 'Se te ha enviado un correo electrónico para restablecer tu contraseña' %}
</h1>
<p class="text-sm font-light text-gray-500 mt-4 dark:text-gray-400">
{% translate '¿Aún no tienes cuenta?' %}
<a href="{% url 'web:register' %}"
class="font-medium text-primary-600 hover:underline dark:text-primary-500">{% translate 'Regístrate aquí' %}</a>
</p>
</div>
</div>
</div>
</section>
{% endblock %}
+3 -2
View File
@@ -1,10 +1,11 @@
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse
from django.conf import settings
from web.models import WebSettings
from django.core.files.uploadedfile import SimpleUploadedFile
class TestManifest(TestCase):
+1 -1
View File
@@ -7,8 +7,8 @@ from django.urls import reverse
from shop.models import (
Cart,
CartItem,
Order,
CustomerAddress,
Order,
ShippingMethod,
ShopSettings,
)
+20 -2
View File
@@ -10,7 +10,15 @@ from web.views.components import (
list_wishlisted_products,
wishlist_button,
)
from web.views.users import login, logout, my_account, register, reset_password
from web.views.users import (
login,
logout,
my_account,
register,
reset_password,
reset_password_confirm,
reset_password_email_sent,
)
from web.views.web import (
add_cart_item,
add_to_wishlist,
@@ -40,7 +48,17 @@ i18n_resolvers = i18n_patterns(
path(_("login/"), login, name="login"),
path(_("logout/"), logout, name="logout"),
path(_("register/"), register, name="register"),
path(_("reset-password/"), reset_password, name="reset-password"),
path(_("reset-password/"), reset_password, name="reset_password"),
path(
_("reset/<uidb64>/<token>/"),
reset_password_confirm,
name="reset_password_confirm",
),
path(
_("reset-password/email-sent/"),
reset_password_email_sent,
name="reset_password_email_sent",
),
path(_("my-account/"), my_account, name="my_account"),
)
+112 -5
View File
@@ -1,16 +1,19 @@
from django.contrib.auth import login as login_user, authenticate
from django.contrib.auth import logout as logout_user
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth import login as login_user
from django.contrib.auth import logout as logout_user
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.shortcuts import redirect, render, reverse
from django.utils.http import url_has_allowed_host_and_scheme, urlsafe_base64_decode
from django.utils.text import gettext_lazy as _
from django.views.generic import TemplateView, View
from shop.models import CustomerAddress
from users.forms import LoginForm, RegisterForm
from users.forms import LoginForm, PasswordResetForm, RegisterForm, SetPasswordForm
from web.models import WebSettings
User = get_user_model()
@@ -123,7 +126,108 @@ class LogoutView(View, RedirectionMixin):
class ResetPasswordView(TemplateView):
template_name = "web/reset-password.html"
template_name = "users/reset_password.html"
form_class = PasswordResetForm
redirect_to = "web:reset_password_email_sent"
email_reset_password_template_name = "mails/reset_password.txt"
html_reset_password_template_name = "mails/reset_password.html"
subject_template_name = "mails/subject_reset_password.txt"
def get_email_options(self):
return {
"extra_email_context": {},
"email_template_name": self.email_reset_password_template_name,
"html_email_template_name": self.html_reset_password_template_name,
"subject_template_name": self.subject_template_name,
}
def get_base_context(self):
settings = WebSettings.load()
return {
"title": settings.web_title,
"description": _("Restablecer contraseña"),
}
def get_context_data(self, **kwargs):
context = self.get_base_context()
context.update({"form": self.form_class()})
return context
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
opts = {
"use_https": request.is_secure(),
"from_email": getattr(settings, "DEFAULT_FROM_EMAIL"),
"request": request,
}
opts.update(self.get_email_options())
form.save(**opts)
return redirect(self.redirect_to)
context = self.get_base_context()
context.update({"form": form})
return render(request, self.template_name, context)
class ResetPasswordEmailSent(TemplateView):
template_name = "users/reset_password_email_sent.html"
class ResetPasswordConfirmView(TemplateView):
template_name = "users/reset_password_confirm.html"
form_class = SetPasswordForm
redirect_to = "web:login"
token_generator = default_token_generator
def get_user(self):
try:
uidb64 = self.kwargs.get("uidb64")
uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid)
except (
TypeError,
ValueError,
OverflowError,
User.DoesNotExist,
ValidationError,
):
user = None
return user
def generate_new_token_for_user(self, user):
return self.token_generator.make_token(user)
def get_context_data(self, **kwargs):
user = self.get_user()
valid_token = self.check_token(user)
new_token = self.generate_new_token_for_user(user)
return {
"title": WebSettings.load().web_title,
"description": _("Establecer nueva contraseña"),
"form": self.form_class(user, {'token': new_token}),
"valid_token": valid_token,
"new_token": new_token,
}
def check_token(self, user):
token = self.kwargs.get("token")
valid_token = self.token_generator.check_token(user, token)
return valid_token
def post(self, request, *args, **kwargs):
user = self.get_user()
form = self.form_class(user, request.POST)
if form.is_valid():
form.save()
return redirect(self.redirect_to)
return render(request, self.template_name, {})
class MyAccountView(TemplateView):
@@ -143,4 +247,7 @@ logout = LogoutView.as_view()
register = RegisterView.as_view()
reset_password = ResetPasswordView.as_view()
reset_password_confirm = ResetPasswordConfirmView.as_view()
reset_password_email_sent = ResetPasswordEmailSent.as_view()
my_account = MyAccountView.as_view()
+1 -1
View File
@@ -19,7 +19,7 @@ from shop.models import (
from shop.redsys import RedsysClient
from shop.utils import create_order_from_cart
from web.forms import CreateOrderForm
from web.mixins import PaginatedQuerysetMixin, FilteredQuerysetMixin
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
from web.models import WebSettings
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
from web.utils import get_or_create_cart