Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/be 19 #259

Merged
merged 12 commits into from
Nov 24, 2022
1 change: 1 addition & 0 deletions App/backend/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


class UserAdmin(admin.ModelAdmin):
# exclude = ('otp',)
list_display = ['username', 'id', 'name', 'surname', 'email', 'created_at', 'updated_at']

class TagAdmin(admin.ModelAdmin):
Expand Down
9 changes: 3 additions & 6 deletions App/backend/api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated by Django 4.1.2 on 2022-11-18 17:42

# Generated by Django 4.1.2 on 2022-11-24 16:11

from django.conf import settings
import django.contrib.auth.models
Expand Down Expand Up @@ -29,7 +28,7 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_level2', models.BooleanField(default=False, verbose_name='active user')),
('is_level2', models.BooleanField(default=False, verbose_name='Level2 user (active)')),
('name', models.CharField(blank=True, max_length=100)),
('surname', models.CharField(blank=True, max_length=100)),
('about', models.TextField(blank=True)),
Expand All @@ -41,6 +40,7 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True)),
('profile_image', models.ImageField(default='avatar/default.png', upload_to='avatar/')),
('profile_path', models.TextField(default='avatar/default.png')),
('otp', models.CharField(blank=True, max_length=256, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
Expand Down Expand Up @@ -109,14 +109,12 @@ class Migration(migrations.Migration):
options={
'abstract': False,
},

),
migrations.AddField(
model_name='artitem',
name='tags',
field=models.ManyToManyField(blank=True, to='api.tag'),
),

migrations.AddConstraint(
model_name='follow',
constraint=models.UniqueConstraint(fields=('from_user', 'to_user'), name='api_follow_unique_relationships'),
Expand All @@ -125,5 +123,4 @@ class Migration(migrations.Migration):
model_name='follow',
constraint=models.CheckConstraint(check=models.Q(('from_user', models.F('to_user')), _negated=True), name='api_follow_prevent_self_follow'),
),

]
35 changes: 34 additions & 1 deletion App/backend/api/models/user.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from email.policy import default
from django.db import models
from django.contrib.auth.models import AbstractUser
import random
import hashlib

class User(AbstractUser):
is_level2 = models.BooleanField('active user', default=False)
is_level2 = models.BooleanField('Level2 user (active)', default=False)
name = models.CharField(max_length=100, blank=True)
surname = models.CharField(max_length=100, blank=True)
about = models.TextField(blank = True)
Expand All @@ -22,6 +24,37 @@ class User(AbstractUser):
profile_image = models.ImageField( default='avatar/default.png', upload_to='avatar/') # amazon
profile_path = models.TextField(default='avatar/default.png') # avatar/profile.png

#OTP (one time password for password reset)
otp = models.CharField(max_length=256, null=True, blank=True)

# # Method to Put a Random OTP in the User table, every time the save is called.
# def save(self, *args, **kwargs):
# number_list = [x for x in range(10)] # Use of list comprehension
# code_items_for_otp = []

# for i in range(6):
# num = random.choice(number_list)
# code_items_for_otp.append(num)

# code_string = "".join(str(item) for item in code_items_for_otp) # list comprehension again
# # A six digit random number from the list will be saved in otp field
# self.otp = code_string
# super().save(*args, **kwargs)

def changeOTP(self, *args, **kwargs):
number_list = [x for x in range(10)] # Use of list comprehension
code_items_for_otp = []

for i in range(6):
num = random.choice(number_list)
code_items_for_otp.append(num)

code_string = "".join(str(item) for item in code_items_for_otp) # list comprehension again
# A six digit random number from the list will be saved in otp field
self.otp = hashlib.sha256(code_string.encode('utf-8')).hexdigest()
super().save(*args, **kwargs)
return code_string

def __str__(self):
return self.name + " " + self.surname

Expand Down
16 changes: 16 additions & 0 deletions App/backend/api/serializers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,19 @@ def validate(self, data):
if user and user.is_active:
return user
raise serializers.ValidationError({"credentials": 'Incorrect password.'})
#just used for swagger
class resetRequestSerializer(serializers.ModelSerializer):

class Meta:
model = User
fields = ["email"]
#just used for swagger
class resetPasswordSerializer(serializers.Serializer):

email = serializers.EmailField(default="user_email@artopia.com")
otp = serializers.CharField(default="six_digit_otp_from_email")
new_password = serializers.CharField(default="new_user_password")

#just used for swagger
class passwordSerializer(serializers.Serializer):
new_password = serializers.CharField(default="new_user_password")
8 changes: 6 additions & 2 deletions App/backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
2. Add a URL to urlpatterns: path('', views.home, name='home')
"""

from django.urls import path
from .views.auth import RegisterView, LoginView
from django.urls import path, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views.auth import RegisterView, LoginView, resetRequestView, resetPasswordView, resetPasswordLoggedView
from .views.profile import profile_api, profile_me_api
from .views.artitem import get_artitems, artitems_by_userid, artitems_by_username, artitems_by_id, post_artitem, delete_artitem, artitems_of_followings
from .views.follow import follow_user, unfollow_user, get_my_followers, get_my_followings, get_followers, get_followings
Expand Down Expand Up @@ -50,6 +51,9 @@
path('auth/register/', RegisterView.as_view(), name="register"),
path('auth/login/', LoginView.as_view(), name="login"),
path('auth/logout/', decorated_logout_view, name='logout'),
path('auth/request-reset/', resetRequestView, name = "resetRequest"),
path('auth/password-reset/', resetPasswordView, name = "resetPassword"),
path('profile/me/password-reset/', resetPasswordLoggedView, name = "resetPasswordLogged"),
path('users/profile/<int:id>', profile_api, name="profile_by_id"),
path('users/profile/me/', profile_me_api, name="profile_me"),
path('artitems/<int:artitemid>/comments/', CommentsView, name="CommentsView"),
Expand Down
203 changes: 202 additions & 1 deletion App/backend/api/views/auth.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from django.shortcuts import render
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.decorators import api_view
from knox.models import AuthToken

from ..serializers.auth import RegisterSerializer, LoginSerializer
from ..serializers.auth import RegisterSerializer, LoginSerializer, resetRequestSerializer, resetPasswordSerializer, passwordSerializer
from rest_framework import permissions
from drf_yasg.utils import swagger_auto_schema

from drf_yasg import openapi

from django.conf import settings
from django.core.mail import send_mail
from ..models.user import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.contrib.auth.decorators import login_required
import hashlib


class RegisterView(generics.GenericAPIView):
serializer_class = RegisterSerializer
Expand Down Expand Up @@ -92,3 +101,195 @@ def post(self, request):
return Response({
"token": AuthToken.objects.create(user)[1]
})

# {
# "email": "user_email@artopia.com"
# }
#Function to send Email with OTP on User Request
@swagger_auto_schema(
method='POST',
request_body=resetRequestSerializer,
operation_description="Password reset request API. This API takes the user email as a parameter and sends an email with OTP (one time password) to user.",
operation_summary="Send password reset request with email (Part1).",
tags=['password_reset'],
responses={
status.HTTP_200_OK: openapi.Response(
description="Successfully sent the password reset email.",
examples={
"application/json": {
"detail": ["Successfully sent the password reset email."]
}
}
),
status.HTTP_400_BAD_REQUEST: openapi.Response(
description="Invalid credentials.",
examples={
"application/json": {
"credentials": ["Incorrect username or email."],
"credentials": ["Incorrect password"]
},
}
),
}
)
@api_view(['POST'])
def resetRequestView(request):
data = request.data
email = data['email']
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
message = {
'detail': 'User with given email does not exist.'}
return Response(message, status=status.HTTP_400_BAD_REQUEST)
if User.objects.filter(email=email).exists():
#create an otp
myotp = user.changeOTP()
#print(settings.EMAIL_HOST_USER)
# send email with otp
send_mail(
'Password Reset', #Subject
f'We have received a request from your account to reset password. Please reset your password using the following OTP (one time password): {myotp}.', #message
settings.EMAIL_HOST_USER,
[user.email],
fail_silently=False,
)
message = {
'detail': 'Successfully sent the password reset email.'}
return Response(message, status=status.HTTP_200_OK)
else:
message = {
'detail': 'User with given email does not exist. Error occured while sending password reset email.'}
return Response(message, status=status.HTTP_400_BAD_REQUEST)

# {
# "email": "user_email@artopia.com",
# "otp": "six_digit_otp_from_email",
# "new_password": "new_user_password"
# }
#Function to verify OTP And reset Password
@swagger_auto_schema(
method='PUT',
request_body=resetPasswordSerializer,
operation_description="Password reset API. After user has received otp via request-reset API, this API takes the user email, otp and new_password as a parameters and changes the user password if everything checks out.",
operation_summary="Password reset API (Part2).",
tags=['password_reset'],
responses={
status.HTTP_200_OK: openapi.Response(
description="Password succesfully reset.",
examples={
"application/json": {
"detail": ["Password succesfully reset."]
}
}
),
status.HTTP_400_BAD_REQUEST: openapi.Response(
description="Invalid credentials.",
examples={
"application/json": {
"detail": ["User with given email does not exist."],
"detail": ["Password cant be empty"],
"detail": ["User is not active. Something went wrong"],
"detail": ["OTP did not match"]
},

}
),
}
)
@api_view(['PUT'])
def resetPasswordView(request):
"""reset_password with email, OTP and new password"""
data = request.data
try:
user = User.objects.get(email=data['email'])
except User.DoesNotExist:
message = {
'detail': 'User with given email does not exist.'}
return Response(message, status=status.HTTP_400_BAD_REQUEST)
if user.is_active:
# Check if otp is valid
if hashlib.sha256(data['otp'].encode('utf-8')).hexdigest() == user.otp:
#here use a function to check password criteria
if data['new_password'] != '':
#validate that new password fits criteria
try:
validate_password(data['new_password'])
except ValidationError as e:
message = {
'detail': e.messages}
return Response(message, status=status.HTTP_400_BAD_REQUEST)
# Change Password
user.set_password(data['new_password'])
user.save()
user.changeOTP()
message = {
'detail': 'Password succesfully reset.'}
return Response(message, status=status.HTTP_200_OK)
else:
message = {
'detail': 'Password cant be empty'}
user.changeOTP()
return Response(message, status=status.HTTP_400_BAD_REQUEST)
else:
message = {
'detail': 'OTP did not match'}
return Response(message, status=status.HTTP_400_BAD_REQUEST)
else:
message = {
'detail': 'User is not active. Something went wrong'}
return Response(message, status=status.HTTP_400_BAD_REQUEST)

# {
# "new_password": "new_user_password"
# }
@swagger_auto_schema(
method='PUT',
request_body=passwordSerializer,
operation_description="Password update API. This API takes the new password as a parameter and updates the user password. Login is required",
operation_summary="Password update API.",
tags=['password_reset'],
responses={
status.HTTP_200_OK: openapi.Response(
description="Password succesfully updated.",
examples={
"application/json": {
"detail": ["Password succesfully updated."]
}
}
),
status.HTTP_400_BAD_REQUEST: openapi.Response(
description="Invalid credentials.",
examples={
"application/json": {
"detail": ["Invalid token."],
"detail": ["Password cant be empty"]
},
}
),
}
)
@api_view(['PUT'])
@login_required
def resetPasswordLoggedView(request):
data = request.data
currentusername = request.user.username
u = User.objects.get(username = currentusername)
if data['new_password'] != '':
#validate that new password fits criteria
try:
validate_password(data['new_password'])
except ValidationError as e:
message = {
'detail': e.messages}
return Response(message, status=status.HTTP_400_BAD_REQUEST)
# Change Password
u.set_password(data['new_password'])
u.save()
message = {
'detail': 'Password succesfully updated.'}
return Response(message, status=status.HTTP_200_OK)
else:
message = {
'detail': 'Password cant be empty'}
return Response(message, status=status.HTTP_400_BAD_REQUEST)
Loading