from datetime import datetime, timezone from uuid import UUID from fastapi import APIRouter, 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.services.kiosk_service import kiosk_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-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, )