1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

build(deps): the OTP e-mail draws its QR code with segno

django-qr-code was used for one thing: the QR code in the OTP e-mail,
as an inline SVG and a PNG. It brought pydantic, pydantic-core,
annotated-types and typing-inspection (about 10 MB) and, from 5.0 on,
Pillow. segno, which it used underneath, now draws both images with the
same options (error correction M, 4-module border, 1.8 mm SVG modules,
12 px PNG modules); the SVG, the PNG and the alt text are byte for byte
what django-qr-code 4.2 produced. account.html loaded the tag library
without using it.
This commit is contained in:
catborise 2026-10-11 23:02:37 +03:00
parent 99b9f35826
commit 72681ab54e
7 changed files with 42 additions and 6 deletions

View file

@ -542,6 +542,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat
- On a host whose libvirt cannot change network interfaces (the udev backend: RHEL 9 and later, Debian 12), the interface pages offer no create, start, stop or delete; they only failed there.
- On a running VM the disk list, its edit form, the QoS table, the title, description, CPU, boot order, video and console fields show the settings as they will be at the next start, so saving an edit no longer reverts another pending change. A boot order that names a device missing from that definition is refused instead of clearing the order. The settings tab of a running VM says so. A QoS edit keeps the inbound floor. Buttons shown as disabled while the VM runs can no longer be submitted with Enter.
- A new disk or CD-ROM on a running VM gets a device name that neither the running VM nor its next-start settings use; it could take the name of a pending disk and stay attached until the next start only.
- The OTP e-mail's QR code is drawn with segno; django-qr-code is no longer required. A `settings.py` kept from an older version still lists `"qr_code"` in `INSTALLED_APPS`: delete that line, or the app does not start in a venv without django-qr-code.
- A disk, NIC or guest agent added to a running VM is requested for both the running VM and its next-start settings in one libvirt call: a disk name or guest agent already pending is refused before anything changes, and a NIC added without a MAC gets the same MAC in both. A failure after the device is plugged can still leave it in the running VM only.
- The VM page shows each NIC's own QoS and each CD-ROM's own pool (they showed the previous device's), and a NIC without a source or model no longer breaks it.
- A volume of an LVM or iSCSI pool can be attached to a VM, and bridge and direct NICs can be boot devices. The NIC model `default` lets libvirt choose, and `rt18139` is `rtl8139`: a VM with either did not start; a migration corrects the default NIC type setting.

View file

@ -2,7 +2,6 @@
{% load i18n %}
{% load bootstrap_icons %}
{% load qr_code %}
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %}

View file

@ -1,9 +1,8 @@
{% load i18n %}
{% load qr_code %}
{% blocktrans %}
Scan this QR code to get OTP for account '{{ user }}'
{% endblocktrans %}
<br>
{% qr_from_text totp_url %}
{{ qr_svg }}
<p class="small">{% trans 'Some e-mail clients does not render SVG, also generating PNG.' %}</p>
{% qr_from_text totp_url size="s" image_format="png" error_correction="M" %}
<img src="{{ qr_png }}" alt="{{ totp_url }}">

View file

@ -1,10 +1,16 @@
import base64
import hashlib
import re
import struct
import zlib
from types import SimpleNamespace
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import TestCase
from django_otp.plugins.otp_totp.models import TOTPDevice
from lxml import html
from accounts.utils import (get_existing_totp_device, get_user_totp_device, send_email_with_otp, validate_ssh_key)
@ -78,3 +84,26 @@ class AccountsUtilsTestCase(TestCase):
self.assertEqual(len(mail.outbox), 1)
self.assertIn("OTP QR Code", mail.outbox[0].subject)
self.assertEqual(mail.outbox[0].to, ["user@example.com"])
# the QR code as inline SVG and, for clients without SVG, as a PNG data URI
doc = html.fromstring(mail.outbox[0].alternatives[0][0])
self.assertEqual(len(doc.xpath("//svg")), 1)
(img,) = doc.xpath("//img")
self.assertEqual(img.get("alt"), device.config_url)
self.assertTrue(base64.b64decode(img.get("src").removeprefix("data:image/png;base64,")).startswith(b"\x89PNG"))
def test_otp_email_qr_codes_are_what_django_qr_code_drew(self):
# sha256 of the SVG and the PNG django-qr-code 4.2.0 made for this URL
url = "otpauth://totp/WebVirtCloud:alice?secret=JBSWY3DPEHPK3PXP&issuer=WebVirtCloud"
with patch("accounts.utils.send_mail") as send:
send_email_with_otp(self.user, SimpleNamespace(config_url=url))
page = send.call_args.kwargs["html_message"]
svg = re.search(r"<svg.*?</svg>", page, re.S).group(0)
png = base64.b64decode(re.search(r'<img src="data:image/png;base64,([^"]+)"', page).group(1))
self.assertEqual(hashlib.sha256(svg.encode()).hexdigest(), "b57323be725891af13d862a3a3351cac3ac64a493793c0258cf3e4a5886467c2")
# the PNG's header and pixel rows: its compressed bytes depend on the zlib build
chunks, pos = {}, 8
while pos < len(png):
size, kind = struct.unpack(">I4s", png[pos:pos + 8])
chunks[kind] = chunks.get(kind, b"") + png[pos + 8:pos + 8 + size]
pos += 12 + size
self.assertEqual(hashlib.sha256(chunks[b"IHDR"] + zlib.decompress(chunks[b"IDAT"])).hexdigest(), "cca16de611c73468827f70001f69dc2e48633e6aae2b570a9391eb40a1f8ae88")

View file

@ -1,9 +1,12 @@
import base64
import binascii
import struct
from decimal import Decimal
import segno
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice
@ -54,6 +57,10 @@ def validate_ssh_key(key):
def send_email_with_otp(user, device):
# An inline SVG (1.8 mm modules) and, for mail clients that show no SVG,
# a PNG (12 px modules); error correction M, 4-module border.
qr = segno.make(device.config_url, error="m", mode="byte", encoding="utf-8", micro=False)
style = {"border": 4, "dark": "black", "light": "white"}
send_mail(
_("OTP QR Code"),
_("Please view HTML version of this message."),
@ -64,6 +71,8 @@ def send_email_with_otp(user, device):
{
"totp_url": device.config_url,
"user": user,
"qr_svg": mark_safe(qr.svg_inline(scale=Decimal("1.8"), unit="mm", **style)),
"qr_png": qr.png_data_uri(scale=12, **style),
},
),
fail_silently=False,

View file

@ -2,7 +2,6 @@ Django==5.2.18
django_bootstrap5==26.3
django-bootstrap-icons==0.9.0
django-otp==1.7.4
django-qr-code==4.2.0
django-auth-ldap==5.3.0
# the installer's venv can otherwise keep older distro copies (openSUSE 15.6)
python-ldap>=3.4.5
@ -25,6 +24,7 @@ paramiko==5.0.0
psycopg[binary]==3.3.6
python-engineio==4.14.0
python-socketio==5.17.0
segno==1.6.6
websockify==0.13.0
whitenoise==6.12.0
crypt-r>=3.13.0; python_version >= "3.13"

View file

@ -79,7 +79,6 @@ INSTALLED_APPS = [
"storages",
"virtsecrets",
"logs",
"qr_code",
"rest_framework",
"axes",
]