From ca8f4e8fef80cb603117ed579da8554f503698e5 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Nov 2024 11:26:32 +0100 Subject: [PATCH] Add basic login. --- src/api/drf_auth.py | 7 ++++- .../migrations/0007_alter_token_consumer.py | 19 ++++++++++++ src/api/models.py | 2 +- src/api/serializers.py | 5 ++++ src/api/urls.py | 1 + src/api/views.py | 29 +++++++++++++++++-- src/wolnelektury/settings/contrib.py | 1 + 7 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/api/migrations/0007_alter_token_consumer.py diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py index adee780d3..813d9298e 100644 --- a/src/api/drf_auth.py +++ b/src/api/drf_auth.py @@ -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 index 000000000..410516a4b --- /dev/null +++ b/src/api/migrations/0007_alter_token_consumer.py @@ -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'), + ), + ] diff --git a/src/api/models.py b/src/api/models.py index 04d08e96d..ff1f09a0d 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -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) diff --git a/src/api/serializers.py b/src/api/serializers.py index e0bf0eeee..dae587c84 100644 --- a/src/api/serializers.py +++ b/src/api/serializers.py @@ -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'}) diff --git a/src/api/urls.py b/src/api/urls.py index 1f05e2d51..703283ef7 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -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'), diff --git a/src/api/views.py b/src/api/views.py index 64a7eb0ee..622332f2f 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -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] diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index d03479f82..aaef8b2ce 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -28,6 +28,7 @@ REST_FRAMEWORK = { 'api.renderers.LegacyXMLRenderer', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'api.drf_auth.WLTokenAuthentication', 'api.drf_auth.PistonOAuthAuthentication', 'rest_framework.authentication.SessionAuthentication', ) -- 2.20.1