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

fix(accounts): an API token form sent twice makes one token

A double click made two tokens: the second key was shown, the first
never, yet it stayed valid until it expired and took a place of the ten.
A user's token names are now unique, checked under a lock on the user's
row, which also keeps two forms sent at once from passing the limit
together. tr and zh too.
This commit is contained in:
catborise 2026-11-05 18:43:43 +03:00
parent be68009432
commit c290cef21a
6 changed files with 70 additions and 44 deletions

View file

@ -138,6 +138,14 @@ class ApiTokenTests(TestCase):
self.assertNotRegex(response.content.decode(), r"wvc_[0-9a-f]{64}")
self.assertIn("at most 10", response.content.decode())
def test_a_form_sent_twice_makes_one_token(self):
self.make("ci")
response = self.client.post(reverse("accounts:api_token_create"), {"name": "ci", "days": "90"})
self.assertContains(response, "You have a token named ci: choose another name.")
self.assertEqual(ApiToken.objects.filter(user=self.user).count(), 1)
self.make("ci-2") # another name is fine
self.assertEqual(ApiToken.objects.filter(user=self.user).count(), 2)
def test_expired_tokens_do_not_count_toward_the_limit(self):
for number in range(10):
self.make(name=f"t{number}")

View file

@ -69,13 +69,21 @@ def api_token_create(request):
form = ApiTokenForm(request.POST)
if not form.is_valid():
return render(request, "common/form.html", {"form": form, "title": _("Create an API token")})
ApiToken.objects.filter(user=request.user, expiry__lt=timezone.now()).delete()
if ApiToken.objects.filter(user=request.user).count() >= ApiToken.LIMIT:
form.add_error(None, _("A user has at most %(limit)s API tokens: delete one first.") % {"limit": ApiToken.LIMIT})
return render(request, "common/form.html", {"form": form, "title": _("Create an API token")})
name, days = form.cleaned_data["name"], form.cleaned_data["days"]
with audit(request, None, f"API token {name}", "add"):
_token, key = ApiToken.objects.create(request.user, expiry=timedelta(days=days) if days else None, name=name)
with transaction.atomic():
# the user's row: two forms sent at once (a double click) take their turns
get_user_model().objects.select_for_update().get(pk=request.user.pk)
ApiToken.objects.filter(user=request.user, expiry__lt=timezone.now()).delete()
if ApiToken.objects.filter(user=request.user).count() >= ApiToken.LIMIT:
form.add_error(None, _("A user has at most %(limit)s API tokens: delete one first.") % {"limit": ApiToken.LIMIT})
elif ApiToken.objects.filter(user=request.user, name=name).exists():
form.add_error("name", _("You have a token named %(name)s: choose another name.") % {"name": name})
else:
with audit(request, None, f"API token {name}", "add"):
_token, key = ApiToken.objects.create(request.user, expiry=timedelta(days=days) if days else None,
name=name)
if form.errors:
return render(request, "common/form.html", {"form": form, "title": _("Create an API token")})
# shown by a GET, so reloading the page does not create another token
request.session["api_token_created"] = {"name": name, "key": key}
return redirect("accounts:api_token_created")