feat(auth): Add OAuth providers and improve authentication flow

This commit is contained in:
cogwheel0
2025-12-11 18:45:18 +05:30
parent ea61168184
commit 8d6c7f5411
19 changed files with 588 additions and 249 deletions

View File

@@ -1,5 +1,80 @@
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<String> 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<String, dynamic>? 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<String, dynamic> 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 {
@@ -14,6 +89,9 @@ class BackendConfig {
this.audioSampleRate,
this.audioFrameSize,
this.vadEnabled,
this.oauthProviders = const OAuthProviders(),
this.enableLdap = false,
this.enableLoginForm = true,
});
/// Mirrors `features.enable_websocket` from OpenWebUI.
@@ -28,6 +106,18 @@ class BackendConfig {
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,
@@ -40,6 +130,9 @@ class BackendConfig {
int? audioSampleRate,
int? audioFrameSize,
bool? vadEnabled,
OAuthProviders? oauthProviders,
bool? enableLdap,
bool? enableLoginForm,
}) {
return BackendConfig(
enableWebsocket: enableWebsocket ?? this.enableWebsocket,
@@ -52,6 +145,9 @@ class BackendConfig {
audioSampleRate: audioSampleRate ?? this.audioSampleRate,
audioFrameSize: audioFrameSize ?? this.audioFrameSize,
vadEnabled: vadEnabled ?? this.vadEnabled,
oauthProviders: oauthProviders ?? this.oauthProviders,
enableLdap: enableLdap ?? this.enableLdap,
enableLoginForm: enableLoginForm ?? this.enableLoginForm,
);
}
@@ -86,6 +182,9 @@ class BackendConfig {
'audio_sample_rate': audioSampleRate,
'audio_frame_size': audioFrameSize,
'vad_enabled': vadEnabled,
'oauth': {'providers': oauthProviders.toJson()},
'enable_ldap': enableLdap,
'enable_login_form': enableLoginForm,
};
}
@@ -100,6 +199,10 @@ class BackendConfig {
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) {
@@ -129,6 +232,21 @@ class BackendConfig {
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<String, dynamic>) {
final providers = oauth['providers'];
if (providers is Map<String, dynamic>) {
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<String, dynamic>) {
@@ -172,6 +290,11 @@ class BackendConfig {
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(
@@ -185,6 +308,9 @@ class BackendConfig {
audioSampleRate: audioSampleRate,
audioFrameSize: audioFrameSize,
vadEnabled: vadEnabled,
oauthProviders: oauthProviders,
enableLdap: enableLdap,
enableLoginForm: enableLoginForm,
);
}
}

View File

@@ -1,8 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'backend_config.dart';
part 'server_config.freezed.dart';
part 'server_config.g.dart';
/// Container for passing server and backend config during authentication flow.
@immutable
class AuthFlowConfig {
const AuthFlowConfig({required this.serverConfig, this.backendConfig});
/// The server configuration (URL, headers, etc.).
final ServerConfig serverConfig;
/// The backend configuration (auth methods, features, etc.).
/// May be null if not yet fetched.
final BackendConfig? backendConfig;
}
@freezed
sealed class ServerConfig with _$ServerConfig {
const factory ServerConfig({