feat(api): add token management and serializer endpoints

Introduced endpoints for generating and managing API tokens,
enabling users to manage and regenerate their tokens via a
web interface. Added Django forms and templates to support
these functionalities.

Enhanced REST API with new endpoints for creating, updating,
and retrieving identifiers and suffixes, and included necessary
serializers.

Updated settings to include Django REST framework and token
authentication.
This commit is contained in:
Kumi 2024-06-23 15:10:44 +02:00
parent 7e328f5554
commit 431879aea8
Signed by: kumi
GPG key ID: ECBCC9082395383F
10 changed files with 266 additions and 40 deletions

View file

@ -2,3 +2,6 @@ from django import forms
class EmailForm(forms.Form):
email = forms.EmailField()
class GenerateTokenForm(forms.Form):
pass

View file

@ -0,0 +1,12 @@
{% extends "base_generic.html" %}
{% block title %}Generate API Token{% endblock title %}
{% block content %}
<div class="container mt-5">
<h1>Generate API Token</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Generate Token</button>
</form>
</div>
{% endblock content %}

View file

@ -0,0 +1,12 @@
{% extends "base_generic.html" %}
{% block title %}
Manage API Tokens
{% endblock title %}
{% block content %}
<div class="container mt-5">
<h1>Manage API Tokens</h1>
<p>Your API token:</p>
<div class="alert alert-info" role="alert">{{ token.key }}</div>
<a class="btn btn-primary" href="{% url 'generate_token' %}">Regenerate Token</a>
</div>
{% endblock content %}

View file

@ -1,6 +1,14 @@
from django.urls import path
from django.views.generic import TemplateView
from .views import SendLoginEmailView, LoginView, LogoutView
from .views import (
SendLoginEmailView,
LoginView,
LogoutView,
APICustomObtainAuthToken,
APIGenerateTokenView,
GenerateTokenView,
ManageTokensView,
)
urlpatterns = [
path("login/<uidb64>/<token>/", LoginView.as_view(), name="login"),
@ -15,4 +23,8 @@ urlpatterns = [
TemplateView.as_view(template_name="accounts/email_sent.html"),
name="email_sent",
),
path("api/token-auth/", APICustomObtainAuthToken.as_view(), name="api_token_auth"),
path("api/generate-token/", APIGenerateTokenView.as_view(), name="generate_token"),
path("generate-token/", GenerateTokenView.as_view(), name="generate_token"),
path("manage-tokens/", ManageTokensView.as_view(), name="manage_tokens"),
]

View file

@ -3,11 +3,19 @@ from django.core.mail import send_mail
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.http import HttpResponse
from django.shortcuts import redirect
from django.shortcuts import redirect, render
from django.views.generic import FormView, View
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth import logout
from .forms import EmailForm
from django.contrib.auth.mixins import LoginRequiredMixin
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from .forms import EmailForm, GenerateTokenForm
User = get_user_model()
@ -54,3 +62,35 @@ class LogoutView(View):
def get(self, request):
logout(request)
return redirect("home")
class APICustomObtainAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
token, created = Token.objects.get_or_create(user=request.user)
return Response({"token": token.key})
class APIGenerateTokenView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
token, created = Token.objects.get_or_create(user=request.user)
return Response({"token": token.key})
class GenerateTokenView(LoginRequiredMixin, View):
def get(self, request):
form = GenerateTokenForm()
return render(request, 'accounts/generate_token.html', {'form': form})
def post(self, request):
form = GenerateTokenForm(request.POST)
if form.is_valid():
token, created = Token.objects.get_or_create(user=request.user)
return redirect('manage_tokens')
return render(request, 'accounts/generate_token.html', {'form': form})
class ManageTokensView(LoginRequiredMixin, View):
def get(self, request):
token, created = Token.objects.get_or_create(user=request.user)
return render(request, 'accounts/manage_tokens.html', {'token': token})

View file

@ -0,0 +1,23 @@
from rest_framework import serializers
from .models import Suffix, Identifier
class SuffixSerializer(serializers.ModelSerializer):
class Meta:
model = Suffix
fields = [
"id",
"name",
"suffix",
"prefix",
"description",
"approved",
"type",
"remote_resolver",
]
class IdentifierSerializer(serializers.ModelSerializer):
class Meta:
model = Identifier
fields = ["id", "suffix", "identifier", "target_url"]

View file

@ -3,11 +3,11 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}FreeDOI{% endblock %}</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
/>
<title>
{% block title %}FreeDOI{% endblock title %}
</title>
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" />
<style>
body {
padding-top: 56px;
@ -18,46 +18,47 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{% url 'home' %}">FreeDOI</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false"
aria-label="Toggle navigation"
>
<button class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
{% if user.is_authenticated %}
{% if user.is_superuser or user.prefixes.exists %}
<li class="nav-item">
<a class="nav-link" href="{% url 'prefix_list' %}">Your Prefixes</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{% url 'suffix_list' %}">Your Suffixes</a>
</li>
{% if user.is_superuser or user.prefixes.exists %}
<li class="nav-item">
<a class="nav-link" href="{% url 'prefix_list' %}">Your Prefixes</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{% url 'suffix_list' %}">Your Suffixes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'manage_tokens' %}">API Tokens</a>
</li>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
</li>
{% else %}
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Login / Register</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
</ul>
</div>
</nav>
<div class="container">{% block content %} {% endblock %}</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</div>
</nav>
<div class="container">
{% block content %}{% endblock content %}
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>

View file

@ -20,6 +20,12 @@ from .views import (
PermissionCreateView,
PendingSuffixesListView,
SuffixApprovalView,
APIIdentifierCreateView,
APIIdentifierUpdateView,
APIIdentifierDetailView,
APIOwnedIdentifiersListView,
APIOwnedSuffixesListView,
APISuffixDetailView,
)
urlpatterns = [
@ -73,4 +79,34 @@ urlpatterns = [
SuffixApprovalView.as_view(),
name="suffix_approval",
),
path(
"api/identifiers/create/",
APIIdentifierCreateView.as_view(),
name="api_identifier_create",
),
path(
"api/identifiers/<int:pk>/update/",
APIIdentifierUpdateView.as_view(),
name="api_identifier_update",
),
path(
"api/suffixes/",
APIOwnedSuffixesListView.as_view(),
name="api_owned_suffixes_list",
),
path(
"api/suffixes/<int:pk>/",
APISuffixDetailView.as_view(),
name="api_suffix_detail",
),
path(
"api/identifiers/",
APIOwnedIdentifiersListView.as_view(),
name="api_owned_identifiers_list",
),
path(
"api/identifiers/<int:pk>/",
APIIdentifierDetailView.as_view(),
name="api_identifier_detail",
),
]

View file

@ -10,6 +10,9 @@ from django.views.generic import (
)
from django.shortcuts import get_object_or_404, redirect
from django.http import HttpResponseBadRequest, Http404
from rest_framework import generics, permissions
from rest_framework.exceptions import PermissionDenied
from .serializers import SuffixSerializer, IdentifierSerializer
from .models import Prefix, Suffix, Identifier, Permission
from .forms import (
PrefixForm,
@ -303,3 +306,73 @@ class PermissionCreateView(LoginRequiredMixin, CreateView):
class HomeView(TemplateView):
template_name = "resolver/home.html"
class APIIdentifierCreateView(generics.CreateAPIView):
queryset = Identifier.objects.all()
serializer_class = IdentifierSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
suffix = serializer.validated_data["suffix"]
if suffix.owner != self.request.user:
raise PermissionDenied(
"You do not have permission to create identifiers for this suffix."
)
serializer.save()
class APIIdentifierUpdateView(generics.UpdateAPIView):
queryset = Identifier.objects.all()
serializer_class = IdentifierSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_update(self, serializer):
identifier = self.get_object()
if identifier.suffix.owner != self.request.user:
raise PermissionDenied(
"You do not have permission to update identifiers for this suffix."
)
serializer.save()
class APIOwnedSuffixesListView(generics.ListAPIView):
serializer_class = SuffixSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Suffix.objects.filter(owner=self.request.user)
class APISuffixDetailView(generics.RetrieveAPIView):
queryset = Suffix.objects.all()
serializer_class = SuffixSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
suffix = super().get_object()
if suffix.owner != self.request.user:
raise PermissionDenied("You do not have permission to view this suffix.")
return suffix
class APIOwnedIdentifiersListView(generics.ListAPIView):
serializer_class = IdentifierSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Identifier.objects.filter(suffix__owner=self.request.user)
class APIIdentifierDetailView(generics.RetrieveAPIView):
queryset = Identifier.objects.all()
serializer_class = IdentifierSerializer
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
identifier = super().get_object()
if identifier.suffix.owner != self.request.user:
raise PermissionDenied(
"You do not have permission to view this identifier."
)
return identifier

View file

@ -34,6 +34,8 @@ INSTALLED_APPS = [
"two_factor",
"crispy_forms",
"crispy_bootstrap5",
"rest_framework",
"rest_framework.authtoken",
]
MIDDLEWARE = [
@ -153,3 +155,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Crispy Forms
CRISPY_TEMPLATE_PACK = "bootstrap5"
# REST Framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}