import 'package:flutter/material.dart'; import '../app_version.dart'; import '../services/api_client.dart'; import 'login_screen.dart'; import 'qr_enrollment_scan_screen.dart'; /// Einmalige Ersteinrichtung: Gerät erzeugt selbst ein Ed25519-Schlüsselpaar /// (privater Schlüssel verlässt nie das Gerät), zeigt den öffentlichen /// Schlüssel an – Admin trägt ihn im Portal bei "PC-Terminal (Ed25519)" ein /// und gibt die dabei erzeugte Geräte-ID zurück (per QR-Scan oder manuell). /// Ersetzt seit dem Security-Audit 2026-09-10 den reinen Bearer-Token-Modus /// (siehe api_client.dart – Token war ohne TLS im Klartext mitschneidbar). class EnrollmentScreen extends StatefulWidget { const EnrollmentScreen({super.key}); @override State createState() => _EnrollmentScreenState(); } class _EnrollmentScreenState extends State { final _urlCtrl = TextEditingController(text: ApiClient.instance.baseUrl); final _deviceIdCtrl = TextEditingController(); String? _publicKey; bool _loadingKey = true; bool _saving = false; String? _error; @override void initState() { super.initState(); _loadPublicKey(); } Future _loadPublicKey() async { final key = await ApiClient.instance.crypto.ensurePublicKeySsh(); if (!mounted) return; setState(() { _publicKey = key; _loadingKey = false; }); } Future _scanQr() async { final result = await Navigator.of(context).push>( MaterialPageRoute(builder: (_) => const QrEnrollmentScanScreen()), ); if (result == null || !mounted) return; setState(() { _urlCtrl.text = result['url'] ?? _urlCtrl.text; _deviceIdCtrl.text = result['deviceId'] ?? result['device_id'] ?? ''; _error = null; }); } Future _save() async { if (_urlCtrl.text.trim().isEmpty || _deviceIdCtrl.text.trim().isEmpty) { setState(() => _error = 'Server-URL und Geräte-ID sind Pflichtfelder.'); return; } setState(() { _saving = true; _error = null; }); await ApiClient.instance.enroll( baseUrl: _urlCtrl.text.trim(), deviceId: _deviceIdCtrl.text.trim(), ); if (!mounted) return; Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const LoginScreen()), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('TimeMaster Kiosk · v$kAppVersion')), body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.tablet_mac, size: 64, color: Colors.indigo), const SizedBox(height: 16), Text('Ersteinrichtung', style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 16), _publicKeyCard(), const SizedBox(height: 24), const Text( 'Danach: Server-URL + Geräte-ID eintragen (aus dem Portal, nach dem Anlegen des Geräts mit obigem Public Key).', textAlign: TextAlign.center, ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _scanQr, icon: const Icon(Icons.qr_code_scanner), label: const Text('QR-Code scannen'), ), ), const SizedBox(height: 16), Row(children: const [ Expanded(child: Divider()), Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Text('oder manuell')), Expanded(child: Divider()), ]), const SizedBox(height: 16), TextField( controller: _urlCtrl, decoration: const InputDecoration(labelText: 'Server-URL', hintText: 'https://.../api/v1'), ), const SizedBox(height: 12), TextField( controller: _deviceIdCtrl, decoration: const InputDecoration(labelText: 'Geräte-ID (UUID aus dem Portal)'), ), const SizedBox(height: 24), if (_error != null) ...[ Text(_error!, style: const TextStyle(color: Colors.red)), const SizedBox(height: 12), ], SizedBox( width: double.infinity, child: FilledButton( onPressed: _saving ? null : _save, child: const Text('Einrichten'), ), ), ], ), ), ), ), ); } Widget _publicKeyCard() { if (_loadingKey) return const CircularProgressIndicator(); return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Öffentlicher Schlüssel dieses Geräts', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 4), const Text('Im Portal unter "Neues Kiosk-Gerät" → PC-Terminal (Ed25519) einfügen.', style: TextStyle(fontSize: 12, color: Colors.grey)), const SizedBox(height: 8), SelectableText(_publicKey ?? '', style: const TextStyle(fontFamily: 'monospace', fontSize: 11)), ], ), ), ); } }