diff --git a/shimatta_kenkyusho/api/ExpiringAuthToken.py b/shimatta_kenkyusho/api/ExpiringAuthToken.py new file mode 100644 index 0000000..a3ef535 --- /dev/null +++ b/shimatta_kenkyusho/api/ExpiringAuthToken.py @@ -0,0 +1,25 @@ +from datetime import timedelta +from django.conf import settings +from django.utils import timezone +from rest_framework.authentication import TokenAuthentication +from rest_framework.authtoken.models import Token +from rest_framework import exceptions +from django.core.exceptions import ObjectDoesNotExist + +EXPIRE_HOURS = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_HOURS', 24) + +class ExpiringTokenAuthentication(TokenAuthentication): + def authenticate_credentials(self, key): + print(key) + try: + token = Token.objects.get(key=key) + except Token.DoesNotExist: + raise exceptions.AuthenticationFailed('Invalid token') + + if not token.user.is_active: + raise exceptions.AuthenticationFailed('User inactive or deleted') + + if token.created < timezone.now() - timedelta(hours=EXPIRE_HOURS): + raise exceptions.AuthenticationFailed('Token has expired') + + return (token.user, token) \ No newline at end of file diff --git a/shimatta_kenkyusho/api/__init__.py b/shimatta_kenkyusho/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shimatta_kenkyusho/api/admin.py b/shimatta_kenkyusho/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/shimatta_kenkyusho/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/shimatta_kenkyusho/api/apps.py b/shimatta_kenkyusho/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/shimatta_kenkyusho/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/shimatta_kenkyusho/api/migrations/__init__.py b/shimatta_kenkyusho/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shimatta_kenkyusho/api/models.py b/shimatta_kenkyusho/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/shimatta_kenkyusho/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/shimatta_kenkyusho/api/serializers.py b/shimatta_kenkyusho/api/serializers.py new file mode 100644 index 0000000..3d22f2a --- /dev/null +++ b/shimatta_kenkyusho/api/serializers.py @@ -0,0 +1,52 @@ +from django.contrib.auth.models import User, Group +from rest_framework import serializers +from parts import models as parts_models + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ['username', 'email', 'first_name', 'last_name', 'groups'] + +class GroupSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Group + fields = ['url', 'id', 'url', 'name'] + +class PackageSerializerNoLink(serializers.ModelSerializer): + class Meta: + model = parts_models.Package + fields = '__all__' + +class PackageSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = parts_models.Package + fields = '__all__' + +class StorageSerializer(serializers.HyperlinkedModelSerializer): + full_path = serializers.ReadOnlyField(source='get_full_path') + + class Meta: + model = parts_models.Storage + fields = ['url', 'id', 'name', 'parent_storage', 'responsible', 'full_path'] + +class ComponentSerializer(serializers.HyperlinkedModelSerializer): + + package_data = PackageSerializerNoLink(source='package', read_only=True) + + class Meta: + model = parts_models.Component + fields = ['url', 'id', 'name', 'package_data', 'package', 'pref_distri'] + +class StockSerializer(serializers.HyperlinkedModelSerializer): + ro_package_name = serializers.ReadOnlyField(source='component.package.name') + ro_component_name = serializers.ReadOnlyField(source='component.name') + ro_manufacturer_name = serializers.ReadOnlyField(source='component.manufacturer.name') + ro_image = serializers.ReadOnlyField(source='component.get_resolved_image') + class Meta: + model = parts_models.Stock + fields = '__all__' + +class DistributorSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = parts_models.Distributor + fields = '__all__' diff --git a/shimatta_kenkyusho/api/tests.py b/shimatta_kenkyusho/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/shimatta_kenkyusho/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shimatta_kenkyusho/api/urls.py b/shimatta_kenkyusho/api/urls.py new file mode 100644 index 0000000..1b58c29 --- /dev/null +++ b/shimatta_kenkyusho/api/urls.py @@ -0,0 +1,19 @@ +from django.urls import include, path +from rest_framework import routers +from .views import * +from django.conf.urls import url + +router = routers.DefaultRouter() +router.register(r'users', UserViewSet) +router.register(r'groups', GroupViewSet) +router.register(r'parts/storages', PartsStorageViewSet) +router.register(r'parts/components', PartsComponentViewSet) +router.register(r'parts/stocks', PartsStockViewSet) +router.register(r'parts/packages', PartsPackageViewSet) +router.register(r'parts/distributors', PartsDistributorviewSet) + +urlpatterns = [ + path('', include(router.urls)), + url(r'^token-auth/', ObtainExpiringAuthToken.as_view()), + url(r'^token-logout/', TokenLogout.as_view()), +] \ No newline at end of file diff --git a/shimatta_kenkyusho/api/views.py b/shimatta_kenkyusho/api/views.py new file mode 100644 index 0000000..202f41b --- /dev/null +++ b/shimatta_kenkyusho/api/views.py @@ -0,0 +1,98 @@ +from django.shortcuts import render +from django.contrib.auth.models import User, Group +from rest_framework import viewsets, status +from rest_framework import permissions +from rest_framework.views import APIView +from .serializers import * +from parts import models as parts_models +from rest_framework.response import Response +from django.db.models.deletion import ProtectedError +from django.db.models import F +import datetime +from datetime import timedelta +from django.conf import settings +from django.utils import timezone +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.authtoken.models import Token +from rest_framework.throttling import AnonRateThrottle + +# Create your views here. +class UserViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + +class GroupViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + queryset = Group.objects.all() + serializer_class = GroupSerializer + permission_classes = [permissions.IsAuthenticated] + +class PartsStorageViewSet(viewsets.ModelViewSet): + queryset = parts_models.Storage.objects.all() + serializer_class = StorageSerializer + permission_classes = [permissions.DjangoModelPermissions] + +class PartsComponentViewSet(viewsets.ModelViewSet): + queryset = parts_models.Component.objects.all() + serializer_class = ComponentSerializer + permission_classes = [permissions.DjangoModelPermissions] + +class PartsStockViewSet(viewsets.ModelViewSet): + queryset = parts_models.Stock.objects.all() + serializer_class = StockSerializer + permission_classes = [permissions.DjangoModelPermissions] + +class PartsPackageViewSet(viewsets.ModelViewSet): + queryset = parts_models.Package.objects.all() + serializer_class = PackageSerializer + permission_classes = [permissions.DjangoModelPermissions] + +class PartsDistributorviewSet(viewsets.ModelViewSet): + queryset = parts_models.Distributor.objects.all() + serializer_class = DistributorSerializer + permission_classes = [permissions.DjangoModelPermissions] + + +## Token Authentication views + +EXPIRE_HOURS = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_HOURS', 24) + + +class ObtainExpiringAuthToken(ObtainAuthToken): + throttle_classes = [AnonRateThrottle] + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + try: + token = Token.objects.get(user=serializer.validated_data['user']) + if token.created < timezone.now() - timedelta(hours=EXPIRE_HOURS): + token.delete() + except Token.DoesNotExist: + pass + + token, created = Token.objects.get_or_create(user=serializer.validated_data['user']) + + if not created: + # update the created time of the token to keep it valid + token.created = datetime.datetime.utcnow() + token.save() + + return Response({'token': token.key, 'username': serializer.validated_data['user'].username, 'expiry': token.created + timedelta(hours=EXPIRE_HOURS)}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class TokenLogout(APIView): + def post(self, request, format=None): + try: + request.user.auth_token.delete() + except AttributeError as e: + pass + return Response(status=status.HTTP_200_OK) + def get(self, request, format=None): + return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) diff --git a/shimatta_kenkyusho/shimatta_kenkyusho/settings.py b/shimatta_kenkyusho/shimatta_kenkyusho/settings.py index 1b5d73b..a0ea56a 100644 --- a/shimatta_kenkyusho/shimatta_kenkyusho/settings.py +++ b/shimatta_kenkyusho/shimatta_kenkyusho/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'parts.apps.PartsConfig', 'qr_code', + 'rest_framework' ] MIDDLEWARE = [ @@ -113,6 +114,27 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'api.ExpiringAuthToken.ExpiringTokenAuthentication', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 10, + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle' + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '100/hour', + 'user': '2000/hour' + } + +} + +REST_FRAMEWORK_TOKEN_EXPIRE_HOURS = 4 + # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ diff --git a/shimatta_kenkyusho/shimatta_kenkyusho/urls.py b/shimatta_kenkyusho/shimatta_kenkyusho/urls.py index b335e8c..60d807e 100644 --- a/shimatta_kenkyusho/shimatta_kenkyusho/urls.py +++ b/shimatta_kenkyusho/shimatta_kenkyusho/urls.py @@ -23,5 +23,6 @@ from parts import views as parts_views urlpatterns = [ url(r'^admin/login/', parts_views.login_view), path('admin/', admin.site.urls), + path('api/v1/', include('api.urls')), path('', include('parts.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)