Merge branch 'fix-auth' into develop

This commit is contained in:
Data-Name-ID
2024-04-01 17:49:10 +03:00
9 changed files with 137 additions and 449 deletions
+7
View File
@@ -12,6 +12,13 @@ schema_view = get_schema_view(
urlpatterns = [ urlpatterns = [
path("ping", include("api.ping.urls")), path("ping", include("api.ping.urls")),
path("auth", include("api.users.urls")), path("auth", include("api.users.urls")),
path(
"",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
# API documentation # API documentation
path( path(
"swagger<format>/", "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 api.users.models
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
import django.core.validators import django.core.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -14,225 +14,72 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("auth", "0012_alter_user_first_name_max_length"), ('auth', '0012_alter_user_first_name_max_length'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Achievements", name='Achievements',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('file', models.FileField(upload_to=api.users.models.get_file_path)),
models.BigAutoField( ('info', models.TextField(max_length=255)),
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)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="Skill", name='Skill',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255, unique=True)),
models.BigAutoField( ('level', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
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={ options={
"abstract": False, 'abstract': False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="Specialization", name='Specialization',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255, unique=True)),
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
], ],
options={ options={
"abstract": False, 'abstract': False,
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="User", name='User',
fields=[ fields=[
( ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('password', models.CharField(max_length=128, verbose_name='password')),
models.BigAutoField( ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
auto_created=True, ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
primary_key=True, ('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')),
serialize=False, ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
verbose_name="ID", ('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')),
("password", models.CharField(max_length=128, verbose_name="password")), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
( ('email', models.EmailField(max_length=254, unique=True)),
"last_login", ('birthday', models.DateField(blank=True, null=True)),
models.DateTimeField( ('avatar', models.ImageField(max_length=200, null=True, upload_to=api.users.models.User.get_file_path)),
blank=True, null=True, verbose_name="last login" ('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)])),
"is_superuser", ('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')),
models.BooleanField( ('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')),
default=False, ('achievements', models.ManyToManyField(blank=True, to='users.achievements')),
help_text="Designates that this user has all permissions without explicitly assigning them.", ('skills', models.ManyToManyField(blank=True, to='users.skill')),
verbose_name="superuser status", ('specialization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.specialization')),
),
),
(
"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",
),
),
], ],
options={ options={
"verbose_name": "user", 'verbose_name': 'user',
"verbose_name_plural": "users", 'verbose_name_plural': 'users',
"abstract": False, 'abstract': False,
}, },
managers=[ 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 from api.core.models import AbstractTag
def get_file_path(filename):
return f"achievements/{uuid.uuid4()}/{filename}"
class Skill(AbstractTag): class Skill(AbstractTag):
level = models.IntegerField( level = models.IntegerField(
validators=[ validators=[
@@ -17,18 +21,12 @@ class Skill(AbstractTag):
class Achievements(models.Model): class Achievements(models.Model):
def get_file_path(self, filename): file = models.FileField(upload_to=get_file_path)
folder_name = str(uuid.uuid4())
return f"achievements/{folder_name}/{filename}"
file = models.FileField( # noqa: DJ012
upload_to=get_file_path,
)
info = models.TextField( info = models.TextField(
max_length=255, max_length=255,
) )
def __str__(self): # noqa: DJ012 def __str__(self):
return self.info 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 rest_framework import serializers
from api.users.models import User from api.users.models import User
class UserRegistrationSerializer(serializers.ModelSerializer): class UserSerializer(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 Meta: class Meta:
model = User model = User
fields = ( fields = (
"first_name",
"last_name",
"username",
"email", "email",
"birthday", "birthday",
"avatar",
"country", "country",
"city", "city",
"bio", "bio",
"avatar",
"experience", "experience",
"password",
"first_name",
"last_name",
"specialization", "specialization",
"achievements", "achievements",
"username",
"skills", "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: class Meta:
model = User model = User
fields = [ fields = ("old_password", "new_password")
"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)
def validate_old_password(self, value):
if not check_password(value, self.instance.password):
msg = "Wrong password"
raise serializers.ValidationError(msg)
return value return value
def update(self, instance, validated_data):
instance.set_password(validated_data["password"])
instance.save()
return instance
+18 -3
View File
@@ -1,4 +1,9 @@
from django.urls import path from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
import api.users.views import api.users.views
@@ -9,9 +14,19 @@ urlpatterns = [
name="signup", name="signup",
), ),
path( path(
"/login/", "/sign-in/",
api.users.views.LoginUserApiView.as_view(), TokenObtainPairView.as_view(),
name="login", name="sign-in",
),
path(
"api/token/refresh/",
TokenRefreshView.as_view(),
name="token_refresh",
),
path(
"api/token/verify/",
TokenVerifyView.as_view(),
name="token_verify",
), ),
path( path(
"/me/profile/", "/me/profile/",
+13 -117
View File
@@ -1,131 +1,27 @@
from datetime import timedelta from rest_framework.generics import CreateAPIView, UpdateAPIView
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.permissions import IsAuthenticated 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 ( from api.users.serializers import (
PasswordChangeSerializer, ChangePasswordSerializer,
UpdateProfileSerializer, UserSerializer,
UserLoginSerializer,
UserProfileSerializer,
UserRegistrationSerializer,
) )
class SignupUserApiView(APIView): class SignupUserApiView(CreateAPIView):
def post(self, request): http_method_names = ("post",)
serializer = UserRegistrationSerializer(data=request.data) serializer_class = UserSerializer
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 LoginUserApiView(APIView): class ProfileMeApiView(UpdateAPIView):
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):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request): def get_object(self):
serializer = UserProfileSerializer(request.user) return self.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)
class PasswordChangeApiView(APIView): class PasswordChangeApiView(UpdateAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = ChangePasswordSerializer
def post(self, request): def get_object(self):
serializer = PasswordChangeSerializer(data=request.data) return self.request.user
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)
+14 -12
View File
@@ -1,5 +1,6 @@
import pathlib import pathlib
import sys import sys
from datetime import timedelta
import environs import environs
@@ -54,7 +55,6 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
@@ -130,15 +130,12 @@ AUTH_PASSWORD_VALIDATORS = [
] ]
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True USE_I18N = True
STATIC_URL = "static/" USE_TZ = True
TIME_ZONE = "UTC"
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static" STATIC_ROOT = BASE_DIR / "static"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -149,15 +146,20 @@ REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [ "DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend" "django_filters.rest_framework.DjangoFilterBackend"
], ],
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": [
"api.users.authentication.JWTAuthentication", "rest_framework_simplejwt.authentication.JWTAuthentication",
), ],
} }
APPEND_SLASH = False
CORS_ORIGIN_ALLOW_ALL = True 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): if DEBUG and not (TESTING or MIGRATING):
INSTALLED_APPS.append("debug_toolbar") INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
-7
View File
@@ -5,13 +5,6 @@ from django.urls import include, path
urlpatterns = [ urlpatterns = [
# Built-in urls # Built-in urls
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path(
"api-auth/",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
# API # API
path("api/", include("api.urls")), path("api/", include("api.urls")),
] ]