1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

feat(accounts): API tokens show their last use, and a user has at most ten

The API Tokens tab gets a "Last used" column, so an unused token can be
told from one a script still needs. The token's last use is written when
it authenticates, at most once a minute (accounts.auth.TokenAuthentication,
knox's class with that one addition; the API schema describes it as
knox's).

A user has at most ten tokens; a new one is refused until one is
deleted. Expired tokens do not count, and are deleted when a new token is
made.
This commit is contained in:
catborise 2026-10-24 23:33:12 +03:00
parent d9088b410e
commit 6b51c30dd6
8 changed files with 85 additions and 2 deletions

View file

@ -3,6 +3,7 @@ passed the login (and its OTP), and scripts use them on the REST API."""
import re
from datetime import timedelta
from unittest.mock import patch
from accounts.models import ApiToken
from django.contrib.auth import get_user_model
@ -98,3 +99,33 @@ class ApiTokenTests(TestCase):
# would reference auth_user unseen, and PostgreSQL could not empty
# auth_user between tests
self.assertNotIn("knox_authtoken", connection.introspection.table_names())
def test_a_token_shows_when_it_was_last_used(self):
token = self.make()
self.assertIsNone(ApiToken.objects.get().last_used)
self.api(token)
first = ApiToken.objects.get().last_used
self.assertAlmostEqual(first, timezone.now(), delta=timedelta(seconds=5))
# within a minute a request writes nothing
self.api(token)
self.assertEqual(ApiToken.objects.get().last_used, first)
later = timezone.now() + timedelta(minutes=2)
with patch("accounts.auth.timezone.now", return_value=later):
self.api(token)
self.assertEqual(ApiToken.objects.get().last_used, later)
self.client.force_login(self.user)
self.assertIn("Last used", self.client.get(reverse("accounts:profile")).content.decode())
def test_a_user_has_at_most_ten_tokens(self):
for number in range(10):
self.make(name=f"t{number}")
response = self.client.post(reverse("accounts:api_token_create"), {"name": "one more", "days": "30"})
self.assertEqual(ApiToken.objects.filter(user=self.user).count(), 10)
self.assertNotRegex(response.content.decode(), r"wvc_[0-9a-f]{64}")
self.assertIn("at most 10", response.content.decode())
def test_expired_tokens_do_not_count_toward_the_limit(self):
for number in range(10):
self.make(name=f"t{number}")
ApiToken.objects.filter(name="t0").update(expiry=timezone.now() - timedelta(days=1))
self.make(name="new")
self.assertEqual(sorted(ApiToken.objects.values_list("name", flat=True))[:2], ["new", "t1"])