diff --git a/backend/app/routers/kiosk.py b/backend/app/routers/kiosk.py index e3fc6b5..8a8ef65 100644 --- a/backend/app/routers/kiosk.py +++ b/backend/app/routers/kiosk.py @@ -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]) diff --git a/backend/app/schemas/kiosk_auth.py b/backend/app/schemas/kiosk_auth.py index e92819f..371a710 100644 --- a/backend/app/schemas/kiosk_auth.py +++ b/backend/app/schemas/kiosk_auth.py @@ -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 diff --git a/backend/tests/test_kiosk_security.py b/backend/tests/test_kiosk_security.py index 528070a..85b21fa 100644 --- a/backend/tests/test_kiosk_security.py +++ b/backend/tests/test_kiosk_security.py @@ -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