Files
iiEsaywebUIapp/lib/core/auth/auth_state_manager.dart

666 lines
20 KiB
Dart
Raw Normal View History

2025-08-10 01:20:45 +05:30
import 'package:flutter/foundation.dart';
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';
2025-08-20 22:15:26 +05:30
import '../utils/debug_logger.dart';
2025-08-10 01:20:45 +05:30
/// 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 dynamic user; // Replace with proper User type
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,
dynamic 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 StateNotifier<AuthState> {
AuthStateManager(this._ref)
: super(const AuthState(status: AuthStatus.initial)) {
_initialize();
}
final Ref _ref;
final AuthCacheManager _cacheManager = AuthCacheManager();
/// Initialize auth state from storage
Future<void> _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) {
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Found stored token during initialization');
2025-08-10 01:20:45 +05:30
// Validate token before setting authenticated state
final isValid = await _validateToken(token);
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Token validation result: $isValid');
2025-08-10 01:20:45 +05:30
if (isValid) {
state = state.copyWith(
status: AuthStatus.authenticated,
token: token,
isLoading: false,
clearError: true,
);
// Update API service with token
_updateApiServiceToken(token);
// Load user data in background
_loadUserData();
} else {
// Token is invalid, clear it
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Token validation failed, deleting token');
2025-08-10 01:20:45 +05:30
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,
);
}
}
2025-08-16 15:51:27 +05:30
/// Perform login with API key
Future<bool> 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();
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// Validate token format (consistent with credentials method)
if (!_isValidTokenFormat(tokenStr)) {
throw Exception('Invalid API key format');
}
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// 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
2025-08-20 22:15:26 +05:30
2025-08-16 15:51:27 +05:30
// 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,
2025-08-20 22:15:26 +05:30
username:
'api_key_user', // Special username to indicate API key auth
2025-08-16 15:51:27 +05:30
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();
2025-08-20 22:15:26 +05:30
DebugLogger.auth('API key login successful');
2025-08-16 15:51:27 +05:30
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;
}
}
2025-08-10 01:20:45 +05:30
/// Perform login with credentials
Future<bool> 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();
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Login successful');
2025-08-10 01:20:45 +05:30
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<void> _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<bool> silentLogin() 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']!;
// Set active server if needed
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;
}
2025-08-16 15:51:27 +05:30
// 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);
}
2025-08-10 01:20:45 +05:30
} 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<void> onTokenInvalidated() async {
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Auth token invalidated');
2025-08-10 01:20:45 +05:30
// Clear token from storage
final storage = _ref.read(optimizedStorageServiceProvider);
await storage.deleteAuthToken();
// 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) {
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Attempting silent re-login after token invalidation');
2025-08-10 01:20:45 +05:30
await silentLogin();
}
}
/// Logout user
Future<void> 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();
// Update state
state = state.copyWith(
status: AuthStatus.unauthenticated,
isLoading: false,
clearToken: true,
clearUser: true,
clearError: true,
);
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Logout complete');
2025-08-10 01:20:45 +05:30
} 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',
);
}
}
/// Load user data in background with JWT extraction fallback
Future<void> _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) {
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Extracted user info from JWT token');
2025-08-10 01:20:45 +05:30
state = state.copyWith(user: jwtUserInfo);
// 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<void> _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);
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Loaded complete user data from server');
2025-08-10 01:20:45 +05:30
}
} 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<bool> _validateToken(String token) async {
// Check cache first
final cachedResult = TokenValidationCache.getCachedResult(token);
if (cachedResult != null) {
2025-08-20 22:15:26 +05:30
DebugLogger.auth(
'Using cached token validation result: ${cachedResult.isValid}',
2025-08-10 01:20:45 +05:30
);
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);
2025-08-20 22:15:26 +05:30
DebugLogger.auth('Cached user data from token validation');
2025-08-10 01:20:45 +05:30
}
TokenValidationCache.cacheResult(token, serverResult);
2025-08-20 22:15:26 +05:30
DebugLogger.auth(
'Server token validation: ${serverResult.isValid} - ${serverResult.message}',
2025-08-10 01:20:45 +05:30
);
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<bool> 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<void> 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<String, dynamic> getPerformanceStats() {
return {
'authCache': _cacheManager.getCacheStats(),
'tokenValidationCache': 'Managed by TokenValidationCache',
'storageCache': 'Managed by OptimizedStorageService',
};
}
}
/// Provider for the unified auth state manager
final authStateManagerProvider =
StateNotifierProvider<AuthStateManager, AuthState>((ref) {
return AuthStateManager(ref);
});
/// Computed providers for common auth state queries
final isAuthenticatedProvider = Provider<bool>((ref) {
return ref.watch(
authStateManagerProvider.select((state) => state.isAuthenticated),
);
});
final authTokenProvider2 = Provider<String?>((ref) {
return ref.watch(authStateManagerProvider.select((state) => state.token));
});
final authUserProvider = Provider<dynamic>((ref) {
return ref.watch(authStateManagerProvider.select((state) => state.user));
});
final authErrorProvider2 = Provider<String?>((ref) {
return ref.watch(authStateManagerProvider.select((state) => state.error));
});
final isAuthLoadingProvider = Provider<bool>((ref) {
return ref.watch(authStateManagerProvider.select((state) => state.isLoading));
});