import 'package:flutter/foundation.dart'; /// Represents the available OAuth providers configured on the server. @immutable class OAuthProviders { const OAuthProviders({ this.google, this.microsoft, this.github, this.oidc, this.feishu, }); /// Google OAuth provider name (if enabled). final String? google; /// Microsoft OAuth provider name (if enabled). final String? microsoft; /// GitHub OAuth provider name (if enabled). final String? github; /// Generic OIDC provider name (if enabled). final String? oidc; /// Feishu OAuth provider name (if enabled). final String? feishu; /// Whether any OAuth provider is enabled. bool get hasAnyProvider => google != null || microsoft != null || github != null || oidc != null || feishu != null; /// Returns the list of enabled provider keys. List get enabledProviders => [ if (google != null) 'google', if (microsoft != null) 'microsoft', if (github != null) 'github', if (oidc != null) 'oidc', if (feishu != null) 'feishu', ]; /// Returns the display name for a provider. String getProviderDisplayName(String key) { return switch (key) { 'google' => google ?? 'Google', 'microsoft' => microsoft ?? 'Microsoft', 'github' => github ?? 'GitHub', 'oidc' => oidc ?? 'SSO', 'feishu' => feishu ?? 'Feishu', _ => key, }; } factory OAuthProviders.fromJson(Map? json) { if (json == null) return const OAuthProviders(); return OAuthProviders( google: json['google'] as String?, microsoft: json['microsoft'] as String?, github: json['github'] as String?, oidc: json['oidc'] as String?, feishu: json['feishu'] as String?, ); } Map toJson() => { if (google != null) 'google': google, if (microsoft != null) 'microsoft': microsoft, if (github != null) 'github': github, if (oidc != null) 'oidc': oidc, if (feishu != null) 'feishu': feishu, }; } /// Subset of the backend `/api/config` response the app cares about. @immutable class BackendConfig { const BackendConfig({ this.enableWebsocket, this.enableAudioInput, this.enableAudioOutput, this.sttProvider, this.ttsProvider, this.ttsVoice, this.defaultSttLocale, this.audioSampleRate, this.audioFrameSize, this.vadEnabled, this.oauthProviders = const OAuthProviders(), this.enableLdap = false, this.enableLoginForm = true, }); /// Mirrors `features.enable_websocket` from OpenWebUI. final bool? enableWebsocket; final bool? enableAudioInput; final bool? enableAudioOutput; final String? sttProvider; final String? ttsProvider; final String? ttsVoice; final String? defaultSttLocale; final int? audioSampleRate; final int? audioFrameSize; final bool? vadEnabled; /// OAuth providers configured on the server. final OAuthProviders oauthProviders; /// Whether LDAP authentication is enabled on the server. final bool enableLdap; /// Whether the standard login form (email/password) is enabled. final bool enableLoginForm; /// Whether SSO (OAuth) login is available. bool get hasSsoEnabled => oauthProviders.hasAnyProvider; /// Returns a copy with updated fields. BackendConfig copyWith({ bool? enableWebsocket, bool? enableAudioInput, bool? enableAudioOutput, String? sttProvider, String? ttsProvider, String? ttsVoice, String? defaultSttLocale, int? audioSampleRate, int? audioFrameSize, bool? vadEnabled, OAuthProviders? oauthProviders, bool? enableLdap, bool? enableLoginForm, }) { return BackendConfig( enableWebsocket: enableWebsocket ?? this.enableWebsocket, enableAudioInput: enableAudioInput ?? this.enableAudioInput, enableAudioOutput: enableAudioOutput ?? this.enableAudioOutput, sttProvider: sttProvider ?? this.sttProvider, ttsProvider: ttsProvider ?? this.ttsProvider, ttsVoice: ttsVoice ?? this.ttsVoice, defaultSttLocale: defaultSttLocale ?? this.defaultSttLocale, audioSampleRate: audioSampleRate ?? this.audioSampleRate, audioFrameSize: audioFrameSize ?? this.audioFrameSize, vadEnabled: vadEnabled ?? this.vadEnabled, oauthProviders: oauthProviders ?? this.oauthProviders, enableLdap: enableLdap ?? this.enableLdap, enableLoginForm: enableLoginForm ?? this.enableLoginForm, ); } /// Whether the backend only allows WebSocket transport. bool get websocketOnly => enableWebsocket == true; /// Whether the backend only allows HTTP polling transport. bool get pollingOnly => enableWebsocket == false; /// Whether the backend permits choosing WebSocket-only mode. bool get supportsWebsocketOnly => !pollingOnly; /// Whether the backend permits choosing polling fallback. bool get supportsPolling => !websocketOnly; /// Returns the enforced transport mode derived from backend policy. String? get enforcedTransportMode { if (websocketOnly) return 'ws'; if (pollingOnly) return 'polling'; return null; } Map toJson() { return { 'enable_websocket': enableWebsocket, 'enable_audio_input': enableAudioInput, 'enable_audio_output': enableAudioOutput, 'stt_provider': sttProvider, 'tts_provider': ttsProvider, 'tts_voice': ttsVoice, 'default_stt_locale': defaultSttLocale, 'audio_sample_rate': audioSampleRate, 'audio_frame_size': audioFrameSize, 'vad_enabled': vadEnabled, 'oauth': {'providers': oauthProviders.toJson()}, 'enable_ldap': enableLdap, 'enable_login_form': enableLoginForm, }; } static BackendConfig fromJson(Map json) { bool? enableWebsocket; bool? enableAudioInput; bool? enableAudioOutput; String? sttProvider; String? ttsProvider; String? ttsVoice; String? defaultSttLocale; int? audioSampleRate; int? audioFrameSize; bool? vadEnabled; OAuthProviders oauthProviders = const OAuthProviders(); bool enableLdap = false; bool enableLoginForm = true; // Try canonical format first final value = json['enable_websocket']; if (value is bool) { enableWebsocket = value; } final audioIn = json['enable_audio_input']; if (audioIn is bool) enableAudioInput = audioIn; final audioOut = json['enable_audio_output']; if (audioOut is bool) enableAudioOutput = audioOut; final stt = json['stt_provider']; if (stt is String) sttProvider = stt; final tts = json['tts_provider']; if (tts is String) ttsProvider = tts; final ttsVoiceValue = json['tts_voice']; if (ttsVoiceValue is String) ttsVoice = ttsVoiceValue; final defaultLocale = json['default_stt_locale']; if (defaultLocale is String) defaultSttLocale = defaultLocale; final sampleRate = json['audio_sample_rate']; if (sampleRate is int) audioSampleRate = sampleRate; final frameSize = json['audio_frame_size']; if (frameSize is int) audioFrameSize = frameSize; final vad = json['vad_enabled']; if (vad is bool) vadEnabled = vad; // Parse OAuth providers from top-level oauth.providers final oauth = json['oauth']; if (oauth is Map) { final providers = oauth['providers']; if (providers is Map) { oauthProviders = OAuthProviders.fromJson(providers); } } // Parse auth features from top-level final ldapValue = json['enable_ldap']; if (ldapValue is bool) enableLdap = ldapValue; final loginFormValue = json['enable_login_form']; if (loginFormValue is bool) enableLoginForm = loginFormValue; // Fallback to nested format for backwards compatibility final features = json['features']; if (features is Map) { final nestedValue = features['enable_websocket']; if (nestedValue is bool && enableWebsocket == null) { enableWebsocket = nestedValue; } final nestedAudioIn = features['enable_audio_input']; if (nestedAudioIn is bool && enableAudioInput == null) { enableAudioInput = nestedAudioIn; } final nestedAudioOut = features['enable_audio_output']; if (nestedAudioOut is bool && enableAudioOutput == null) { enableAudioOutput = nestedAudioOut; } final nestedStt = features['stt_provider']; if (nestedStt is String && sttProvider == null) { sttProvider = nestedStt; } final nestedTts = features['tts_provider']; if (nestedTts is String && ttsProvider == null) { ttsProvider = nestedTts; } final nestedVoice = features['tts_voice']; if (nestedVoice is String && ttsVoice == null) { ttsVoice = nestedVoice; } final nestedLocale = features['default_stt_locale']; if (nestedLocale is String && defaultSttLocale == null) { defaultSttLocale = nestedLocale; } final nestedSample = features['audio_sample_rate']; if (nestedSample is int && audioSampleRate == null) { audioSampleRate = nestedSample; } final nestedFrame = features['audio_frame_size']; if (nestedFrame is int && audioFrameSize == null) { audioFrameSize = nestedFrame; } final nestedVad = features['vad_enabled']; if (nestedVad is bool && vadEnabled == null) { vadEnabled = nestedVad; } // Auth features in nested format final nestedLdap = features['enable_ldap']; if (nestedLdap is bool) enableLdap = nestedLdap; final nestedLoginForm = features['enable_login_form']; if (nestedLoginForm is bool) enableLoginForm = nestedLoginForm; } return BackendConfig( enableWebsocket: enableWebsocket, enableAudioInput: enableAudioInput, enableAudioOutput: enableAudioOutput, sttProvider: sttProvider, ttsProvider: ttsProvider, ttsVoice: ttsVoice, defaultSttLocale: defaultSttLocale, audioSampleRate: audioSampleRate, audioFrameSize: audioFrameSize, vadEnabled: vadEnabled, oauthProviders: oauthProviders, enableLdap: enableLdap, enableLoginForm: enableLoginForm, ); } }