feat: agent-02-kiosk Phase 2A - Auth endpoints (PIN/NFC/QR/List)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 12:49:09 +02:00
parent 094863f94b
commit 30828c69e9
4 changed files with 378 additions and 1 deletions
+129 -1
View File
@@ -1,7 +1,8 @@
import uuid as _uuid
from datetime import datetime, timezone
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
@@ -18,7 +19,17 @@ from app.schemas.kiosk import (
KioskDeviceOut,
KioskDeviceUpdate,
)
from app.schemas.kiosk_auth import (
KioskNfcLoginRequest,
KioskPinLoginRequest,
KioskQrLoginRequest,
KioskSessionResponse,
KioskUserListEntry,
KioskUserListResponse,
)
from app.services.kiosk_auth_service import _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
router = APIRouter(prefix="/kiosk", tags=["Kiosk"])
@@ -126,6 +137,123 @@ async def revoke_device(
)
# ── Kiosk-User-Auth (signierte Requests via Ed25519) ─────────────────────────
@router.post("/auth/pin", response_model=KioskSessionResponse)
async def kiosk_login_pin(
data: KioskPinLoginRequest,
device: KioskDevice = Depends(verify_kiosk_request),
db: AsyncSession = Depends(get_db),
):
"""PIN-Login: Personalnummer + 4-16-stellige PIN."""
user, session_token = await kiosk_auth_service.login_pin(
personnel_number=data.personnel_number,
pin=data.pin,
company_id=device.company_id,
device_id=device.id,
db=db,
)
return KioskSessionResponse(
session_token=session_token,
user_id=user.id,
user_name=_display_name(user),
expires_in_seconds=SESSION_TTL_SECONDS,
auth_method="pin",
)
@router.post("/auth/nfc", response_model=KioskSessionResponse)
async def kiosk_login_nfc(
data: KioskNfcLoginRequest,
device: KioskDevice = Depends(verify_kiosk_request),
db: AsyncSession = Depends(get_db),
):
"""NFC-Login: NFC-UID der Karte."""
user, session_token = await kiosk_auth_service.login_nfc(
nfc_uid=data.nfc_uid,
company_id=device.company_id,
device_id=device.id,
db=db,
)
return KioskSessionResponse(
session_token=session_token,
user_id=user.id,
user_name=_display_name(user),
expires_in_seconds=SESSION_TTL_SECONDS,
auth_method="nfc",
)
@router.post("/auth/qr", response_model=KioskSessionResponse)
async def kiosk_login_qr(
data: KioskQrLoginRequest,
device: KioskDevice = Depends(verify_kiosk_request),
db: AsyncSession = Depends(get_db),
):
"""QR-Login: QR-Token (aus Web-App gescannt)."""
user, session_token = await kiosk_auth_service.login_qr(
qr_token=data.qr_token,
company_id=device.company_id,
device_id=device.id,
db=db,
)
return KioskSessionResponse(
session_token=session_token,
user_id=user.id,
user_name=_display_name(user),
expires_in_seconds=SESSION_TTL_SECONDS,
auth_method="qr",
)
@router.post("/auth/list", response_model=KioskSessionResponse)
async def kiosk_login_list(
user_id: _uuid.UUID = Body(..., embed=True),
device: KioskDevice = Depends(verify_kiosk_request),
db: AsyncSession = Depends(get_db),
):
"""List-Login: User wählt sich aus Mitarbeiterliste aus (kein Passwort)."""
user, session_token = await kiosk_auth_service.login_list(
user_id=user_id,
company_id=device.company_id,
device_id=device.id,
db=db,
)
return KioskSessionResponse(
session_token=session_token,
user_id=user.id,
user_name=_display_name(user),
expires_in_seconds=SESSION_TTL_SECONDS,
auth_method="list",
)
@router.get("/auth/users", response_model=KioskUserListResponse)
async def kiosk_user_list(
device: KioskDevice = Depends(verify_kiosk_request),
db: AsyncSession = Depends(get_db),
):
"""Mitarbeiterliste für Kiosk-Auswahl."""
users = await kiosk_auth_service.get_user_list(device.company_id, db)
return KioskUserListResponse(
users=[
KioskUserListEntry(id=u.id, display_name=_display_name(u))
for u in users
]
)
@router.post("/auth/logout")
async def kiosk_logout(
session_token: str = Body(..., embed=True),
device: KioskDevice = Depends(verify_kiosk_request),
):
"""Session invalidieren."""
await kiosk_session_service.invalidate_session(session_token)
return {"message": "Abgemeldet."}
# ── Kiosk-Endpunkte (signierte Requests via Ed25519) ──────────────────────────
@router.post("/heartbeat", response_model=HeartbeatResponse)