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:
parent
7e328f5554
commit
431879aea8
10 changed files with 266 additions and 40 deletions
|
@ -2,3 +2,6 @@ from django import forms
|
||||||
|
|
||||||
class EmailForm(forms.Form):
|
class EmailForm(forms.Form):
|
||||||
email = forms.EmailField()
|
email = forms.EmailField()
|
||||||
|
|
||||||
|
class GenerateTokenForm(forms.Form):
|
||||||
|
pass
|
12
freedoi/accounts/templates/accounts/generate_token.html
Normal file
12
freedoi/accounts/templates/accounts/generate_token.html
Normal 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 %}
|
12
freedoi/accounts/templates/accounts/manage_tokens.html
Normal file
12
freedoi/accounts/templates/accounts/manage_tokens.html
Normal 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 %}
|
|
@ -1,6 +1,14 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from .views import SendLoginEmailView, LoginView, LogoutView
|
from .views import (
|
||||||
|
SendLoginEmailView,
|
||||||
|
LoginView,
|
||||||
|
LogoutView,
|
||||||
|
APICustomObtainAuthToken,
|
||||||
|
APIGenerateTokenView,
|
||||||
|
GenerateTokenView,
|
||||||
|
ManageTokensView,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("login/<uidb64>/<token>/", LoginView.as_view(), name="login"),
|
path("login/<uidb64>/<token>/", LoginView.as_view(), name="login"),
|
||||||
|
@ -15,4 +23,8 @@ urlpatterns = [
|
||||||
TemplateView.as_view(template_name="accounts/email_sent.html"),
|
TemplateView.as_view(template_name="accounts/email_sent.html"),
|
||||||
name="email_sent",
|
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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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.http import urlsafe_base64_encode, urlsafe_base64_decode
|
||||||
from django.utils.encoding import force_bytes, force_str
|
from django.utils.encoding import force_bytes, force_str
|
||||||
from django.http import HttpResponse
|
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.views.generic import FormView, View
|
||||||
from django.contrib.auth.tokens import default_token_generator
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
from django.contrib.auth import logout
|
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()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -54,3 +62,35 @@ class LogoutView(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
logout(request)
|
logout(request)
|
||||||
return redirect("home")
|
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})
|
23
freedoi/resolver/serializers.py
Normal file
23
freedoi/resolver/serializers.py
Normal 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"]
|
|
@ -3,11 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% block title %}FreeDOI{% endblock %}</title>
|
<title>
|
||||||
<link
|
{% block title %}FreeDOI{% endblock title %}
|
||||||
rel="stylesheet"
|
</title>
|
||||||
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
<link rel="stylesheet"
|
||||||
/>
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
padding-top: 56px;
|
padding-top: 56px;
|
||||||
|
@ -18,15 +18,13 @@
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="{% url 'home' %}">FreeDOI</a>
|
<a class="navbar-brand" href="{% url 'home' %}">FreeDOI</a>
|
||||||
<button
|
<button class="navbar-toggler"
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
type="button"
|
||||||
data-toggle="collapse"
|
data-toggle="collapse"
|
||||||
data-target="#navbarResponsive"
|
data-target="#navbarResponsive"
|
||||||
aria-controls="navbarResponsive"
|
aria-controls="navbarResponsive"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-label="Toggle navigation"
|
aria-label="Toggle navigation">
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||||
|
@ -40,6 +38,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'suffix_list' %}">Your Suffixes</a>
|
<a class="nav-link" href="{% url 'suffix_list' %}">Your Suffixes</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'manage_tokens' %}">API Tokens</a>
|
||||||
|
</li>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
|
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
|
||||||
|
@ -53,11 +54,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="container">
|
||||||
<div class="container">{% block content %} {% endblock %}</div>
|
{% block content %}{% endblock content %}
|
||||||
|
</div>
|
||||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
<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://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>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -20,6 +20,12 @@ from .views import (
|
||||||
PermissionCreateView,
|
PermissionCreateView,
|
||||||
PendingSuffixesListView,
|
PendingSuffixesListView,
|
||||||
SuffixApprovalView,
|
SuffixApprovalView,
|
||||||
|
APIIdentifierCreateView,
|
||||||
|
APIIdentifierUpdateView,
|
||||||
|
APIIdentifierDetailView,
|
||||||
|
APIOwnedIdentifiersListView,
|
||||||
|
APIOwnedSuffixesListView,
|
||||||
|
APISuffixDetailView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -73,4 +79,34 @@ urlpatterns = [
|
||||||
SuffixApprovalView.as_view(),
|
SuffixApprovalView.as_view(),
|
||||||
name="suffix_approval",
|
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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,6 +10,9 @@ from django.views.generic import (
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.http import HttpResponseBadRequest, Http404
|
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 .models import Prefix, Suffix, Identifier, Permission
|
||||||
from .forms import (
|
from .forms import (
|
||||||
PrefixForm,
|
PrefixForm,
|
||||||
|
@ -303,3 +306,73 @@ class PermissionCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
template_name = "resolver/home.html"
|
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
|
||||||
|
|
|
@ -34,6 +34,8 @@ INSTALLED_APPS = [
|
||||||
"two_factor",
|
"two_factor",
|
||||||
"crispy_forms",
|
"crispy_forms",
|
||||||
"crispy_bootstrap5",
|
"crispy_bootstrap5",
|
||||||
|
"rest_framework",
|
||||||
|
"rest_framework.authtoken",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -153,3 +155,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
# Crispy Forms
|
# Crispy Forms
|
||||||
|
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
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",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue