diff --git a/page/src/apps/account/api.py b/page/src/apps/account/api.py index cb32fe4..25e2788 100644 --- a/page/src/apps/account/api.py +++ b/page/src/apps/account/api.py @@ -1,10 +1,13 @@ from rest_framework import generics, permissions, status from rest_framework.response import Response -from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.views import ( + TokenObtainPairView, +) from apps.core.models import User from .serializers import RegistrationSerializer +from .services.auth_service import AuthService class RegistrationApi(generics.CreateAPIView): @@ -15,13 +18,30 @@ class RegistrationApi(generics.CreateAPIView): def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - #user = self.perform_create(serializer) - user = serializer.save() - refresh = RefreshToken.for_user(user) + try: + data = AuthService.register(**serializer.validated_data) + return Response( + data, + status=status.HTTP_201_CREATED + ) + except Exception as e: + return Response( + {"error": str(e)}, + status=status.HTTP_400_BAD_REQUEST + ) + + +class LoginView(TokenObtainPairView): + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + login_data = AuthService.login( + username=request.data['username'], + password=request.data['password'] + ) + if login_data: + return Response(login_data) return Response( - { - "refresh": str(refresh), - "access": str(refresh.access_token), - }, - status=status.HTTP_201_CREATED - ) \ No newline at end of file + {'error': 'Invalid credentials'}, + status=status.HTTP_401_UNAUTHORIZED + ) diff --git a/page/src/apps/account/repos/user_repo.py b/page/src/apps/account/repos/user_repo.py new file mode 100644 index 0000000..fde5033 --- /dev/null +++ b/page/src/apps/account/repos/user_repo.py @@ -0,0 +1,28 @@ +from django.core.exceptions import ObjectDoesNotExist + +from apps.core.models import User + + +class UserRepo: + @staticmethod + def create_user( + username: str, email: str, password: str + ) -> User: + user = User( + username=username, + email=email, + password=password + ) + user.set_password(password) + user.save() + return user + + @staticmethod + def get_user_by_username( + username: str + ) -> User: + try: + user = User.objects.get(username=username) + return user + except ObjectDoesNotExist: + return None diff --git a/page/src/apps/account/services/auth_service.py b/page/src/apps/account/services/auth_service.py new file mode 100644 index 0000000..7c988a8 --- /dev/null +++ b/page/src/apps/account/services/auth_service.py @@ -0,0 +1,41 @@ +from typing import Optional + +from django.contrib.auth import authenticate +from rest_framework_simplejwt.tokens import RefreshToken + +from apps.core.models import User + +from ..repos.user_repo import UserRepo + + +class AuthService: + @staticmethod + def register( + username: str, email: str, password: str, password2: str + ) -> dict: + if password != password2: + return {"error": "Passwords do not match"} + + user: User = UserRepo.create_user(username, email, password) + refresh = RefreshToken.for_user(user) + return { + "refresh": str(refresh), + "access": str(refresh.access_token), + } + + @staticmethod + def login( + username: str, password: str + ) -> dict: + user: Optional[User] = authenticate(username=username, password=password) + if not user: + return {"error": "User not found"} + + if not user.check_password(password): + return {"error": "Invalid password"} + + refresh = RefreshToken.for_user(user) + return { + "refresh": str(refresh), + "access": str(refresh.access_token), + } diff --git a/page/src/apps/account/urls.py b/page/src/apps/account/urls.py index ad0b78e..14924a3 100644 --- a/page/src/apps/account/urls.py +++ b/page/src/apps/account/urls.py @@ -1,7 +1,16 @@ from django.urls import path +from rest_framework_simplejwt.views import ( + TokenRefreshView, + TokenVerifyView, +) -from .api import RegistrationApi +from .api import LoginView, RegistrationApi +from .views import ProtectedView urlpatterns = [ - path("api/register", RegistrationApi.as_view(), name="register"), + path("api/register/", RegistrationApi.as_view(), name="register"), + path('api/login/', LoginView.as_view(), name='login'), + path('api/login/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path("api/protected/", ProtectedView.as_view(), name="protected"), ] diff --git a/page/src/apps/account/views.py b/page/src/apps/account/views.py index 8b13789..d905177 100644 --- a/page/src/apps/account/views.py +++ b/page/src/apps/account/views.py @@ -1 +1,12 @@ +from rest_framework import generics, permissions +from rest_framework.response import Response + +class ProtectedView(generics.RetrieveAPIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, *args, **kwargs): + content = { + 'message': 'This is a protected view, only accessible with a valid token.' + } + return Response(content) diff --git a/page/src/apps/core/migrations/0001_initial.py b/page/src/apps/core/migrations/0001_initial.py index 3c36f1d..974448e 100644 --- a/page/src/apps/core/migrations/0001_initial.py +++ b/page/src/apps/core/migrations/0001_initial.py @@ -1,7 +1,8 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:28 +# Generated by Django 4.1.9 on 2025-04-18 16:24 import uuid +import django.contrib.auth.models import django.contrib.auth.validators import django.db.models.deletion import django.utils.timezone @@ -13,9 +14,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'), ('auth', '0012_alter_user_first_name_max_length'), ('contenttypes', '0002_remove_content_type_name'), - ('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'), ] operations = [ @@ -58,7 +59,12 @@ class Migration(migrations.Migration): ('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', 'ordering': ['username'], }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], ), ] diff --git a/page/src/apps/core/migrations/0002_source.py b/page/src/apps/core/migrations/0002_source.py index bfa378d..50e67cf 100644 --- a/page/src/apps/core/migrations/0002_source.py +++ b/page/src/apps/core/migrations/0002_source.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:33 +# Generated by Django 4.1.9 on 2025-04-18 16:48 import uuid diff --git a/page/src/apps/core/migrations/0003_state.py b/page/src/apps/core/migrations/0003_state.py index c47195c..ccbcb16 100644 --- a/page/src/apps/core/migrations/0003_state.py +++ b/page/src/apps/core/migrations/0003_state.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:35 +# Generated by Django 4.1.9 on 2025-04-18 16:49 import uuid @@ -16,6 +16,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='State', fields=[ + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('restored_at', models.DateTimeField(blank=True, null=True)), + ('transaction_id', models.UUIDField(blank=True, null=True)), ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('ref', models.UUIDField(editable=False)), ('state', models.CharField(max_length=32)), diff --git a/page/src/apps/core/migrations/0004_code.py b/page/src/apps/core/migrations/0004_code.py index 79a4d1b..d7e190c 100644 --- a/page/src/apps/core/migrations/0004_code.py +++ b/page/src/apps/core/migrations/0004_code.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:39 +# Generated by Django 4.1.9 on 2025-04-18 16:50 import uuid diff --git a/page/src/apps/core/migrations/0005_language.py b/page/src/apps/core/migrations/0005_language.py index a54fc4d..d3dc1b9 100644 --- a/page/src/apps/core/migrations/0005_language.py +++ b/page/src/apps/core/migrations/0005_language.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:40 +# Generated by Django 4.1.9 on 2025-04-18 16:50 import uuid diff --git a/page/src/apps/core/migrations/0006_coordinate.py b/page/src/apps/core/migrations/0006_coordinate.py index ce36eba..6a8e2fc 100644 --- a/page/src/apps/core/migrations/0006_coordinate.py +++ b/page/src/apps/core/migrations/0006_coordinate.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:46 +# Generated by Django 4.1.9 on 2025-04-18 16:51 import uuid diff --git a/page/src/apps/core/migrations/0007_location.py b/page/src/apps/core/migrations/0007_location.py index a325ba4..4533199 100644 --- a/page/src/apps/core/migrations/0007_location.py +++ b/page/src/apps/core/migrations/0007_location.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 16:55 +# Generated by Django 4.1.9 on 2025-04-18 16:51 import uuid diff --git a/page/src/apps/core/migrations/0008_address.py b/page/src/apps/core/migrations/0008_address.py index 03af989..caf18a3 100644 --- a/page/src/apps/core/migrations/0008_address.py +++ b/page/src/apps/core/migrations/0008_address.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-06 18:32 +# Generated by Django 4.1.9 on 2025-04-18 16:51 import uuid diff --git a/page/src/apps/core/migrations/0009_url.py b/page/src/apps/core/migrations/0009_url.py index 10fcbef..d11b629 100644 --- a/page/src/apps/core/migrations/0009_url.py +++ b/page/src/apps/core/migrations/0009_url.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-13 16:52 +# Generated by Django 4.1.9 on 2025-04-18 16:52 import uuid @@ -30,8 +30,8 @@ class Migration(migrations.Migration): ('language', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='urls', to='core.language')), ], options={ - 'verbose_name': 'URL', - 'verbose_name_plural': 'URLs', + 'verbose_name': 'Url', + 'verbose_name_plural': 'Urls', 'ordering': ['url'], }, ), diff --git a/page/src/apps/core/migrations/0010_platform.py b/page/src/apps/core/migrations/0010_platform.py index 772496c..ef99a7e 100644 --- a/page/src/apps/core/migrations/0010_platform.py +++ b/page/src/apps/core/migrations/0010_platform.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.9 on 2025-04-13 17:31 +# Generated by Django 4.1.9 on 2025-04-18 16:53 import uuid diff --git a/page/src/apps/core/models.py b/page/src/apps/core/models.py index faa142a..8a147cd 100644 --- a/page/src/apps/core/models.py +++ b/page/src/apps/core/models.py @@ -10,13 +10,13 @@ from taggit.models import GenericUUIDTaggedItemBase, TaggedItemBase -class Tag(SoftDeleteModel, GenericUUIDTaggedItemBase, TaggedItemBase): +class Tag(GenericUUIDTaggedItemBase, TaggedItemBase, SoftDeleteModel): class Meta: verbose_name = _("Tag") verbose_name_plural = _("Tags") -class User(SoftDeleteModel, AbstractUser): +class User(AbstractUser, SoftDeleteModel): # Django's AbstractUser already includes the following fields by default: # - id (AutoField, primary key) # - password @@ -35,6 +35,8 @@ class User(SoftDeleteModel, AbstractUser): class Meta: ordering = ["username"] + verbose_name = _("User") + verbose_name_plural = _("Users") def __str__(self) -> str: return f"[User: {self.username} {self.email}]" @@ -59,7 +61,7 @@ def __str__(self) -> str: return f"[Source: {self.type}, {self.subtype}, {self.origin}, {self.source}]" -class State(models.Model): +class State(SoftDeleteModel): id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4) ref = models.UUIDField(editable=False, blank=False, null=False) source = models.ForeignKey( @@ -350,11 +352,11 @@ class Type(models.TextChoices): class Meta: ordering = ["url"] - verbose_name = _("URL") - verbose_name_plural = _("URLs") + verbose_name = _("Url") + verbose_name_plural = _("Urls") def __str__(self): - return f"[URL: {self.url}]" + return f"[Url: {self.url}]" class Platform(SoftDeleteModel):