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:
parent
be68009432
commit
c290cef21a
6 changed files with 70 additions and 44 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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 "未设置用户邮箱,无法发送二维码"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue