feature/sending-email #15
@ -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': [
|
||||||
|
0
tools/__init__.py
Normal file
0
tools/__init__.py
Normal file
5
tools/cons.py
Normal file
5
tools/cons.py
Normal 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.")
|
50
tools/emails/data.py
Normal file
50
tools/emails/data.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from mailjet_rest import Client
|
||||||
|
|
||||||
|
from django.template.loader import render_to_string, get_template
|
||||||
|
from config import settings
|
||||||
|
from tools.tools import EmailException
|
||||||
|
|
||||||
|
|
||||||
|
class EmailSender:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.client_id = settings.EMAIL_ID
|
||||||
|
self.client_secret = settings.EMAIL_SECRET
|
||||||
|
self.email_from = settings.EMAIL_FROM
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def authenticate_user(self):
|
||||||
|
return Client(
|
||||||
|
auth=(self.client_id, self.client_secret),
|
||||||
|
version='v3.1'
|
||||||
|
)
|
||||||
|
|
||||||
|
def prepare_email_template(self, template_name):
|
||||||
|
template = get_template(template_name)
|
||||||
|
return template.render(self.data)
|
||||||
|
|
||||||
|
def prepare_email(self):
|
||||||
|
return {
|
||||||
|
'Messages': [
|
||||||
|
{
|
||||||
|
"From": {
|
||||||
|
"Email": "hubertjan98@gmail.com",
|
||||||
|
"Name": "Hubert"
|
||||||
|
},
|
||||||
|
"To": [
|
||||||
|
{
|
||||||
|
"Email": f"{self.data['email_to']}",
|
||||||
|
"Name": f"{self.data['user_first_name']}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Subject": f"{self.data['subject']}",
|
||||||
|
"HTMLPart": self.prepare_email_template('reset_password.html'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def send_email(self):
|
||||||
|
response = self.authenticate_user().send.create(data=self.prepare_email())
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise EmailException()
|
||||||
|
else:
|
||||||
|
return response
|
126
tools/emails/templates/reset_password.html
Normal file
126
tools/emails/templates/reset_password.html
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" style="width:100%;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta content="telephone=no" name="format-detection">
|
||||||
|
<title>template_reset_password</title>
|
||||||
|
<!--[if (mso 16)]>
|
||||||
|
<style type="text/css">
|
||||||
|
a {text-decoration: none;}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if gte mso 9]><style>sup { font-size: 100% !important; }</style><![endif]-->
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG></o:AllowPNG>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
.ExternalClass {
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height:100%;
|
||||||
|
}
|
||||||
|
.es-button {
|
||||||
|
mso-style-priority:100!important;
|
||||||
|
text-decoration:none!important;
|
||||||
|
}
|
||||||
|
a[x-apple-data-detectors] {
|
||||||
|
color:inherit!important;
|
||||||
|
text-decoration:none!important;
|
||||||
|
font-size:inherit!important;
|
||||||
|
font-family:inherit!important;
|
||||||
|
font-weight:inherit!important;
|
||||||
|
line-height:inherit!important;
|
||||||
|
}
|
||||||
|
.es-desk-hidden {
|
||||||
|
display:none;
|
||||||
|
float:left;
|
||||||
|
overflow:hidden;
|
||||||
|
width:0;
|
||||||
|
max-height:0;
|
||||||
|
line-height:0;
|
||||||
|
mso-hide:all;
|
||||||
|
}
|
||||||
|
.es-button-border:hover {
|
||||||
|
background:#ffffff!important;
|
||||||
|
border-style:solid solid solid solid!important;
|
||||||
|
border-color:#3d5ca3 #3d5ca3 #3d5ca3 #3d5ca3!important;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width:600px) {p, ul li, ol li, a { font-size:16px!important; line-height:150%!important } h1 { font-size:20px!important; text-align:center; line-height:120%!important } h2 { font-size:16px!important; text-align:left; line-height:120%!important } h3 { font-size:20px!important; text-align:center; line-height:120%!important } h1 a { font-size:20px!important } h2 a { font-size:16px!important; text-align:left } h3 a { font-size:20px!important } .es-menu td a { font-size:14px!important } .es-header-body p, .es-header-body ul li, .es-header-body ol li, .es-header-body a { font-size:10px!important } .es-footer-body p, .es-footer-body ul li, .es-footer-body ol li, .es-footer-body a { font-size:12px!important } .es-infoblock p, .es-infoblock ul li, .es-infoblock ol li, .es-infoblock a { font-size:12px!important } *[class="gmail-fix"] { display:none!important } .es-m-txt-c, .es-m-txt-c h1, .es-m-txt-c h2, .es-m-txt-c h3 { text-align:center!important } .es-m-txt-r, .es-m-txt-r h1, .es-m-txt-r h2, .es-m-txt-r h3 { text-align:right!important } .es-m-txt-l, .es-m-txt-l h1, .es-m-txt-l h2, .es-m-txt-l h3 { text-align:left!important } .es-m-txt-r img, .es-m-txt-c img, .es-m-txt-l img { display:inline!important } .es-button-border { display:block!important } .es-btn-fw { border-width:10px 0px!important; text-align:center!important } .es-adaptive table, .es-btn-fw, .es-btn-fw-brdr, .es-left, .es-right { width:100%!important } .es-content table, .es-header table, .es-footer table, .es-content, .es-footer, .es-header { width:100%!important; max-width:600px!important } .es-adapt-td { display:block!important; width:100%!important } .adapt-img { width:100%!important; height:auto!important } .es-m-p0 { padding:0px!important } .es-m-p0r { padding-right:0px!important } .es-m-p0l { padding-left:0px!important } .es-m-p0t { padding-top:0px!important } .es-m-p0b { padding-bottom:0!important } .es-m-p20b { padding-bottom:20px!important } .es-mobile-hidden, .es-hidden { display:none!important } tr.es-desk-hidden, td.es-desk-hidden, table.es-desk-hidden { width:auto!important; overflow:visible!important; float:none!important; max-height:inherit!important; line-height:inherit!important } tr.es-desk-hidden { display:table-row!important } table.es-desk-hidden { display:table!important } td.es-desk-menu-hidden { display:table-cell!important } .es-menu td { width:1%!important } table.es-table-not-adapt, .esd-block-html table { width:auto!important } table.es-social { display:inline-block!important } table.es-social td { display:inline-block!important } a.es-button, button.es-button { font-size:14px!important; display:block!important; border-left-width:0px!important; border-right-width:0px!important } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="width:100%;font-family:helvetica, 'helvetica neue', arial, verdana, sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0">
|
||||||
|
<div class="es-wrapper-color" style="background-color:#FAFAFA">
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||||
|
<v:fill type="tile" color="#fafafa"></v:fill>
|
||||||
|
</v:background>
|
||||||
|
<![endif]-->
|
||||||
|
<table class="es-wrapper" width="100%" cellspacing="0" cellpadding="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;padding:0;Margin:0;width:100%;height:100%;background-repeat:repeat;background-position:center top">
|
||||||
|
<tr style="border-collapse:collapse">
|
||||||
|
<td valign="top" style="padding:0;Margin:0">
|
||||||
|
<table class="es-content" cellspacing="0" cellpadding="0" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%">
|
||||||
|
<tr style="border-collapse:collapse">
|
||||||
|
<td style="padding:0;Margin:0;background-color:#FAFAFA" bgcolor="#fafafa" align="center">
|
||||||
|
<table class="es-content-body" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#FFFFFF;width:600px" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
|
||||||
|
<tr style="border-collapse:collapse">
|
||||||
|
<td style="padding:0;Margin:0;padding-left:20px;padding-right:20px;padding-top:40px;background-color:transparent;background-position:left top" bgcolor="transparent" align="left">
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
|
||||||
|
<tr style="border-collapse:collapse">
|
||||||
|
<td valign="top" align="center" style="padding:0;Margin:0;width:560px">
|
||||||
|
<table style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-position:left top" width="100%" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr style="border-collapse:collapse">
|
||||||
|
<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 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>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> HASŁA?</strong></h1></td>
|
||||||
|
</tr>
|
||||||
|
<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">Cześć, {{ user_first_name }} {{ user_last_name }}</p></td>
|
||||||
|
</tr>
|
||||||
|
<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">Dostaliśmy prośbę o zmianę Twojego hasła!</p></td>
|
||||||
|
</tr>
|
||||||
|
<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">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 }} </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 }} </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 }} </p></td>
|
||||||
|
</tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
</table></td>
|
||||||
|
</tr>
|
||||||
|
</table></td>
|
||||||
|
</tr>
|
||||||
|
</table></td>
|
||||||
|
</tr>
|
||||||
|
</table></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
tools/emails/templates/reset_password.txt
Normal file
1
tools/emails/templates/reset_password.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{ variable }}
|
41
tools/tools.py
Normal file
41
tools/tools.py
Normal 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
8
users/cons.py
Normal 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"
|
@ -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"])
|
||||||
|
18
users/migrations/0002_alter_user_reset_code.py
Normal file
18
users/migrations/0002_alter_user_reset_code.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.9 on 2021-12-11 20:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='reset_code',
|
||||||
|
field=models.IntegerField(),
|
||||||
|
),
|
||||||
|
]
|
18
users/migrations/0003_alter_user_reset_code.py
Normal file
18
users/migrations/0003_alter_user_reset_code.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.9 on 2021-12-11 20:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0002_alter_user_reset_code'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='reset_code',
|
||||||
|
field=models.IntegerField(null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -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"
|
||||||
|
@ -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)
|
@ -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()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user