feat(frontend): Bearer-Kiosk-Gerät + QR-Code-Einrichtung im Portal
KioskDevicesPage: Modus-Umschalter im Create-Modal (Ed25519 PC-Terminal vs.
Bearer/VPN-Tablet). Bearer-Modus ruft neuen POST /kiosk/devices/bearer auf,
zeigt Token danach einmalig als QR-Code (JSON {url, token}, scanbar von der
Kiosk-App) + Klartext mit Kopier-Button. Kein Public-Key-Feld im Bearer-Modus.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
This commit is contained in:
1 parent
d80fc59f17
commit
62a19d17e8
1 file changed
+101
-17
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import QRCode from 'qrcode'
|
||||
import { api } from '../api/client'
|
||||
import { Spinner } from '../components/Spinner'
|
||||
import { Layout } from '../components/Layout'
|
||||
@@ -76,6 +77,7 @@ export function KioskDevicesPage() {
|
||||
|
||||
// Create modal
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [createMode, setCreateMode] = useState<'ed25519' | 'bearer'>('ed25519')
|
||||
const [createName, setCreateName] = useState('')
|
||||
const [createLocation, setCreateLocation] = useState('')
|
||||
const [createPublicKey, setCreatePublicKey] = useState('')
|
||||
@@ -83,6 +85,16 @@ export function KioskDevicesPage() {
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
|
||||
// Bearer-Token-Ergebnis (nur einmal sichtbar, danach nicht mehr abrufbar)
|
||||
const [bearerResult, setBearerResult] = useState<{ token: string; deviceName: string } | null>(null)
|
||||
const [bearerQrDataUrl, setBearerQrDataUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!bearerResult) { setBearerQrDataUrl(''); return }
|
||||
const payload = JSON.stringify({ url: `${window.location.origin}/api/v1`, token: bearerResult.token })
|
||||
QRCode.toDataURL(payload, { width: 260, margin: 2 }).then(setBearerQrDataUrl)
|
||||
}, [bearerResult])
|
||||
|
||||
// Edit modal
|
||||
const [editDevice, setEditDevice] = useState<KioskDevice | null>(null)
|
||||
const [editName, setEditName] = useState('')
|
||||
@@ -115,20 +127,30 @@ export function KioskDevicesPage() {
|
||||
async function handleCreate() {
|
||||
setCreateError(null)
|
||||
if (!createName.trim()) { setCreateError('Name ist erforderlich.'); return }
|
||||
if (!createPublicKey.trim()) { setCreateError('Public Key ist erforderlich.'); return }
|
||||
if (createMode === 'ed25519' && !createPublicKey.trim()) { setCreateError('Public Key ist erforderlich.'); return }
|
||||
setCreateLoading(true)
|
||||
try {
|
||||
await api.post<KioskDevice>('/kiosk/devices', {
|
||||
name: createName.trim(),
|
||||
location: createLocation.trim() || null,
|
||||
public_key: createPublicKey.trim(),
|
||||
ip_whitelist: createIpWhitelist.trim() || null,
|
||||
})
|
||||
if (createMode === 'bearer') {
|
||||
const res = await api.post<{ device: KioskDevice; token: string }>('/kiosk/devices/bearer', {
|
||||
name: createName.trim(),
|
||||
location: createLocation.trim() || null,
|
||||
ip_whitelist: createIpWhitelist.trim() || null,
|
||||
})
|
||||
setBearerResult({ token: res.token, deviceName: res.device.name })
|
||||
} else {
|
||||
await api.post<KioskDevice>('/kiosk/devices', {
|
||||
name: createName.trim(),
|
||||
location: createLocation.trim() || null,
|
||||
public_key: createPublicKey.trim(),
|
||||
ip_whitelist: createIpWhitelist.trim() || null,
|
||||
})
|
||||
}
|
||||
setShowCreate(false)
|
||||
setCreateName('')
|
||||
setCreateLocation('')
|
||||
setCreatePublicKey('')
|
||||
setCreateIpWhitelist('')
|
||||
setCreateMode('ed25519')
|
||||
setActiveTab('pending')
|
||||
load()
|
||||
} catch (e: unknown) {
|
||||
@@ -382,6 +404,27 @@ export function KioskDevicesPage() {
|
||||
<div className='bg-blue-50 border border-blue-200 rounded-lg px-3 py-2 text-xs text-blue-800'>
|
||||
Das Gerät erscheint nach dem Anlegen im Tab „Wartet auf Freigabe". Ein Admin muss es freigeben.
|
||||
</div>
|
||||
<div className='flex rounded-lg border border-gray-300 overflow-hidden text-sm'>
|
||||
<button
|
||||
onClick={() => setCreateMode('ed25519')}
|
||||
className={`flex-1 px-3 py-2 font-medium ${createMode === 'ed25519' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'}`}
|
||||
>
|
||||
PC-Terminal (Ed25519)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCreateMode('bearer')}
|
||||
className={`flex-1 px-3 py-2 font-medium ${createMode === 'bearer' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'}`}
|
||||
>
|
||||
Android-Tablet (Bearer/VPN)
|
||||
</button>
|
||||
</div>
|
||||
{createMode === 'bearer' && (
|
||||
<div className='bg-indigo-50 border border-indigo-200 rounded-lg px-3 py-2 text-xs text-indigo-800'>
|
||||
Kein Public Key nötig – nach dem Anlegen bekommst du ein Token
|
||||
+ QR-Code, den die Kiosk-App beim Einrichten scannen kann.
|
||||
Setzt WireGuard-VPN mit fester Geräte-IP voraus.
|
||||
</div>
|
||||
)}
|
||||
<label className='block'>
|
||||
<span className='text-xs font-medium text-gray-700'>Name *</span>
|
||||
<input
|
||||
@@ -401,16 +444,18 @@ export function KioskDevicesPage() {
|
||||
placeholder='z.B. Erdgeschoss, Halle A'
|
||||
/>
|
||||
</label>
|
||||
<label className='block'>
|
||||
<span className='text-xs font-medium text-gray-700'>Public Key *</span>
|
||||
<textarea
|
||||
value={createPublicKey}
|
||||
onChange={e => setCreatePublicKey(e.target.value)}
|
||||
rows={4}
|
||||
className={inputClass + ' resize-none font-mono text-xs'}
|
||||
placeholder='ssh-ed25519 AAAA...'
|
||||
/>
|
||||
</label>
|
||||
{createMode === 'ed25519' && (
|
||||
<label className='block'>
|
||||
<span className='text-xs font-medium text-gray-700'>Public Key *</span>
|
||||
<textarea
|
||||
value={createPublicKey}
|
||||
onChange={e => setCreatePublicKey(e.target.value)}
|
||||
rows={4}
|
||||
className={inputClass + ' resize-none font-mono text-xs'}
|
||||
placeholder='ssh-ed25519 AAAA...'
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className='block'>
|
||||
<span className='text-xs font-medium text-gray-700'>IP-Whitelist (optional)</span>
|
||||
<input
|
||||
@@ -431,6 +476,45 @@ export function KioskDevicesPage() {
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Bearer-Token-Ergebnis (nur einmal sichtbar) */}
|
||||
{bearerResult && (
|
||||
<Modal title='Kiosk-Gerät angelegt' onClose={() => setBearerResult(null)}>
|
||||
<div className='space-y-4'>
|
||||
<div className='bg-yellow-50 border border-yellow-200 rounded-lg px-3 py-2 text-xs text-yellow-800'>
|
||||
Token wird nur hier angezeigt – danach nicht mehr abrufbar. Beim
|
||||
erneuten Öffnen dieses Dialogs (z.B. Seiten-Reload) ist er weg.
|
||||
</div>
|
||||
<p className='text-sm text-gray-700'>
|
||||
Gerät <strong>{bearerResult.deviceName}</strong> – QR-Code in der
|
||||
Kiosk-App unter „Ersteinrichtung" scannen, oder Server-URL +
|
||||
Token manuell eintragen.
|
||||
</p>
|
||||
{bearerQrDataUrl && (
|
||||
<div className='flex justify-center'>
|
||||
<img src={bearerQrDataUrl} alt='QR-Code für Kiosk-Einrichtung' className='rounded-lg border border-gray-200' />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className='text-xs font-medium text-gray-700'>Token (manuell)</span>
|
||||
<div className='flex gap-2 mt-1'>
|
||||
<input readOnly value={bearerResult.token} className={inputClass + ' font-mono text-xs'} />
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(bearerResult.token)}
|
||||
className='px-3 py-2 text-xs font-medium text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 whitespace-nowrap'
|
||||
>
|
||||
Kopieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-end pt-2'>
|
||||
<button onClick={() => setBearerResult(null)} className='px-4 py-2 text-sm font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700'>
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Bearbeiten Modal */}
|
||||
{editDevice && (
|
||||
<Modal title={`Gerät bearbeiten – ${editDevice.name}`} onClose={() => setEditDevice(null)}>
|
||||
|
||||
Reference in new issue
Block a user