Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pdfding/core/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
'allauth.socialaccount',
'allauth.socialaccount.providers.openid_connect',
'allauth.mfa',
'rest_framework',
'rest_framework.authtoken',
'django_htmx',
'huey.contrib.djhuey',
'admin',
Expand Down Expand Up @@ -207,6 +209,17 @@

log_level = environ.get('LOG_LEVEL', 'ERROR')

# Django REST Framework Configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
Expand Down
37 changes: 37 additions & 0 deletions pdfding/users/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from rest_framework import serializers, status, viewsets
from rest_framework.authtoken.models import Token
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
from rest_framework.response import Response


class TokenSerializer(serializers.ModelSerializer):
class Meta:
model = Token
fields = ['key', 'created']


class TokenViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
renderer_classes = [TemplateHTMLRenderer, JSONRenderer]
serializer_class = TokenSerializer

def list(self, request):
token = Token.objects.filter(user=request.user).first()
return Response({'token': token}, template_name='partials/token_management.html')

def create(self, request):
token, created = Token.objects.get_or_create(user=request.user)
return Response({'token': token}, template_name='partials/token_management.html', status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)

@action(detail=False, methods=['post'], url_path='rotate')
def rotate(self, request):
Token.objects.filter(user=request.user).delete()
token = Token.objects.create(user=request.user)
return Response({'token': token}, template_name='partials/token_management.html')

@action(detail=False, methods=['post'], url_path='delete')
def delete(self, request):
Token.objects.filter(user=request.user).delete()
return Response({'token': None}, template_name='partials/token_management.html')
68 changes: 68 additions & 0 deletions pdfding/users/templates/access_token.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{% extends 'layouts/blank.html' %}
{% load i18n %}

{% block content %}
{% trans "Edit" as edit %}
<div class="flex flex-col md:flex-row md:justify-end">
<div class="w-full! md:w-72! lg:w-72! px-2 md:px-4 pt-2">
{% include 'includes/settings_sidebar.html' with page='access_token' %}
</div>
<div class="flex w-full justify-center items-start py-2 px-2 md:px-8">
<div class="rounded-md w-full max-w-3xl mx-auto px-4 py-4 md:pb-8 border
bg-slate-100 dark:bg-slate-800
border-slate-300 dark:border-slate-700">
<span class="text-2xl font-bold">{% trans 'Access Token' %}</span>
<p class="text-sm text-slate-500 dark:text-slate-400 pt-1">{% trans 'Generate a single API token to authenticate your requests without sharing your password.' %}</p>
<div class="pt-3 space-y-4 text-slate-600 dark:text-slate-400" id="token_management">
<div class="w-full">
{% if token %}
<div class="flex flex-col gap-2">
<div class="font-mono text-sm bg-slate-200 dark:bg-slate-700 p-2 rounded break-all">
{% if token.key|length > 20 %}
{{ token.key|slice:":4" }}...{{ token.key|slice:"-4:" }}
{% else %}
{{ token.key }}
{% endif %}
</div>
<div class="text-xs text-slate-500 dark:text-slate-400">
{% trans 'Created' %}: {{ token.created|date:"SHORT_DATE_FORMAT" }}
</div>
</div>
{% else %}
<span class="text-slate-500 dark:text-slate-500">{% trans 'No API token generated yet' %}</span>
{% endif %}
</div>

<div class="flex flex-wrap gap-2">
{% if token %}
<button type="button" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2"
onclick="(async function(btn){const k='{{ token.key }}';try{await navigator.clipboard.writeText(k);const old=btn.innerText;btn.innerText='{% trans "Copied" %}';setTimeout(()=>btn.innerText=old,1400);}catch(e){alert('{% trans "Copy failed" %}');}})(this)">
{% trans 'Copy' %}
</button>

<form method="post" action="{% url 'token-rotate' %}" class="inline" hx-post="{% url 'token-rotate' %}" hx-target="#token_management" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2">
{% trans 'Rotate' %}
</button>
</form>
<form method="post" action="{% url 'token-delete' %}" class="inline" hx-post="{% url 'token-delete' %}" hx-target="#token_management" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2">
{% trans 'Delete' %}
</button>
</form>
{% else %}
<form method="post" action="{% url 'token-list' %}" class="inline" hx-post="{% url 'token-list' %}" hx-target="#token_management" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2">
{% trans 'Generate' %}
</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
6 changes: 3 additions & 3 deletions pdfding/users/templates/account_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<div class="w-full! md:w-72! lg:w-72! px-2 md:px-4 pt-2">
{% include 'includes/settings_sidebar.html' with page='account_settings' %}
</div>
<div class="flex w-full justify-start items-center py-2 px-2 md:px-8">
<div class="rounded-md w-full min-[1200px]:w-3xl! md:ml-10 min-[1600px]:ml-40! px-4 py-4 md:pb-8 border
<div class="flex flex-col w-full items-center py-2 px-2 md:px-8 space-y-4">
<div class="rounded-md w-full max-w-3xl mx-auto px-4 py-4 md:pb-8 border
bg-slate-100 dark:bg-slate-800
border-slate-300 dark:border-slate-700">
<span class="text-2xl font-bold">{% trans 'Account Information' %}</span>
Expand Down Expand Up @@ -100,7 +100,7 @@
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
8 changes: 8 additions & 0 deletions pdfding/users/templates/includes/settings_sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
<span class="hidden md:block">{% trans 'Account' %}</span>
</a>
</div>
<div {% if page == 'access_token' %} class="bg-slate-200 md:bg-slate-100 dark:bg-slate-800" {% endif %}>
<a href="{% url 'access_token' %}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock" viewBox="0 0 16 16">
<path d="M8 0a4 4 0 0 1 4 4v2.05a2.5 2.5 0 0 1 2 2.45v5a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 2 13.5v-5a2.5 2.5 0 0 1 2-2.45V4a4 4 0 0 1 4-4M4.5 7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7zM8 1a3 3 0 0 0-3 3v2h6V4a3 3 0 0 0-3-3"/>
</svg>
<span class="hidden md:block">{% trans 'Access Token' %}</span>
</a>
</div>
<div {% if page == 'viewer_settings' %} class="bg-slate-200 md:bg-slate-100 dark:bg-slate-800" {% endif %}>
<a href="{% url 'viewer_settings' %}">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand Down
51 changes: 51 additions & 0 deletions pdfding/users/templates/partials/token_management.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% load i18n %}

<div class="space-y-4 text-slate-600 dark:text-slate-400">
<div class="w-full">
{% if token %}
<div class="flex flex-col gap-2">
<div class="font-mono text-sm bg-slate-200 dark:bg-slate-700 p-2 rounded break-all">
{% if token.key|length > 20 %}
{{ token.key|slice:":4" }}...{{ token.key|slice:"-4:" }}
{% else %}
{{ token.key }}
{% endif %}
</div>
<div class="text-xs text-slate-500 dark:text-slate-400">
{% trans 'Created' %}: {{ token.created|date:"SHORT_DATE_FORMAT" }}
</div>
</div>
{% else %}
<span class="text-slate-500 dark:text-slate-500">{% trans 'No API token generated yet' %}</span>
{% endif %}
</div>

<div class="flex flex-wrap gap-2">
{% if token %}
<button type="button" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2"
onclick="(async function(btn){const k='{{ token.key }}';try{await navigator.clipboard.writeText(k);const old=btn.innerText;btn.innerText='{% trans "Copied" %}';setTimeout(()=>btn.innerText=old,1400);}catch(e){alert('{% trans "Copy failed" %}');}})(this)">
{% trans 'Copy' %}
</button>

<form method="post" action="{% url 'token-rotate' %}" class="inline" hx-post="{% url 'token-rotate' %}" hx-target="#token_management" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2">
{% trans 'Rotate' %}
</button>
</form>
<form method="post" action="{% url 'token-delete' %}" class="inline" hx-post="{% url 'token-delete' %}" hx-target="#token_management" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="text-white bg-primary hover:bg-secondary rounded px-4 py-2">
{% trans 'Delete' %}
</button>
</form>
{% else %}
<form method="post" action="{% url 'token-list' %}" class="inline" hx-post="{% url 'token-list' %}" hx-target="#token_management" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="text-primary hover:text-secondary cursor-pointer text-sm">
{% trans 'Generate' %}
</button>
</form>
{% endif %}
</div>
</div>
9 changes: 8 additions & 1 deletion pdfding/users/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from django.urls import path
from django.urls import include, path
from rest_framework.routers import SimpleRouter
from users import views
from users.api import TokenViewSet

router = SimpleRouter()
router.register('token', TokenViewSet, basename='token')

urlpatterns = [
path('account_settings', views.account_settings, name="account_settings"),
path('access_token', views.access_token_settings, name="access_token"),
path('danger_settings', views.danger_settings, name="danger_settings"),
path('ui_settings', views.ui_settings, name="ui_settings"),
path('viewer_settings', views.viewer_settings, name="viewer_settings"),
Expand All @@ -16,4 +22,5 @@
path('change_collection/<collection_id>', views.ChangeCollection.as_view(), name="change_collection"),
path('open_collapse_tags', views.OpenCollapseTags.as_view(), name="open_collapse_tags"),
path('signatures', views.Signatures.as_view(), name="signatures"),
path('', include(router.urls)),
]
13 changes: 13 additions & 0 deletions pdfding/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.views import View
from django_htmx.http import HttpResponseClientRefresh
from pdf.services.workspace_services import check_if_collection_part_of_workspace
from rest_framework.authtoken.models import Token
from users import forms
from users.models import Profile
from users.service import create_demo_user, get_secondary_color
Expand All @@ -32,6 +33,18 @@ def account_settings(request):
return render(request, 'account_settings.html', {'uses_social': uses_social})


def access_token_settings(request):
"""View for the access token settings page"""

try:
token = Token.objects.get(user=request.user)
except Token.DoesNotExist:
token = None

# pragma: no cover
return render(request, 'access_token.html', context={'token': token})


def ui_settings(request): # pragma: no cover
"""View for the ui settings page"""

Expand Down
26 changes: 17 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ license-files = ["LICENSE"]
python = ">=3.11 <4.0"
django = "==5.2.13"
django-allauth = { extras = ["mfa", "socialaccount"], version = "==65.15.1" }
djangorestframework = "==3.15.0"
django-htmx = "==1.27.0"
gunicorn = "==25.3.0"
huey = "==2.6.0"
Expand Down