diff --git a/app/templates/app/base_profile.html b/app/templates/app/base_profile.html index 6dc27646954f8d5b97e55725285ec708edfe6233..081dc2ef7b2c7c987d31d20c31301d3e81849d8b 100644 --- a/app/templates/app/base_profile.html +++ b/app/templates/app/base_profile.html @@ -24,6 +24,8 @@ <!-- Custom styles for this page --> <link href="{% static 'vendor/datatables/dataTables.bootstrap4.min.css' %}" rel="stylesheet"> {% block stylesheets %}{% endblock %} + + <script type="text/javascript" src="{% static 'vendor/jquery/jquery-3.2.1.min.js' %}"></script> </head> <body id="page-top" style="font-family: 'Poppins', sans-serif;"> diff --git a/app/templates/sunting.html b/app/templates/sunting.html index 62171b1654cd60c9c9a1b262ccba0289578b65c4..02dfee9e0c555e4348098396e4b15ba87fdfb6a5 100644 --- a/app/templates/sunting.html +++ b/app/templates/sunting.html @@ -3,18 +3,18 @@ {% block title %} <title>Sunting Profil | Digipus</title> -{% endblock %} +{% endblock %} {% block content %} <div class="container"> <div class="col-20"> <h1 class="mt-2"> {% if user.is_admin %} - Sunting Profil Admin + Sunting Profil Admin {% elif user.is_contributor %} - Sunting Profil Kontributor + Sunting Profil Kontributor {% else %} - Sunting Profil User + Sunting Profil User {% endif %} </h1> <hr class="mt-0 mb-4"> @@ -24,6 +24,26 @@ <div class="col-md-6"> <div class="fieldWrapper"> {{ field.label_tag }} {{ field }} + + {% if field.name == 'email' %} + <div class="my-3"> + + {% if user.is_email_verified %} + <span class="text-success"> + Email Status: Verified + </span> + {% else %} + <span class="text-danger"> + Email Status: Unverified + <button class="btn btn-success" id='verify-button' + style="background-color: #615CFD; border-color: #615CFD;"> + Verify + </button> + </span> + {% endif %} + </div> + {% endif %} + {% if field.errors %} <span class="text-danger"> {{ field.errors }} @@ -33,15 +53,41 @@ <p class="help">{{ field.help_text|safe }}</p> {% endif %} </div> + </div> {% endfor %} <div class="form-margin"></div> <div class="col-md-6"> <div class="fieldWrapper"> - <button type="submit" class="btn btn-success btn-edit" style="background-color: #615CFD; border-color: #615CFD;">Simpan</button> + <button type="submit" class="btn btn-success btn-edit" + style="background-color: #615CFD; border-color: #615CFD;">Simpan</button> </div> </div> </form> </div> </div> + +<script type="text/javascript"> + $(document).ready(function () { + const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value; + $('#verify-button').click(function () { + $.ajax({ + url: '/registrasi/send-verify-email', + headers: { + 'X-CSRFToken': csrftoken + }, + method: 'POST', + data: { + email: $('#id_email').val() + }, + success: function (data, status) { + alert(data) + }, + }) + + return false; + }); + }); +</script> + {% endblock %} \ No newline at end of file diff --git a/authentication/models.py b/authentication/models.py index 721b69c9f6d364de73e08ad1cbd4583be39e1172..84b835807a6b7e58b49389c35296466de2bc82a2 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -81,6 +81,7 @@ class User(AbstractUser): default_profile_picture = models.BooleanField(blank=True, default=False) profile_picture = models.ImageField(default="default-image.jpg") is_subscribing_to_material_comments = models.BooleanField(blank=False, default=True) + is_email_verified = models.BooleanField(default=False) objects = CustomUserManager() diff --git a/register/migrations/__init__.py b/register/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/register/models.py b/register/models.py index 71a836239075aa6e6e4ecb700e9c42c95c022d91..56cabee4d822988bebddb13863282faef88b581f 100644 --- a/register/models.py +++ b/register/models.py @@ -1,3 +1,11 @@ +from authentication.models import User from django.db import models +import uuid # Create your models here. + +class EmailVerification(models.Model): + token = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created_on = models.DateTimeField(auto_now_add=True) + expire_on = models.DateTimeField(null=True) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) diff --git a/register/services.py b/register/services.py index 603855b114790a85861dc06a4200f2c777256cd9..a668e0426d20c444831f43d430fffe01deddcdc2 100644 --- a/register/services.py +++ b/register/services.py @@ -1,6 +1,12 @@ +import datetime from django.contrib.auth.hashers import make_password from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError +from .models import EmailVerification +from django.utils import timezone +from django.conf import settings +from django.core.mail import send_mail +from django.urls import reverse class RegistrationService: @@ -69,3 +75,27 @@ class RegistrationService: create_result["form"] = form return create_result + + + @staticmethod + def create_email_verification(request, user): + verify = EmailVerification() + verify.created_on = timezone.now() + verify.expire_on = verify.created_on + datetime.timedelta(hours=24) + verify.user = user + verify.save() + + path = reverse('register:verify-email', args=[verify.token]) + url = request.build_absolute_uri(path) + + email_content = f""" + Dear {user.name},\n\n + Mohon verifikasi email Anda dengan klik pada link berikut: {url} + """ + + send_mail( + subject = 'DIGIPUS: Verifikasi Alamat Email', + message = email_content, + from_email = getattr(settings, 'EMAIL_HOST_USER'), + recipient_list = [user.email], + fail_silently = False) diff --git a/register/templates/email_verify.html b/register/templates/email_verify.html new file mode 100644 index 0000000000000000000000000000000000000000..90909d04e636e165b11a42e1c83d78ebcd19b0ee --- /dev/null +++ b/register/templates/email_verify.html @@ -0,0 +1,69 @@ +{% load static %} + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Verifikasi Email</title> + <link rel="icon" type="image/png" href="{% static 'images/icons/logo.ico' %}" /> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'vendor/bootstrap/css/bootstrap.min.css' %}"> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'fonts/font-awesome-4.7.0/css/font-awesome.min.css' %}"> + <!--===============================================================================================--> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'vendor/animate/animate.css' %}"> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'vendor/css-hamburgers/hamburgers.min.css' %}"> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'vendor/animsition/css/animsition.min.css' %}"> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'vendor/select2/select2.min.css' %}"> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'vendor/daterangepicker/daterangepicker.css' %}"> + <!--===============================================================================================--> + <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}"> + <link rel="stylesheet" type="text/css" href="{% static 'css/util.css' %}"> + <link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}"> + <!--===============================================================================================--> + <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital@1&display=swap" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet"> +</head> +<body> + + <main> + + {% if message %} + + <div> + {{ message }} + </div> + + <div> + Kembali ke <a href="/">home</a> + </div> + + {% endif %} + + </main> + <!--===============================================================================================--> + <script src="../static/vendor/jquery/jquery-3.2.1.min.js"></script> + <!--===============================================================================================--> + <script src="../static/vendor/animsition/js/animsition.min.js"></script> + <!--===============================================================================================--> + <script src="../static/vendor/bootstrap/js/popper.js"></script> + <script + src="../static/../static/../static/../static/../static/../static/vendor/bootstrap/js/bootstrap.min.js"></script> + <!--===============================================================================================--> + <script src="../static/../static/../static/../static/../static/vendor/select2/select2.min.js"></script> + <!--===============================================================================================--> + <script src="../static/../static/../static/../static/vendor/daterangepicker/moment.min.js"></script> + <script src="../static/../static/../static/vendor/daterangepicker/daterangepicker.js"></script> + <!--===============================================================================================--> + <script src="../static/../static/vendor/countdowntime/countdowntime.js"></script> + <!--===============================================================================================--> + <script src="../static/js/login.js"></script> + <script src="../static/js/navbar.js"></script> +</body> +</html> \ No newline at end of file diff --git a/register/tests.py b/register/tests.py index ef8205860cad0b4362a48bf9b5c1b7634595d772..799c0d2ea8e8610447e7397ca152b6a7f64e0b07 100644 --- a/register/tests.py +++ b/register/tests.py @@ -1,3 +1,8 @@ +import datetime +from django.core import mail + +from django.utils import timezone +from register.models import EmailVerification from django.test import Client, TestCase from django.urls import resolve @@ -883,3 +888,47 @@ class RegisterPublicTest(TestCase): self.assertEqual(User.objects.all().count(), 0) self.assertIn(b"Password must have at least 8 characters", response.content) + +class VerifyEmailTest(TestCase): + + def setUp(self): + self.credential = { + 'email':'example@test.com', + 'password':'P@ssw0rd' + } + self.user = User.objects.create_contributor(**self.credential) + self.user.save() + + self.verif_valid = EmailVerification( + expire_on=timezone.now() + datetime.timedelta(hours=24), + user = self.user) + self.verif_valid.save() + + self.verif_expire = EmailVerification( + created_on=timezone.now() + datetime.timedelta(hours=-24), + expire_on=timezone.now() + datetime.timedelta(hours=-1), + user = self.user) + self.verif_expire.save() + + self.client = Client() + + def test_email_notif_anonymous(self): + response = self.client.post('/registrasi/send-verify-email', {'email': self.user.email}) + self.assertEqual(response.status_code, 403) + + def test_email_notif_authorized(self): + login = self.client.login(**self.credential) + self.client.post('/registrasi/send-verify-email', {'email': self.user.email}) + self.assertEqual(len(mail.outbox), 1) + + def test_verify_email_before_24h(self): + response = self.client.get(f'/registrasi/verify-email/{self.verif_valid.token}') + self.assertContains(response, f'Email Anda {self.verif_valid.user.email} berhasil diverifikasi') + + def test_verify_email_after_24h(self): + response = self.client.get(f'/registrasi/verify-email/{self.verif_expire.token}') + self.assertContains(response, 'Link verifikasi telah expire') + + def test_verify_email_wrong_token(self): + response = self.client.get(f'/registrasi/verify-email/not-a-valid-token') + self.assertContains(response, 'Email gagal diverifikasi') diff --git a/register/urls.py b/register/urls.py index 0acd03ba88ac4292c91e311da81766e4f8faf4e3..53d6a6ed6a4629ab084d3bac362718086b6126f6 100644 --- a/register/urls.py +++ b/register/urls.py @@ -7,5 +7,7 @@ app_name = "register" urlpatterns = [ path("", views.index.as_view()), path("umum/", views.RegistrasiUmum.as_view()), - path("admin/", views.RegistrasiAdmin.as_view()) + path("admin/", views.RegistrasiAdmin.as_view()), + path("verify-email/<str:token>", views.verify_email, name='verify-email'), + path("send-verify-email", views.send_verify_email), ] diff --git a/register/views.py b/register/views.py index 3d8112fef3563207ce1cdb5b294e6ff26c340acc..91952d6907cb91b74dfd2a6e5e451f0c0a0f26c4 100644 --- a/register/views.py +++ b/register/views.py @@ -1,9 +1,16 @@ +from django.http import response +from authentication.models import User +from django.utils import timezone + +from django.shortcuts import render +from register.models import EmailVerification from django.contrib.auth import login +from django.db import models from django.http import HttpResponseRedirect from django.views.generic import TemplateView +from django.core.exceptions import PermissionDenied from register.forms import UserForm -# Create your views here. from register.services import RegistrationService @@ -98,3 +105,36 @@ class RegistrasiUmum(TemplateView): context = self.get_context_data(**kwargs) context["form"] = UserForm return self.render_to_response(context=context) + + +def send_verify_email(request): + if not request.user.is_authenticated: + raise PermissionDenied(request) + + if request.method == 'POST': + RegistrationService.create_email_verification(request, request.user) + return response.HttpResponse('Email verifikasi telah dikirim') + + +def verify_email(request, token): + template_name = 'email_verify.html' + + try: + verif = EmailVerification.objects.get(token=token) + + if verif.expire_on > timezone.now(): + user = User.objects.get(email=verif.user.email) + user.is_email_verified = True + user.save() + + return render(request, template_name, { + 'message': f'Email Anda {verif.user.email} berhasil diverifikasi' + }) + else: + return render(request, template_name, { + 'message': f'Link verifikasi telah expire' + }) + except: + return render(request, template_name, { + 'message': f'Email gagal diverifikasi' + })