[fix] auth

This commit is contained in:
Data-Name-ID
2024-04-01 17:31:33 +03:00
parent c8b070368b
commit 851017847c
9 changed files with 135 additions and 447 deletions
+7
View File
@@ -12,6 +12,13 @@ schema_view = get_schema_view(
urlpatterns = [
path("ping", include("api.ping.urls")),
path("auth", include("api.users.urls")),
path(
"",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
# API documentation
path(
"swagger<format>/",
@@ -1,51 +0,0 @@
import bcrypt
import jwt
from django.conf import settings
from rest_framework.authentication import (
BaseAuthentication,
)
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated
from rest_framework.permissions import IsAuthenticated
from api.users.models import User
class JWTAuthentication(BaseAuthentication):
def authenticate_header(self, request): # noqa: ARG002
return "Provide a valid token in the 'Authorization' header"
def authenticate(self, request):
if IsAuthenticated not in getattr(
request.resolver_match.func.cls, "permission_classes", []
):
return None
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
if not token:
raise NotAuthenticated
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=["HS256"]
)
user = User.objects.get(id=payload["id"])
if not bcrypt.checkpw(
payload["password"].encode("utf-8"),
user.password.encode("utf-8"),
):
error = "Token has expired"
raise AuthenticationFailed(error)
except User.DoesNotExist:
error = "Invalid token"
raise AuthenticationFailed(error) from None
except jwt.ExpiredSignatureError:
error = "Token has expired"
raise AuthenticationFailed(error) from None
except jwt.InvalidTokenError:
error = "Invalid token"
raise AuthenticationFailed(error) from None
else:
return (user, None)
@@ -1,12 +1,12 @@
# Generated by Django 4.2.11 on 2024-04-01 01:58
# Generated by Django 5.0.3 on 2024-04-01 13:44
import api.users.models
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -14,225 +14,72 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name="Achievements",
name='Achievements',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
upload_to=api.users.models.Achievements.get_file_path
),
),
("info", models.TextField(max_length=255)),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=api.users.models.get_file_path)),
('info', models.TextField(max_length=255)),
],
),
migrations.CreateModel(
name="Skill",
name='Skill',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
(
"level",
models.IntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(10),
]
),
),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('level', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
],
options={
"abstract": False,
'abstract': False,
},
),
migrations.CreateModel(
name="Specialization",
name='Specialization',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
options={
"abstract": False,
'abstract': False,
},
),
migrations.CreateModel(
name="User",
name='User',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"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"
),
),
("email", models.EmailField(max_length=254, unique=True)),
("birthday", models.DateField(blank=True, null=True)),
(
"avatar",
models.ImageField(
max_length=200,
null=True,
upload_to=api.users.models.User.get_file_path,
),
),
("country", models.TextField(blank=True)),
("city", models.TextField(blank=True)),
(
"experience",
models.IntegerField(
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MinValueValidator(100),
],
),
),
(
"bio",
models.TextField(
blank=True,
validators=[django.core.validators.MaxLengthValidator(512)],
),
),
(
"achievements",
models.ManyToManyField(blank=True, to="users.achievements"),
),
(
"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",
),
),
("skills", models.ManyToManyField(blank=True, to="users.skill")),
(
"specialization",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="users.specialization",
),
),
(
"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",
),
),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('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')),
('email', models.EmailField(max_length=254, unique=True)),
('birthday', models.DateField(blank=True, null=True)),
('avatar', models.ImageField(max_length=200, null=True, upload_to=api.users.models.User.get_file_path)),
('country', models.TextField(blank=True)),
('city', models.TextField(blank=True)),
('experience', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MinValueValidator(100)])),
('bio', models.TextField(blank=True, validators=[django.core.validators.MaxLengthValidator(512)])),
('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')),
('achievements', models.ManyToManyField(blank=True, to='users.achievements')),
('skills', models.ManyToManyField(blank=True, to='users.skill')),
('specialization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.specialization')),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
('objects', django.contrib.auth.models.UserManager()),
],
),
]
+6 -8
View File
@@ -7,6 +7,10 @@ from django.db import models
from api.core.models import AbstractTag
def get_file_path(filename):
return f"achievements/{uuid.uuid4()}/{filename}"
class Skill(AbstractTag):
level = models.IntegerField(
validators=[
@@ -17,18 +21,12 @@ class Skill(AbstractTag):
class Achievements(models.Model):
def get_file_path(self, filename):
folder_name = str(uuid.uuid4())
return f"achievements/{folder_name}/{filename}"
file = models.FileField( # noqa: DJ012
upload_to=get_file_path,
)
file = models.FileField(upload_to=get_file_path)
info = models.TextField(
max_length=255,
)
def __str__(self): # noqa: DJ012
def __str__(self):
return self.info
+36 -55
View File
@@ -1,79 +1,60 @@
from django.contrib.auth.password_validation import validate_password
from django.contrib.auth.hashers import check_password, make_password
from rest_framework import serializers
from api.users.models import User
class UserRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"first_name",
"last_name",
"username",
"email",
"password",
"country",
"city",
]
def validate_password(self, value):
validate_password(value)
return value
class UserLoginSerializer(serializers.Serializer):
remember_me = serializers.BooleanField(default=False, required=False)
username = serializers.CharField()
password = serializers.CharField()
class UserProfileSerializer(serializers.ModelSerializer):
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"first_name",
"last_name",
"username",
"email",
"birthday",
"avatar",
"country",
"city",
"bio",
"avatar",
"experience",
"password",
"first_name",
"last_name",
"specialization",
"achievements",
"username",
"skills",
)
extra_kwargs = {"password": {"write_only": True}}
def validate(self, attrs):
if User.objects.filter(username=attrs["username"]).exists():
raise serializers.ValidationError(
{"username": "Username already exists"}
)
return super().validate(attrs)
def create(self, validated_data):
validated_data["password"] = make_password(
validated_data["password"],
)
return super().create(validated_data)
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(write_only=True)
new_password = serializers.CharField(source="password", write_only=True)
class UpdateProfileSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"countryCode",
"isPublic",
"phone",
"image",
]
def to_representation(self, instance):
data = super().to_representation(instance)
if data["image"] is None:
del data["image"]
if data["phone"] is None:
del data["phone"]
return data
class PasswordChangeSerializer(serializers.Serializer):
# ruff: noqa: N815
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
def validate_password(self, value):
validate_password(value)
fields = ("old_password", "new_password")
def validate_old_password(self, value):
if not check_password(value, self.instance.password):
msg = "Wrong password"
raise serializers.ValidationError(msg)
return value
def update(self, instance, validated_data):
instance.set_password(validated_data["password"])
instance.save()
return instance
+16 -1
View File
@@ -1,4 +1,9 @@
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
import api.users.views
@@ -10,9 +15,19 @@ urlpatterns = [
),
path(
"/sign-in/",
api.users.views.SigninUserApiView.as_view(),
TokenObtainPairView.as_view(),
name="sign-in",
),
path(
"api/token/refresh/",
TokenRefreshView.as_view(),
name="token_refresh",
),
path(
"api/token/verify/",
TokenVerifyView.as_view(),
name="token_verify",
),
path(
"/me/profile/",
api.users.views.ProfileMeApiView.as_view(),
+13 -117
View File
@@ -1,131 +1,27 @@
from datetime import timedelta
import bcrypt
import jwt
from django.conf import settings
from django.utils import timezone
from rest_framework import status
from rest_framework.exceptions import (
NotAuthenticated,
PermissionDenied,
ValidationError,
)
from rest_framework.generics import CreateAPIView, UpdateAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from api.users.models import User
from api.users.serializers import (
PasswordChangeSerializer,
UpdateProfileSerializer,
UserLoginSerializer,
UserProfileSerializer,
UserRegistrationSerializer,
ChangePasswordSerializer,
UserSerializer,
)
class SignupUserApiView(APIView):
def post(self, request):
serializer = UserRegistrationSerializer(data=request.data)
if serializer.is_valid():
password = serializer.validated_data["password"]
password_hash = bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
serializer.validated_data["password"] = password_hash
serializer.save()
return Response("ok", status=status.HTTP_201_CREATED)
raise ValidationError(serializer.errors)
class SignupUserApiView(CreateAPIView):
http_method_names = ("post",)
serializer_class = UserSerializer
class SigninUserApiView(APIView):
def post(self, request):
serializer = UserLoginSerializer(data=request.data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)
username = serializer.validated_data.get("username")
password = serializer.validated_data.get("password")
user = User.objects.filter(username=username).first()
if user is not None:
if not bcrypt.checkpw(
password.encode("utf-8"), user.password.encode("utf-8")
):
raise NotAuthenticated(
{"error": "Invalid credentials"},
)
else:
raise NotAuthenticated(
{"error": "Invalid credentials"},
)
token = jwt.encode(
{
"id": user.id,
"password": password,
"exp": timezone.now() + timedelta(hours=24),
},
settings.SECRET_KEY,
algorithm="HS256",
)
return Response({"token": token})
class ProfileMeApiView(APIView):
class ProfileMeApiView(UpdateAPIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserProfileSerializer(request.user)
return Response(serializer.data)
def patch(self, request):
user = request.user
serializer = UpdateProfileSerializer(
user, data=request.data, partial=True
)
if serializer.is_valid():
errors = User.check_unique(user.id, serializer.validated_data)
if errors:
return Response(
{"reason:": str(errors)}, status=status.HTTP_409_CONFLICT
)
serializer.save()
return Response(self._get_profile_data(user))
raise ValidationError(serializer.errors)
def get_object(self):
return self.request.user
class PasswordChangeApiView(APIView):
class PasswordChangeApiView(UpdateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = ChangePasswordSerializer
def post(self, request):
serializer = PasswordChangeSerializer(data=request.data)
if serializer.is_valid():
old_password = serializer.validated_data.get("oldPassword")
new_password = serializer.validated_data.get("newPassword")
if bcrypt.checkpw(
old_password.encode("utf-8"),
request.user.password.encode("utf-8"),
):
password_hash = bcrypt.hashpw(
new_password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
request.user.password = password_hash
request.user.save()
return Response({"status": "ok"}, status=status.HTTP_200_OK)
raise PermissionDenied({"error": "Invalid old password"})
raise ValidationError(serializer.errors)
def get_object(self):
return self.request.user
+14 -12
View File
@@ -1,5 +1,6 @@
import pathlib
import sys
from datetime import timedelta
import environs
@@ -54,7 +55,6 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
@@ -130,15 +130,12 @@ AUTH_PASSWORD_VALIDATORS = [
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True
STATIC_URL = "static/"
USE_TZ = True
TIME_ZONE = "UTC"
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -149,15 +146,20 @@ REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend"
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"api.users.authentication.JWTAuthentication",
),
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
APPEND_SLASH = False
CORS_ORIGIN_ALLOW_ALL = True
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
}
if DEBUG and not (TESTING or MIGRATING):
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
-7
View File
@@ -5,13 +5,6 @@ from django.urls import include, path
urlpatterns = [
# Built-in urls
path("admin/", admin.site.urls),
path(
"api-auth/",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
# API
path("api/", include("api.urls")),
]