Reoraganized project
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api"
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CountriesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api.countries"
|
||||
@@ -0,0 +1,15 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Country(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
alpha2 = models.CharField(max_length=2)
|
||||
alpha3 = models.CharField(max_length=3)
|
||||
region = models.CharField()
|
||||
|
||||
class Meta:
|
||||
db_table = "countries"
|
||||
managed = False
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.countries.models import Country
|
||||
|
||||
|
||||
class CountrySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Country
|
||||
fields = ["name", "alpha2", "alpha3", "region"]
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
import api.countries.views
|
||||
|
||||
urlpatterns = [
|
||||
path("", api.countries.views.CountryListView.as_view(), name="countries"),
|
||||
path(
|
||||
"/<str:alpha2>",
|
||||
api.countries.views.CountryByAlpha2View.as_view(),
|
||||
name="country_by_alpha2",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||
|
||||
from api.countries.models import Country
|
||||
from api.countries.serializers import CountrySerializer
|
||||
|
||||
|
||||
class CountryListView(ListAPIView):
|
||||
queryset = Country.objects.all().order_by("alpha2")
|
||||
serializer_class = CountrySerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
regions = self.request.query_params.get("region")
|
||||
if regions:
|
||||
regions_list = regions.split(",")
|
||||
invalid_regions = [
|
||||
region
|
||||
for region in regions_list
|
||||
if region not in settings.REGIONS
|
||||
]
|
||||
if invalid_regions:
|
||||
invalid_regions_str = ", ".join(invalid_regions)
|
||||
error_message = f"Invalid region(s): {invalid_regions_str}"
|
||||
raise ValidationError(error_message)
|
||||
|
||||
queryset = queryset.filter(region__in=regions_list)
|
||||
return queryset
|
||||
|
||||
|
||||
class CountryByAlpha2View(RetrieveAPIView):
|
||||
queryset = Country.objects.all()
|
||||
serializer_class = CountrySerializer
|
||||
lookup_field = "alpha2"
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PingConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api.ping"
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
import api.ping.views
|
||||
|
||||
urlpatterns = [
|
||||
path("", api.ping.views.PingView.as_view(), name="ping"),
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
class PingView(APIView):
|
||||
def get(self, request):
|
||||
data = {"message": "ok"}
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("ping", include("api.ping.urls")),
|
||||
path("countries", include("api.countries.urls")),
|
||||
path("", include("api.users.urls")),
|
||||
]
|
||||
@@ -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,43 @@
|
||||
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 Profile
|
||||
|
||||
|
||||
class JWTAuthentication(BaseAuthentication):
|
||||
def authenticate_header(self, request):
|
||||
return "Provide a valid token in the 'Authorization' header"
|
||||
|
||||
def authenticate(self, request):
|
||||
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
|
||||
|
||||
if not token:
|
||||
if IsAuthenticated in getattr(
|
||||
request.resolver_match.func.cls, "permission_classes", []
|
||||
):
|
||||
raise NotAuthenticated
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=["HS256"]
|
||||
)
|
||||
|
||||
user = Profile.objects.get(id=payload["id"])
|
||||
except Profile.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,29 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-02 10:14
|
||||
|
||||
import api.users.validators
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('login', models.CharField(max_length=30, validators=[django.core.validators.RegexValidator('^[a-zA-Z0-9-]+$')])),
|
||||
('email', models.EmailField(max_length=50)),
|
||||
('password', models.CharField(max_length=100, validators=[django.core.validators.RegexValidator('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$')])),
|
||||
('countryCode', models.CharField(max_length=2, validators=[django.core.validators.RegexValidator('[a-zA-Z]{2}'), api.users.validators.CountryCodeValidator()])),
|
||||
('isPublic', models.BooleanField()),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator('\\+[\\d]+')])),
|
||||
('image', models.URLField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
from django.core.validators import (
|
||||
MaxLengthValidator,
|
||||
RegexValidator,
|
||||
)
|
||||
from django.db import models
|
||||
|
||||
from api.users.validators import CountryCodeValidator
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
login = models.CharField(
|
||||
max_length=30,
|
||||
validators=[RegexValidator(r"^[a-zA-Z0-9-]+$")],
|
||||
)
|
||||
email = models.EmailField(max_length=50)
|
||||
password = models.CharField(
|
||||
max_length=100,
|
||||
validators=[
|
||||
RegexValidator(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$"),
|
||||
],
|
||||
)
|
||||
# ruff: noqa: DJ001 N815
|
||||
countryCode = models.CharField(
|
||||
max_length=2,
|
||||
validators=[RegexValidator(r"[a-zA-Z]{2}"), CountryCodeValidator()],
|
||||
)
|
||||
isPublic = models.BooleanField()
|
||||
phone = models.CharField(
|
||||
max_length=20,
|
||||
validators=[MaxLengthValidator(20), RegexValidator(r"\+[\d]+")],
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
image = models.URLField(max_length=200, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.login
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_unique(cls, user_id, validated_data):
|
||||
errors = {}
|
||||
|
||||
if (
|
||||
cls.objects.filter(login=validated_data.get("login"))
|
||||
.exclude(id=user_id)
|
||||
.exists()
|
||||
):
|
||||
errors["login"] = {"User with this login already exists"}
|
||||
|
||||
if (
|
||||
cls.objects.filter(email=validated_data.get("email"))
|
||||
.exclude(id=user_id)
|
||||
.exists()
|
||||
):
|
||||
errors["email"] = {"User with this email already exists"}
|
||||
|
||||
if (
|
||||
cls.objects.filter(phone=validated_data.get("phone"))
|
||||
.exclude(id=user_id)
|
||||
.exists()
|
||||
):
|
||||
errors["phone"] = {"User with this phone already exists"}
|
||||
|
||||
return errors
|
||||
@@ -0,0 +1,30 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.users.models import Profile
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = [
|
||||
"login",
|
||||
"email",
|
||||
"password",
|
||||
"countryCode",
|
||||
"isPublic",
|
||||
"phone",
|
||||
"image",
|
||||
]
|
||||
|
||||
|
||||
class UpdateProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = [
|
||||
"login",
|
||||
"email",
|
||||
"countryCode",
|
||||
"isPublic",
|
||||
"phone",
|
||||
"image",
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.urls import path
|
||||
|
||||
import api.users.views
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"auth/register",
|
||||
api.users.views.RegisterUserApiView.as_view(),
|
||||
name="register",
|
||||
),
|
||||
path(
|
||||
"auth/sign-in",
|
||||
api.users.views.SigninUserApiView.as_view(),
|
||||
name="sign-in",
|
||||
),
|
||||
path(
|
||||
"me/profile",
|
||||
api.users.views.ProfileMeApiView.as_view(),
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.deconstruct import deconstructible
|
||||
|
||||
from api.countries.models import Country
|
||||
|
||||
|
||||
@deconstructible
|
||||
class CountryCodeValidator(BaseValidator):
|
||||
def __init__(self, message=None):
|
||||
self.message = message or "There is no such country"
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
Country.objects.get(alpha2=value)
|
||||
except Country.DoesNotExist:
|
||||
raise ValidationError(self.message) from None
|
||||
@@ -0,0 +1,118 @@
|
||||
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.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from api.users.models import Profile
|
||||
from api.users.serializers import ProfileSerializer, UpdateProfileSerializer
|
||||
|
||||
|
||||
class RegisterUserApiView(APIView):
|
||||
def post(self, request):
|
||||
serializer = ProfileSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
errors = Profile.check_unique(None, serializer.validated_data)
|
||||
if errors:
|
||||
return Response(errors, status=status.HTTP_409_CONFLICT)
|
||||
|
||||
password = serializer.validated_data["password"]
|
||||
password_hash = bcrypt.hashpw(
|
||||
password.encode("utf-8"), bcrypt.gensalt()
|
||||
).decode("utf-8")
|
||||
serializer.validated_data["password"] = password_hash
|
||||
|
||||
user = serializer.save()
|
||||
|
||||
profile = {
|
||||
"profile": {
|
||||
"login": user.login,
|
||||
"email": user.email,
|
||||
"countryCode": user.countryCode,
|
||||
"isPublic": user.isPublic,
|
||||
}
|
||||
}
|
||||
if user.phone is not None:
|
||||
profile["profile"]["phone"] = user.phone
|
||||
if user.image is not None:
|
||||
profile["profile"]["image"] = user.image
|
||||
|
||||
return Response(profile, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class SigninUserApiView(APIView):
|
||||
def post(self, request):
|
||||
login = request.data.get("login")
|
||||
password = request.data.get("password")
|
||||
user = Profile.objects.filter(login=login).first()
|
||||
|
||||
if user is not None:
|
||||
if not bcrypt.checkpw(
|
||||
password.encode("utf-8"), user.password.encode("utf-8")
|
||||
):
|
||||
return Response(
|
||||
{"error": "Invalid credentials"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Invalid credentials"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
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):
|
||||
user = request.user
|
||||
profile_data = self._get_profile_data(user)
|
||||
return Response(profile_data)
|
||||
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
serializer = UpdateProfileSerializer(
|
||||
user, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
errors = Profile.check_unique(user.id, serializer.validated_data)
|
||||
if errors:
|
||||
return Response(errors, status=status.HTTP_409_CONFLICT)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def _get_profile_data(self, user):
|
||||
profile = {
|
||||
"login": user.login,
|
||||
"email": user.email,
|
||||
"countryCode": user.countryCode,
|
||||
"isPublic": user.isPublic,
|
||||
}
|
||||
|
||||
if user.phone is not None:
|
||||
profile["phone"] = user.phone
|
||||
if user.image is not None:
|
||||
profile["image"] = user.image
|
||||
|
||||
return profile
|
||||
Reference in New Issue
Block a user