feat(kiosk): Stufe 2 – Ed25519-Auth, CLI-Tool, neue KioskDevicesPage
2A – Backend Ed25519-Verifizierung: - app/core/kiosk_security.py (NEU): verify_kiosk_request() Dependency - Timestamp-Check (30s Drift), Nonce-Cache (Redis/In-Memory), IP-Whitelist - Ed25519-Signatur über METHOD+PATH+TIMESTAMP+NONCE+sha256(BODY) - PEM + OpenSSH Key-Format unterstützt - app/routers/kiosk.py: approve/revoke Endpunkte, POST /heartbeat (Ed25519-signiert) - app/services/kiosk_service.py: token-basierte Methoden entfernt, approve/revoke/heartbeat - app/schemas/kiosk.py: KioskDeviceOut mit heartbeat_status, HeartbeatRequest/Response 2B – CLI-Tool: - cli.py (NEU, 529 Zeilen): Typer-CLI mit kiosk add/list/approve/revoke/info - Public-Key-Fingerprint (SHA256), Rich-Tabellen, CIDR-Validierung - Direkter DB-Zugriff mit RLS-Bypass 2C – Frontend: - KioskDevicesPage.tsx: Zwei-Tab-Layout (Wartet/Aktiv), Status-Ampel, Auto-Refresh 30s, Ed25519-Workflow (kein Token mehr) - Layout.tsx: KioskHealthBadge (online/total, 30s Refresh, nur COMPANY_ADMIN) requirements.txt: typer>=0.12.0, rich>=13.7.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { api } from '../api/client'
|
||||
|
||||
interface NavItem {
|
||||
path: string
|
||||
@@ -8,6 +9,11 @@ interface NavItem {
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
interface KioskHealthDevice {
|
||||
status: 'pending' | 'approved' | 'revoked'
|
||||
heartbeat_status: 'online' | 'stale' | 'offline'
|
||||
}
|
||||
|
||||
const MAIN_NAV: NavItem[] = [
|
||||
{ path: '/dashboard', label: 'Dashboard' },
|
||||
{ path: '/time', label: 'Zeiterfassung' },
|
||||
@@ -38,6 +44,62 @@ const ROLE_LABELS: Record<string, string> = {
|
||||
EMPLOYEE: 'Mitarbeiter',
|
||||
}
|
||||
|
||||
const KIOSK_HEALTH_ROLES = ['COMPANY_ADMIN', 'SUPER_ADMIN']
|
||||
|
||||
function KioskHealthBadge({ userRole }: { userRole: string }) {
|
||||
const navigate = useNavigate()
|
||||
const [online, setOnline] = useState(0)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
async function fetchHealth() {
|
||||
try {
|
||||
const devices = await api.get<KioskHealthDevice[]>('/kiosk/devices')
|
||||
const approved = devices.filter(d => d.status === 'approved')
|
||||
const onlineCount = approved.filter(d => d.heartbeat_status === 'online').length
|
||||
setTotal(approved.length)
|
||||
setOnline(onlineCount)
|
||||
setVisible(true)
|
||||
} catch {
|
||||
setVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!KIOSK_HEALTH_ROLES.includes(userRole)) return
|
||||
fetchHealth()
|
||||
const interval = setInterval(fetchHealth, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [userRole])
|
||||
|
||||
if (!KIOSK_HEALTH_ROLES.includes(userRole) || !visible || total === 0) return null
|
||||
|
||||
let dotColor = 'bg-green-500'
|
||||
let textColor = 'text-green-700'
|
||||
let bgColor = 'bg-green-50 border-green-200'
|
||||
|
||||
if (online === 0) {
|
||||
dotColor = 'bg-red-500'
|
||||
textColor = 'text-red-700'
|
||||
bgColor = 'bg-red-50 border-red-200'
|
||||
} else if (online < total) {
|
||||
dotColor = 'bg-yellow-400'
|
||||
textColor = 'text-yellow-700'
|
||||
bgColor = 'bg-yellow-50 border-yellow-200'
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate('/settings/kiosk')}
|
||||
title='Kiosk-Geräte-Status – klicken zum Öffnen'
|
||||
className={`flex items-center gap-1.5 text-xs px-2 py-1 rounded-full border font-medium transition-opacity hover:opacity-80 ${bgColor} ${textColor}`}
|
||||
>
|
||||
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${dotColor}`} />
|
||||
{online}/{total} Kiosks
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Layout({ children, userRole, userName }: {
|
||||
children: React.ReactNode
|
||||
userRole: string
|
||||
@@ -97,8 +159,11 @@ export function Layout({ children, userRole, userName }: {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Rechte Seite: Einstellungen + User + Abmelden */}
|
||||
<div className='flex items-center gap-1 flex-shrink-0'>
|
||||
{/* Rechte Seite: Health-Badge + Einstellungen + User + Abmelden */}
|
||||
<div className='flex items-center gap-1.5 flex-shrink-0'>
|
||||
|
||||
{/* Kiosk Health-Badge */}
|
||||
<KioskHealthBadge userRole={userRole} />
|
||||
|
||||
{/* Zahnrad-Dropdown */}
|
||||
{visibleSettings.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user