feat(kiosk): QR-Login-Token-Endpunkt für Employee-App
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s

POST /kiosk/my-qr-token (normale JWT-Auth) – Mitarbeiter fordert einen 5min
gültigen, einmalig einlösbaren QR-Token an, den die Employee-App als QR-Code
zeigt. Kiosk scannt ihn (POST /kiosk/auth/qr, bereits vorhanden) und meldet
den Mitarbeiter an. Backend-Funktion (generate_qr_token) existierte schon,
war aber nie über einen Endpunkt erreichbar. Test für den kompletten Ablauf
inkl. Einmaligkeit (zweiter Versuch mit selbem Token → 401).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-10 14:47:45 +02:00
1 parent 62a19d17e8
commit 71db3bdd57
3 files changed
+47 -2

No files matched your search

+14 -2
View File
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import require_role
from app.core.dependencies import CurrentUser, require_role
from app.core.kiosk_security import verify_kiosk_request
from app.models.company import Company
from app.models.kiosk_device import KioskDevice, KioskDeviceStatus
@@ -25,6 +25,7 @@ from app.schemas.kiosk_auth import (
KioskNfcLoginRequest,
KioskPinLoginRequest,
KioskQrLoginRequest,
KioskQrTokenResponse,
KioskSessionResponse,
KioskStampRequest,
KioskStampResponse,
@@ -35,7 +36,7 @@ from app.schemas.kiosk_auth import (
)
from app.schemas.time_entry import StampInRequest
from app.models.time_entry import EntrySource
from app.services.kiosk_auth_service import _display_name, kiosk_auth_service
from app.services.kiosk_auth_service import QR_TOKEN_TTL, _display_name, kiosk_auth_service
from app.services.kiosk_service import kiosk_service
from app.services.kiosk_session_service import SESSION_TTL_SECONDS, kiosk_session_service
from app.services.time_service import time_service
@@ -45,6 +46,17 @@ router = APIRouter(prefix="/kiosk", tags=["Kiosk"])
_admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
# ── Mitarbeiter-QR-Login (normale JWT-Auth, für die Employee-App) ────────────
@router.post("/my-qr-token", response_model=KioskQrTokenResponse)
async def get_my_qr_token(current_user: CurrentUser):
"""Erzeugt einen 5 min gültigen, einmalig einlösbaren QR-Login-Token für
den eingeloggten Mitarbeiter – die Employee-App zeigt ihn als QR-Code,
der Kiosk scannt ihn (POST /kiosk/auth/qr) und meldet den Mitarbeiter an."""
token = await kiosk_auth_service.generate_qr_token(current_user.id, current_user.company_id)
return KioskQrTokenResponse(qr_token=token, expires_in_seconds=QR_TOKEN_TTL)
# ── Geräteverwaltung (COMPANY_ADMIN) ──────────────────────────────────────────
@router.get("/devices", response_model=list[KioskDeviceOut])
+7
View File
@@ -61,3 +61,10 @@ class KioskStampStatusResponse(BaseModel):
stamped_in: bool
on_break: bool
start_time: str | None = None
class KioskQrTokenResponse(BaseModel):
"""Für die Mitarbeiter-App: eigenen QR-Login-Token anfordern, der am
Kiosk gescannt werden kann (5 min gültig, einmalig einlösbar)."""
qr_token: str
expires_in_seconds: int
+26
View File
@@ -517,3 +517,29 @@ async def test_create_bearer_device_via_admin_api(client: AsyncClient, kiosk_adm
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
)
assert hb.status_code == 200, hb.text
async def test_my_qr_token_and_kiosk_login(client: AsyncClient, kiosk_admin_headers, approved_bearer_device):
"""Mitarbeiter fordert eigenen QR-Token an, Kiosk scannt ihn → Session."""
_, bearer_token = approved_bearer_device
r = await client.post("/api/v1/kiosk/my-qr-token", headers=kiosk_admin_headers)
assert r.status_code == 200, r.text
qr_token = r.json()["qr_token"]
assert r.json()["expires_in_seconds"] > 0
login = await client.post(
"/api/v1/kiosk/auth/qr",
json={"qr_token": qr_token},
headers={"Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json"},
)
assert login.status_code == 200, login.text
assert login.json()["auth_method"] == "qr"
# Einmalig: zweiter Versuch mit demselben Token scheitert.
login2 = await client.post(
"/api/v1/kiosk/auth/qr",
json={"qr_token": qr_token},
headers={"Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json"},
)
assert login2.status_code == 401, login2.text