From a8eb6504ac100c14b10f57511fde8f1202deeffe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 08:34:22 +0000
Subject: [PATCH] fix(qr-login): render QR code server-side using segno instead
of CDN JS library
The QR code on /qr-login was not rendering because it depended on loading
qrcode@1.5.4 from the jsdelivr CDN, which may be blocked in some network
environments.
- Add segno>=1.6.0 (pure-Python QR library, no Pillow needed) to requirements.txt
- Generate QR code as a base64 SVG data URI server-side in the challenge endpoint
- Add qr_code_svg field to CreateChallengeResponse Pydantic model
- Replace canvas+CDN script in qr_login.html with an
- Remove the $nextTick/QRCode.toCanvas() client-side rendering block
- Extract QR rendering parameters (_QR_ERROR_LEVEL, _QR_SCALE) as module constants
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
app/api/qr_auth.py | 28 ++++++++++++++++++++++++++++
frontend/templates/qr_login.html | 25 ++++++++++---------------
requirements.txt | 1 +
3 files changed, 39 insertions(+), 15 deletions(-)
diff --git a/app/api/qr_auth.py b/app/api/qr_auth.py
index b9dbaa5d..8f891e4e 100644
--- a/app/api/qr_auth.py
+++ b/app/api/qr_auth.py
@@ -19,10 +19,13 @@ Security properties:
from __future__ import annotations
+import base64
+import io
import logging
from datetime import datetime
from typing import Annotated, Any
+import segno
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
@@ -72,6 +75,7 @@ class CreateChallengeResponse(BaseModel):
expires_at: datetime
ttl_seconds: int = Field(description="Seconds until the challenge expires (use for client-side countdown).")
qr_payload: str = Field(description="The string to encode in the QR code.")
+ qr_code_svg: str = Field(description="Base64-encoded SVG data URI of the QR code, ready for use in an
src.")
class ChallengeStatusResponse(BaseModel):
@@ -106,6 +110,29 @@ class ClaimChallengeResponse(BaseModel):
created_at: datetime
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+# QR code rendering parameters
+_QR_ERROR_LEVEL = "M" # Medium error correction (~15% recovery); sufficient for on-screen display
+_QR_SCALE = 4 # Each QR module is rendered as 4×4 SVG pixels
+
+
+def _generate_qr_svg(payload: str) -> str:
+ """Generate a QR code for *payload* and return it as a base64 SVG data URI.
+
+ Using ``segno`` (pure-Python, no Pillow dependency) and SVG output so the
+ QR code scales crisply at any resolution without requiring a canvas or any
+ client-side JavaScript library.
+ """
+ qr = segno.make(payload, error=_QR_ERROR_LEVEL)
+ buf = io.BytesIO()
+ qr.save(buf, kind="svg", scale=_QR_SCALE, xmldecl=False, svgclass=None, lineclass=None, omitsize=True)
+ svg_bytes = buf.getvalue()
+ return "data:image/svg+xml;base64," + base64.b64encode(svg_bytes).decode("ascii")
+
+
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@@ -143,6 +170,7 @@ async def create_challenge(
"expires_at": challenge.expires_at,
"ttl_seconds": ttl_seconds,
"qr_payload": qr_payload,
+ "qr_code_svg": _generate_qr_svg(qr_payload),
}
diff --git a/frontend/templates/qr_login.html b/frontend/templates/qr_login.html
index 7b29f594..a2ecac26 100644
--- a/frontend/templates/qr_login.html
+++ b/frontend/templates/qr_login.html
@@ -32,7 +32,13 @@
id="qr-container"
aria-label="{{ _('qr_login.description') }}"
>
-
+
{{ _("qr_login.description") }} @@ -110,8 +116,7 @@ - - +