diff --git a/core/api/auth.py b/core/api/auth.py index 452668653c4cb2c1312f5a1bb712905c5853e21d..4e2da3e54182eb2d74aa6985a19fbe498dfaa0ee 100644 --- a/core/api/auth.py +++ b/core/api/auth.py @@ -1,3 +1,4 @@ +from django.shortcuts import render from rest_framework import generics from rest_framework.response import Response from knox.models import AuthToken @@ -33,3 +34,30 @@ class LoginAPI(generics.GenericAPIView): "user": UserSerializer(user, context=self.get_serializer_context()).data, "token": token[1], }) + + +def verify_user_and_activate(request, token): + #print(AuthToken.objects.filter(pk=token).first()) + print(AuthToken.objects.all()) + try: + auth = AuthToken.objects.filter(digest=token).first() + auth.user.is_active = True + auth.user.save() + return render( + request, + template_name='email/verification_success.html', + context={ + 'msg': 'Your Email is verified successfully and account has been activated.', + 'status': 'Verification Successful!', + } + ) + except: + return render( + request, + template_name='email/verification_fail.html', + context={ + 'msg': 'There is something wrong with this link, unable to verify the user...', + 'minor_msg': 'There is something wrong with this link...', + 'status': 'Verification Failed!', + } + ) diff --git a/core/api/profile.py b/core/api/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..3248d778e4a29929d45bdd83fe1f89232b25cee7 --- /dev/null +++ b/core/api/profile.py @@ -0,0 +1,25 @@ +from rest_framework import status +from rest_framework import generics +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from core.models.profile import Profile +from core.serializers.profile import ProfileSerializer + + +class ProfileViewSet(generics.RetrieveUpdateAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = ProfileSerializer + + def retrieve(self, request, *args, **kwargs): + instance = request.user.profile + serializer = self.get_serializer(instance) + return Response(serializer.data) + + def update(self, request, *args, **kwargs): + partial = kwargs.pop('partial', False) + instance = request.user.profile + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + return Response(serializer.data) diff --git a/core/models/profile.py b/core/models/profile.py index 2d56ab17e65b25ae81dad86aea02e05115948355..b20f6d797ee262932f42bd72e1db26cafaee84d1 100644 --- a/core/models/profile.py +++ b/core/models/profile.py @@ -6,11 +6,9 @@ from django.db import models from django_countries.fields import CountryField from django_extensions.db.models import TimeStampedModel -from .utils import UUIDModel - -class Profile(TimeStampedModel, UUIDModel): - user = models.OneToOneField(User, on_delete=models.CASCADE) +class Profile(TimeStampedModel): + user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) bio = models.TextField(blank=True) birthday = models.DateField(verbose_name="Birthday", null=True) country = CountryField(null=True, verbose_name="Country") diff --git a/core/serializers/login.py b/core/serializers/login.py index 0c753e33b4e7f2a09fb1c3f696a2ae5731377cd3..e16315796a7b35f8e83508a1aceb065a49288bf9 100644 --- a/core/serializers/login.py +++ b/core/serializers/login.py @@ -10,4 +10,4 @@ class LoginSerializer(serializers.Serializer): user = authenticate(**data) if user and user.is_active: return user - raise serializers.ValidationError('Incorrect Credentials Passed.') + raise serializers.ValidationError('Account is not activated.') diff --git a/core/serializers/profile.py b/core/serializers/profile.py index e35bd295f7077a27e20438aeba6b86f69fa77921..5d1e599a3c2c58e6fdaecd9393b3ce994de3132a 100644 --- a/core/serializers/profile.py +++ b/core/serializers/profile.py @@ -4,6 +4,7 @@ from core.models.profile import Profile class ProfileSerializer(serializers.ModelSerializer): + country = serializers.CharField() class Meta: model = Profile - fields = ('id', 'user', 'bio', 'birthday', 'country', 'city', 'affiliation', 'photo') + fields = ('user', 'bio', 'birthday', 'country', 'city', 'affiliation', 'photo') diff --git a/core/serializers/register.py b/core/serializers/register.py index 6f338de4fa0e4d816c2c1e2dd2d1e9f65ac80582..72400c02e7499dd0ea4d3c6f2b6f94c3a43a674b 100644 --- a/core/serializers/register.py +++ b/core/serializers/register.py @@ -1,7 +1,9 @@ from django.contrib.auth.models import User from rest_framework import serializers -# Register Serializer +from core.models.profile import Profile + + class RegisterSerializer(serializers.ModelSerializer): class Meta: model = User diff --git a/core/signals.py b/core/signals.py index 5d4aaf4927f6df8c6ddd887127f95f583064b1bf..107318cfc542baa92120b7647480159ffd5a97e2 100644 --- a/core/signals.py +++ b/core/signals.py @@ -1,25 +1,51 @@ from django.contrib.auth.models import User -from django.core.mail import EmailMultiAlternatives +from django.core.mail import EmailMessage from django.db.models.signals import post_save from django.dispatch import receiver from django_rest_passwordreset.signals import reset_password_token_created from django.template.loader import render_to_string from django.urls import reverse +from knox.models import AuthToken from core.models.profile import Profile -# @receiver(post_save, sender=User) -# def create_related_profile(sender, instance, created, *args, **kwargs): -# # Notice that we're checking for `created` here. We only want to do this -# # the first time the `User` instance is created. If the save that caused -# # this signal to be run was an update action, we know the user already -# # has a profile. -# if instance and created: -# print(instance,created) -# instance.profile = Profile.objects.create(user=instance) +@receiver(post_save, sender=User) +def create_related_profile(sender, instance, created, *args, **kwargs): + if instance and created: + # create profile for new user + instance.profile = Profile.objects.create(user=instance) + # reset user's status to inactive + instance.is_active = False +@receiver(post_save, sender=User) +def send_activation_email(sender, instance, created, *args, **kwargs): + # send an e-mail to the user + if instance and created: + context = { + 'current_user': instance, + 'username': instance.username, + 'email': instance.email, + 'domain': 'localhost:8000', + 'token': AuthToken.objects.create(instance)[0].digest + } + + # render email text + email_html_message = render_to_string('email/user_verification.html', context) + + msg = EmailMessage( + # title: + "User Verification for {title}".format(title="Some website title"), + # message: + email_html_message, + # from: + "noreply@somehost.local", + # to: + [instance.email] + ) + msg.send() + @receiver(reset_password_token_created) def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs): # send an e-mail to the user @@ -34,18 +60,15 @@ def password_reset_token_created(sender, instance, reset_password_token, *args, # render email text email_html_message = render_to_string('email/user_reset_password.html', context) - email_plaintext_message = render_to_string('email/user_reset_password.txt', context) - # email_plaintext_message = "{}?token={}".format(reverse('password_reset:reset-password-request'), reset_password_token.key) - msg = EmailMultiAlternatives( + msg = EmailMessage( # title: "Password Reset for {title}".format(title="Some website title"), # message: - email_plaintext_message, + email_html_message, # from: "noreply@somehost.local", # to: [reset_password_token.user.email] ) - msg.attach_alternative(email_html_message, "text/html") msg.send() diff --git a/core/urls.py b/core/urls.py index 17ed4572663c5981fcad843b374a52587327f8b9..6258c45a5852e266b0c5e96447e248b81ce31e55 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,14 +1,24 @@ from django.urls import path, include from knox import views as knox_views +from rest_framework import routers -from core.api.auth import RegisterAPI, LoginAPI +from core.api.auth import RegisterAPI, LoginAPI, verify_user_and_activate from core.api.password import ChangePasswordView +from core.api.profile import ProfileViewSet +router = routers.DefaultRouter() -urlpatterns = [ +urlpatterns = router.urls + +urlpatterns += [ + # auth path('api/auth/register', RegisterAPI.as_view(), name='register'), + path('api/auth/activate/<token>', verify_user_and_activate, name='activate'), path('api/auth/login', LoginAPI.as_view(), name='login'), path('api/auth/logout', knox_views.LogoutView.as_view(), name='logout'), + # passwd path('api/change-password', ChangePasswordView.as_view(), name='change-password'), - path('api/password_reset', include('django_rest_passwordreset.urls', namespace='password_reset')), + path('api/password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')), + # profile + path('api/profile', ProfileViewSet.as_view(), name='profile') ] diff --git a/ece651_backend/settings.py b/ece651_backend/settings.py index 5f2876a6a9836233657cd6aac74820e056a99d9c..5bca86afb83f70d5e8998d46280e1173d4c70a65 100644 --- a/ece651_backend/settings.py +++ b/ece651_backend/settings.py @@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ +from datetime import timedelta import os from pathlib import Path @@ -52,6 +53,11 @@ REST_FRAMEWORK = { ], } +REST_KNOX = { + 'TOKEN_TTL': timedelta(days=3), + 'AUTO_REFRESH': True, +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/templates/user_reset_password.html b/templates/email/user_reset_password.html similarity index 83% rename from templates/user_reset_password.html rename to templates/email/user_reset_password.html index b44376b6bf91b8d8b8c03d348475b3354d823594..d31ed7fdf27fe28a0c31a6ab10857faa0f4c1f85 100644 --- a/templates/user_reset_password.html +++ b/templates/email/user_reset_password.html @@ -2,11 +2,11 @@ To initiate the password reset process for you {{ username }} for your BuyforFree Account, click the link below: -http://josla.com/confirm{{reset_password_url}} +{{reset_password_url}} If clicking the link above doesn't work, please copy and paste the URL in a new browser window instead. Sincerely, -Josla +ECE651_DEMO {% endautoescape %} \ No newline at end of file diff --git a/templates/email/user_verification.html b/templates/email/user_verification.html new file mode 100644 index 0000000000000000000000000000000000000000..2ed1c06997aa313912701d158c59dd7e56362e7c --- /dev/null +++ b/templates/email/user_verification.html @@ -0,0 +1,12 @@ +{% autoescape off %} +To initiate the password reset process for you {{ username }} for your Account, +click the link below: + +http://{{ domain }}{% url 'activate' token=token %} + +If clicking the link above doesn't work, please copy and paste the URL in a new browser +window instead. + +Sincerely, +ECE651_DEMO +{% endautoescape %} \ No newline at end of file diff --git a/templates/email/verification_fail.html b/templates/email/verification_fail.html new file mode 100644 index 0000000000000000000000000000000000000000..94e8b2a320bf6c9975a2b76a297748c98384b8e2 --- /dev/null +++ b/templates/email/verification_fail.html @@ -0,0 +1,33 @@ +{% extends "email_index.html" %} + +{%block content%} + + +<div class="container" style="height: auto; margin-bottom: 0px; padding-bottom: 0px; border: 0px solid red;"> + + + <div class="row" style="height: auto; border: 0px solid green;"> + <div class="col" style="width: auto; border: 0px solid red;"> + <div class="container" style="border: 0px solid; margin-top: 200px; padding-bottom: 10px;"> + <h1 style="font-family: 'Mulish', sans-serif; text-align: center; color: white; font-size: calc(25px + 4vw);"> + {{status}} + </h1> + </div> + <hr style="background-color: white; width: 150px;"> + + <div class="container" style="border: 0px solid;"> + <h1 style="font-family: 'Mulish', sans-serif; text-align: center; color: white; font-size: calc(15px + 1.5vw);"> + {{msg}} + </h1> + </div> + + <div class="mt-5"> + <h1 style="font-family: 'Mulish', sans-serif; text-align: center; color: white; font-size: calc(15px + .5vw);"> + {{ minor_msg }} + </h1> + </div> + </div> + </div> +</div> + +{%endblock%} \ No newline at end of file diff --git a/templates/email/verification_success.html b/templates/email/verification_success.html new file mode 100644 index 0000000000000000000000000000000000000000..550f598bdcbe91fbe196ffa267c7194750f179fe --- /dev/null +++ b/templates/email/verification_success.html @@ -0,0 +1,28 @@ +{% extends "email_index.html" %} + +{%block content%} + + +<div class="container" style="height: auto; margin-bottom: 0px; padding-bottom: 0px; border: 0px solid red;"> + + + <div class="row" style="height: auto; border: 0px solid green;"> + <div class="col" style="width: auto; border: 0px solid red;"> + <div class="container" style="border: 0px solid; margin-top: 200px; padding-bottom: 10px;"> + <h1 style="font-family: 'Mulish', sans-serif; text-align: center; color: white; font-size: calc(25px + 4vw);"> + {{status}} + </h1> + </div> + <hr style="background-color: white; width: 150px;"> + + <div class="container" style="border: 0px solid;"> + <h1 style="font-family: 'Mulish', sans-serif; text-align: center; color: white; font-size: calc(15px + .8vw);"> + {{msg}} + </h1> + </div> + + </div> + </div> +</div> + +{%endblock%} \ No newline at end of file diff --git a/templates/email_index.html b/templates/email_index.html new file mode 100644 index 0000000000000000000000000000000000000000..7859e21e3c73476db07755dd7a28aec12673b166 --- /dev/null +++ b/templates/email_index.html @@ -0,0 +1,42 @@ +{%load static%} + + +<!doctype html> +<html lang="en"> + <head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <!-- Bootstrap CSS --> + <link href="https://fonts.googleapis.com/css2?family=Mulish:wght@200&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> + + <title>{{status}}</title> + </head> + <style> + body { + background-image: linear-gradient(to right, rgb(218, 28, 123) , rgb(0, 110, 253)); + /* background-image: linear-gradient(rgb(117, 224, 67), rgb(15, 231, 247)); */ + + } +</style> + <body> + {% if messages%} + {% for message in messages %} + <div class="alert alert-{{message.tags}}"> + {{message}} + {% endfor %} + </div> + {% endif %} + + + {%block content%}{%endblock%} + + <!-- Optional JavaScript --> + <!-- jQuery first, then Popper.js, then Bootstrap JS --> + <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script> + </body> +</html> \ No newline at end of file diff --git a/templates/user_reset_password.txt b/templates/user_reset_password.txt deleted file mode 100644 index 608af9a6bb32cfd1dca42deb35e93e517517c957..0000000000000000000000000000000000000000 --- a/templates/user_reset_password.txt +++ /dev/null @@ -1 +0,0 @@ -Josla password reset