feat(kiosk): QR-Login-Token-Endpunkt für Employee-App
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:
1 parent
62a19d17e8
commit
71db3bdd57
3 files changed
+47
-2
No files matched your search
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
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.core.kiosk_security import verify_kiosk_request
|
||||||
from app.models.company import Company
|
from app.models.company import Company
|
||||||
from app.models.kiosk_device import KioskDevice, KioskDeviceStatus
|
from app.models.kiosk_device import KioskDevice, KioskDeviceStatus
|
||||||
@@ -25,6 +25,7 @@ from app.schemas.kiosk_auth import (
|
|||||||
KioskNfcLoginRequest,
|
KioskNfcLoginRequest,
|
||||||
KioskPinLoginRequest,
|
KioskPinLoginRequest,
|
||||||
KioskQrLoginRequest,
|
KioskQrLoginRequest,
|
||||||
|
KioskQrTokenResponse,
|
||||||
KioskSessionResponse,
|
KioskSessionResponse,
|
||||||
KioskStampRequest,
|
KioskStampRequest,
|
||||||
KioskStampResponse,
|
KioskStampResponse,
|
||||||
@@ -35,7 +36,7 @@ from app.schemas.kiosk_auth import (
|
|||||||
)
|
)
|
||||||
from app.schemas.time_entry import StampInRequest
|
from app.schemas.time_entry import StampInRequest
|
||||||
from app.models.time_entry import EntrySource
|
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_service import kiosk_service
|
||||||
from app.services.kiosk_session_service import SESSION_TTL_SECONDS, kiosk_session_service
|
from app.services.kiosk_session_service import SESSION_TTL_SECONDS, kiosk_session_service
|
||||||
from app.services.time_service import time_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)
|
_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) ──────────────────────────────────────────
|
# ── Geräteverwaltung (COMPANY_ADMIN) ──────────────────────────────────────────
|
||||||
|
|
||||||
@router.get("/devices", response_model=list[KioskDeviceOut])
|
@router.get("/devices", response_model=list[KioskDeviceOut])
|
||||||
|
|||||||
@@ -61,3 +61,10 @@ class KioskStampStatusResponse(BaseModel):
|
|||||||
stamped_in: bool
|
stamped_in: bool
|
||||||
on_break: bool
|
on_break: bool
|
||||||
start_time: str | None = None
|
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
|
||||||
@@ -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"},
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
assert hb.status_code == 200, hb.text
|
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
|
||||||
Reference in new issue
Block a user