feat: add delete button for revoked API tokens

Revoked tokens previously stayed in the list with no way to remove them.
Adds a delete action (hard delete, scoped to the owner, gated behind
demo mode) shown on revoked rows, alongside the existing revoke action on
active ones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
obervinov
2026-06-29 23:55:38 +04:00
parent 106d721279
commit 0fb37a59fa
4 changed files with 59 additions and 0 deletions
+28
View File
@@ -1,6 +1,7 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from apps.users.models import APIToken
@@ -43,3 +44,30 @@ class UserAPITokenViewsTests(TestCase):
token.refresh_from_db()
self.assertIsNotNone(token.revoked_at)
self.assertContains(response, "Revoked")
def test_can_delete_revoked_api_token(self):
token, _ = APIToken.objects.create_token(user=self.user, name="n8n")
token.revoked_at = timezone.now()
token.save(update_fields=["revoked_at"])
response = self.client.delete(
reverse("user_api_token_delete", kwargs={"token_id": token.id}),
**self.htmx_headers,
)
self.assertEqual(response.status_code, 200)
self.assertFalse(APIToken.objects.filter(id=token.id).exists())
def test_cannot_delete_other_users_api_token(self):
other = get_user_model().objects.create_user(
email="other@example.com", password="test-password"
)
token, _ = APIToken.objects.create_token(user=other, name="theirs")
response = self.client.delete(
reverse("user_api_token_delete", kwargs={"token_id": token.id}),
**self.htmx_headers,
)
self.assertEqual(response.status_code, 404)
self.assertTrue(APIToken.objects.filter(id=token.id).exists())
+5
View File
@@ -42,6 +42,11 @@ urlpatterns = [
views.api_token_revoke,
name="user_api_token_revoke",
),
path(
"user/api-tokens/<int:token_id>/delete/",
views.api_token_delete,
name="user_api_token_delete",
),
path(
"users/",
views.users_index,
+11
View File
@@ -168,6 +168,17 @@ def api_token_revoke(request, token_id):
return _render_api_tokens(request)
@only_htmx
@htmx_login_required
@disabled_on_demo
@require_http_methods(["DELETE"])
def api_token_delete(request, token_id):
token = get_object_or_404(APIToken, id=token_id, user=request.user)
token.delete()
messages.success(request, _("API token deleted successfully"))
return _render_api_tokens(request)
@only_htmx
@htmx_login_required
@require_http_methods(["GET"])
@@ -87,6 +87,21 @@
_="install prompt_swal">
<i class="fa-solid fa-ban fa-fw"></i>
</a>
{% else %}
<a class="btn btn-error btn-sm"
role="button"
data-tippy-content="{% translate 'Delete' %}"
hx-delete="{% url 'user_api_token_delete' token_id=token.id %}"
hx-target="#api-token-settings"
hx-swap="innerHTML"
hx-trigger="confirmed"
data-bypass-on-ctrl="true"
data-title="{% translate 'Delete token?' %}"
data-text="{% translate 'This permanently removes the token from the list. It cannot be undone.' %}"
data-confirm-text="{% translate 'Yes, delete it!' %}"
_="install prompt_swal">
<i class="fa-solid fa-trash fa-fw"></i>
</a>
{% endif %}
</td>
</tr>