1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +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")

Binary file not shown.

View file

@ -523,7 +523,7 @@ msgid "Add"
msgstr "Ekle"
#: accounts/templates/profile.html:64 accounts/templates/profile.html:70
#: accounts/views.py:71 accounts/views.py:75
#: accounts/views.py:71 accounts/views.py:86
msgid "Create an API token"
msgstr "API anahtarı oluştur"
@ -568,85 +568,90 @@ msgstr ""
msgid "Profile updated"
msgstr "Profil güncellendi"
#: accounts/views.py:74
#: accounts/views.py:78
#, python-format
msgid "A user has at most %(limit)s API tokens: delete one first."
msgstr ""
"Bir kullanıcının en fazla %(limit)s API anahtarı olabilir: önce birini silin."
#: accounts/views.py:109
#: accounts/views.py:80
#, python-format
msgid "You have a token named %(name)s: choose another name."
msgstr "%(name)s adlı bir token'ınız var: başka bir ad seçin."
#: accounts/views.py:117
msgid "API token deleted"
msgstr "API anahtarı silindi"
#: accounts/views.py:117
#: accounts/views.py:125
msgid "Delete API token"
msgstr "API anahtarını sil"
#: accounts/views.py:118
#: accounts/views.py:126
msgid "Scripts and tools that use it can no longer reach the API."
msgstr "Onu kullanan betikler ve araçlar artık API'ye erişemez."
#: accounts/views.py:128
#: accounts/views.py:136
msgid "SSH key added"
msgstr "SSH anahtarı eklendi"
#: accounts/views.py:136
#: accounts/views.py:144
msgid "Add SSH key"
msgstr "SSH Anahtarı Ekle"
#: accounts/views.py:157
#: accounts/views.py:165
msgid "SSH key deleted"
msgstr "SSH anahtarı silindi"
#: accounts/views.py:165
#: accounts/views.py:173
msgid "Delete SSH key"
msgstr "SSH anahtarı sil"
#: accounts/views.py:166
#: accounts/views.py:174
msgid ""
"VMs that already have it keep it; VMs created from now on do not get it."
msgstr ""
"Ona zaten sahip olan sanal makinelerde kalır; bundan sonra oluşturulan sanal "
"makinelere eklenmez."
#: accounts/views.py:215
#: accounts/views.py:223
msgid "Password Changed"
msgstr "Parola Değişti"
#: accounts/views.py:244
#: accounts/views.py:252
msgid "Create User Instance"
msgstr "Yeni Kullanıcı Sanal Makinesi Oluştur"
#: accounts/views.py:278
#: accounts/views.py:286
msgid "Update User Instance"
msgstr "Kullanıcı Sanal Makinesini Güncelle"
#: accounts/views.py:296
#: accounts/views.py:304
msgid "The user loses access to this VM; the VM itself is not changed."
msgstr ""
"Kullanıcı bu sanal makineye erişimini kaybeder; sanal makinenin kendisi "
"değişmez."
#: accounts/views.py:329
#: accounts/views.py:337
#, python-format
msgid "OTP Sent to %(email)s"
msgstr "OTP %(email)s ye gönderildi"
#: accounts/views.py:338
#: accounts/views.py:346
msgid "Email OTP"
msgstr "OTP E-Postası"
#: accounts/views.py:372
#: accounts/views.py:380
#, python-format
msgid "The OTP QR code could not be mailed to %(user)s: %(error)s"
msgstr "OTP QR kodu %(user)s kullanıcısına e-postayla gönderilemedi: %(error)s"
#: accounts/views.py:376
#: accounts/views.py:384
#, python-format
msgid "OTP QR code was emailed to user %(user)s"
msgstr "OTP QR kodu %(user)s kullanıcısına gönderildi"
#: accounts/views.py:380
#: accounts/views.py:388
msgid "User email not set, failed to send QR code"
msgstr "Kullanıcı e-postası girilmemiş, QR kod gönderimi başarısız"

Binary file not shown.

View file

@ -517,7 +517,7 @@ msgid "Add"
msgstr "添加"
#: accounts/templates/profile.html:64 accounts/templates/profile.html:70
#: accounts/views.py:71 accounts/views.py:75
#: accounts/views.py:71 accounts/views.py:86
msgid "Create an API token"
msgstr "创建 API 令牌"
@ -560,80 +560,85 @@ msgstr ""
msgid "Profile updated"
msgstr "资料已更新"
#: accounts/views.py:74
#: accounts/views.py:78
#, python-format
msgid "A user has at most %(limit)s API tokens: delete one first."
msgstr "每个用户最多有 %(limit)s 个 API 令牌:请先删除一个。"
#: accounts/views.py:109
#: accounts/views.py:80
#, python-format
msgid "You have a token named %(name)s: choose another name."
msgstr "您已有名为 %(name)s 的令牌:请换一个名称。"
#: accounts/views.py:117
msgid "API token deleted"
msgstr "API 令牌已删除"
#: accounts/views.py:117
#: accounts/views.py:125
msgid "Delete API token"
msgstr "删除 API 令牌"
#: accounts/views.py:118
#: accounts/views.py:126
msgid "Scripts and tools that use it can no longer reach the API."
msgstr "使用它的脚本和工具将无法再访问 API。"
#: accounts/views.py:128
#: accounts/views.py:136
msgid "SSH key added"
msgstr "SSH 密钥已添加"
#: accounts/views.py:136
#: accounts/views.py:144
msgid "Add SSH key"
msgstr "添加 SSH 密钥"
#: accounts/views.py:157
#: accounts/views.py:165
msgid "SSH key deleted"
msgstr "SSH 密钥已删除"
#: accounts/views.py:165
#: accounts/views.py:173
msgid "Delete SSH key"
msgstr "删除 SSH 密钥"
#: accounts/views.py:166
#: accounts/views.py:174
msgid ""
"VMs that already have it keep it; VMs created from now on do not get it."
msgstr "已有此密钥的虚拟机会保留它;此后创建的虚拟机不会再获得它。"
#: accounts/views.py:215
#: accounts/views.py:223
msgid "Password Changed"
msgstr "密码已修改"
#: accounts/views.py:244
#: accounts/views.py:252
msgid "Create User Instance"
msgstr "创建用户虚拟机"
#: accounts/views.py:278
#: accounts/views.py:286
msgid "Update User Instance"
msgstr "更新用户虚拟机"
#: accounts/views.py:296
#: accounts/views.py:304
msgid "The user loses access to this VM; the VM itself is not changed."
msgstr "该用户将失去对此虚拟机的访问权限;虚拟机本身不受影响。"
#: accounts/views.py:329
#: accounts/views.py:337
#, python-format
msgid "OTP Sent to %(email)s"
msgstr "OTP 已发送至 %(email)s"
#: accounts/views.py:338
#: accounts/views.py:346
msgid "Email OTP"
msgstr "邮件发送 OTP"
#: accounts/views.py:372
#: accounts/views.py:380
#, python-format
msgid "The OTP QR code could not be mailed to %(user)s: %(error)s"
msgstr "无法将 OTP 二维码通过邮件发送给 %(user)s:%(error)s"
#: accounts/views.py:376
#: accounts/views.py:384
#, python-format
msgid "OTP QR code was emailed to user %(user)s"
msgstr "OTP 二维码已通过邮件发送给用户 %(user)s"
#: accounts/views.py:380
#: accounts/views.py:388
msgid "User email not set, failed to send QR code"
msgstr "未设置用户邮箱,无法发送二维码"