import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter_riverpod/flutter_riverpod.dart'; // Types are used through app_providers.dart import '../providers/app_providers.dart'; import '../models/user.dart'; import 'token_validator.dart'; import 'auth_cache_manager.dart'; import '../utils/debug_logger.dart'; void debugPrint(String? message, {int? wrapWidth}) { if (message == null) return; DebugLogger.fromLegacy(message, scope: 'auth/state'); } /// Comprehensive auth state representation @immutable class AuthState { const AuthState({ required this.status, this.token, this.user, this.error, this.isLoading = false, }); final AuthStatus status; final String? token; final User? user; final String? error; final bool isLoading; bool get isAuthenticated => status == AuthStatus.authenticated && token != null; bool get hasValidToken => token != null && token!.isNotEmpty; bool get needsLogin => status == AuthStatus.unauthenticated || status == AuthStatus.tokenExpired; AuthState copyWith({ AuthStatus? status, String? token, User? user, String? error, bool? isLoading, bool clearToken = false, bool clearUser = false, bool clearError = false, }) { return AuthState( status: status ?? this.status, token: clearToken ? null : (token ?? this.token), user: clearUser ? null : (user ?? this.user), error: clearError ? null : (error ?? this.error), isLoading: isLoading ?? this.isLoading, ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is AuthState && other.status == status && other.token == token && other.user == user && other.error == error && other.isLoading == isLoading; } @override int get hashCode => Object.hash(status, token, user, error, isLoading); @override String toString() => 'AuthState(status: $status, hasToken: ${token != null}, hasUser: ${user != null}, error: $error, isLoading: $isLoading)'; } enum AuthStatus { initial, loading, authenticated, unauthenticated, tokenExpired, error, } /// Unified auth state manager - single source of truth for all auth operations class AuthStateManager extends Notifier { final AuthCacheManager _cacheManager = AuthCacheManager(); // Prevent overlapping silent-login attempts from multiple triggers Future? _silentLoginFuture; bool _initialized = false; @override AuthState build() { if (!_initialized) { _initialized = true; Future.microtask(_initialize); } return const AuthState(status: AuthStatus.initial); } /// Initialize auth state from storage Future _initialize() async { state = state.copyWith(status: AuthStatus.loading, isLoading: true); try { final storage = ref.read(optimizedStorageServiceProvider); final token = await storage.getAuthToken(); if (token != null && token.isNotEmpty) { DebugLogger.auth('Found stored token during initialization'); // Fast path: trust token format to avoid blocking startup on network final formatOk = _isValidTokenFormat(token); if (formatOk) { state = state.copyWith( status: AuthStatus.authenticated, token: token, isLoading: false, clearError: true, ); // Update API service with token and load user data in background _updateApiServiceToken(token); _loadUserData(); // Background server validation; if it fails, invalidate token gracefully Future.microtask(() async { try { final ok = await _validateToken(token); DebugLogger.auth('Deferred token validation result: $ok'); if (!ok) { await onTokenInvalidated(); } } catch (_) {} }); } else { // Token format invalid; clear and require login DebugLogger.auth('Token format invalid, deleting token'); await storage.deleteAuthToken(); state = state.copyWith( status: AuthStatus.unauthenticated, isLoading: false, clearToken: true, clearError: true, ); } } else { state = state.copyWith( status: AuthStatus.unauthenticated, isLoading: false, clearToken: true, clearError: true, ); } } catch (e) { debugPrint('ERROR: Auth initialization failed: $e'); state = state.copyWith( status: AuthStatus.error, error: 'Failed to initialize auth: $e', isLoading: false, ); } } /// Perform login with API key Future loginWithApiKey( String apiKey, { bool rememberCredentials = false, }) async { state = state.copyWith( status: AuthStatus.loading, isLoading: true, clearError: true, ); try { // Validate API key format if (apiKey.trim().isEmpty) { throw Exception('API key cannot be empty'); } // Ensure API service is available await _ensureApiServiceAvailable(); final api = ref.read(apiServiceProvider); if (api == null) { throw Exception('No server connection available'); } // Use API key directly as Bearer token final tokenStr = apiKey.trim(); // Validate token format (consistent with credentials method) if (!_isValidTokenFormat(tokenStr)) { throw Exception('Invalid API key format'); } // Update API service with the API key _updateApiServiceToken(tokenStr); // Validate by attempting to fetch user info try { await api.getCurrentUser(); // Just validate, don't store user data yet // Save token to storage final storage = ref.read(optimizedStorageServiceProvider); await storage.saveAuthToken(tokenStr); // Save API key if requested (for convenience, though less secure than credentials) if (rememberCredentials) { final activeServer = await ref.read(activeServerProvider.future); if (activeServer != null) { // Store API key as a special credential type await storage.saveCredentials( serverId: activeServer.id, username: 'api_key_user', // Special username to indicate API key auth password: tokenStr, // Store API key in password field ); await storage.setRememberCredentials(true); } } // Update state (without user data initially) state = state.copyWith( status: AuthStatus.authenticated, token: tokenStr, isLoading: false, clearError: true, ); // Update API service with token _updateApiServiceToken(tokenStr); // Cache the successful auth state _cacheManager.cacheAuthState(state); // Load user data in background (consistent with credentials method) _loadUserData(); DebugLogger.auth('API key login successful'); return true; } catch (e) { // If user fetch fails, the API key might be invalid throw Exception('Invalid API key or insufficient permissions'); } } catch (e) { debugPrint('ERROR: API key login failed: $e'); state = state.copyWith( status: AuthStatus.error, error: e.toString(), isLoading: false, clearToken: true, ); return false; } } /// Perform login with credentials Future login( String username, String password, { bool rememberCredentials = false, }) async { state = state.copyWith( status: AuthStatus.loading, isLoading: true, clearError: true, ); try { // Ensure API service is available (active server/provider rebuild race) await _ensureApiServiceAvailable(); final api = ref.read(apiServiceProvider); if (api == null) { throw Exception('No server connection available'); } // Perform login API call final response = await api.login(username, password); // Extract and validate token final token = response['token'] ?? response['access_token']; if (token == null || token.toString().trim().isEmpty) { throw Exception('No authentication token received'); } final tokenStr = token.toString(); if (!_isValidTokenFormat(tokenStr)) { throw Exception('Invalid authentication token format'); } // Save token to storage final storage = ref.read(optimizedStorageServiceProvider); await storage.saveAuthToken(tokenStr); // Save credentials if requested if (rememberCredentials) { final activeServer = await ref.read(activeServerProvider.future); if (activeServer != null) { await storage.saveCredentials( serverId: activeServer.id, username: username, password: password, ); await storage.setRememberCredentials(true); } } // Update state and API service state = state.copyWith( status: AuthStatus.authenticated, token: tokenStr, isLoading: false, clearError: true, ); _updateApiServiceToken(tokenStr); // Cache the successful auth state _cacheManager.cacheAuthState(state); // Load user data in background _loadUserData(); DebugLogger.auth('Login successful'); return true; } catch (e) { debugPrint('ERROR: Login failed: $e'); state = state.copyWith( status: AuthStatus.error, error: e.toString(), isLoading: false, clearToken: true, ); return false; } } /// Wait briefly until the API service becomes available Future _ensureApiServiceAvailable({ Duration timeout = const Duration(seconds: 2), }) async { final end = DateTime.now().add(timeout); while (DateTime.now().isBefore(end)) { final api = ref.read(apiServiceProvider); if (api != null) return; await Future.delayed(const Duration(milliseconds: 50)); } } /// Perform silent auto-login with saved credentials Future silentLogin() async { // Coalesce concurrent calls (e.g., UI + interceptor retry) if (_silentLoginFuture != null) { return await _silentLoginFuture!; } final thisAttempt = _performSilentLogin(); _silentLoginFuture = thisAttempt; try { return await thisAttempt; } finally { if (identical(_silentLoginFuture, thisAttempt)) { _silentLoginFuture = null; } } } Future _performSilentLogin() async { state = state.copyWith( status: AuthStatus.loading, isLoading: true, clearError: true, ); try { final storage = ref.read(optimizedStorageServiceProvider); final savedCredentials = await storage.getSavedCredentials(); if (savedCredentials == null) { state = state.copyWith( status: AuthStatus.unauthenticated, isLoading: false, clearError: true, ); return false; } final serverId = savedCredentials['serverId']!; final username = savedCredentials['username']!; final password = savedCredentials['password']!; // Ensure the saved server still exists before switching final serverConfigs = await ref.read(serverConfigsProvider.future); final hasServer = serverConfigs.any((config) => config.id == serverId); if (!hasServer) { await storage.deleteSavedCredentials(); await storage.setActiveServerId(null); ref.invalidate(serverConfigsProvider); ref.invalidate(activeServerProvider); state = state.copyWith( status: AuthStatus.error, error: 'Saved server configuration is no longer available. Please reconnect.', isLoading: false, ); return false; } // Set active server once we know it exists await storage.setActiveServerId(serverId); ref.invalidate(activeServerProvider); // Wait for server connection final activeServer = await ref.read(activeServerProvider.future); if (activeServer == null) { await storage.setActiveServerId(null); state = state.copyWith( status: AuthStatus.error, error: 'Server configuration not found', isLoading: false, ); return false; } // Attempt login (detect API key vs normal credentials) if (username == 'api_key_user') { // This is a saved API key return await loginWithApiKey(password, rememberCredentials: false); } else { // Normal username/password credentials return await login(username, password, rememberCredentials: false); } } catch (e) { debugPrint('ERROR: Silent login failed: $e'); // Clear invalid credentials on auth errors if (e.toString().contains('401') || e.toString().contains('403') || e.toString().contains('authentication') || e.toString().contains('unauthorized')) { final storage = ref.read(optimizedStorageServiceProvider); await storage.deleteSavedCredentials(); } state = state.copyWith( status: AuthStatus.unauthenticated, error: e.toString(), isLoading: false, clearToken: true, ); return false; } } /// Handle token invalidation (called by API service) Future onTokenInvalidated() async { // Avoid spamming logs if multiple requests invalidate at once final reloginInProgress = _silentLoginFuture != null; if (!reloginInProgress) { DebugLogger.auth('Auth token invalidated'); } // Clear token from storage final storage = ref.read(optimizedStorageServiceProvider); await storage.deleteAuthToken(); _updateApiServiceToken(null); // Update state state = state.copyWith( status: AuthStatus.tokenExpired, clearToken: true, clearUser: true, clearError: true, ); // Attempt silent re-login if credentials are available final hasCredentials = await storage.getSavedCredentials() != null; if (hasCredentials) { if (!reloginInProgress) { DebugLogger.auth('Attempting silent re-login after token invalidation'); } await silentLogin(); } } /// Logout user Future logout() async { state = state.copyWith(status: AuthStatus.loading, isLoading: true); try { // Call server logout if possible final api = ref.read(apiServiceProvider); if (api != null) { try { await api.logout(); } catch (e) { debugPrint('Warning: Server logout failed: $e'); } } // Clear all local auth data final storage = ref.read(optimizedStorageServiceProvider); await storage.clearAuthData(); _updateApiServiceToken(null); // Update state state = state.copyWith( status: AuthStatus.unauthenticated, isLoading: false, clearToken: true, clearUser: true, clearError: true, ); DebugLogger.auth('Logout complete'); } catch (e) { debugPrint('ERROR: Logout failed: $e'); // Even if logout fails, clear local state state = state.copyWith( status: AuthStatus.unauthenticated, isLoading: false, clearToken: true, clearUser: true, error: 'Logout error: $e', ); _updateApiServiceToken(null); } } /// Load user data in background with JWT extraction fallback Future _loadUserData() async { try { // First try to extract user info from JWT token if available if (state.token != null) { final jwtUserInfo = TokenValidator.extractUserInfo(state.token!); if (jwtUserInfo != null) { final userFromJwt = _userFromJwtClaims(jwtUserInfo); if (userFromJwt != null) { DebugLogger.auth('Extracted user info from JWT token'); state = state.copyWith(user: userFromJwt); } // Still try to load from server in background for complete data Future.microtask(() => _loadServerUserData()); return; } } // Fall back to server data loading await _loadServerUserData(); } catch (e) { debugPrint('Warning: Failed to load user data: $e'); // Don't update state on user data load failure } } /// Load complete user data from server Future _loadServerUserData() async { try { final api = ref.read(apiServiceProvider); if (api != null && state.isAuthenticated) { // Check if we already have user data from token validation if (state.user != null) { debugPrint( 'DEBUG: User data already available from token validation', ); return; } final user = await api.getCurrentUser(); state = state.copyWith(user: user); DebugLogger.auth('Loaded complete user data from server'); } } catch (e) { debugPrint('Warning: Failed to load server user data: $e'); // Don't update state on server data load failure - keep JWT data if available } } /// Update API service with current token void _updateApiServiceToken(String? token) { final api = ref.read(apiServiceProvider); api?.updateAuthToken(token); } /// Validate token format using advanced validation bool _isValidTokenFormat(String token) { final result = TokenValidator.validateTokenFormat(token); return result.isValid; } /// Validate token with comprehensive validation (format + server) Future _validateToken(String token) async { // Check cache first final cachedResult = TokenValidationCache.getCachedResult(token); if (cachedResult != null) { DebugLogger.auth( 'Using cached token validation result: ${cachedResult.isValid}', ); return cachedResult.isValid; } // Fast format validation first final formatResult = TokenValidator.validateTokenFormat(token); if (!formatResult.isValid) { debugPrint('DEBUG: Token format invalid: ${formatResult.message}'); TokenValidationCache.cacheResult(token, formatResult); return false; } // If format is valid but token is expiring soon, try server validation if (formatResult.isExpiringSoon) { debugPrint('DEBUG: Token expiring soon, validating with server'); } // Server validation (async with timeout) try { final api = ref.read(apiServiceProvider); if (api == null) { debugPrint('DEBUG: No API service available for token validation'); return formatResult.isValid; // Fall back to format validation } User? validationUser; final serverResult = await TokenValidator.validateTokenWithServer( token, () async { // Update API with token for validation api.updateAuthToken(token); // Try to fetch user data as validation validationUser = await api.getCurrentUser(); return validationUser!; }, ); // Store the user data if validation was successful if (serverResult.isValid && validationUser != null && state.isAuthenticated) { state = state.copyWith(user: validationUser); DebugLogger.auth('Cached user data from token validation'); } TokenValidationCache.cacheResult(token, serverResult); DebugLogger.auth( 'Server token validation: ${serverResult.isValid} - ${serverResult.message}', ); return serverResult.isValid; } catch (e) { debugPrint('DEBUG: Token server validation failed: $e'); // On network error, fall back to format validation if it was valid return formatResult.isValid; } } /// Check if user has saved credentials (with caching) Future hasSavedCredentials() async { // Check cache first final cachedResult = _cacheManager.getCachedCredentialsExist(); if (cachedResult != null) { return cachedResult; } try { final storage = ref.read(optimizedStorageServiceProvider); final hasCredentials = await storage.hasCredentials(); // Cache the result _cacheManager.cacheCredentialsExist(hasCredentials); return hasCredentials; } catch (e) { return false; } } /// Refresh current auth state Future refresh() async { // Clear cache before refresh to ensure fresh data _cacheManager.clearAuthCache(); TokenValidationCache.clearCache(); await _initialize(); } /// Clean up expired caches (called periodically) void cleanupCaches() { _cacheManager.cleanExpiredCache(); _cacheManager.optimizeCache(); } /// Get performance statistics Map getPerformanceStats() { return { 'authCache': _cacheManager.getCacheStats(), 'tokenValidationCache': 'Managed by TokenValidationCache', 'storageCache': 'Managed by OptimizedStorageService', }; } User? _userFromJwtClaims(Map claims) { final id = (claims['sub'] ?? claims['username'] ?? claims['email']) ?.toString() .trim() ?? ''; final username = (claims['username'] ?? claims['name'])?.toString().trim() ?? ''; final emailValue = claims['email']; final email = emailValue == null ? '' : emailValue.toString().trim(); if (id.isEmpty && username.isEmpty && email.isEmpty) { return null; } String resolvedRole = 'user'; final roles = claims['roles']; if (roles is List && roles.isNotEmpty) { resolvedRole = roles.first.toString(); } else if (roles is String && roles.isNotEmpty) { resolvedRole = roles; } return User( id: id.isNotEmpty ? id : (username.isNotEmpty ? username : email.ifEmptyReturn('user')), username: username.ifEmptyReturn( email.ifEmptyReturn(id.ifEmptyReturn('user')), ), email: email, role: resolvedRole, isActive: true, ); } } extension _StringFallbackExtension on String { String ifEmptyReturn(String fallback) => isEmpty ? fallback : this; } /// Provider for the unified auth state manager final authStateManagerProvider = NotifierProvider( AuthStateManager.new, ); /// Computed providers for common auth state queries final isAuthenticatedProvider = Provider((ref) { return ref.watch( authStateManagerProvider.select((state) => state.isAuthenticated), ); }); final authTokenProvider2 = Provider((ref) { return ref.watch(authStateManagerProvider.select((state) => state.token)); }); final authUserProvider = Provider((ref) { return ref.watch(authStateManagerProvider.select((state) => state.user)); }); final authErrorProvider2 = Provider((ref) { return ref.watch(authStateManagerProvider.select((state) => state.error)); }); final isAuthLoadingProvider = Provider((ref) { return ref.watch(authStateManagerProvider.select((state) => state.isLoading)); });