feat(umgebung): Server-Umgebung im Login-Screen waehlbar (Produktion/Test)
- Umgebungen (Name, API-URL, Keycloak-Issuer) kommen aus assets/hl_server_config.json statt hartcodiertem BackendConfig.prod; Testumgebung 192.168.1.9:3001 ist dort hinterlegt - AppBloc laedt die Config, aktiviert die gespeicherte Wahl (Fallback Default) und registriert das Networking; Wechsel persistiert per SharedPreferences und registriert neu - Bloc-Baum ist pro Umgebung gekeyt: beim Wechsel entstehen alle Blocs mit dem neuen API-Client neu - Login-Screen: Zahnrad oben rechts oeffnet Einstellungen mit Umgebungs- Cards (Name + IP:Port); Hinweis auf aktive Umgebung unter dem Button - Fahrzeugauswahl pro Umgebung getrennt (eigene DB = eigene Car-IDs) - Tests fuer Config-Parsing, Auswahl-Fallback, Card-Wechsel, Re-Registrierung Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
123
lib/data/network/backend_environment.dart
Normal file
123
lib/data/network/backend_environment.dart
Normal file
@ -0,0 +1,123 @@
|
||||
import 'backend_config.dart';
|
||||
|
||||
/// Eine auswählbare Server-Umgebung (z. B. Produktion, Test).
|
||||
///
|
||||
/// Quelle ist ausschließlich `assets/hl_server_config.json` — die App
|
||||
/// kennt keine hartcodierten Server. Siehe [ServerCatalog].
|
||||
class BackendEnvironment {
|
||||
const BackendEnvironment({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.apiBaseUrl,
|
||||
required this.keycloakIssuerUrl,
|
||||
});
|
||||
|
||||
/// Stabiler Schlüssel; wird als Auswahl persistiert und trennt lokale,
|
||||
/// umgebungsabhängige Daten (z. B. Fahrzeugauswahl).
|
||||
final String id;
|
||||
|
||||
/// Anzeigename in der Umgebungsauswahl.
|
||||
final String name;
|
||||
|
||||
/// Basis-URL der Rust-API (ohne abschließenden Slash).
|
||||
final String apiBaseUrl;
|
||||
|
||||
/// Realm-Issuer der Keycloak-Instanz dieser Umgebung.
|
||||
final String keycloakIssuerUrl;
|
||||
|
||||
/// „IP:Port“ der API für die Anzeige, z. B. `192.168.1.9:3001`.
|
||||
String get hostAndPort {
|
||||
final uri = Uri.parse(apiBaseUrl);
|
||||
return uri.hasPort ? '${uri.host}:${uri.port}' : uri.host;
|
||||
}
|
||||
|
||||
factory BackendEnvironment.fromJson(Map<String, dynamic> json) {
|
||||
return BackendEnvironment(
|
||||
id: _requireString(json, 'id'),
|
||||
name: _requireString(json, 'name'),
|
||||
apiBaseUrl: _stripTrailingSlash(_requireString(json, 'apiBaseUrl')),
|
||||
keycloakIssuerUrl:
|
||||
_stripTrailingSlash(_requireString(json, 'keycloakIssuerUrl')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Alle in der Config-Datei hinterlegten Umgebungen plus die
|
||||
/// umgebungsübergreifenden Keycloak-Client-Daten.
|
||||
///
|
||||
/// Client-Id und Redirect-URL sind global, weil sie an die native
|
||||
/// Plattform-Konfiguration gebunden sind (Redirect-Scheme in
|
||||
/// `build.gradle.kts` / `Info.plist`) und nicht pro Server variieren.
|
||||
class ServerCatalog {
|
||||
ServerCatalog({
|
||||
required this.environments,
|
||||
required this.defaultEnvironmentId,
|
||||
required this.keycloakClientId,
|
||||
required this.keycloakRedirectUrl,
|
||||
}) {
|
||||
if (environments.isEmpty) {
|
||||
throw const FormatException('Config enthält keine Umgebungen.');
|
||||
}
|
||||
final ids = environments.map((e) => e.id).toSet();
|
||||
if (ids.length != environments.length) {
|
||||
throw const FormatException('Umgebungs-IDs in der Config sind doppelt.');
|
||||
}
|
||||
if (!ids.contains(defaultEnvironmentId)) {
|
||||
throw FormatException(
|
||||
'defaultEnvironment "$defaultEnvironmentId" existiert nicht.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final List<BackendEnvironment> environments;
|
||||
final String defaultEnvironmentId;
|
||||
final String keycloakClientId;
|
||||
final String keycloakRedirectUrl;
|
||||
|
||||
BackendEnvironment get defaultEnvironment => byId(defaultEnvironmentId)!;
|
||||
|
||||
BackendEnvironment? byId(String? id) {
|
||||
for (final env in environments) {
|
||||
if (env.id == id) return env;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Vollständige Netzwerk-Konfiguration für [environment].
|
||||
BackendConfig configFor(BackendEnvironment environment) {
|
||||
return BackendConfig(
|
||||
apiBaseUrl: environment.apiBaseUrl,
|
||||
keycloakIssuerUrl: environment.keycloakIssuerUrl,
|
||||
keycloakClientId: keycloakClientId,
|
||||
keycloakRedirectUrl: keycloakRedirectUrl,
|
||||
);
|
||||
}
|
||||
|
||||
factory ServerCatalog.fromJson(Map<String, dynamic> json) {
|
||||
final keycloak = json['keycloak'];
|
||||
if (keycloak is! Map<String, dynamic>) {
|
||||
throw const FormatException('Abschnitt "keycloak" fehlt in der Config.');
|
||||
}
|
||||
final rawEnvironments = json['environments'];
|
||||
if (rawEnvironments is! List) {
|
||||
throw const FormatException('Liste "environments" fehlt in der Config.');
|
||||
}
|
||||
return ServerCatalog(
|
||||
environments: rawEnvironments
|
||||
.map((e) => BackendEnvironment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(growable: false),
|
||||
defaultEnvironmentId: _requireString(json, 'defaultEnvironment'),
|
||||
keycloakClientId: _requireString(keycloak, 'clientId'),
|
||||
keycloakRedirectUrl: _requireString(keycloak, 'redirectUrl'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _requireString(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is String && value.trim().isNotEmpty) return value.trim();
|
||||
throw FormatException('Feld "$key" fehlt oder ist leer in der Config.');
|
||||
}
|
||||
|
||||
String _stripTrailingSlash(String url) =>
|
||||
url.endsWith('/') ? url.substring(0, url.length - 1) : url;
|
||||
Reference in New Issue
Block a user