Reset password with email

This commit is contained in:
Hubert Jankowski 2021-12-11 22:01:14 +01:00
parent f7e2065c58
commit a7d32dc30a
11 changed files with 223 additions and 15 deletions

View File

@ -31,6 +31,10 @@ def get_secret(setting, keys=keys):
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret("SECRET_KEY") SECRET_KEY = get_secret("SECRET_KEY")
EMAIL_ID = get_secret("EMAIL_ID")
EMAIL_SECRET = get_secret("EMAIL_SECRET")
EMAIL_FROM = get_secret("EMAIL_FROM")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = get_secret("DEBUG") DEBUG = get_secret("DEBUG")
@ -68,7 +72,10 @@ ROOT_URLCONF = get_secret("ROOT_URLCONF")
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates")], 'DIRS': [
os.path.join(BASE_DIR, "templates"),
os.path.join(BASE_DIR, "tools/emails/templates")
],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [

5
tools/cons.py Normal file
View File

@ -0,0 +1,5 @@
from django.utils.translation import gettext_lazy as _
PASSWORD_HAS_BEEN_CHANGED = _("Password has been changed.")
EMAIL_HAS_BEEN_SENT = _("Email was sent.")
EMAIL_COULD_NOT_BE_SENT = _("The email could not be sent.")

View File

@ -2,7 +2,7 @@ from mailjet_rest import Client
from django.template.loader import render_to_string, get_template from django.template.loader import render_to_string, get_template
from config import settings from config import settings
from utils.exceptions import EmailException from tools.tools import EmailException
class EmailSender: class EmailSender:
@ -33,7 +33,7 @@ class EmailSender:
"To": [ "To": [
{ {
"Email": f"{self.data['email_to']}", "Email": f"{self.data['email_to']}",
"Name": f"{self.data['owner_first_name']}" "Name": f"{self.data['user_first_name']}"
} }
], ],
"Subject": f"{self.data['subject']}", "Subject": f"{self.data['subject']}",
@ -44,6 +44,7 @@ class EmailSender:
def send_email(self): def send_email(self):
response = self.authenticate_user().send.create(data=self.prepare_email()) response = self.authenticate_user().send.create(data=self.prepare_email())
# import pdb;pdb.set_trace()
if response.status_code != 200: if response.status_code != 200:
raise EmailException() raise EmailException()
else: else:

View File

@ -89,16 +89,25 @@ a[x-apple-data-detectors] {
<td align="center" style="padding:0;Margin:0;padding-top:5px;padding-bottom:5px;font-size:0"><img src="https://novmaz.stripocdn.email/content/guids/CABINET_dd354a98a803b60e2f0411e893c82f56/images/23891556799905703.png" alt style="display:block;border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic" width="175"></td> <td align="center" style="padding:0;Margin:0;padding-top:5px;padding-bottom:5px;font-size:0"><img src="https://novmaz.stripocdn.email/content/guids/CABINET_dd354a98a803b60e2f0411e893c82f56/images/23891556799905703.png" alt style="display:block;border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic" width="175"></td>
</tr> </tr>
<tr style="border-collapse:collapse"> <tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-top:15px;padding-bottom:15px"><h1 style="Margin:0;line-height:24px;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;font-size:20px;font-style:normal;font-weight:normal;color:#333333"><strong>FORGOT YOUR </strong></h1><h1 style="Margin:0;line-height:24px;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;font-size:20px;font-style:normal;font-weight:normal;color:#333333"><strong>&nbsp;PASSWORD?</strong></h1></td> <td align="center" style="padding:0;Margin:0;padding-top:15px;padding-bottom:15px"><h1 style="Margin:0;line-height:24px;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;font-size:20px;font-style:normal;font-weight:normal;color:#333333"><strong>ZAPOMNIAŁEŚ </strong></h1><h1 style="Margin:0;line-height:24px;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;font-size:20px;font-style:normal;font-weight:normal;color:#333333"><strong>&nbsp;HASŁA?</strong></h1></td>
</tr> </tr>
<tr style="border-collapse:collapse"> <tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">HI,&nbsp;{{ owner_first_name }} {{ owner_last_name }}</p></td> <td align="left" style="padding:0;Margin:0;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">Cześć,&nbsp;{{ user_first_name }} {{ user_last_name }}</p></td>
</tr> </tr>
<tr style="border-collapse:collapse"> <tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;padding-right:35px;padding-left:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">There was a request to change your password!</p></td> <td align="left" style="padding:0;Margin:0;padding-right:35px;padding-left:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">Dostaliśmy prośbę o zmianę Twojego hasła!</p></td>
</tr> </tr>
<tr style="border-collapse:collapse"> <tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-top:25px;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666">If did not make this request, just ignore this email. Otherwise, please click the button below to change your password:</p></td> <td align="center" style="padding:0;Margin:0;padding-top:25px;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666">Jeśli nie zgłaszałeś takiej prośby, proszę zignoruj tę wiadomość. Jeśli jednak chcesz zmienić swoje naciśnij poniższy przycisk i przepisz odpowiednio dane:</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">UID: {{ uid }}&nbsp;</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">Token: {{ token }}&nbsp;</p></td>
</tr>
<tr style="border-collapse:collapse">
<td align="center" style="padding:0;Margin:0;padding-left:40px;padding-right:40px"><p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-size:16px;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;line-height:24px;color:#666666;text-align:center">Kod: {{ reset_code }}&nbsp;</p></td>
</tr> </tr>
<tr style="border-collapse:collapse"> <tr style="border-collapse:collapse">
<td align="center" style="Margin:0;padding-left:10px;padding-right:10px;padding-top:40px;padding-bottom:40px"><span class="es-button-border" style="border-style:solid;border-color:#3D5CA3;background:#FFFFFF;border-width:2px;display:inline-block;border-radius:10px;width:auto"><a href={{ link }} class="es-button" target="_blank" style="mso-style-priority:100 !important;text-decoration:none;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;font-size:14px;color:#3D5CA3;border-style:solid;border-color:#FFFFFF;border-width:15px 20px 15px 20px;display:inline-block;background:#FFFFFF;border-radius:10px;font-weight:bold;font-style:normal;line-height:17px;width:auto;text-align:center">RESET PASSWORD</a></span></td> <td align="center" style="Margin:0;padding-left:10px;padding-right:10px;padding-top:40px;padding-bottom:40px"><span class="es-button-border" style="border-style:solid;border-color:#3D5CA3;background:#FFFFFF;border-width:2px;display:inline-block;border-radius:10px;width:auto"><a href={{ link }} class="es-button" target="_blank" style="mso-style-priority:100 !important;text-decoration:none;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:arial, 'helvetica neue', helvetica, sans-serif;font-size:14px;color:#3D5CA3;border-style:solid;border-color:#FFFFFF;border-width:15px 20px 15px 20px;display:inline-block;background:#FFFFFF;border-radius:10px;font-weight:bold;font-style:normal;line-height:17px;width:auto;text-align:center">RESET PASSWORD</a></span></td>

41
tools/tools.py Normal file
View File

@ -0,0 +1,41 @@
from rest_framework import status
from rest_framework.response import Response
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_text, force_bytes
from rest_framework.exceptions import APIException
from . import cons
def encode_uid(pk):
return force_text(urlsafe_base64_encode(force_bytes(pk)))
def decode_uid(pk):
return force_text(urlsafe_base64_decode(pk))
class EmailException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = cons.EMAIL_COULD_NOT_BE_SENT
def __init__(self):
self.detail = {"detail": force_text(self.default_detail)}
class PasswordResetShortcut:
def post(self, request):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
return Response({"detail": cons.EMAIL_HAS_BEEN_SENT}, status=status.HTTP_200_OK)
else:
return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PasswordResetConfirmShortcut:
def post(self, request):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
return Response({"detail": cons.PASSWORD_HAS_BEEN_CHANGED}, status=status.HTTP_200_OK)
else:
return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)

8
users/cons.py Normal file
View File

@ -0,0 +1,8 @@
from django.utils.translation import gettext_lazy as _
USER_WITH_FOLLOWING_EMAIL_DOES_NOT_EXIST = "Uzytkownik o podanym adresie email nie istnieje."
INVALID_UID = "Niepoprawne UID"
PASSWORDS_ARE_NOT_THE_SAME = "Hasła nie są takie same"
INVALID_TOKEN = "Niepoprawny token"
INVALID_CODE = "Niepoprawny kod"
PASSWORD_RESET = "Zmiana hasła"

View File

@ -1,4 +1,13 @@
import random
from django.apps import apps
from django.contrib.auth.base_user import BaseUserManager from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.tokens import default_token_generator
from config import settings
from users import cons
from tools.emails.data import EmailSender
from tools.tools import encode_uid, decode_uid
from .querysets import UserQuerySet from .querysets import UserQuerySet
@ -9,13 +18,50 @@ class UserManager(BaseUserManager):
return UserQuerySet(self.model, using=self._db) return UserQuerySet(self.model, using=self._db)
def create(self, email, password=None, **kwargs): def create(self, email, password=None, **kwargs):
user = self.model(email=email, password=password, **kwargs)
if password is None:
message = "User must have valid password"
raise ValueError(message)
user = self.model(email=email, **kwargs)
user.set_password(password) user.set_password(password)
user.save() user.save()
return user return user
def reset_password(self, user_email):
User = apps.get_model("users", "User")
user = User.objects.get(email=user_email["email"])
credits = self.get_uid_and_activation_token(user)
self.prepare_code(user, credits["token"], credits["uid"])
def get_uid_and_activation_token(self, user):
token_generator = default_token_generator
token = token_generator.make_token(user)
uid = encode_uid(user.id)
return {"uid": uid, "token": token}
def prepare_code(self, user, token, uid):
reset_code = int("".join(random.choice("0123456789") for _ in range(6)))
user.reset_code = reset_code
user.save()
self.send_email(user, uid, token, user.reset_code)
# def create_link(self, uid, token, code):
# return f"{settings.CLIENT_DOMAIN}/password/reset/?uid={uid}&token={token}&code={code}"
def send_email(self, user, uid, token, reset_code):
EmailSender(self.prepare_data_to_send_email(user, uid, token, reset_code)).send_email()
def prepare_data_to_send_email(self, user, uid, token, reset_code):
return {
"subject": f"{cons.PASSWORD_RESET}",
"email_to": user.email,
"user_first_name": user.first_name,
"user_last_name": user.last_name,
"uid": uid,
"token": token,
"reset_code": reset_code,
"link": "http://localhost:8000/users/password/reset/confirm"
}
def confirm_reset_password(self, **validated_data):
User = apps.get_model("users", "User")
user = User.objects.get(id=decode_uid(validated_data["uid"]))
user.set_password(validated_data["new_password"])
user.reset_code = None
user.save(update_fields=["password", "reset_code"])

View File

@ -12,7 +12,7 @@ class User(AbstractBaseUser):
is_active = models.BooleanField(default=False) is_active = models.BooleanField(default=False)
confirmation_number = models.CharField(max_length=100) confirmation_number = models.CharField(max_length=100)
reset_code = models.CharField(max_length=100) reset_code = models.IntegerField(null=True)
avatar = models.ImageField(upload_to="avatars/", null=True) avatar = models.ImageField(upload_to="avatars/", null=True)
USERNAME_FIELD = "email" USERNAME_FIELD = "email"

View File

@ -1,6 +1,8 @@
from rest_framework import serializers from rest_framework import serializers
from users.models import User from users.models import User
from users import cons
from tools.tools import decode_uid, encode_uid
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
@ -24,3 +26,56 @@ class UserSerializer(serializers.ModelSerializer):
"is_active", "is_active",
"password" "password"
) )
class UserPasswordResetSerializer(serializers.Serializer):
email = serializers.CharField()
def validate(self, data):
email = data.get("email")
user = User.objects.filter(email__iexact=email)
if not user:
msg = {"detail": cons.USER_WITH_FOLLOWING_EMAIL_DOES_NOT_EXIST}
raise serializers.ValidationError(msg)
self.do_actions(data)
return data
def do_actions(self, validated_data):
return User.objects.reset_password(validated_data)
class UserPasswordResetConfirmSerializer(serializers.Serializer):
uid = serializers.CharField()
token = serializers.CharField()
code = serializers.IntegerField()
new_password = serializers.CharField(style={"input_type": "password"})
repeat_new_password = serializers.CharField(style={"input_type": "password"})
def validate_uid(self, value):
try:
uid = decode_uid(value)
self.user = User.objects.get(pk=uid)
except (User.DoesNotExist, ValueError, TypeError, OverflowError):
raise serializers.ValidationError(cons.INVALID_UID)
return value
def validate_new_passwords(self, attrs):
new_password = attrs["new_password"]
repeat_new_password = attrs["repeat_new_password"]
if new_password != repeat_new_password:
raise serializers.ValidationError(cons.PASSWORDS_ARE_NOT_THE_SAME)
return attrs
def validate(self, attrs):
token = attrs["token"]
code = attrs["code"]
if not self.context["view"].token_generator.check_token(self.user, token):
raise serializers.ValidationError(cons.INVALID_TOKEN)
if self.user.reset_code != code:
raise serializers.ValidationError(cons.INVALID_CODE)
self.validate_new_passwords(attrs)
self.do_actions(attrs)
return attrs
def do_actions(self, validated_data):
return User.objects.confirm_reset_password(**validated_data)

View File

@ -4,6 +4,7 @@ from django.urls import path
from users.views import UserModelViewSet from users.views import UserModelViewSet
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView from rest_framework_simplejwt.views import TokenRefreshView
from .views import PasswordReset, UserPasswordResetConfirmView
router = DefaultRouter(trailing_slash=False) router = DefaultRouter(trailing_slash=False)
@ -13,5 +14,7 @@ urlpatterns = [
path("", include(router.urls)), path("", include(router.urls)),
path('api/token', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh', TokenRefreshView.as_view(), name='token_refresh'), path('api/token/refresh', TokenRefreshView.as_view(), name='token_refresh'),
path("password/reset", PasswordReset.as_view()),
path("password/reset/confirm", UserPasswordResetConfirmView.as_view()),
] ]

View File

@ -2,8 +2,41 @@ from rest_framework import viewsets
from users.models import User from users.models import User
from users.serializers import UserSerializer from users.serializers import UserSerializer
from rest_framework import views, exceptions, status, viewsets, permissions, generics
from tools.tools import PasswordResetShortcut, PasswordResetConfirmShortcut
from django.contrib.auth.tokens import default_token_generator
from .serializers import UserPasswordResetSerializer, UserPasswordResetConfirmSerializer
class UserModelViewSet(viewsets.ModelViewSet): class UserModelViewSet(viewsets.ModelViewSet):
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
class PasswordReset(PasswordResetShortcut, generics.GenericAPIView):
"""
```json
{
"email": "testowymail@gmail.com"
}
```
"""
serializer_class = UserPasswordResetSerializer
permission_classes = (permissions.AllowAny, )
class UserPasswordResetConfirmView(PasswordResetConfirmShortcut, generics.GenericAPIView):
"""
```json
{
"uid": "NYz",
"token": "asdasdasd",
"code": 123456,
"newPassword": "testowe",
"repeatNewPassword": "testowe"
}
```
"""
serializer_class = UserPasswordResetConfirmSerializer
permission_classes = (permissions.AllowAny, )
token_generator = default_token_generator