diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..e09b9df --- /dev/null +++ b/api/admin.py @@ -0,0 +1,21 @@ +from django.contrib import admin +from rest_framework_api_key.admin import APIKeyModelAdmin +from rest_framework_api_key.models import APIKey as DefaultAPIKey +from .models import APIKey + +# Unregister the default APIKey admin +admin.site.unregister(DefaultAPIKey) + + +@admin.register(APIKey) +class APIKeyAdmin(APIKeyModelAdmin): + list_display = [*APIKeyModelAdmin.list_display, "user"] + list_filter = [*APIKeyModelAdmin.list_filter, "user"] + search_fields = [*APIKeyModelAdmin.search_fields, "user__username", "user__email"] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + # Make user field required + if 'user' in form.base_fields: + form.base_fields['user'].required = True + return form \ No newline at end of file diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..a5ed1ab --- /dev/null +++ b/api/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' + verbose_name = 'API' \ No newline at end of file diff --git a/api/authentication.py b/api/authentication.py new file mode 100644 index 0000000..2f225b5 --- /dev/null +++ b/api/authentication.py @@ -0,0 +1,24 @@ +from rest_framework_api_key.permissions import BaseHasAPIKey +from .models import APIKey + + +class HasAPIKey(BaseHasAPIKey): + model = APIKey + + def has_permission(self, request, view): + # First check if we have a valid API key + has_api_key = super().has_permission(request, view) + + if has_api_key: + # Get the API key from the request + key = self.get_key(request) + if key: + try: + api_key = APIKey.objects.get_from_key(key) + # Set the request.user to the user associated with the API key + request.user = api_key.user + return True + except APIKey.DoesNotExist: + pass + + return False diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..cc1e140 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1 on 2025-09-17 07:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='APIKey', + fields=[ + ('id', models.CharField(editable=False, max_length=150, primary_key=True, serialize=False, unique=True)), + ('prefix', models.CharField(editable=False, max_length=8, unique=True)), + ('hashed_key', models.CharField(editable=False, max_length=150)), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('name', models.CharField(default=None, help_text='A free-form name for the API key. Need not be unique. 50 characters max.', max_length=50)), + ('revoked', models.BooleanField(blank=True, default=False, help_text='If the API key is revoked, clients cannot use it anymore. (This cannot be undone.)')), + ('expiry_date', models.DateTimeField(blank=True, help_text='Once API key expires, clients cannot use it anymore.', null=True, verbose_name='Expires')), + ('user', models.ForeignKey(help_text='The user this API key belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='api_keys', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'API Key', + 'verbose_name_plural': 'API Keys', + 'ordering': ('-created',), + 'abstract': False, + }, + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..4ce9e1d --- /dev/null +++ b/api/models.py @@ -0,0 +1,23 @@ +from django.db import models +from rest_framework_api_key.models import AbstractAPIKey +from tournaments.models import CustomUser + + +class APIKey(AbstractAPIKey): + """ + API Key model linked to a specific user. + This allows filtering API access based on the user associated with the API key. + """ + user = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name='api_keys', + help_text='The user this API key belongs to' + ) + + class Meta(AbstractAPIKey.Meta): + verbose_name = "API Key" + verbose_name_plural = "API Keys" + + def __str__(self): + return f"API Key for {self.user.username}" \ No newline at end of file diff --git a/api/views.py b/api/views.py index 6f9a37a..4778afb 100644 --- a/api/views.py +++ b/api/views.py @@ -4,6 +4,7 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework import status from rest_framework.exceptions import MethodNotAllowed from rest_framework.permissions import IsAuthenticated +from .authentication import HasAPIKey from django.conf import settings from django.http import Http404, HttpResponse, JsonResponse @@ -76,10 +77,11 @@ class ClubViewSet(SoftDeleteViewSet): class TournamentSummaryViewSet(SoftDeleteViewSet): queryset = Tournament.objects.all() serializer_class = TournamentSummarySerializer + permission_classes = [HasAPIKey] def get_queryset(self): if self.request.user.is_anonymous: - return [] + return Tournament.objects.none() return self.queryset.filter( Q(event__creator=self.request.user) | Q(related_user=self.request.user) diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index 28ad40d..eab2309 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -37,6 +37,7 @@ INSTALLED_APPS = [ 'tournaments', 'shop', 'biz', + 'api', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -50,6 +51,7 @@ INSTALLED_APPS = [ 'channels_redis', 'django_filters', 'background_task', + 'rest_framework_api_key', ] diff --git a/requirements.txt b/requirements.txt index 892df2f..9a55a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ stripe==11.6.0 django-background-tasks==1.2.8 Pillow==10.2.0 playwright==1.40.0 +djangorestframework-api-key==3.1.0