import uuid as _uuid from datetime import datetime, timezone from uuid import UUID 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.kiosk_security import verify_kiosk_request from app.models.company import Company from app.models.kiosk_device import KioskDevice, KioskDeviceStatus from app.models.user import User, UserRole from app.schemas.kiosk import ( HeartbeatRequest, HeartbeatResponse, KioskApproveResponse, KioskDeviceCreate, 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"]) _admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) # ── Geräteverwaltung (COMPANY_ADMIN) ────────────────────────────────────────── @router.get("/devices", response_model=list[KioskDeviceOut]) async def list_devices( status: str | None = Query(None, description="Filter: pending, approved oder revoked"), current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Alle registrierten Kiosk-Geräte der Firma auflisten (optional nach Status filtern).""" status_filter: KioskDeviceStatus | None = None if status is not None: try: status_filter = KioskDeviceStatus(status) except ValueError: raise HTTPException( status_code=400, detail=f"Ungültiger Status-Filter '{status}'. Erlaubt: pending, approved, revoked.", ) devices = await kiosk_service.list_devices(current_user.company_id, db, status_filter) return [KioskDeviceOut.model_validate(d) for d in devices] @router.post("/devices", response_model=KioskDeviceOut, status_code=201) async def create_device( data: KioskDeviceCreate, current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Neues Kiosk-Gerät mit Ed25519-Public-Key registrieren.""" device = await kiosk_service.create_device(current_user.company_id, data, db) await db.commit() return KioskDeviceOut.model_validate(device) @router.get("/devices/{device_id}", response_model=KioskDeviceOut) async def get_device( device_id: UUID, current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Einzelnes Kiosk-Gerät abrufen.""" device = await kiosk_service.get_device(device_id, current_user.company_id, db) return KioskDeviceOut.model_validate(device) @router.patch("/devices/{device_id}", response_model=KioskDeviceOut) async def update_device( device_id: UUID, data: KioskDeviceUpdate, current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Name, Standort oder IP-Whitelist eines Kiosk-Geräts aktualisieren.""" device = await kiosk_service.update_device(device_id, current_user.company_id, data, db) await db.commit() return KioskDeviceOut.model_validate(device) @router.delete("/devices/{device_id}", status_code=204) async def delete_device( device_id: UUID, current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Kiosk-Gerät dauerhaft löschen.""" await kiosk_service.delete_device(device_id, current_user.company_id, db) await db.commit() @router.post("/devices/{device_id}/approve", response_model=KioskApproveResponse) async def approve_device( device_id: UUID, current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Kiosk-Gerät freigeben (Status: pending → approved).""" device = await kiosk_service.approve_device(device_id, current_user.company_id, db) await db.commit() return KioskApproveResponse( id=device.id, status=device.status.value, message=f"Gerät '{device.name}' wurde freigegeben.", ) @router.post("/devices/{device_id}/revoke", response_model=KioskApproveResponse) async def revoke_device( device_id: UUID, current_user: User = require_role(*_admin_roles), db: AsyncSession = Depends(get_db), ): """Kiosk-Gerät sperren (Status → revoked). Erfordert Re-Enrollment.""" device = await kiosk_service.revoke_device(device_id, current_user.company_id, db) await db.commit() return KioskApproveResponse( id=device.id, status=device.status.value, message=f"Gerät '{device.name}' wurde gesperrt. Re-Enrollment erforderlich.", ) # ── 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) async def heartbeat( data: HeartbeatRequest, device: KioskDevice = Depends(verify_kiosk_request), db: AsyncSession = Depends(get_db), ): """ Heartbeat-Endpunkt für Kiosk-Geräte. Muss mit Ed25519-Signatur-Headern aufgerufen werden: X-Kiosk-Key-Id, X-Kiosk-Timestamp, X-Kiosk-Nonce, X-Kiosk-Signature Gibt Server-Zeit und Konfiguration zurück. """ company = await db.get(Company, device.company_id) if company is None: raise HTTPException(status_code=500, detail="Firma nicht gefunden.") await kiosk_service.process_heartbeat(device, data, company, db) await db.commit() return HeartbeatResponse( server_time=datetime.now(timezone.utc).isoformat(), heartbeat_interval_sec=company.kiosk_heartbeat_interval_sec, device_status=device.status.value, )