From ff097b31d1fc75b761b36678658a4eee8f8ee4c4 Mon Sep 17 00:00:00 2001
From: Ettore <=>
Date: Sun, 10 May 2026 00:41:36 +0200
Subject: [PATCH] Add QR Code for Keypasses
---
requirements.txt | 1 +
src/main.py | 2 +-
src/routers/keypasses.py | 29 ++++++++++++++++++++++++++++-
src/static/admin.html | 16 ++++++++++++++++
src/static/admin.js | 38 +++++++++++++++++++++++++++++++++++++-
src/static/app.js | 24 ++++++++++++++++++++++++
6 files changed, 107 insertions(+), 3 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index c87dabe..a82223b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,3 +7,4 @@ cryptography>=42.0.0
requests>=2.31.0
fake-useragent>=1.5.0
python-multipart>=0.0.9
+qrcode[pil]>=7.4
diff --git a/src/main.py b/src/main.py
index 3d3e46e..b6f0ca8 100644
--- a/src/main.py
+++ b/src/main.py
@@ -87,7 +87,7 @@ async def _security_headers(request: Request, call_next) -> Response:
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = (
- "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:"
)
return response
diff --git a/src/routers/keypasses.py b/src/routers/keypasses.py
index c349fb6..c7217bd 100644
--- a/src/routers/keypasses.py
+++ b/src/routers/keypasses.py
@@ -1,10 +1,12 @@
+import io
import json
import secrets
import string
from datetime import datetime, timezone
from typing import Optional
-from fastapi import APIRouter, Depends, HTTPException
+import qrcode
+from fastapi import APIRouter, Depends, HTTPException, Request, Response
from sqlalchemy.orm import Session
from core.config import utcnow
@@ -88,3 +90,28 @@ async def revoke_keypass(
kp.revoked = True
kp.revoked_at = utcnow()
db.commit()
+
+
+@router.get("/{kp_id}/qr")
+async def keypass_qr(
+ kp_id: int,
+ request: Request,
+ db: Session = Depends(get_db),
+ _: dict = Depends(require_manager),
+):
+ kp: Optional[Keypass] = db.query(Keypass).filter(Keypass.id == kp_id).first()
+ if not kp:
+ raise HTTPException(404, "Keypass not found")
+
+ base_url = str(request.base_url).rstrip("/")
+ url = f"{base_url}/?k={kp.code}"
+
+ qr = qrcode.QRCode(box_size=10, border=4)
+ qr.add_data(url)
+ qr.make(fit=True)
+ img = qr.make_image(fill_color="black", back_color="white")
+
+ buf = io.BytesIO()
+ img.save(buf, format="PNG")
+ buf.seek(0)
+ return Response(content=buf.read(), media_type="image/png")
diff --git a/src/static/admin.html b/src/static/admin.html
index 1bf8ee8..d79f580 100644
--- a/src/static/admin.html
+++ b/src/static/admin.html
@@ -418,6 +418,22 @@
+
+
+
+
Keypass QR Code
+
+
+
![QR Code]()
+
+
Scan to open the app and login automatically.
+
+
+
+
diff --git a/src/static/admin.js b/src/static/admin.js
index d179168..ebb1718 100644
--- a/src/static/admin.js
+++ b/src/static/admin.js
@@ -142,7 +142,9 @@ async function loadKeypasses() {
${expiresCell} |
${badge} |
- ${!kp.revoked ? ` |