feat(settings): Konto-Konsole (Keycloak) oeffnen + Logout mit Bestaetigung

- 'Konto verwalten' oeffnet die Keycloak-Account-Konsole (issuer/account/) im externen Browser (Passwort/E-Mail/2FA)
- 'Ausloggen': ganze Zeile tappbar + Bestaetigungsdialog -> LogoutRequested

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-06-18 13:08:18 +02:00
parent a206636ed0
commit 7c362549ee

View File

@ -1,5 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:hl_lieferservice/data/network/backend_config.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart'; import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_event.dart'; import 'package:hl_lieferservice/feature/settings/bloc/settings_event.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart'; import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart';
@ -13,9 +18,63 @@ class SettingsPage extends StatefulWidget {
} }
class _SettingsPage extends State<SettingsPage> { class _SettingsPage extends State<SettingsPage> {
void _logout() {} /// Bestätigt das Abmelden und triggert dann `LogoutRequested` am
/// [AuthBloc]. Die Navigation zur LoginPage übernimmt der globale
/// `LoginEnforcer` automatisch beim Wechsel auf `Unauthenticated` —
/// gleicher Pfad wie der Abmelden-Button im Drawer
/// (`home_drawer._confirmLogout`).
///
/// Wording bewusst identisch zum Drawer-Dialog, damit es egal ist, von
/// welchem Einstiegspunkt der Fahrer kommt.
Future<void> _logout() async {
final authBloc = context.read<AuthBloc>();
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text("Abmelden"),
content: const Text(
"Möchten Sie sich wirklich abmelden? "
"Beim nächsten Start ist eine erneute Anmeldung erforderlich.",
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text("Abbrechen"),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text("Abmelden"),
),
],
),
);
if (confirmed != true) return;
authBloc.add(const LogoutRequested());
}
void _changePassword() {} // ─── Konto / Keycloak ─────────────────────────────────────────────────
/// Öffnet die Keycloak-Account-Konsole im externen Browser. Dort kann
/// der Fahrer sein Passwort ändern, hinterlegte E-Mail aktualisieren
/// und (sofern aktiviert) Zwei-Faktor-Authentifizierung verwalten.
///
/// URL-Pfad `/account/` ist die Standard-Route der Keycloak-26-Account-
/// Konsole; sie hängt sich an den Realm-Issuer aus [BackendConfig].
/// Externer Browser statt In-App-WebView, damit ggf. im Browser
/// gespeicherte Credentials / Authenticator-Apps weiter funktionieren.
Future<void> _openAccountConsole() async {
final issuer = BackendConfig.fromEnvironment.keycloakIssuerUrl;
final uri = Uri.parse('$issuer/account/');
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Kontoeinstellungen konnten nicht geöffnet werden: $uri'),
),
);
}
}
Widget _scanSettings() { Widget _scanSettings() {
return BlocBuilder<SettingsBloc, SettingsState>( return BlocBuilder<SettingsBloc, SettingsState>(
@ -97,15 +156,16 @@ class _SettingsPage extends State<SettingsPage> {
), ),
ListTile( ListTile(
title: const Text("Passwort öndern"), title: const Text("Konto verwalten"),
subtitle: const Text(
"Passwort, E-Mail und Sicherheitseinstellungen — öffnet die Konto-Seite im Browser",
),
trailing: Padding( trailing: Padding(
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
child: IconButton( child: FilledButton.icon(
onPressed: _changePassword, onPressed: _openAccountConsole,
icon: FilledButton( icon: const Icon(Icons.open_in_new),
onPressed: _changePassword, label: const Text("Öffnen"),
child: const Text("Ändern"),
),
), ),
), ),
tileColor: Theme.of(context).colorScheme.onSecondary, tileColor: Theme.of(context).colorScheme.onSecondary,
@ -113,10 +173,10 @@ class _SettingsPage extends State<SettingsPage> {
ListTile( ListTile(
title: const Text("Ausloggen"), title: const Text("Ausloggen"),
trailing: IconButton( // Ganze Zeile tappbar machen — sonst muss der Fahrer das kleine
onPressed: _logout, // Icon präzise treffen.
icon: Icon(Icons.logout, color: Colors.redAccent), onTap: _logout,
), trailing: const Icon(Icons.logout, color: Colors.redAccent),
tileColor: Theme.of(context).colorScheme.onSecondary, tileColor: Theme.of(context).colorScheme.onSecondary,
), ),
], ],