Skip to content

Commit 3b476ba

Browse files
Improvements to core
- added user_type to User model - permissions make use of user_type - admins now have is_staff set to True - changed url of default admin page to api/test/admin - authentication views: login, logout, forgot password, password reset -- added seperate authentication views for admin too -- extracted logic of admin, staff, students auth views to single place - login views only accepts login for respective user types - renamed change password to forgot password everywhere - added proper client urls to verification and password reset emails - fixed bug where admin was able to edit profile pic of students, staff - added success message in response for deleting user - updated requirements.txt
1 parent fa4374b commit 3b476ba

File tree

13 files changed

+255
-311
lines changed

13 files changed

+255
-311
lines changed

api/api/settings.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,9 @@
159159
EMAIL_HOST_PASSWORD = secrets.EMAIL_HOST_PASSWORD
160160

161161
MEDIA_ROOT = Path(BASE_DIR) / 'media'
162-
MEDIA_URL = '/media/'
162+
MEDIA_URL = '/media/'
163+
164+
165+
# Client URLs to be sent in verification and password reset emails
166+
FORGOT_PASSWORD_URL = 'http://localhost:8000/type/forgot-password'
167+
RESET_PASSWORD_URL = 'http://localhost:8000/type/reset-password'

api/api/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from django.contrib import admin
1717
from django.urls import path, include
1818

19+
from core.urls import urlpatterns as admin_urls
1920
from student.urls import router as student_router
2021
from staff.urls import router as staff_router
2122
from academics.urls import urlpatterns as academics_urls
@@ -24,7 +25,8 @@
2425
from django.conf.urls.static import static
2526

2627
urlpatterns = [
27-
path('api/admin/', admin.site.urls),
28+
path('api/test/admin/', admin.site.urls),
29+
path('api/', include(admin_urls)),
2830
path('api/', include(student_router.urls)),
2931
path('api/', include(staff_router.urls)),
3032
path('api/', include(academics_urls)),

api/core/models.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def name_validator(value):
2626

2727
# Custom User model manager
2828
class UserManager(BaseUserManager):
29-
def create_user(self, id, email, name, password=None, **extra_fields):
30-
user = self.model(id=id, email=email, name=name, password=password, **extra_fields)
29+
def create_user(self, id, email, name, user_type, password=None, **extra_fields):
30+
user = self.model(id=id, email=email, name=name, user_type=user_type, password=password, **extra_fields)
3131

3232
if password: user.set_password(password)
3333
else: user.set_unusable_password()
@@ -36,19 +36,19 @@ def create_user(self, id, email, name, password=None, **extra_fields):
3636
return user
3737

3838
def create_superuser(self, id, email, name, password, **extra_fields):
39-
# extra_fields.setdefault('is_staff', True)
39+
extra_fields.setdefault('is_staff', True)
4040
extra_fields.setdefault('is_superuser', True)
4141

42-
# if extra_fields.get('is_staff') is not True:
43-
# raise ValueError('Superuser must have is_staff=True.')
42+
if extra_fields.get('is_staff') is not True:
43+
raise ValueError('Superuser must have is_staff=True.')
4444

4545
if password is None:
4646
raise ValueError("Superuser must have a password.")
4747

4848
if extra_fields.get('is_superuser') is not True:
4949
raise ValueError('Superuser must have is_superuser=True.')
5050

51-
return self.create_user(id, email, name, password, **extra_fields)
51+
return self.create_user(id, email, name, 'Admin', password, **extra_fields)
5252

5353
# Custom User model
5454
class User(AbstractBaseUser, PermissionsMixin):
@@ -57,6 +57,11 @@ class User(AbstractBaseUser, PermissionsMixin):
5757
name = models.CharField(max_length=100, null=False, validators=[name_validator])
5858

5959
is_staff = models.BooleanField(default=False)
60+
user_type = models.CharField(max_length=10, null=False, editable=False, choices=(
61+
('Admin', 'Admin'),
62+
('Student', 'Student'),
63+
('Staff', 'Staff'),
64+
))
6065

6166
objects = UserManager()
6267

api/core/permissions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33

44
class isStaff(IsAuthenticated):
55
def has_permission(self, request, view):
6-
return super().has_permission(request, view) and request.user.is_staff
6+
return super().has_permission(request, view) and request.user.user_type == 'Staff'
77

88
def has_object_permission(self, request, view, obj):
9-
return super().has_object_permission(request, view, obj) and request.user.is_staff
9+
return super().has_object_permission(request, view, obj) and request.user.user_type == 'Staff'
1010

1111
class isAdmin(IsAuthenticated):
1212
def has_permission(self, request, view):
13-
return super().has_permission(request, view) and request.user.is_superuser
13+
return super().has_permission(request, view) and request.user.user_type == 'Admin'
1414

1515
def has_object_permission(self, request, view, obj):
16-
return super().has_object_permission(request, view, obj) and request.user.is_superuser
16+
return super().has_object_permission(request, view, obj) and request.user.user_type == 'Admin'
1717

1818
class isCreator(IsAuthenticated):
1919
def has_object_permission(self, request, view, obj):

api/core/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
class UserCreationSerializer(serializers.ModelSerializer):
66
class Meta:
77
model = get_user_model()
8-
exclude = ('password',)
8+
exclude = ('password', 'user_type')
99

1010
class UserUpdationSerializer(serializers.ModelSerializer):
1111
class Meta:
1212
model = get_user_model()
13-
fields = '__all__'
13+
exclude = ('user_type',)
1414
extra_kwargs = {
1515
'password': {'write_only': True},
1616
}

api/core/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.urls import path
2+
from core.views import login, logout, forgot_password, password_reset
3+
4+
urlpatterns = [
5+
path('admin/login/', login, name='admin_login'),
6+
path('admin/logout/', logout, name='admin_logout'),
7+
path('admin/forgot_password/', forgot_password, name='admin_forgot_password'),
8+
path('admin/password_reset/', password_reset, name='admin_password_reset'),
9+
]

api/core/views.py

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,174 @@
1-
from django.shortcuts import render
1+
from rest_framework.decorators import api_view
22

3-
# Create your views here.
3+
from django.db.models import Q
4+
from django.conf import settings
5+
from django.utils import timezone
6+
from rest_framework import status
7+
from django.core.mail import send_mail
8+
from rest_framework.response import Response
9+
from core.models import PasswordResetRequest
10+
from django.contrib.auth import get_user_model
11+
from rest_framework.authtoken.models import Token
12+
from django.core.exceptions import ValidationError
13+
from django.contrib.auth import authenticate, login as auth_login
14+
from core.authentication import ExpiringTokenAuthentication
15+
from django.contrib.auth.password_validation import validate_password
16+
17+
18+
def core_login(request, user_type='Admin'):
19+
# get id/email and password
20+
id = request.data.get('id')
21+
password = request.data.get('password')
22+
23+
errors = {}
24+
if not id: errors['id'] = "User id or email is required."
25+
if not password: errors['password'] = "Password is required."
26+
27+
if errors: return Response(errors, status=status.HTTP_400_BAD_REQUEST)
28+
29+
# get user
30+
try:
31+
user = get_user_model().objects.get(Q(id=id) | Q(email=id))
32+
33+
except get_user_model().DoesNotExist:
34+
return Response({'id': "User with the given id/email does not exist."}, status=status.HTTP_404_NOT_FOUND)
35+
36+
# validate user type
37+
if user.user_type != user_type:
38+
if user_type == 'Admin':
39+
return Response({'id': f"User with id {id} is not an admin."}, status=status.HTTP_400_BAD_REQUEST)
40+
41+
elif user_type == 'Student':
42+
return Response({'id': f"User with id {id} is not a student."}, status=status.HTTP_400_BAD_REQUEST)
43+
44+
elif user_type == 'Staff':
45+
return Response({'id': f"User with id {id} is not a staff member."}, status=status.HTTP_400_BAD_REQUEST)
46+
47+
# authenticate user
48+
user = authenticate(username=user.id, password=password)
49+
50+
if not user:
51+
return Response({'password': "Wrong credentials provided."}, status=status.HTTP_401_UNAUTHORIZED)
52+
53+
# log user in
54+
auth_login(request, user)
55+
56+
# create token
57+
token, created = Token.objects.get_or_create(user=user)
58+
59+
utc_now = timezone.now()
60+
if not created and token.created < utc_now - ExpiringTokenAuthentication.validity_time:
61+
token.delete()
62+
token = Token.objects.create(user=user)
63+
token.created = timezone.now()
64+
token.save()
65+
66+
# set token as cookie and return response
67+
response = Response({'token': token.key}, content_type="application/json")
68+
response.set_cookie('token', token.key, expires=utc_now+ExpiringTokenAuthentication.validity_time)
69+
70+
return response
71+
72+
def core_logout(request):
73+
request.user.auth_token.delete()
74+
return Response({'success': "Logged out successfully"}, status=status.HTTP_200_OK)
75+
76+
def core_forgot_password(request, user_type='Admin'):
77+
# get id or email
78+
id = request.data.get('id')
79+
80+
if not id:
81+
return Response({'id': "User id or email is required"}, status=status.HTTP_400_BAD_REQUEST)
82+
83+
# get user
84+
try:
85+
user = get_user_model().objects.get(Q(id=id) | Q(email=id))
86+
87+
except get_user_model().DoesNotExist:
88+
return Response({'id': "User with the given id/email does not exist."}, status=status.HTTP_404_NOT_FOUND)
89+
90+
91+
# generate password reset request
92+
req, created = PasswordResetRequest.objects.get_or_create(user=user)
93+
94+
first_time = not user.has_usable_password()
95+
time_limit = timezone.timedelta(hours=48) if first_time else timezone.timedelta(minutes=30)
96+
97+
if not created and req.created < timezone.now() - time_limit:
98+
req.delete()
99+
req = PasswordResetRequest.objects.create(user=user)
100+
req.save()
101+
102+
103+
# send email
104+
forgot_password_url = settings.FORGOT_PASSWORD_URL.replace('type', user.user_type.lower())
105+
reset_password_url = f"{settings.RESET_PASSWORD_URL.replace('type', user.user_type.lower())}?token={req.key}"
106+
107+
send_mail(
108+
f"BIT Online Portal password account verification" if first_time else f"BIT Online Portal password reset",
109+
f'''An account has been created for you in the {user_type} Portal of the BIT website.\n\n
110+
Please click on the following link to verify the account by setting your password: {reset_password_url}\n\n
111+
This link is only valid for the next 48 hours. In order to issue a password-reset request again, visit {forgot_password_url}
112+
If you are not {user.name}, then please ignore this email.'''.replace('\t\t', '') if first_time else
113+
f'''We have received a request to reset the password for your account in the {user_type} Portal of the BIT website.
114+
Please click on the following link to reset your password: {reset_password_url}\n\n
115+
This link is only valid for the next 30 minutes. In order to issue a password-reset request again, visit {forgot_password_url}
116+
If you did not request a password reset, please ignore this email.'''.replace('\t\t', ''),
117+
"superuser.bit@gmail.com",
118+
[user.email]
119+
)
120+
121+
return Response({'success': f"{'Verification' if first_time else 'Password reset'} email sent to {user.email}."}, status=status.HTTP_200_OK)
122+
123+
def core_password_reset(request):
124+
token = request.GET.get('token')
125+
126+
try:
127+
req = PasswordResetRequest.objects.get(key=token)
128+
129+
except PasswordResetRequest.DoesNotExist:
130+
return Response("Invalid password reset request token.", status=status.HTTP_404_NOT_FOUND)
131+
132+
user = req.user
133+
password = request.data.get('password')
134+
135+
try:
136+
validate_password(password, user=user)
137+
138+
except ValidationError as e:
139+
return Response({'password': e}, status=status.HTTP_400_BAD_REQUEST)
140+
141+
first_time = not user.has_usable_password()
142+
143+
if settings.TESTING: time_limit = timezone.timedelta(seconds=15) if first_time else timezone.timedelta(seconds=25)
144+
else: time_limit = timezone.timedelta(hours=48) if first_time else timezone.timedelta(minutes=30)
145+
146+
if req.created < timezone.now() - time_limit:
147+
req.delete()
148+
return Response({'token': "Password reset token expired."}, status=status.HTTP_400_BAD_REQUEST)
149+
150+
else:
151+
user = req.user
152+
153+
user.set_password(password)
154+
user.save()
155+
req.delete()
156+
157+
return Response({'success': f"Password for user {user} changed successfully."}, status=status.HTTP_200_OK)
158+
159+
160+
@api_view(['POST'])
161+
def login(request):
162+
return core_login(request)
163+
164+
@api_view(['POST'])
165+
def logout(request):
166+
return core_logout(request)
167+
168+
@api_view(['POST'])
169+
def forgot_password(request):
170+
return core_forgot_password(request)
171+
172+
@api_view(['POST'])
173+
def password_reset(request):
174+
return core_password_reset(request)

api/db.sqlite3

8 KB
Binary file not shown.

api/requirements.txt

4 Bytes
Binary file not shown.

api/staff/serializers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ def create(self, validated_data):
2525
user = get_user_model().objects.create_user(
2626
id=validated_data['user']['id'],
2727
email=validated_data['user']['email'],
28-
name=validated_data['user']['name']
28+
name=validated_data['user']['name'],
29+
user_type='Staff'
2930
)
3031

3132
return Staff.objects.create(
@@ -73,8 +74,8 @@ class StaffUpdationSerializer_Admin(StaffDefaultSerializer):
7374

7475
class Meta:
7576
model = Staff
76-
fields = ('id', 'image', 'name', 'branch')
77-
restricted = ('id', 'email', 'phone', 'password')
77+
fields = ('id', 'name', 'branch')
78+
restricted = ('id', 'image', 'email', 'phone', 'password')
7879

7980

8081
# delete old image when adding new image

0 commit comments

Comments
 (0)