authease 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- authease-1.0.1/LICENSE +21 -0
- authease-1.0.1/PKG-INFO +24 -0
- authease-1.0.1/README.md +2 -0
- authease-1.0.1/auth_core/__init__.py +0 -0
- authease-1.0.1/auth_core/admin.py +6 -0
- authease-1.0.1/auth_core/apps.py +6 -0
- authease-1.0.1/auth_core/manager.py +46 -0
- authease-1.0.1/auth_core/migrations/0001_initial.py +47 -0
- authease-1.0.1/auth_core/migrations/__init__.py +0 -0
- authease-1.0.1/auth_core/models.py +80 -0
- authease-1.0.1/auth_core/serializers.py +171 -0
- authease-1.0.1/auth_core/tests/__init__.py +0 -0
- authease-1.0.1/auth_core/tests/test_login_views.py +243 -0
- authease-1.0.1/auth_core/tests/test_models.py +59 -0
- authease-1.0.1/auth_core/tests/test_registration_view.py +116 -0
- authease-1.0.1/auth_core/tests/test_serializers.py +374 -0
- authease-1.0.1/auth_core/tests/test_utils.py +60 -0
- authease-1.0.1/auth_core/tests/test_verification_passwordreset_views.py +357 -0
- authease-1.0.1/auth_core/urls.py +25 -0
- authease-1.0.1/auth_core/utils.py +38 -0
- authease-1.0.1/auth_core/views/__init__.py +2 -0
- authease-1.0.1/auth_core/views/authentication_views.py +81 -0
- authease-1.0.1/auth_core/views/password_views.py +48 -0
- authease-1.0.1/authease.egg-info/PKG-INFO +24 -0
- authease-1.0.1/authease.egg-info/SOURCES.txt +38 -0
- authease-1.0.1/authease.egg-info/dependency_links.txt +1 -0
- authease-1.0.1/authease.egg-info/requires.txt +8 -0
- authease-1.0.1/authease.egg-info/top_level.txt +2 -0
- authease-1.0.1/oauth/__init__.py +0 -0
- authease-1.0.1/oauth/admin.py +3 -0
- authease-1.0.1/oauth/apps.py +6 -0
- authease-1.0.1/oauth/github.py +29 -0
- authease-1.0.1/oauth/models.py +3 -0
- authease-1.0.1/oauth/serializers.py +50 -0
- authease-1.0.1/oauth/tests.py +3 -0
- authease-1.0.1/oauth/urls.py +8 -0
- authease-1.0.1/oauth/utils.py +62 -0
- authease-1.0.1/oauth/views.py +29 -0
- authease-1.0.1/setup.cfg +4 -0
- authease-1.0.1/setup.py +38 -0
authease-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Oluwatemmy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
authease-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: authease
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: An authentication package
|
|
5
|
+
Home-page: https://github.com/Oluwatemmy/authease
|
|
6
|
+
Author: Oluwaseyi Ajayi
|
|
7
|
+
Author-email: oluwaseyitemitope456@gmail.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/Oluwatemmy/authease#readme
|
|
9
|
+
Project-URL: Source, https://github.com/Oluwatemmy/authease
|
|
10
|
+
Project-URL: Issues, https://github.com/Oluwatemmy/authease/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Requires-Python: >=3.6
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: Django>=5.0.6
|
|
18
|
+
Requires-Dist: djangorestframework>=3.15.1
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
20
|
+
Requires-Dist: django-environ>=0.11.2
|
|
21
|
+
Requires-Dist: djangorestframework-simplejwt>=5.3.1
|
|
22
|
+
Requires-Dist: google-api-python-client>=2.136.0
|
|
23
|
+
Requires-Dist: environs>=11.0.0
|
|
24
|
+
Requires-Dist: marshmallow>=3.21.3
|
authease-1.0.1/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from django.contrib.auth.models import BaseUserManager
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
|
+
from django.core.validators import validate_email
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserManager(BaseUserManager):
|
|
8
|
+
def email_validator(self, email):
|
|
9
|
+
try:
|
|
10
|
+
validate_email(email)
|
|
11
|
+
except ValidationError:
|
|
12
|
+
raise ValueError(_("Please enter a valid email address"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_user(self, email, first_name, last_name, password, **extra_fields):
|
|
16
|
+
if email:
|
|
17
|
+
email=self.normalize_email(email)
|
|
18
|
+
self.email_validator(email)
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError(_("An Email Address is required"))
|
|
21
|
+
if not first_name:
|
|
22
|
+
raise ValueError(_("First Name is required"))
|
|
23
|
+
if not last_name:
|
|
24
|
+
raise ValueError(_("Last Name is required"))
|
|
25
|
+
|
|
26
|
+
user = self.model(email=email, first_name=first_name, last_name=last_name, **extra_fields)
|
|
27
|
+
user.set_password(password)
|
|
28
|
+
user.save(using=self._db)
|
|
29
|
+
|
|
30
|
+
return user
|
|
31
|
+
|
|
32
|
+
def create_superuser(self, email, first_name, last_name, password, **extra_fields):
|
|
33
|
+
extra_fields.setdefault("is_staff", True)
|
|
34
|
+
extra_fields.setdefault("is_superuser", True)
|
|
35
|
+
extra_fields.setdefault("is_verified", True)
|
|
36
|
+
|
|
37
|
+
if extra_fields.get("is_staff") is not True:
|
|
38
|
+
raise ValueError (_("is staff must be true for admin user"))
|
|
39
|
+
|
|
40
|
+
if extra_fields.get("is_superuser") is not True:
|
|
41
|
+
raise ValueError (_("is superuser must be true for admin user"))
|
|
42
|
+
|
|
43
|
+
user = self.create_user(
|
|
44
|
+
email, first_name, last_name, password, **extra_fields
|
|
45
|
+
)
|
|
46
|
+
return user
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Generated by Django 5.0.6 on 2024-10-12 18:17
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
('auth', '0012_alter_user_first_name_max_length'),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='User',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
22
|
+
('email', models.EmailField(max_length=255, unique=True, verbose_name='Email Address')),
|
|
23
|
+
('first_name', models.CharField(max_length=150, verbose_name='First name')),
|
|
24
|
+
('last_name', models.CharField(max_length=150, verbose_name='Last name')),
|
|
25
|
+
('is_staff', models.BooleanField(default=False)),
|
|
26
|
+
('is_active', models.BooleanField(default=True)),
|
|
27
|
+
('is_superuser', models.BooleanField(default=False)),
|
|
28
|
+
('is_verified', models.BooleanField(default=False)),
|
|
29
|
+
('date_joined', models.DateTimeField(auto_now_add=True)),
|
|
30
|
+
('last_login', models.DateTimeField(auto_now=True)),
|
|
31
|
+
('auth_provider', models.CharField(default='email', max_length=50)),
|
|
32
|
+
('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')),
|
|
33
|
+
('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')),
|
|
34
|
+
],
|
|
35
|
+
options={
|
|
36
|
+
'abstract': False,
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
migrations.CreateModel(
|
|
40
|
+
name='OneTimePassword',
|
|
41
|
+
fields=[
|
|
42
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
43
|
+
('code', models.CharField(max_length=6, unique=True)),
|
|
44
|
+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
45
|
+
],
|
|
46
|
+
),
|
|
47
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.contrib.auth.models import (
|
|
3
|
+
AbstractBaseUser,
|
|
4
|
+
PermissionsMixin,
|
|
5
|
+
)
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
from .manager import UserManager
|
|
8
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
9
|
+
from django.core.validators import RegexValidator
|
|
10
|
+
|
|
11
|
+
# Create your models here.
|
|
12
|
+
|
|
13
|
+
AUTH_PROVIDERS = {
|
|
14
|
+
'email': 'email',
|
|
15
|
+
'google': 'google',
|
|
16
|
+
'github': 'github',
|
|
17
|
+
'facebook': 'facebook'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class User(AbstractBaseUser, PermissionsMixin):
|
|
21
|
+
# Abstractbaseuser has password, last_login, is_active by default
|
|
22
|
+
|
|
23
|
+
email = models.EmailField(unique=True, max_length=255, verbose_name= _("Email Address"), help_text=_("Required. Enter a valid email address."))
|
|
24
|
+
first_name = models.CharField(max_length=150, verbose_name=_("First name"))
|
|
25
|
+
last_name = models.CharField(max_length=150, verbose_name=_("Last name"))
|
|
26
|
+
|
|
27
|
+
is_staff = models.BooleanField(
|
|
28
|
+
default=False
|
|
29
|
+
) # must needed, otherwise you won't be able to loginto django-admin.
|
|
30
|
+
is_active = models.BooleanField(
|
|
31
|
+
default=True
|
|
32
|
+
) # must needed, otherwise you won't be able to loginto django-admin.
|
|
33
|
+
is_superuser = models.BooleanField(
|
|
34
|
+
default=False
|
|
35
|
+
) # this field inherit from PermissionsMixin.
|
|
36
|
+
is_verified = models.BooleanField(
|
|
37
|
+
default=False
|
|
38
|
+
)
|
|
39
|
+
date_joined = models.DateTimeField(
|
|
40
|
+
auto_now_add=True
|
|
41
|
+
)
|
|
42
|
+
last_login = models.DateTimeField(
|
|
43
|
+
auto_now=True
|
|
44
|
+
)
|
|
45
|
+
auth_provider = models.CharField(max_length=50, default=AUTH_PROVIDERS.get("email"))
|
|
46
|
+
|
|
47
|
+
USERNAME_FIELD = "email"
|
|
48
|
+
|
|
49
|
+
REQUIRED_FIELDS = ["first_name", "last_name"]
|
|
50
|
+
|
|
51
|
+
objects = UserManager()
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
return self.email
|
|
55
|
+
|
|
56
|
+
def get_full_name(self):
|
|
57
|
+
return f"{self.first_name} {self.last_name}"
|
|
58
|
+
|
|
59
|
+
def tokens(self):
|
|
60
|
+
refresh = RefreshToken.for_user(self)
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
'refresh': str(refresh),
|
|
64
|
+
'access': str(refresh.access_token)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class Meta:
|
|
68
|
+
verbose_name = "User"
|
|
69
|
+
verbose_name_plural = "Users"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OneTimePassword(models.Model):
|
|
73
|
+
code_validator = RegexValidator(regex=r'^\d{6}$', message="Code must be 6 digits.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
|
77
|
+
code = models.CharField(max_length=6, unique=True, validators=[code_validator])
|
|
78
|
+
|
|
79
|
+
def __str__(self):
|
|
80
|
+
return f"{self.user.first_name} passcode"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .models import User
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from django.urls import reverse
|
|
5
|
+
from .utils import send_normal_email
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
from django.contrib.auth import authenticate
|
|
8
|
+
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
|
9
|
+
from django.utils.encoding import smart_str, smart_bytes, force_str
|
|
10
|
+
from rest_framework.exceptions import AuthenticationFailed
|
|
11
|
+
from django.contrib.sites.shortcuts import get_current_site
|
|
12
|
+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
|
13
|
+
from rest_framework_simplejwt.tokens import RefreshToken, TokenError
|
|
14
|
+
|
|
15
|
+
load_dotenv()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserRegisterSerializer(serializers.ModelSerializer):
|
|
19
|
+
password=serializers.CharField(min_length=8, max_length=70, write_only=True)
|
|
20
|
+
confirm_password=serializers.CharField(min_length=8, max_length=70, write_only=True)
|
|
21
|
+
|
|
22
|
+
class Meta:
|
|
23
|
+
model=User
|
|
24
|
+
fields=["email", "first_name", "last_name", "password", "confirm_password"]
|
|
25
|
+
|
|
26
|
+
def validate(self, attrs):
|
|
27
|
+
password = attrs.get("password", "")
|
|
28
|
+
confirm_password = attrs.get("confirm_password", "")
|
|
29
|
+
|
|
30
|
+
if password != confirm_password:
|
|
31
|
+
raise serializers.ValidationError("Passwords do not match")
|
|
32
|
+
|
|
33
|
+
return attrs
|
|
34
|
+
|
|
35
|
+
def create(self, validated_data):
|
|
36
|
+
user = User.objects.create_user(
|
|
37
|
+
email=validated_data["email"],
|
|
38
|
+
first_name=validated_data["first_name"],
|
|
39
|
+
last_name=validated_data["last_name"],
|
|
40
|
+
password=validated_data["password"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return user
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LoginSerializer(serializers.ModelSerializer):
|
|
47
|
+
email = serializers.EmailField(min_length=8, max_length=255)
|
|
48
|
+
password = serializers.CharField(min_length=8, max_length=70, write_only=True)
|
|
49
|
+
full_name = serializers.CharField(max_length=255, read_only=True)
|
|
50
|
+
access_token = serializers.CharField(max_length=255, read_only=True)
|
|
51
|
+
refresh_token = serializers.CharField(max_length=255, read_only=True)
|
|
52
|
+
|
|
53
|
+
class Meta:
|
|
54
|
+
model = User
|
|
55
|
+
fields = ["email", "password", "full_name", "access_token", "refresh_token"]
|
|
56
|
+
|
|
57
|
+
def validate(self, attrs):
|
|
58
|
+
email = attrs.get("email", "")
|
|
59
|
+
password = attrs.get("password", "")
|
|
60
|
+
request = self.context.get('request')
|
|
61
|
+
user = authenticate(request=request, email=email, password=password)
|
|
62
|
+
|
|
63
|
+
if not user:
|
|
64
|
+
raise AuthenticationFailed("Invalid email or password. Please try again")
|
|
65
|
+
|
|
66
|
+
if not user.is_verified:
|
|
67
|
+
raise AuthenticationFailed("Your account is not verified. Please verify your email address")
|
|
68
|
+
token = user.tokens()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
'email': user.email,
|
|
72
|
+
'full_name': user.get_full_name(),
|
|
73
|
+
'access_token': token['access'],
|
|
74
|
+
'refresh_token': token['refresh']
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PasswordResetRequestSerializer(serializers.ModelSerializer):
|
|
79
|
+
email = serializers.EmailField(max_length=255)
|
|
80
|
+
|
|
81
|
+
class Meta:
|
|
82
|
+
model=User
|
|
83
|
+
fields = ["email"]
|
|
84
|
+
|
|
85
|
+
def validate(self, attrs):
|
|
86
|
+
email = attrs.get('email')
|
|
87
|
+
|
|
88
|
+
if User.objects.filter(email=email).exists():
|
|
89
|
+
user = User.objects.get(email=email)
|
|
90
|
+
|
|
91
|
+
# Check if the user is verified
|
|
92
|
+
if not user.is_verified:
|
|
93
|
+
raise serializers.ValidationError("Email is not verified. Please verify your email before resetting the password.")
|
|
94
|
+
|
|
95
|
+
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
|
|
96
|
+
token = PasswordResetTokenGenerator().make_token(user)
|
|
97
|
+
request = self.context.get('request')
|
|
98
|
+
relative_link = reverse('password-reset-confirm', kwargs={'uidb64': uidb64, 'token': token})
|
|
99
|
+
absink = f"http://{os.environ.get('DOMAIN')}{relative_link}"
|
|
100
|
+
|
|
101
|
+
email_body = f"Hi, Use the link below to reset your password \n {absink}"
|
|
102
|
+
data= {
|
|
103
|
+
'email_body': email_body,
|
|
104
|
+
'email_subject': "Reset your Password",
|
|
105
|
+
'to_email': user.email
|
|
106
|
+
}
|
|
107
|
+
send_normal_email(data)
|
|
108
|
+
else:
|
|
109
|
+
raise serializers.ValidationError("User with this email does not exist")
|
|
110
|
+
|
|
111
|
+
return super().validate(attrs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SetNewPasswordSerializer(serializers.Serializer):
|
|
115
|
+
password = serializers.CharField(min_length=6, max_length=100, write_only=True)
|
|
116
|
+
confirm_password = serializers.CharField(min_length=6, max_length=100, write_only=True)
|
|
117
|
+
token = serializers.CharField(write_only=True)
|
|
118
|
+
uidb64 = serializers.CharField(write_only=True)
|
|
119
|
+
|
|
120
|
+
class Meta:
|
|
121
|
+
fields = [
|
|
122
|
+
"password",
|
|
123
|
+
"confirm_password",
|
|
124
|
+
"token",
|
|
125
|
+
"uidb64"
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def validate(self, attrs):
|
|
129
|
+
password = attrs.get("password")
|
|
130
|
+
confirm_password = attrs.get("confirm_password")
|
|
131
|
+
|
|
132
|
+
if password != confirm_password:
|
|
133
|
+
raise AuthenticationFailed("Password and Confirm Password doesn't match", 401)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
token = attrs.get("token")
|
|
137
|
+
uidb64 = attrs.get("uidb64")
|
|
138
|
+
user_id = force_str(urlsafe_base64_decode(uidb64))
|
|
139
|
+
user = User.objects.get(id=user_id)
|
|
140
|
+
if not PasswordResetTokenGenerator().check_token(user, token):
|
|
141
|
+
raise AuthenticationFailed("The reset link is invalid", 401)
|
|
142
|
+
|
|
143
|
+
if password != confirm_password:
|
|
144
|
+
raise AuthenticationFailed("Password and Confirm Password doesn't match", 401)
|
|
145
|
+
|
|
146
|
+
user.set_password(password)
|
|
147
|
+
user.save()
|
|
148
|
+
return user
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise AuthenticationFailed("The reset link is invalid", 401)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class LogoutSerializer(serializers.Serializer):
|
|
154
|
+
refresh_token = serializers.CharField()
|
|
155
|
+
|
|
156
|
+
default_error_messages = {
|
|
157
|
+
"bad_token": "Token is invalid or expired",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def validate(self, attrs):
|
|
161
|
+
self.token = attrs.get('refresh_token')
|
|
162
|
+
return attrs
|
|
163
|
+
|
|
164
|
+
def save(self, **kwargs):
|
|
165
|
+
try:
|
|
166
|
+
token = RefreshToken(self.token)
|
|
167
|
+
token.blacklist()
|
|
168
|
+
|
|
169
|
+
except TokenError:
|
|
170
|
+
self.fail("bad_token")
|
|
171
|
+
|
|
File without changes
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from django.core import mail
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
from rest_framework import status
|
|
5
|
+
from rest_framework.test import APIClient
|
|
6
|
+
from auth_core.models import User, OneTimePassword
|
|
7
|
+
from django.core import mail
|
|
8
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
9
|
+
from rest_framework_simplejwt.exceptions import TokenError, TokenBackendError
|
|
10
|
+
|
|
11
|
+
class LoginUserViewTests(TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.client = APIClient()
|
|
14
|
+
|
|
15
|
+
self.login_url = reverse('login') # the login url has been saved as 'login'
|
|
16
|
+
self.user_data = {
|
|
17
|
+
"email": 'testuser@example.com',
|
|
18
|
+
"password": 'testpassword123',
|
|
19
|
+
"first_name": 'Test',
|
|
20
|
+
"last_name": 'User'
|
|
21
|
+
}
|
|
22
|
+
self.user = User.objects.create_user(**self.user_data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_login_success(self):
|
|
26
|
+
"""
|
|
27
|
+
Test successful login with valid credentials.
|
|
28
|
+
"""
|
|
29
|
+
# Get the user and set them as active
|
|
30
|
+
self.user = User.objects.get(email=self.user_data["email"])
|
|
31
|
+
self.user.is_verified = True # a user cannot login without verifying their account
|
|
32
|
+
self.user.save()
|
|
33
|
+
|
|
34
|
+
# Attempt to log in with correct credentials
|
|
35
|
+
data = {
|
|
36
|
+
'email': 'testuser@example.com',
|
|
37
|
+
'password': 'testpassword123'
|
|
38
|
+
}
|
|
39
|
+
response = self.client.post(self.login_url, data, format='json')
|
|
40
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
41
|
+
self.assertIn('access_token', response.data)
|
|
42
|
+
self.assertIn('email', response.data)
|
|
43
|
+
self.assertEqual(response.data['email'], data['email'])
|
|
44
|
+
self.assertIn('full_name', response.data)
|
|
45
|
+
|
|
46
|
+
def test_login_invalid_email(self):
|
|
47
|
+
"""
|
|
48
|
+
Test login with an invalid email.
|
|
49
|
+
"""
|
|
50
|
+
data = {
|
|
51
|
+
'email': 'invalid@example.com',
|
|
52
|
+
'password': 'testpassword123'
|
|
53
|
+
}
|
|
54
|
+
response = self.client.post(self.login_url, data, format='json')
|
|
55
|
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
56
|
+
self.assertEqual(str(response.data['detail']), 'Invalid email or password. Please try again')
|
|
57
|
+
|
|
58
|
+
def test_login_invalid_password(self):
|
|
59
|
+
"""
|
|
60
|
+
Test login with an invalid password.
|
|
61
|
+
"""
|
|
62
|
+
data = {
|
|
63
|
+
'email': 'testuser@example.com',
|
|
64
|
+
'password': 'wrongpassword'
|
|
65
|
+
}
|
|
66
|
+
response = self.client.post(self.login_url, data, format='json')
|
|
67
|
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
68
|
+
self.assertEqual(str(response.data['detail']), 'Invalid email or password. Please try again')
|
|
69
|
+
|
|
70
|
+
def test_login_missing_fields(self):
|
|
71
|
+
"""
|
|
72
|
+
Test login with missing fields.
|
|
73
|
+
"""
|
|
74
|
+
data = {
|
|
75
|
+
'email': '',
|
|
76
|
+
'password': ''
|
|
77
|
+
}
|
|
78
|
+
response = self.client.post(self.login_url, data, format='json')
|
|
79
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
80
|
+
self.assertIn('email', response.data)
|
|
81
|
+
self.assertIn('password', response.data)
|
|
82
|
+
|
|
83
|
+
def test_login_with_unverified_email(self):
|
|
84
|
+
"""
|
|
85
|
+
Test login with an unverified email.
|
|
86
|
+
"""
|
|
87
|
+
data = {
|
|
88
|
+
'email': 'testuser@example.com',
|
|
89
|
+
'password': 'testpassword123'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Attempt to log in with correct credentials
|
|
93
|
+
response = self.client.post(self.login_url, data, format='json')
|
|
94
|
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
95
|
+
self.assertEqual(str(response.data['detail']), 'Your account is not verified. Please verify your email address')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LogoutViewTests(TestCase):
|
|
99
|
+
def setUp(self):
|
|
100
|
+
self.client = APIClient()
|
|
101
|
+
|
|
102
|
+
self.logout_url = reverse('logout') # Ensure you use the correct URL name
|
|
103
|
+
self.user_data = {
|
|
104
|
+
"email": 'testuser@example.com',
|
|
105
|
+
"password": 'testpassword123',
|
|
106
|
+
"first_name": 'Test',
|
|
107
|
+
"last_name": 'User'
|
|
108
|
+
}
|
|
109
|
+
self.user = User.objects.create_user(**self.user_data)
|
|
110
|
+
|
|
111
|
+
def test_logout_success(self):
|
|
112
|
+
"""
|
|
113
|
+
Test successful logout.
|
|
114
|
+
"""
|
|
115
|
+
# Get the user and set them as active
|
|
116
|
+
self.user = User.objects.get(email=self.user_data["email"])
|
|
117
|
+
self.user.is_verified = True # a user cannot login without verifying their account
|
|
118
|
+
self.user.save()
|
|
119
|
+
|
|
120
|
+
# Authenticate the user first by logging in (you can use the login endpoint)
|
|
121
|
+
user_data = {
|
|
122
|
+
"email": "testuser@example.com",
|
|
123
|
+
"password": "testpassword123",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
login_response = self.client.post(reverse("login"), user_data, format='json')
|
|
127
|
+
|
|
128
|
+
# Check response status code for successful login (expecting 200 OK)
|
|
129
|
+
self.assertEqual(login_response.status_code, status.HTTP_200_OK)
|
|
130
|
+
|
|
131
|
+
# Extract the tokens from the login response
|
|
132
|
+
access_token = login_response.data['access_token']
|
|
133
|
+
refresh_token = login_response.data['refresh_token']
|
|
134
|
+
|
|
135
|
+
# Add the access token to the Authorization header for logout request
|
|
136
|
+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
|
|
137
|
+
|
|
138
|
+
# Make the logout request with the refresh token
|
|
139
|
+
response = self.client.post(self.logout_url, {"refresh_token": refresh_token}, format='json')
|
|
140
|
+
|
|
141
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
142
|
+
|
|
143
|
+
# Test that the refresh token is blacklisted
|
|
144
|
+
with self.assertRaises(TokenError):
|
|
145
|
+
# Try to use the blacklisted refresh token to get a new access token
|
|
146
|
+
blacklisted_token = RefreshToken(refresh_token)
|
|
147
|
+
blacklisted_token.check_blacklist() # Should raise an error if the token is blacklisted
|
|
148
|
+
|
|
149
|
+
def test_logout_without_authentication(self):
|
|
150
|
+
"""
|
|
151
|
+
Test logout without authentication.
|
|
152
|
+
"""
|
|
153
|
+
# Get the user and set them as active
|
|
154
|
+
self.user = User.objects.get(email=self.user_data["email"])
|
|
155
|
+
self.user.is_verified = True # a user cannot login without verifying their account
|
|
156
|
+
self.user.save()
|
|
157
|
+
|
|
158
|
+
response = self.client.post(self.logout_url, format='json')
|
|
159
|
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
160
|
+
self.assertEqual(response.data['detail'], 'Authentication credentials were not provided.')
|
|
161
|
+
|
|
162
|
+
def test_logout_invalid_token(self):
|
|
163
|
+
"""
|
|
164
|
+
Test logout with an invalid token.
|
|
165
|
+
"""
|
|
166
|
+
# Get the user and set them as active
|
|
167
|
+
self.user = User.objects.get(email=self.user_data["email"])
|
|
168
|
+
self.user.is_verified = True # Ensure the user is verified to allow login
|
|
169
|
+
self.user.save()
|
|
170
|
+
|
|
171
|
+
# Add the access token to the Authorization header for logout request
|
|
172
|
+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {'access_token'}')
|
|
173
|
+
|
|
174
|
+
# Make a logout request with an invalid refresh token
|
|
175
|
+
response = self.client.post(self.logout_url, {'refresh_token': 'invalid_token'}, format='json')
|
|
176
|
+
|
|
177
|
+
with self.assertRaises(TokenError) as context:
|
|
178
|
+
# Try to use the blacklisted refresh token to get a new access token
|
|
179
|
+
blacklisted_token = RefreshToken('invalid_token')
|
|
180
|
+
blacklisted_token.check_blacklist() # Should raise an error if the token is blacklisted
|
|
181
|
+
|
|
182
|
+
# Check the error message
|
|
183
|
+
self.assertEqual(str(context.exception), "Token is invalid or expired")
|
|
184
|
+
|
|
185
|
+
# Check that the response has a 401 status code
|
|
186
|
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
187
|
+
|
|
188
|
+
# Check that the error message matches the one in the LogoutSerializer's default error message
|
|
189
|
+
self.assertEqual(response.data['messages'][0]['message'], "Token is invalid or expired")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestAuthenticationViewTests(TestCase):
|
|
193
|
+
def setUp(self):
|
|
194
|
+
self.client = APIClient()
|
|
195
|
+
|
|
196
|
+
self.test_auth_url = reverse('test-auth') # Ensure you use the correct URL name
|
|
197
|
+
self.user_data = {
|
|
198
|
+
"email": 'testuser@example.com',
|
|
199
|
+
"password": 'testpassword123',
|
|
200
|
+
"first_name": 'Test',
|
|
201
|
+
"last_name": 'User'
|
|
202
|
+
}
|
|
203
|
+
self.user = User.objects.create_user(**self.user_data)
|
|
204
|
+
|
|
205
|
+
# Get the user and set them as active
|
|
206
|
+
self.user = User.objects.get(email=self.user_data["email"])
|
|
207
|
+
self.user.is_verified = True # a user cannot login without verifying their account
|
|
208
|
+
self.user.save()
|
|
209
|
+
|
|
210
|
+
self.client.login(email='testuser@example.com', password='testpassword123') # Log in to get a token
|
|
211
|
+
|
|
212
|
+
def test_authentication_success(self):
|
|
213
|
+
"""
|
|
214
|
+
Test access to an authenticated view.
|
|
215
|
+
"""
|
|
216
|
+
# Authenticate the user first by logging in (you can use the login endpoint)
|
|
217
|
+
user_data = {
|
|
218
|
+
"email": "testuser@example.com",
|
|
219
|
+
"password": "testpassword123",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
login_response = self.client.post(reverse("login"), user_data, format='json')
|
|
223
|
+
|
|
224
|
+
# Extract the tokens from the login response
|
|
225
|
+
access_token = login_response.data['access_token']
|
|
226
|
+
|
|
227
|
+
# Add the access token to the Authorization header for logout request
|
|
228
|
+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
|
|
229
|
+
|
|
230
|
+
response = self.client.get(self.test_auth_url, format='json')
|
|
231
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
232
|
+
self.assertEqual(response.data['message'], "Hello, authenticated user!")
|
|
233
|
+
|
|
234
|
+
def test_authentication_failure(self):
|
|
235
|
+
"""
|
|
236
|
+
Test access to an authenticated view without authentication.
|
|
237
|
+
"""
|
|
238
|
+
self.client.logout() # Log out the user
|
|
239
|
+
|
|
240
|
+
response = self.client.get(self.test_auth_url, format='json')
|
|
241
|
+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
242
|
+
self.assertEqual(response.data['detail'], 'Authentication credentials were not provided.')
|
|
243
|
+
|