Merge pull request #168 from cogwheel0/refactor-auth-cache-manager
refactor-auth-cache-manager
This commit is contained in:
@@ -1,198 +1,131 @@
|
|||||||
import 'auth_state_manager.dart';
|
import '../services/cache_manager.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
import 'auth_state_manager.dart';
|
||||||
|
|
||||||
/// Comprehensive caching manager for auth-related operations
|
/// Comprehensive caching manager for auth-related operations.
|
||||||
/// Reduces redundant operations and improves app performance
|
///
|
||||||
|
/// Delegates to the shared [CacheManager] to keep TTL and eviction behavior
|
||||||
|
/// consistent across the app.
|
||||||
class AuthCacheManager {
|
class AuthCacheManager {
|
||||||
static final AuthCacheManager _instance = AuthCacheManager._internal();
|
static final AuthCacheManager _instance = AuthCacheManager._internal();
|
||||||
factory AuthCacheManager() => _instance;
|
factory AuthCacheManager() => _instance;
|
||||||
AuthCacheManager._internal();
|
AuthCacheManager._internal();
|
||||||
|
|
||||||
// Cache for various auth-related operations
|
static const Duration _shortCache = Duration(minutes: 2);
|
||||||
final Map<String, dynamic> _cache = {};
|
static const Duration _mediumCache = Duration(minutes: 5);
|
||||||
final Map<String, DateTime> _cacheTimestamps = {};
|
static const Duration _longCache = Duration(minutes: 15);
|
||||||
|
|
||||||
// Cache timeouts for different types of data
|
|
||||||
static const Duration _shortCache = Duration(
|
|
||||||
minutes: 2,
|
|
||||||
); // For frequently changing data
|
|
||||||
static const Duration _mediumCache = Duration(
|
|
||||||
minutes: 5,
|
|
||||||
); // For moderately stable data
|
|
||||||
static const Duration _longCache = Duration(minutes: 15); // For stable data
|
|
||||||
|
|
||||||
// Cache keys
|
|
||||||
static const String _userDataKey = 'user_data';
|
static const String _userDataKey = 'user_data';
|
||||||
static const String _serverConnectionKey = 'server_connection';
|
static const String _serverConnectionKey = 'server_connection';
|
||||||
static const String _credentialsExistKey = 'credentials_exist';
|
static const String _credentialsExistKey = 'credentials_exist';
|
||||||
static const String _serverConfigsKey = 'server_configs';
|
static const String _serverConfigsKey = 'server_configs';
|
||||||
|
static const String _authStatusKey = 'auth_status';
|
||||||
|
|
||||||
|
final CacheManager _cache = CacheManager(
|
||||||
|
defaultTtl: _mediumCache,
|
||||||
|
maxEntries: 32,
|
||||||
|
);
|
||||||
|
|
||||||
/// Cache user data with medium timeout
|
|
||||||
void cacheUserData(dynamic userData) {
|
void cacheUserData(dynamic userData) {
|
||||||
_cache[_userDataKey] = userData;
|
_cache.write<dynamic>(_userDataKey, userData, ttl: _mediumCache);
|
||||||
_cacheTimestamps[_userDataKey] = DateTime.now();
|
|
||||||
DebugLogger.storage('User data cached');
|
DebugLogger.storage('User data cached');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached user data
|
|
||||||
dynamic getCachedUserData() {
|
dynamic getCachedUserData() {
|
||||||
if (_isCacheValid(_userDataKey, _mediumCache)) {
|
final (hit: hit, value: user) = _cache.lookup<dynamic>(_userDataKey);
|
||||||
|
if (hit) {
|
||||||
DebugLogger.storage('Using cached user data');
|
DebugLogger.storage('Using cached user data');
|
||||||
return _cache[_userDataKey];
|
|
||||||
}
|
}
|
||||||
return null;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache server connection status with short timeout
|
|
||||||
void cacheServerConnection(bool isConnected) {
|
void cacheServerConnection(bool isConnected) {
|
||||||
_cache[_serverConnectionKey] = isConnected;
|
_cache.write<bool>(_serverConnectionKey, isConnected, ttl: _shortCache);
|
||||||
_cacheTimestamps[_serverConnectionKey] = DateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached server connection status
|
|
||||||
bool? getCachedServerConnection() {
|
bool? getCachedServerConnection() {
|
||||||
if (_isCacheValid(_serverConnectionKey, _shortCache)) {
|
final (hit: hit, value: connection) = _cache.lookup<bool>(
|
||||||
return _cache[_serverConnectionKey] as bool?;
|
_serverConnectionKey,
|
||||||
}
|
);
|
||||||
return null;
|
return hit ? connection : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache credentials existence with medium timeout
|
|
||||||
void cacheCredentialsExist(bool exist) {
|
void cacheCredentialsExist(bool exist) {
|
||||||
_cache[_credentialsExistKey] = exist;
|
_cache.write<bool>(_credentialsExistKey, exist, ttl: _mediumCache);
|
||||||
_cacheTimestamps[_credentialsExistKey] = DateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached credentials existence
|
|
||||||
bool? getCachedCredentialsExist() {
|
bool? getCachedCredentialsExist() {
|
||||||
if (_isCacheValid(_credentialsExistKey, _mediumCache)) {
|
final (hit: hit, value: hasCreds) = _cache.lookup<bool>(
|
||||||
return _cache[_credentialsExistKey] as bool?;
|
_credentialsExistKey,
|
||||||
}
|
);
|
||||||
return null;
|
return hit ? hasCreds : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache server configurations with long timeout
|
|
||||||
void cacheServerConfigs(List<dynamic> configs) {
|
void cacheServerConfigs(List<dynamic> configs) {
|
||||||
_cache[_serverConfigsKey] = configs;
|
_cache.write<List<dynamic>>(_serverConfigsKey, configs, ttl: _longCache);
|
||||||
_cacheTimestamps[_serverConfigsKey] = DateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached server configurations
|
|
||||||
List<dynamic>? getCachedServerConfigs() {
|
List<dynamic>? getCachedServerConfigs() {
|
||||||
if (_isCacheValid(_serverConfigsKey, _longCache)) {
|
final (hit: hit, value: configs) = _cache.lookup<List<dynamic>>(
|
||||||
return _cache[_serverConfigsKey] as List<dynamic>?;
|
_serverConfigsKey,
|
||||||
}
|
);
|
||||||
return null;
|
return hit ? configs : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if cache entry is valid
|
|
||||||
bool _isCacheValid(String key, Duration timeout) {
|
|
||||||
final timestamp = _cacheTimestamps[key];
|
|
||||||
if (timestamp == null) return false;
|
|
||||||
|
|
||||||
return DateTime.now().difference(timestamp) < timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear specific cache entry
|
|
||||||
void clearCacheEntry(String key) {
|
void clearCacheEntry(String key) {
|
||||||
_cache.remove(key);
|
_cache.invalidate(key);
|
||||||
_cacheTimestamps.remove(key);
|
|
||||||
DebugLogger.storage('Cache entry cleared: $key');
|
DebugLogger.storage('Cache entry cleared: $key');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all auth-related cache including server configs
|
|
||||||
void clearAuthCache() {
|
void clearAuthCache() {
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
_cacheTimestamps.clear();
|
|
||||||
DebugLogger.storage('All auth cache cleared (including server configs and custom headers)');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear expired cache entries
|
|
||||||
void cleanExpiredCache() {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final expiredKeys = <String>[];
|
|
||||||
|
|
||||||
for (final entry in _cacheTimestamps.entries) {
|
|
||||||
// Use the longest timeout for cleanup to be conservative
|
|
||||||
if (now.difference(entry.value) > _longCache) {
|
|
||||||
expiredKeys.add(entry.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final key in expiredKeys) {
|
|
||||||
_cache.remove(key);
|
|
||||||
_cacheTimestamps.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expiredKeys.isNotEmpty) {
|
|
||||||
DebugLogger.storage(
|
|
||||||
'Cleaned ${expiredKeys.length} expired cache entries',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get cache statistics for monitoring
|
|
||||||
Map<String, dynamic> getCacheStats() {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final stats = <String, dynamic>{};
|
|
||||||
|
|
||||||
stats['totalEntries'] = _cache.length;
|
|
||||||
stats['entries'] = <String, Map<String, dynamic>>{};
|
|
||||||
|
|
||||||
for (final key in _cache.keys) {
|
|
||||||
final timestamp = _cacheTimestamps[key];
|
|
||||||
if (timestamp != null) {
|
|
||||||
stats['entries'][key] = {
|
|
||||||
'age': now.difference(timestamp).inSeconds,
|
|
||||||
'hasData': _cache[key] != null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optimize cache by removing least recently used entries if cache gets too large
|
|
||||||
void optimizeCache() {
|
|
||||||
const maxCacheSize = 20; // Reasonable limit for auth cache
|
|
||||||
|
|
||||||
if (_cache.length <= maxCacheSize) return;
|
|
||||||
|
|
||||||
// Sort by timestamp (oldest first)
|
|
||||||
final sortedEntries = _cacheTimestamps.entries.toList()
|
|
||||||
..sort((a, b) => a.value.compareTo(b.value));
|
|
||||||
|
|
||||||
// Remove oldest entries
|
|
||||||
final entriesToRemove = sortedEntries.length - maxCacheSize;
|
|
||||||
for (int i = 0; i < entriesToRemove; i++) {
|
|
||||||
final key = sortedEntries[i].key;
|
|
||||||
_cache.remove(key);
|
|
||||||
_cacheTimestamps.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugLogger.storage(
|
DebugLogger.storage(
|
||||||
'Cache optimized, removed $entriesToRemove old entries',
|
'All auth cache cleared (including server configs and custom headers)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache state from AuthState for quick access
|
void cleanExpiredCache() {
|
||||||
|
final stats = _cache.stats();
|
||||||
|
final entries = stats['entries'];
|
||||||
|
if (entries is! Map<String, dynamic>) return;
|
||||||
|
|
||||||
|
var expiredCount = 0;
|
||||||
|
entries.forEach((key, value) {
|
||||||
|
if (value is! Map) return;
|
||||||
|
final ageSeconds = value['ageSeconds'];
|
||||||
|
final ttlSeconds = value['ttlSeconds'];
|
||||||
|
if (ageSeconds is num && ttlSeconds is num && ageSeconds > ttlSeconds) {
|
||||||
|
_cache.invalidate(key);
|
||||||
|
expiredCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expiredCount > 0) {
|
||||||
|
DebugLogger.storage('Cleaned $expiredCount expired auth cache entries');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> getCacheStats() => _cache.stats();
|
||||||
|
|
||||||
|
void optimizeCache() {
|
||||||
|
// CacheManager enforces maxEntries using LRU; no extra work needed.
|
||||||
|
}
|
||||||
|
|
||||||
void cacheAuthState(AuthState authState) {
|
void cacheAuthState(AuthState authState) {
|
||||||
if (authState.user != null) {
|
if (authState.user != null) {
|
||||||
cacheUserData(authState.user);
|
cacheUserData(authState.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't cache loading or error states
|
|
||||||
if (authState.status == AuthStatus.authenticated) {
|
if (authState.status == AuthStatus.authenticated) {
|
||||||
_cache['auth_status'] = authState.status;
|
_cache.write<AuthStatus>(
|
||||||
_cacheTimestamps['auth_status'] = DateTime.now();
|
_authStatusKey,
|
||||||
|
authState.status,
|
||||||
|
ttl: _shortCache,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached auth status
|
|
||||||
AuthStatus? getCachedAuthStatus() {
|
AuthStatus? getCachedAuthStatus() {
|
||||||
if (_isCacheValid('auth_status', _shortCache)) {
|
final (hit: hit, value: status) = _cache.lookup<AuthStatus>(_authStatusKey);
|
||||||
return _cache['auth_status'] as AuthStatus?;
|
return hit ? status : null;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,55 @@ import 'package:flutter/foundation.dart';
|
|||||||
/// Subset of the backend `/api/config` response the app cares about.
|
/// Subset of the backend `/api/config` response the app cares about.
|
||||||
@immutable
|
@immutable
|
||||||
class BackendConfig {
|
class BackendConfig {
|
||||||
const BackendConfig({this.enableWebsocket});
|
const BackendConfig({
|
||||||
|
this.enableWebsocket,
|
||||||
|
this.enableAudioInput,
|
||||||
|
this.enableAudioOutput,
|
||||||
|
this.sttProvider,
|
||||||
|
this.ttsProvider,
|
||||||
|
this.ttsVoice,
|
||||||
|
this.defaultSttLocale,
|
||||||
|
this.audioSampleRate,
|
||||||
|
this.audioFrameSize,
|
||||||
|
this.vadEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
/// Mirrors `features.enable_websocket` from OpenWebUI.
|
/// Mirrors `features.enable_websocket` from OpenWebUI.
|
||||||
final bool? enableWebsocket;
|
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;
|
||||||
|
|
||||||
/// Returns a copy with updated fields.
|
/// Returns a copy with updated fields.
|
||||||
BackendConfig copyWith({bool? enableWebsocket}) {
|
BackendConfig copyWith({
|
||||||
|
bool? enableWebsocket,
|
||||||
|
bool? enableAudioInput,
|
||||||
|
bool? enableAudioOutput,
|
||||||
|
String? sttProvider,
|
||||||
|
String? ttsProvider,
|
||||||
|
String? ttsVoice,
|
||||||
|
String? defaultSttLocale,
|
||||||
|
int? audioSampleRate,
|
||||||
|
int? audioFrameSize,
|
||||||
|
bool? vadEnabled,
|
||||||
|
}) {
|
||||||
return BackendConfig(
|
return BackendConfig(
|
||||||
enableWebsocket: enableWebsocket ?? this.enableWebsocket,
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,28 +77,114 @@ class BackendConfig {
|
|||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'enable_websocket': enableWebsocket,
|
'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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static BackendConfig fromJson(Map<String, dynamic> json) {
|
static BackendConfig fromJson(Map<String, dynamic> json) {
|
||||||
bool? enableWebsocket;
|
bool? enableWebsocket;
|
||||||
|
bool? enableAudioInput;
|
||||||
|
bool? enableAudioOutput;
|
||||||
|
String? sttProvider;
|
||||||
|
String? ttsProvider;
|
||||||
|
String? ttsVoice;
|
||||||
|
String? defaultSttLocale;
|
||||||
|
int? audioSampleRate;
|
||||||
|
int? audioFrameSize;
|
||||||
|
bool? vadEnabled;
|
||||||
// Try canonical format first
|
// Try canonical format first
|
||||||
final value = json['enable_websocket'];
|
final value = json['enable_websocket'];
|
||||||
if (value is bool) {
|
if (value is bool) {
|
||||||
enableWebsocket = value;
|
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;
|
||||||
|
|
||||||
// Fallback to nested format for backwards compatibility
|
// Fallback to nested format for backwards compatibility
|
||||||
if (enableWebsocket == null) {
|
final features = json['features'];
|
||||||
final features = json['features'];
|
if (features is Map<String, dynamic>) {
|
||||||
if (features is Map<String, dynamic>) {
|
final nestedValue = features['enable_websocket'];
|
||||||
final nestedValue = features['enable_websocket'];
|
if (nestedValue is bool && enableWebsocket == null) {
|
||||||
if (nestedValue is bool) {
|
enableWebsocket = nestedValue;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return BackendConfig(enableWebsocket: enableWebsocket);
|
return BackendConfig(
|
||||||
|
enableWebsocket: enableWebsocket,
|
||||||
|
enableAudioInput: enableAudioInput,
|
||||||
|
enableAudioOutput: enableAudioOutput,
|
||||||
|
sttProvider: sttProvider,
|
||||||
|
ttsProvider: ttsProvider,
|
||||||
|
ttsVoice: ttsVoice,
|
||||||
|
defaultSttLocale: defaultSttLocale,
|
||||||
|
audioSampleRate: audioSampleRate,
|
||||||
|
audioFrameSize: audioFrameSize,
|
||||||
|
vadEnabled: vadEnabled,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -547,17 +547,88 @@ final apiTokenUpdaterProvider = Provider<void>((ref) {
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Future<User?> currentUser(Ref ref) async {
|
Future<User?> currentUser(Ref ref) async {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
final isAuthenticated = ref.watch(isAuthenticatedProvider2);
|
final authState = ref.watch(authStateManagerProvider);
|
||||||
|
final isAuthenticated = authState.maybeWhen(
|
||||||
|
data: (state) => state.isAuthenticated,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
|
||||||
if (api == null || !isAuthenticated) return null;
|
if (api == null || !isAuthenticated) return null;
|
||||||
|
|
||||||
|
// Fast path: use user already in auth state.
|
||||||
|
final authUser = authState.maybeWhen(
|
||||||
|
data: (state) => state.user,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
if (authUser != null) return authUser;
|
||||||
|
|
||||||
|
// Next: try cached user from storage, then refresh in the background.
|
||||||
|
final storage = ref.read(optimizedStorageServiceProvider);
|
||||||
|
final cachedUser = await _getCachedUserWithAvatar(storage);
|
||||||
|
if (cachedUser != null) {
|
||||||
|
final lastRefresh = ref.read(_lastUserRefreshProvider);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final shouldRefresh =
|
||||||
|
lastRefresh == null ||
|
||||||
|
now.difference(lastRefresh) > const Duration(minutes: 5);
|
||||||
|
|
||||||
|
if (shouldRefresh) {
|
||||||
|
Future.microtask(() async {
|
||||||
|
final fresh = await _refreshCurrentUser(ref);
|
||||||
|
if (fresh != null && ref.mounted) {
|
||||||
|
ref.read(_lastUserRefreshProvider.notifier).set(now);
|
||||||
|
ref.invalidate(currentUserProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cachedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: fetch fresh.
|
||||||
|
final fresh = await _refreshCurrentUser(ref);
|
||||||
|
if (fresh != null) {
|
||||||
|
ref.read(_lastUserRefreshProvider.notifier).set(DateTime.now());
|
||||||
|
}
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<User?> _getCachedUserWithAvatar(OptimizedStorageService storage) async {
|
||||||
|
final cachedUser = await storage.getLocalUser();
|
||||||
|
if (cachedUser == null) return null;
|
||||||
|
final cachedAvatar = await storage.getLocalUserAvatar();
|
||||||
|
if (cachedAvatar == null ||
|
||||||
|
cachedAvatar.isEmpty ||
|
||||||
|
cachedUser.profileImage == cachedAvatar) {
|
||||||
|
return cachedUser;
|
||||||
|
}
|
||||||
|
return cachedUser.copyWith(profileImage: cachedAvatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<User?> _refreshCurrentUser(Ref ref) async {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
if (api == null) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await api.getCurrentUser();
|
final user = await api.getCurrentUser();
|
||||||
} catch (e) {
|
final storage = ref.read(optimizedStorageServiceProvider);
|
||||||
|
await storage.saveLocalUser(user);
|
||||||
|
if (user.profileImage != null && user.profileImage!.isNotEmpty) {
|
||||||
|
await storage.saveLocalUserAvatar(user.profileImage);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class _LastUserRefresh extends _$LastUserRefresh {
|
||||||
|
@override
|
||||||
|
DateTime? build() => null;
|
||||||
|
|
||||||
|
void set(DateTime? timestamp) => state = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper provider to force refresh auth state - now using unified system
|
// Helper provider to force refresh auth state - now using unified system
|
||||||
final refreshAuthStateProvider = Provider<void>((ref) {
|
final refreshAuthStateProvider = Provider<void>((ref) {
|
||||||
// This provider can be invalidated to force refresh the unified auth system
|
// This provider can be invalidated to force refresh the unified auth system
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ class OptimizedStorageService {
|
|||||||
static const String _localFoldersKey = HiveStoreKeys.localFolders;
|
static const String _localFoldersKey = HiveStoreKeys.localFolders;
|
||||||
static const String _onboardingSeenKey = PreferenceKeys.onboardingSeen;
|
static const String _onboardingSeenKey = PreferenceKeys.onboardingSeen;
|
||||||
static const String _reviewerModeKey = PreferenceKeys.reviewerMode;
|
static const String _reviewerModeKey = PreferenceKeys.reviewerMode;
|
||||||
|
// Longer TTLs to reduce secure storage churn for OpenWebUI sessions.
|
||||||
|
static const Duration _authTokenTtl = Duration(hours: 12);
|
||||||
|
static const Duration _serverIdTtl = Duration(days: 7);
|
||||||
|
static const Duration _credentialsFlagTtl = Duration(hours: 12);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Auth token APIs (secure storage + in-memory cache)
|
// Auth token APIs (secure storage + in-memory cache)
|
||||||
@@ -66,7 +70,7 @@ class OptimizedStorageService {
|
|||||||
Future<void> saveAuthToken(String token) async {
|
Future<void> saveAuthToken(String token) async {
|
||||||
try {
|
try {
|
||||||
await _secureCredentialStorage.saveAuthToken(token);
|
await _secureCredentialStorage.saveAuthToken(token);
|
||||||
_cacheManager.write(_authTokenKey, token);
|
_cacheManager.write(_authTokenKey, token, ttl: _authTokenTtl);
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'Auth token saved and cached',
|
'Auth token saved and cached',
|
||||||
scope: 'storage/optimized',
|
scope: 'storage/optimized',
|
||||||
@@ -91,7 +95,7 @@ class OptimizedStorageService {
|
|||||||
try {
|
try {
|
||||||
final token = await _secureCredentialStorage.getAuthToken();
|
final token = await _secureCredentialStorage.getAuthToken();
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
_cacheManager.write(_authTokenKey, token);
|
_cacheManager.write(_authTokenKey, token, ttl: _authTokenTtl);
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -136,7 +140,7 @@ class OptimizedStorageService {
|
|||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
|
|
||||||
_cacheManager.write('has_credentials', true);
|
_cacheManager.write('has_credentials', true, ttl: _credentialsFlagTtl);
|
||||||
|
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'Credentials saved via optimized storage',
|
'Credentials saved via optimized storage',
|
||||||
@@ -154,7 +158,11 @@ class OptimizedStorageService {
|
|||||||
Future<Map<String, String>?> getSavedCredentials() async {
|
Future<Map<String, String>?> getSavedCredentials() async {
|
||||||
try {
|
try {
|
||||||
final credentials = await _secureCredentialStorage.getSavedCredentials();
|
final credentials = await _secureCredentialStorage.getSavedCredentials();
|
||||||
_cacheManager.write('has_credentials', credentials != null);
|
_cacheManager.write(
|
||||||
|
'has_credentials',
|
||||||
|
credentials != null,
|
||||||
|
ttl: _credentialsFlagTtl,
|
||||||
|
);
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
@@ -243,7 +251,7 @@ class OptimizedStorageService {
|
|||||||
} else {
|
} else {
|
||||||
await _preferencesBox.delete(_activeServerIdKey);
|
await _preferencesBox.delete(_activeServerIdKey);
|
||||||
}
|
}
|
||||||
_cacheManager.write(_activeServerIdKey, serverId);
|
_cacheManager.write(_activeServerIdKey, serverId, ttl: _serverIdTtl);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getActiveServerId() async {
|
Future<String?> getActiveServerId() async {
|
||||||
@@ -254,7 +262,7 @@ class OptimizedStorageService {
|
|||||||
return cachedId;
|
return cachedId;
|
||||||
}
|
}
|
||||||
final serverId = _preferencesBox.get(_activeServerIdKey) as String?;
|
final serverId = _preferencesBox.get(_activeServerIdKey) as String?;
|
||||||
_cacheManager.write(_activeServerIdKey, serverId);
|
_cacheManager.write(_activeServerIdKey, serverId, ttl: _serverIdTtl);
|
||||||
return serverId;
|
return serverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import '../providers/chat_providers.dart';
|
|||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
import '../../../core/utils/user_display_name.dart';
|
import '../../../core/utils/user_display_name.dart';
|
||||||
import '../../../core/utils/model_icon_utils.dart';
|
import '../../../core/utils/model_icon_utils.dart';
|
||||||
import '../../auth/providers/unified_auth_providers.dart';
|
|
||||||
import '../../../core/utils/android_assistant_handler.dart';
|
import '../../../core/utils/android_assistant_handler.dart';
|
||||||
import '../widgets/modern_chat_input.dart';
|
import '../widgets/modern_chat_input.dart';
|
||||||
import '../widgets/user_message_bubble.dart';
|
import '../widgets/user_message_bubble.dart';
|
||||||
@@ -1128,8 +1127,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
data: (user) => user,
|
data: (user) => user,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final authUser = ref.watch(currentUserProvider2);
|
final user = userFromProfile;
|
||||||
final user = userFromProfile ?? authUser;
|
|
||||||
String? greetingName;
|
String? greetingName;
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
final derived = deriveUserDisplayName(user, fallback: '').trim();
|
final derived = deriveUserDisplayName(user, fallback: '').trim();
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import '../../../shared/widgets/themed_dialogs.dart';
|
|||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
import '../../../core/utils/user_display_name.dart';
|
import '../../../core/utils/user_display_name.dart';
|
||||||
import '../../../core/utils/model_icon_utils.dart';
|
import '../../../core/utils/model_icon_utils.dart';
|
||||||
import '../../auth/providers/unified_auth_providers.dart';
|
|
||||||
import '../../../core/utils/user_avatar_utils.dart';
|
import '../../../core/utils/user_avatar_utils.dart';
|
||||||
import '../../../shared/utils/conversation_context_menu.dart';
|
import '../../../shared/utils/conversation_context_menu.dart';
|
||||||
import '../../../shared/widgets/user_avatar.dart';
|
import '../../../shared/widgets/user_avatar.dart';
|
||||||
@@ -1483,8 +1482,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
data: (u) => u,
|
data: (u) => u,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final authUser = ref.watch(currentUserProvider2);
|
final user = userFromProfile;
|
||||||
final user = userFromProfile ?? authUser;
|
|
||||||
final api = ref.watch(apiServiceProvider);
|
final api = ref.watch(apiServiceProvider);
|
||||||
|
|
||||||
String initialFor(String name) {
|
String initialFor(String name) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import '../../../core/providers/app_providers.dart';
|
|||||||
import '../../../core/utils/user_display_name.dart';
|
import '../../../core/utils/user_display_name.dart';
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
import '../../../shared/widgets/sheet_handle.dart';
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
import '../../auth/providers/unified_auth_providers.dart';
|
|
||||||
|
|
||||||
class OnboardingSheet extends ConsumerStatefulWidget {
|
class OnboardingSheet extends ConsumerStatefulWidget {
|
||||||
const OnboardingSheet({super.key});
|
const OnboardingSheet({super.key});
|
||||||
@@ -73,8 +72,7 @@ class _OnboardingSheetState extends ConsumerState<OnboardingSheet> {
|
|||||||
data: (user) => user,
|
data: (user) => user,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final authUser = ref.watch(currentUserProvider2);
|
final user = userFromProfile;
|
||||||
final user = userFromProfile ?? authUser;
|
|
||||||
final greetingName = deriveUserDisplayName(user);
|
final greetingName = deriveUserDisplayName(user);
|
||||||
final pages = _buildPages(l10n, greetingName);
|
final pages = _buildPages(l10n, greetingName);
|
||||||
final pageCount = pages.length;
|
final pageCount = pages.length;
|
||||||
|
|||||||
Reference in New Issue
Block a user