diff --git a/config/settings.py b/config/settings.py
index 1c49906..a30645b 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -31,6 +31,10 @@ def get_secret(setting, keys=keys):
# SECURITY WARNING: keep the secret key used in production secret!
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!
DEBUG = get_secret("DEBUG")
@@ -68,7 +72,10 @@ ROOT_URLCONF = get_secret("ROOT_URLCONF")
TEMPLATES = [
{
'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,
'OPTIONS': {
'context_processors': [
diff --git a/tools/__init__.py b/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tools/cons.py b/tools/cons.py
new file mode 100644
index 0000000..2643d4d
--- /dev/null
+++ b/tools/cons.py
@@ -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.")
\ No newline at end of file
diff --git a/tools/emails/data.py b/tools/emails/data.py
new file mode 100644
index 0000000..45b81ed
--- /dev/null
+++ b/tools/emails/data.py
@@ -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
diff --git a/tools/emails/templates/reset_password.html b/tools/emails/templates/reset_password.html
new file mode 100644
index 0000000..75d2b7c
--- /dev/null
+++ b/tools/emails/templates/reset_password.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+ template_reset_password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ ZAPOMNIAŁEŚ HASŁA? |
+
+
+ Cześć, {{ user_first_name }} {{ user_last_name }} |
+
+
+ Dostaliśmy prośbę o zmianę Twojego hasła! |
+
+
+ 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: |
+
+
+ UID: {{ uid }} |
+
+
+ Token: {{ token }} |
+
+
+ Kod: {{ reset_code }} |
+
+
+ RESET PASSWORD |
+
+ |
+
+ |
+
+ |
+
+ |
+
+
+
+
+
diff --git a/tools/emails/templates/reset_password.txt b/tools/emails/templates/reset_password.txt
new file mode 100644
index 0000000..ced40d6
--- /dev/null
+++ b/tools/emails/templates/reset_password.txt
@@ -0,0 +1 @@
+{{ variable }}
diff --git a/tools/tools.py b/tools/tools.py
new file mode 100644
index 0000000..4341193
--- /dev/null
+++ b/tools/tools.py
@@ -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)
\ No newline at end of file
diff --git a/users/cons.py b/users/cons.py
new file mode 100644
index 0000000..0d1ca79
--- /dev/null
+++ b/users/cons.py
@@ -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"
\ No newline at end of file
diff --git a/users/managers.py b/users/managers.py
index d4dece3..8e34a97 100644
--- a/users/managers.py
+++ b/users/managers.py
@@ -1,4 +1,13 @@
+import random
+
+from django.apps import apps
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
@@ -9,13 +18,50 @@ class UserManager(BaseUserManager):
return UserQuerySet(self.model, using=self._db)
def create(self, email, password=None, **kwargs):
-
- if password is None:
- message = "User must have valid password"
- raise ValueError(message)
-
- user = self.model(email=email, **kwargs)
+ user = self.model(email=email, password=password, **kwargs)
user.set_password(password)
user.save()
-
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"])
diff --git a/users/migrations/0002_alter_user_reset_code.py b/users/migrations/0002_alter_user_reset_code.py
new file mode 100644
index 0000000..26b124d
--- /dev/null
+++ b/users/migrations/0002_alter_user_reset_code.py
@@ -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(),
+ ),
+ ]
diff --git a/users/migrations/0003_alter_user_reset_code.py b/users/migrations/0003_alter_user_reset_code.py
new file mode 100644
index 0000000..c82a26a
--- /dev/null
+++ b/users/migrations/0003_alter_user_reset_code.py
@@ -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),
+ ),
+ ]
diff --git a/users/models.py b/users/models.py
index 886360f..8e351fd 100644
--- a/users/models.py
+++ b/users/models.py
@@ -12,7 +12,7 @@ class User(AbstractBaseUser):
is_active = models.BooleanField(default=False)
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)
USERNAME_FIELD = "email"
diff --git a/users/serializers.py b/users/serializers.py
index 2209870..cc2725c 100644
--- a/users/serializers.py
+++ b/users/serializers.py
@@ -1,6 +1,8 @@
from rest_framework import serializers
from users.models import User
+from users import cons
+from tools.tools import decode_uid, encode_uid
class UserSerializer(serializers.ModelSerializer):
@@ -24,3 +26,56 @@ class UserSerializer(serializers.ModelSerializer):
"is_active",
"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)
\ No newline at end of file
diff --git a/users/urls.py b/users/urls.py
index cf7ca52..d377d91 100644
--- a/users/urls.py
+++ b/users/urls.py
@@ -4,6 +4,7 @@ from django.urls import path
from users.views import UserModelViewSet
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView
+from .views import PasswordReset, UserPasswordResetConfirmView
router = DefaultRouter(trailing_slash=False)
@@ -13,5 +14,7 @@ urlpatterns = [
path("", include(router.urls)),
path('api/token', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh', TokenRefreshView.as_view(), name='token_refresh'),
+ path("password/reset", PasswordReset.as_view()),
+ path("password/reset/confirm", UserPasswordResetConfirmView.as_view()),
]
diff --git a/users/views.py b/users/views.py
index a441865..45088d3 100644
--- a/users/views.py
+++ b/users/views.py
@@ -2,8 +2,41 @@ from rest_framework import viewsets
from users.models import User
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):
queryset = User.objects.all()
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
\ No newline at end of file