diff --git a/README.md b/README.md index 94a8dde53a2b2994cd0dcbaeb79b4598db44e044..0bd97e1421850f4d0c025aef2b0c720e361c98f8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ git merge <branch-name> - [ ] Install [Miniconda](https://conda.io/projects/conda/en/stable/user-guide/install/download.html) or [Anaconda](https://www.anaconda.com/) to manage your python packages. -- [ ] Create a virtual env via conda: `conda create --name <env> --file requirements.txt`. +- [ ] Create a virtual env via conda: `conda env create -f environment.yml`. - [ ] Start the Django server: diff --git a/core/api/auth.py b/core/api/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..218450dd0143a0f70f40824719547ef9751ae8b2 --- /dev/null +++ b/core/api/auth.py @@ -0,0 +1,63 @@ +from django.shortcuts import render +from rest_framework import generics +from rest_framework.response import Response +from knox.models import AuthToken + +from core.serializers.login import LoginSerializer +from core.serializers.register import RegisterSerializer +from core.serializers.user import UserSerializer + + +class RegisterAPI(generics.GenericAPIView): + serializer_class = RegisterSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.save() + user.is_active = False + user.save() + token = AuthToken.objects.create(user) + return Response({ + "user": UserSerializer(user, context=self.get_serializer_context()).data, + "token": token[1] + }) + + +class LoginAPI(generics.GenericAPIView): + serializer_class = LoginSerializer + + def post(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data + token = AuthToken.objects.create(user) + return Response({ + "user": UserSerializer(user, context=self.get_serializer_context()).data, + "token": token[1], + }) + + +def verify_user_and_activate(request, token): + 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/password.py b/core/api/password.py new file mode 100644 index 0000000000000000000000000000000000000000..47b8ee6479c00a771f74e420c9affd8c45562703 --- /dev/null +++ b/core/api/password.py @@ -0,0 +1,42 @@ +from rest_framework import status +from rest_framework import generics +from rest_framework.response import Response +from django.contrib.auth.models import User +from core.serializers.password import ChangePasswordSerializer +from rest_framework.permissions import IsAuthenticated + +class ChangePasswordView(generics.UpdateAPIView): + """ + An endpoint for changing password. + """ + serializer_class = ChangePasswordSerializer + model = User + permission_classes = (IsAuthenticated,) + + def get_object(self, queryset=None): + obj = self.request.user + return obj + + def update(self, request, *args, **kwargs): + self.object = self.get_object() + serializer = self.get_serializer(data=request.data) + + if serializer.is_valid(): + # Check old password + if not self.object.check_password(serializer.data.get("old_password")): + return Response({ + "old_password": ["Wrong password."] + }, status=status.HTTP_400_BAD_REQUEST) + # set_password also hashes the password that the user will get + self.object.set_password(serializer.data.get("new_password")) + self.object.save() + response = { + 'status': 'success', + 'code': status.HTTP_200_OK, + 'message': 'Password updated successfully', + 'data': [] + } + + return Response(response) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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/apps.py b/core/apps.py index 8115ae60bc647249211ecbd4bbf6aa65478e9b5c..df7e09fb2b8e148039be6bec2bff7151d092fe97 100644 --- a/core/apps.py +++ b/core/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class CoreConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'core' + + def ready(self) -> None: + import core.signals diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..a8fc15f365b8c784d6375e365fb1edb1b9d05a8d --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 4.1 on 2023-01-20 17:39 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_countries.fields +import django_extensions.db.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('bio', models.TextField(blank=True)), + ('birthday', models.DateField(null=True, verbose_name='Birthday')), + ('country', django_countries.fields.CountryField(max_length=2, null=True, verbose_name='Country')), + ('city', models.CharField(max_length=100, verbose_name='City in English')), + ('affiliation', models.CharField(max_length=100, verbose_name='Name of your organization in English')), + ('photo', models.ImageField(blank=True, default='https://static.productionready.io/images/smiley-cyrus.jpg', max_length=100000, null=True, upload_to='media/')), + ], + options={ + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/core/models/profile.py b/core/models/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..b20f6d797ee262932f42bd72e1db26cafaee84d1 --- /dev/null +++ b/core/models/profile.py @@ -0,0 +1,22 @@ +from datetime import datetime, timedelta + +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models +from django_countries.fields import CountryField +from django_extensions.db.models import TimeStampedModel + + +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") + city = models.CharField(max_length=100, verbose_name="City in English") + affiliation = models.CharField( + max_length=100, verbose_name="Name of your organization in English", + ) + photo = models.ImageField(upload_to='media/', max_length=100000, null=True, blank=True, default="https://static.productionready.io/images/smiley-cyrus.jpg") + + def __str__(self): + return self.user.username diff --git a/core/models/utils.py b/core/models/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c4d01da8ecace2abd4fd34b765e6a138d048b994 --- /dev/null +++ b/core/models/utils.py @@ -0,0 +1,9 @@ +import uuid +from django.db import models + + +class UUIDModel(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + + class Meta: + abstract = True diff --git a/core/serializers/__init__.py b/core/serializers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/core/serializers/login.py b/core/serializers/login.py new file mode 100644 index 0000000000000000000000000000000000000000..f7d9ed2da840b58a4e4d59b3e788a47833f9bb92 --- /dev/null +++ b/core/serializers/login.py @@ -0,0 +1,16 @@ +from django.contrib.auth import authenticate +from rest_framework import serializers + + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField() + + def validate(self, data): + user = authenticate(**data) + if not user: + raise serializers.ValidationError('Invalid Credentials.') + if not user.is_active: + raise serializers.ValidationError('Account is not activated.') + return user + diff --git a/core/serializers/password.py b/core/serializers/password.py new file mode 100644 index 0000000000000000000000000000000000000000..4b8748ae5b8103bb129726fd5df7f44e5bae1113 --- /dev/null +++ b/core/serializers/password.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from django.contrib.auth.models import User + + +class ChangePasswordSerializer(serializers.Serializer): + model = User + + """ + Serializer for password change endpoint. + """ + old_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True) diff --git a/core/serializers/profile.py b/core/serializers/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..5d1e599a3c2c58e6fdaecd9393b3ce994de3132a --- /dev/null +++ b/core/serializers/profile.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from core.models.profile import Profile + + +class ProfileSerializer(serializers.ModelSerializer): + country = serializers.CharField() + class Meta: + model = Profile + fields = ('user', 'bio', 'birthday', 'country', 'city', 'affiliation', 'photo') diff --git a/core/serializers/register.py b/core/serializers/register.py new file mode 100644 index 0000000000000000000000000000000000000000..72400c02e7499dd0ea4d3c6f2b6f94c3a43a674b --- /dev/null +++ b/core/serializers/register.py @@ -0,0 +1,19 @@ +from django.contrib.auth.models import User +from rest_framework import serializers + +from core.models.profile import Profile + + +class RegisterSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'username', 'email', 'password') + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + user = User.objects.create_user( + validated_data['username'], + validated_data['email'], + validated_data['password'] + ) + return user diff --git a/core/serializers/user.py b/core/serializers/user.py new file mode 100644 index 0000000000000000000000000000000000000000..0d9f8ecafda782d11ced061649a95183d19678ee --- /dev/null +++ b/core/serializers/user.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from django.contrib.auth.models import User + +# User Serializer +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'username', 'email') diff --git a/core/signals.py b/core/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..565a0a81729237eb4d3d9b50d16cb0605c0332fe --- /dev/null +++ b/core/signals.py @@ -0,0 +1,72 @@ +from django.contrib.auth.models import User +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): + if instance and created: + # create profile for new user + instance.profile = Profile.objects.create(user=instance) + + +@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 + context = { + 'current_user': reset_password_token.user, + 'username': reset_password_token.user.username, + 'email': reset_password_token.user.email, + 'reset_password_url': "{}?token={}".format( + instance.request.build_absolute_uri(reverse('password_reset:reset-password-confirm')), + reset_password_token.key) + } + + # render email text + email_html_message = render_to_string('email/user_reset_password.html', context) + + msg = EmailMessage( + # title: + "Password Reset for {title}".format(title="Some website title"), + # message: + email_html_message, + # from: + "noreply@somehost.local", + # to: + [reset_password_token.user.email] + ) + msg.send() diff --git a/core/tests.py b/core/tests.py index 7ce503c2dd97ba78597f6ff6e4393132753573f6..10cbc14cc3ad6d17b3f45744b488bf50888cf87f 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,33 @@ -from django.test import TestCase +from django.contrib.auth.models import User +from rest_framework.test import APIClient +from rest_framework.test import APITestCase +from rest_framework import status -# Create your tests here. +from .models.profile import Profile + + +class ProfileTestCase(APITestCase): + + """ + Test suite for Contact + """ + def setUp(self): + self.client = APIClient() + self.data = { + "username": "xjhmlcy", + "email": "xjhmlcy@gmail.com", + "password": "abcdefg123" + } + self.url = "/api/auth/register" + + def test_create_contact(self): + ''' + test ContactViewSet create method + ''' + data = self.data + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(User.objects.count(), 1) + self.assertEqual(Profile.objects.count(), 1) + self.assertEqual(User.objects.get().username, "xjhmlcy") + self.assertEqual(Profile.objects.get().user.email, "xjhmlcy@gmail.com") diff --git a/core/urls.py b/core/urls.py index 6b77c069f0ded81a4d99191018a56ab64809a0e7..6258c45a5852e266b0c5e96447e248b81ce31e55 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,6 +1,24 @@ -from django.urls import path +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, verify_user_and_activate +from core.api.password import ChangePasswordView +from core.api.profile import ProfileViewSet -urlpatterns = [ +router = routers.DefaultRouter() +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')), + # profile + path('api/profile', ProfileViewSet.as_view(), name='profile') ] diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000000000000000000000000000000000000..fc791acaa8d8a0c89b5b60c0f4c41df2ddbf3103 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,38 @@ +# API + +## Authentication + +- url: api/auth/register + + ```json + // request + { + "username": "admin", + "email": "admin@bot.com", + "password": "Password@123" + } + // response + { + "user": { + "id": 2, + "username": "admin1", + "email": "admin1@bot.com" + }, + "token": "790e890d571753148bbc9c4447f106e74ecf4d1404f080245f3e259703d58b09" + } + ``` + +- url: api/auth/login + +```json +// request +{ + "username": "admin", + "password": "Password@123" +} +//response +{ + "expiry": "2020-06-29T02:56:44.924698Z", + "token": "99a27b2ebe718a2f0db6224e55b622a59ccdae9cf66861c60979a25ffb4f133e" +} +``` diff --git a/ece651_backend/settings.py b/ece651_backend/settings.py index 64daeb9ad30508f677fb97484603a94802a6e237..5bca86afb83f70d5e8998d46280e1173d4c70a65 100644 --- a/ece651_backend/settings.py +++ b/ece651_backend/settings.py @@ -9,7 +9,8 @@ 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 # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -31,15 +32,32 @@ ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ - 'core.apps.CoreConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_extensions', + 'rest_framework', + 'django_rest_passwordreset', + 'knox', + 'core', ] +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'knox.auth.TokenAuthentication', + ], +} + +REST_KNOX = { + 'TOKEN_TTL': timedelta(days=3), + 'AUTO_REFRESH': True, +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -55,7 +73,9 @@ ROOT_URLCONF = 'ece651_backend.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + os.path.join(BASE_DIR, 'templates'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -122,3 +142,10 @@ STATIC_URL = 'static/' # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Email settings +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# Media Settings +MEDIA_URL = 'media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') diff --git a/ece651_backend/urls.py b/ece651_backend/urls.py index 44b03322bb3d3e711667e3980d264ba24838db1d..85e0eb68ae62dd9310bf02e5f6e6aaf08a8c857c 100644 --- a/ece651_backend/urls.py +++ b/ece651_backend/urls.py @@ -1,22 +1,16 @@ -"""ece651_backend URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include +from rest_framework import routers + +router = routers.DefaultRouter() -urlpatterns = [ - path('api/core/', include('core.urls')), +urlpatterns = router.urls + +urlpatterns += [ + path('', include('core.urls')), path('admin/', admin.site.urls), ] + +urlpatterns += static('media/', document_root=settings.MEDIA_ROOT) diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000000000000000000000000000000000000..55728713163d1372684bc70ee60f76ff2190fc3b --- /dev/null +++ b/environment.yml @@ -0,0 +1,35 @@ +name: django +channels: + - conda-forge + - defaults +dependencies: + - asgiref=3.5.* + - ca-certificates=2022.10.11 + - certifi=2022.12.7 + - cryptography=38.0.* + - django=4.1 + - django-extensions=3.2.* + - djangorestframework=3.14.* + - freetype=2.12.* + - ncurses=6.3 + - openssl + - pillow=9.3.* + - pip + - pycparser=2.21 + - pyjwt=2.6.0 + - python=3.9.15 + - pytz=2022.7.1 + - readline=8.2 + - setuptools=65.6.* + - sqlite=3.40.* + - sqlparse=0.4.* + - tk=8.6.* + - tzdata + - wheel=0.37.* + - zlib=1.2.* + - zstd=1.5.* + - pip: + - django-countries==7.5 + - django-rest-knox==4.2.0 + - django-rest-passwordreset==1.3.0 + - typing-extensions==4.4.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ba4c57693086bd303bcff151b4871946aa7928e4..0000000000000000000000000000000000000000 --- a/requirements.txt +++ /dev/null @@ -1,25 +0,0 @@ -# This file may be used to create an environment using: -# $ conda create --name <env> --file <this file> -asgiref=3.5.2=py39hecd8cb5_0 -ca-certificates=2022.10.11=hecd8cb5_0 -certifi=2022.12.7=py39hecd8cb5_0 -cffi=1.15.1=py39h6c40b1e_3 -cryptography=38.0.4=py39hf6deb26_0 -django=4.1=py39hecd8cb5_0 -libcxx=14.0.6=h9765a3e_0 -libffi=3.4.2=hecd8cb5_6 -ncurses=6.3=hca72f7f_3 -openssl=1.1.1s=hca72f7f_0 -pip=22.3.1=py39hecd8cb5_0 -pycparser=2.21=pyhd3eb1b0_0 -pyjwt=2.6.0=pyhd8ed1ab_0 -python=3.9.15=h218abb5_2 -readline=8.2=hca72f7f_0 -setuptools=65.6.3=py39hecd8cb5_0 -sqlite=3.40.1=h880c91c_0 -sqlparse=0.4.3=py39hecd8cb5_0 -tk=8.6.12=h5d9f67b_0 -tzdata=2022g=h04d1e81_0 -wheel=0.37.1=pyhd3eb1b0_0 -xz=5.2.8=h6c40b1e_0 -zlib=1.2.13=h4dc903c_0 diff --git a/templates/email/user_reset_password.html b/templates/email/user_reset_password.html new file mode 100644 index 0000000000000000000000000000000000000000..d31ed7fdf27fe28a0c31a6ab10857faa0f4c1f85 --- /dev/null +++ b/templates/email/user_reset_password.html @@ -0,0 +1,12 @@ +{% autoescape off %} +To initiate the password reset process for you {{ username }} for your BuyforFree Account, +click the link below: + +{{reset_password_url}} + +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/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