Files
marktvogt.de/app/lib/features/user/presentation/security_screen.dart
T
vikingowl 284299b946 feat: implement full Flutter MVP app
Complete Flutter app (Android + iOS) mirroring the web frontend:

- Core: Riverpod state, Dio networking with auth interceptor + auto-refresh,
  go_router navigation, flutter_secure_storage, light/dark theme with
  MedievalSharp/Crimson Pro fonts, German l10n
- Market: search with text/GPS/radius/date/sort filters, list + map views
  (flutter_map + OSM), detail screen with opening hours, admission prices,
  single-marker map, pagination
- Auth: login (password + magic link tabs), register, OAuth button placeholders,
  2FA code prompt on 401, sealed auth state provider
- User: profile view/edit/delete with confirm dialog, 2FA setup/disable
  on security screen
- GPS: geolocator with IP-based fallback (geojs.io) matching web behavior
- Platform: Android internet + location permissions, iOS NSLocation description
- Tests: date/currency/distance formatter unit tests (13 passing)
- Zero analysis issues, debug APK builds successfully
2026-02-21 07:10:30 +01:00

292 lines
8.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../shared/widgets/alert_banner.dart';
import '../../../shared/widgets/app_button.dart';
import '../../auth/domain/auth_provider.dart';
import '../../auth/domain/models/totp_setup_data.dart';
class SecurityScreen extends ConsumerStatefulWidget {
const SecurityScreen({super.key});
@override
ConsumerState<SecurityScreen> createState() => _SecurityScreenState();
}
class _SecurityScreenState extends ConsumerState<SecurityScreen> {
TotpSetupData? _setupData;
bool _settingUp = false;
bool _disabling = false;
String? _error;
String? _success;
final _codeController = TextEditingController();
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
Future<void> _startSetup() async {
setState(() {
_settingUp = true;
_error = null;
});
try {
final data = await ref.read(authRepositoryProvider).setupTotp();
setState(() {
_setupData = data;
_settingUp = false;
});
} catch (e) {
setState(() {
_error = 'Fehler beim Starten der 2FA-Einrichtung.';
_settingUp = false;
});
}
}
Future<void> _verifyCode() async {
final code = _codeController.text.trim();
if (code.length != 6) return;
setState(() {
_settingUp = true;
_error = null;
});
try {
await ref.read(authRepositoryProvider).verifyTotp(code);
setState(() {
_setupData = null;
_settingUp = false;
_success = '2FA wurde erfolgreich aktiviert.';
});
_codeController.clear();
} catch (e) {
setState(() {
_error = 'Ungültiger Code. Bitte versuche es erneut.';
_settingUp = false;
});
}
}
Future<void> _disableTotp() async {
final code = _codeController.text.trim();
if (code.length != 6) return;
setState(() {
_disabling = true;
_error = null;
});
try {
await ref.read(authRepositoryProvider).disableTotp(code);
setState(() {
_disabling = false;
_success = '2FA wurde deaktiviert.';
});
_codeController.clear();
} catch (e) {
setState(() {
_error = 'Ungültiger Code. Bitte versuche es erneut.';
_disabling = false;
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Sicherheit')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Zwei-Faktor-Authentifizierung',
style: theme.textTheme.headlineMedium),
const SizedBox(height: 16),
if (_success != null) ...[
AlertBanner(
message: _success!,
type: AlertType.success,
onDismiss: () => setState(() => _success = null),
),
const SizedBox(height: 16),
],
if (_error != null) ...[
AlertBanner(
message: _error!,
type: AlertType.error,
onDismiss: () => setState(() => _error = null),
),
const SizedBox(height: 16),
],
// Setup flow
if (_setupData != null) ...[
_buildSetupFlow(theme),
] else ...[
_buildActions(theme),
],
],
),
),
),
),
);
}
Widget _buildSetupFlow(ThemeData theme) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('2FA einrichten', style: theme.textTheme.titleLarge),
const SizedBox(height: 12),
Text(
'Scanne diesen QR-Code mit deiner Authenticator-App:',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
// QR code URL display (user would use the URL in their auth app)
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(Icons.qr_code_2, size: 120, color: theme.colorScheme.onSurface),
const SizedBox(height: 8),
SelectableText(
_setupData!.secret,
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 16),
TextField(
controller: _codeController,
decoration: const InputDecoration(
labelText: 'Bestätigungscode',
hintText: '6-stelliger Code',
prefixIcon: Icon(Icons.pin),
),
keyboardType: TextInputType.number,
maxLength: 6,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _verifyCode(),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => setState(() => _setupData = null),
child: const Text('Abbrechen'),
),
),
const SizedBox(width: 12),
Expanded(
child: AppButton(
label: 'Bestätigen',
isLoading: _settingUp,
onPressed: _verifyCode,
),
),
],
),
],
),
),
);
}
Widget _buildActions(ThemeData theme) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(Icons.security, color: theme.colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('2FA-Status', style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
'Schütze dein Konto mit einem zweiten Faktor.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.outline,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Enable 2FA
AppButton(
label: '2FA aktivieren',
icon: Icons.shield,
isLoading: _settingUp,
onPressed: _startSetup,
fullWidth: true,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
// Disable 2FA section
Text('2FA deaktivieren', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
TextField(
controller: _codeController,
decoration: const InputDecoration(
labelText: 'Aktueller 2FA-Code',
hintText: '6-stelliger Code',
prefixIcon: Icon(Icons.pin),
),
keyboardType: TextInputType.number,
maxLength: 6,
),
const SizedBox(height: 8),
AppButton(
label: '2FA deaktivieren',
variant: AppButtonVariant.danger,
isLoading: _disabling,
onPressed: _disableTotp,
fullWidth: true,
),
],
),
),
);
}
}