[chore] Global project refactoring

This commit is contained in:
ITQ
2024-04-01 11:20:07 +03:00
parent 0a8b3773f5
commit 5c64e1f3b9
49 changed files with 731 additions and 496 deletions
+5
View File
@@ -0,0 +1,5 @@
from django.contrib import admin
from api.users.models import User
admin.site.register(User)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.users"
@@ -0,0 +1,51 @@
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)
@@ -0,0 +1,238 @@
# Generated by Django 4.2.11 on 2024-04-01 01:58
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
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
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)),
],
),
migrations.CreateModel(
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),
]
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
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)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
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",
),
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]
+94
View File
@@ -0,0 +1,94 @@
import uuid
from django.contrib.auth.models import AbstractUser
from django.core import validators
from django.db import models
from api.core.models import AbstractTag
class Skill(AbstractTag):
level = models.IntegerField(
validators=[
validators.MinValueValidator(1),
validators.MaxValueValidator(10),
],
)
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,
)
info = models.TextField(
max_length=255,
)
def __str__(self): # noqa: DJ012
return self.info
class Specialization(AbstractTag):
pass
class User(AbstractUser):
def get_file_path(self, filename):
folder_name = str(uuid.uuid4())
return f"avatars/{folder_name}/{filename}"
email = models.EmailField(unique=True)
birthday = models.DateField(
blank=True,
null=True,
)
avatar = models.ImageField(
upload_to=get_file_path,
max_length=200,
null=True,
)
country = models.TextField(
blank=True,
)
city = models.TextField(
blank=True,
)
experience = models.IntegerField(
validators=[
validators.MinValueValidator(0),
validators.MinValueValidator(100),
],
null=True,
)
bio = models.TextField(
blank=True,
validators=[
validators.MaxLengthValidator(
512,
),
],
)
skills = models.ManyToManyField(
Skill,
blank=True,
)
achievements = models.ManyToManyField(
Achievements,
blank=True,
)
specialization = models.ForeignKey(
Specialization,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
def __str__(self):
return self.username
+79
View File
@@ -0,0 +1,79 @@
from django.contrib.auth.password_validation import validate_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 Meta:
model = User
fields = (
"first_name",
"last_name",
"username",
"email",
"birthday",
"country",
"city",
"bio",
"avatar",
"experience",
"specialization",
"achievements",
"skills",
)
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)
return value
+26
View File
@@ -0,0 +1,26 @@
from django.urls import path
import api.users.views
urlpatterns = [
path(
"/sign-up/",
api.users.views.SignupUserApiView.as_view(),
name="sign-up",
),
path(
"/sign-in/",
api.users.views.SigninUserApiView.as_view(),
name="sign-in",
),
path(
"/me/profile/",
api.users.views.ProfileMeApiView.as_view(),
name="profile-me",
),
path(
"/me/updatePassword/",
api.users.views.PasswordChangeApiView.as_view(),
name="password-change",
),
]
+131
View File
@@ -0,0 +1,131 @@
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.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,
)
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 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):
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)
class PasswordChangeApiView(APIView):
permission_classes = [IsAuthenticated]
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)