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') }}

{{ _("qr_login.description") }} @@ -110,8 +116,7 @@ - - +