Add basic login.
authorRadek Czajka <rczajka@rczajka.pl>
Fri, 8 Nov 2024 10:26:32 +0000 (11:26 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Fri, 8 Nov 2024 10:30:08 +0000 (11:30 +0100)
src/api/drf_auth.py
src/api/migrations/0007_alter_token_consumer.py [new file with mode: 0644]
src/api/models.py
src/api/serializers.py
src/api/urls.py
src/api/views.py
src/wolnelektury/settings/contrib.py

index adee780..813d929 100644 (file)
@@ -2,9 +2,10 @@
 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
 #
 from oauthlib.oauth1 import ResourceEndpoint
-from rest_framework.authentication import BaseAuthentication
+from rest_framework.authentication import BaseAuthentication, TokenAuthentication
 from .request_validator import PistonRequestValidator
 from .utils import oauthlib_request
+from .models import Token
 
 
 class PistonOAuthAuthentication(BaseAuthentication):
@@ -21,3 +22,7 @@ class PistonOAuthAuthentication(BaseAuthentication):
         )
         if v:
             return r.token.user, r.token
+
+
+class WLTokenAuthentication(TokenAuthentication):
+    model = Token
diff --git a/src/api/migrations/0007_alter_token_consumer.py b/src/api/migrations/0007_alter_token_consumer.py
new file mode 100644 (file)
index 0000000..410516a
--- /dev/null
@@ -0,0 +1,19 @@
+# Generated by Django 4.0.8 on 2024-11-08 10:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0006_alter_bookuserdata_unique_together'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='token',
+            name='consumer',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.consumer'),
+        ),
+    ]
index 04d08e9..ff1f09a 100644 (file)
@@ -112,7 +112,7 @@ class Token(models.Model):
         settings.AUTH_USER_MODEL, models.CASCADE,
         null=True, blank=True, related_name='tokens'
     )
-    consumer = models.ForeignKey(Consumer, models.CASCADE)
+    consumer = models.ForeignKey(Consumer, models.CASCADE, null=True, blank=True)
 
     def __str__(self):
         return "%s Token %s for %s" % (self.get_token_type_display(), self.key, self.consumer)
index e0bf0ee..dae587c 100644 (file)
@@ -25,3 +25,8 @@ class BookUserDataSerializer(serializers.ModelSerializer):
     class Meta:
         model = BookUserData
         fields = ['state']
+
+
+class LoginSerializer(serializers.Serializer):
+    username = serializers.CharField()
+    password = serializers.CharField(style={'input_type': 'password'})
index 1f05e2d..703283e 100644 (file)
@@ -13,6 +13,7 @@ urlpatterns = [
     path('oauth/request_token/', csrf_exempt(views.OAuth1RequestTokenView.as_view())),
     path('oauth/authorize/', views.oauth_user_auth, name='oauth_user_auth'),
     path('oauth/access_token/', csrf_exempt(views.OAuth1AccessTokenView.as_view())),
+    path('login/', csrf_exempt(views.LoginView.as_view())),
 
     path('', TemplateView.as_view(template_name='api/main.html'), name='api'),
 
index 64a7eb0..622332f 100644 (file)
@@ -1,21 +1,23 @@
 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
 #
+from time import time
+from django.contrib.auth import authenticate
 from django.contrib.auth.decorators import login_required
 from django import forms
 from django.http import HttpResponse
 from django.http import Http404
 from django.shortcuts import render
 from django.views.generic.base import View
-from oauthlib.common import urlencode
+from oauthlib.common import urlencode, generate_token
 from oauthlib.oauth1 import RequestTokenEndpoint, AccessTokenEndpoint
 from oauthlib.oauth1 import AuthorizationEndpoint, OAuth1Error
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.views import APIView
-from rest_framework.generics import RetrieveAPIView, get_object_or_404
+from rest_framework.generics import GenericAPIView, RetrieveAPIView, get_object_or_404
 from catalogue.models import Book
-from .models import BookUserData, KEY_SIZE, SECRET_SIZE
+from .models import BookUserData, KEY_SIZE, SECRET_SIZE, Token
 from . import serializers
 from .request_validator import PistonRequestValidator
 from .utils import oauthlib_request, oauthlib_response, vary_on_auth
@@ -131,6 +133,27 @@ class OAuth1AccessTokenView(View):
         )
 
 
+class LoginView(GenericAPIView):
+    serializer_class = serializers.LoginSerializer
+
+    def post(self, request):
+        serializer = self.get_serializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        d = serializer.validated_data
+        user = authenticate(username=d['username'], password=d['password'])
+        if user is None:
+            return Response({"detail": "Invalid credentials."})
+
+        key = generate_token()[:KEY_SIZE]
+        Token.objects.create(
+            key=key,
+            token_type=Token.ACCESS,
+            timestamp=time(),
+            user=user,
+        )
+        return Response({"access_token": key})
+
+
 @vary_on_auth
 class UserView(RetrieveAPIView):
     permission_classes = [IsAuthenticated]
index d03479f..aaef8b2 100644 (file)
@@ -28,6 +28,7 @@ REST_FRAMEWORK = {
         'api.renderers.LegacyXMLRenderer',
     ),
     'DEFAULT_AUTHENTICATION_CLASSES': (
+        'api.drf_auth.WLTokenAuthentication',
         'api.drf_auth.PistonOAuthAuthentication',
         'rest_framework.authentication.SessionAuthentication',
     )