2025-09-28 23:18:24 +05:30
|
|
|
import 'dart:async';
|
|
|
|
|
|
2025-09-25 23:22:48 +05:30
|
|
|
import 'package:flutter/foundation.dart';
|
2025-09-28 23:18:24 +05:30
|
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
2025-08-10 01:20:45 +05:30
|
|
|
// 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
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
part 'auth_state_manager.g.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;
|
2025-09-24 10:52:15 +05:30
|
|
|
final User? user;
|
2025-08-10 01:20:45 +05:30
|
|
|
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,
|
2025-09-24 10:52:15 +05:30
|
|
|
User? user,
|
2025-08-10 01:20:45 +05:30
|
|
|
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
|
2025-09-28 23:18:24 +05:30
|
|
|
@Riverpod(keepAlive: true)
|
|
|
|
|
class AuthStateManager extends _$AuthStateManager {
|
2025-08-10 01:20:45 +05:30
|
|
|
final AuthCacheManager _cacheManager = AuthCacheManager();
|
2025-08-29 12:58:56 +05:30
|
|
|
Future<bool>? _silentLoginFuture;
|
2025-09-21 22:31:44 +05:30
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
AuthState get _current =>
|
|
|
|
|
state.asData?.value ?? const AuthState(status: AuthStatus.initial);
|
|
|
|
|
|
|
|
|
|
void _set(AuthState next, {bool cache = false}) {
|
|
|
|
|
state = AsyncValue.data(next);
|
|
|
|
|
if (cache) {
|
|
|
|
|
_cacheManager.cacheAuthState(next);
|
2025-09-21 22:31:44 +05:30
|
|
|
}
|
2025-09-28 23:18:24 +05:30
|
|
|
}
|
2025-09-21 22:31:44 +05:30
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
void _update(
|
|
|
|
|
AuthState Function(AuthState current) transform, {
|
|
|
|
|
bool cache = false,
|
|
|
|
|
}) {
|
|
|
|
|
final next = transform(_current);
|
|
|
|
|
_set(next, cache: cache);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<AuthState> build() async {
|
|
|
|
|
await _initialize();
|
|
|
|
|
return _current;
|
2025-09-21 22:31:44 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
/// Initialize auth state from storage
|
|
|
|
|
Future<void> _initialize() async {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) =>
|
|
|
|
|
current.copyWith(status: AuthStatus.loading, isLoading: true),
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
try {
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
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-09-16 20:10:53 +05:30
|
|
|
// Fast path: trust token format to avoid blocking startup on network
|
|
|
|
|
final formatOk = _isValidTokenFormat(token);
|
|
|
|
|
if (formatOk) {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.authenticated,
|
|
|
|
|
token: token,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
|
|
|
|
cache: true,
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
2025-09-16 20:10:53 +05:30
|
|
|
// Update API service with token and load user data in background
|
2025-08-10 01:20:45 +05:30
|
|
|
_updateApiServiceToken(token);
|
|
|
|
|
_loadUserData();
|
2025-09-16 20:10:53 +05:30
|
|
|
|
|
|
|
|
// 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 (_) {}
|
|
|
|
|
});
|
2025-08-10 01:20:45 +05:30
|
|
|
} else {
|
2025-09-16 20:10:53 +05:30
|
|
|
// Token format invalid; clear and require login
|
|
|
|
|
DebugLogger.auth('Token format invalid, deleting token');
|
2025-08-10 01:20:45 +05:30
|
|
|
await storage.deleteAuthToken();
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
2025-08-10 01:20:45 +05:30
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
clearError: true,
|
2025-09-28 23:18:24 +05:30
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.error('auth-init-failed', scope: 'auth/state', error: e);
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: 'Failed to initialize auth: $e',
|
|
|
|
|
isLoading: false,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 15:51:27 +05:30
|
|
|
/// Perform login with API key
|
|
|
|
|
Future<bool> loginWithApiKey(
|
|
|
|
|
String apiKey, {
|
|
|
|
|
bool rememberCredentials = false,
|
|
|
|
|
}) async {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.loading,
|
|
|
|
|
isLoading: true,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
2025-08-16 15:51:27 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Validate API key format
|
|
|
|
|
if (apiKey.trim().isEmpty) {
|
|
|
|
|
throw Exception('API key cannot be empty');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure API service is available
|
|
|
|
|
await _ensureApiServiceAvailable();
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-08-16 15:51:27 +05:30
|
|
|
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
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-16 15:51:27 +05:30
|
|
|
await storage.saveAuthToken(tokenStr);
|
|
|
|
|
|
|
|
|
|
// Save API key if requested (for convenience, though less secure than credentials)
|
|
|
|
|
if (rememberCredentials) {
|
2025-09-21 22:31:44 +05:30
|
|
|
final activeServer = await ref.read(activeServerProvider.future);
|
2025-08-16 15:51:27 +05:30
|
|
|
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)
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.authenticated,
|
|
|
|
|
token: tokenStr,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
|
|
|
|
cache: true,
|
2025-08-16 15:51:27 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update API service with token
|
|
|
|
|
_updateApiServiceToken(tokenStr);
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.error('api-key-login-failed', scope: 'auth/state', error: e);
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: e.toString(),
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
),
|
2025-08-16 15:51:27 +05:30
|
|
|
);
|
|
|
|
|
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 {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.loading,
|
|
|
|
|
isLoading: true,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Ensure API service is available (active server/provider rebuild race)
|
|
|
|
|
await _ensureApiServiceAvailable();
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
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
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
await storage.saveAuthToken(tokenStr);
|
|
|
|
|
|
|
|
|
|
// Save credentials if requested
|
|
|
|
|
if (rememberCredentials) {
|
2025-09-21 22:31:44 +05:30
|
|
|
final activeServer = await ref.read(activeServerProvider.future);
|
2025-08-10 01:20:45 +05:30
|
|
|
if (activeServer != null) {
|
|
|
|
|
await storage.saveCredentials(
|
|
|
|
|
serverId: activeServer.id,
|
|
|
|
|
username: username,
|
|
|
|
|
password: password,
|
|
|
|
|
);
|
|
|
|
|
await storage.setRememberCredentials(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update state and API service
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.authenticated,
|
|
|
|
|
token: tokenStr,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
|
|
|
|
cache: true,
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
_updateApiServiceToken(tokenStr);
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.error('login-failed', scope: 'auth/state', error: e);
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: e.toString(),
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
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)) {
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
if (api != null) return;
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Perform silent auto-login with saved credentials
|
|
|
|
|
Future<bool> silentLogin() async {
|
2025-08-29 12:58:56 +05:30
|
|
|
// 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<bool> _performSilentLogin() async {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.loading,
|
|
|
|
|
isLoading: true,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
final savedCredentials = await storage.getSavedCredentials();
|
|
|
|
|
|
|
|
|
|
if (savedCredentials == null) {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final serverId = savedCredentials['serverId']!;
|
|
|
|
|
final username = savedCredentials['username']!;
|
|
|
|
|
final password = savedCredentials['password']!;
|
|
|
|
|
|
2025-09-23 00:58:58 +05:30
|
|
|
// 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);
|
|
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error:
|
|
|
|
|
'Saved server configuration is no longer available. Please reconnect.',
|
|
|
|
|
isLoading: false,
|
|
|
|
|
),
|
2025-09-23 00:58:58 +05:30
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set active server once we know it exists
|
2025-08-10 01:20:45 +05:30
|
|
|
await storage.setActiveServerId(serverId);
|
2025-09-21 22:31:44 +05:30
|
|
|
ref.invalidate(activeServerProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// Wait for server connection
|
2025-09-21 22:31:44 +05:30
|
|
|
final activeServer = await ref.read(activeServerProvider.future);
|
2025-08-10 01:20:45 +05:30
|
|
|
if (activeServer == null) {
|
|
|
|
|
await storage.setActiveServerId(null);
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: 'Server configuration not found',
|
|
|
|
|
isLoading: false,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.error('silent-login-failed', scope: 'auth/state', error: e);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// Clear invalid credentials on auth errors
|
|
|
|
|
if (e.toString().contains('401') ||
|
|
|
|
|
e.toString().contains('403') ||
|
|
|
|
|
e.toString().contains('authentication') ||
|
|
|
|
|
e.toString().contains('unauthorized')) {
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
await storage.deleteSavedCredentials();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
error: e.toString(),
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle token invalidation (called by API service)
|
|
|
|
|
Future<void> onTokenInvalidated() async {
|
2025-08-29 12:58:56 +05:30
|
|
|
// Avoid spamming logs if multiple requests invalidate at once
|
|
|
|
|
final reloginInProgress = _silentLoginFuture != null;
|
|
|
|
|
if (!reloginInProgress) {
|
|
|
|
|
DebugLogger.auth('Auth token invalidated');
|
|
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// Clear token from storage
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
await storage.deleteAuthToken();
|
2025-09-24 10:52:15 +05:30
|
|
|
_updateApiServiceToken(null);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// Update state
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.tokenExpired,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
clearUser: true,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Attempt silent re-login if credentials are available
|
|
|
|
|
final hasCredentials = await storage.getSavedCredentials() != null;
|
|
|
|
|
if (hasCredentials) {
|
2025-08-29 12:58:56 +05:30
|
|
|
if (!reloginInProgress) {
|
|
|
|
|
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 {
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) =>
|
|
|
|
|
current.copyWith(status: AuthStatus.loading, isLoading: true),
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Call server logout if possible
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
if (api != null) {
|
|
|
|
|
try {
|
|
|
|
|
await api.logout();
|
|
|
|
|
} catch (e) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.warning(
|
|
|
|
|
'server-logout-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'error': e.toString()},
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear all local auth data
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
await storage.clearAuthData();
|
2025-09-24 10:52:15 +05:30
|
|
|
_updateApiServiceToken(null);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// Update state
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
clearUser: true,
|
|
|
|
|
clearError: true,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
2025-08-20 22:15:26 +05:30
|
|
|
DebugLogger.auth('Logout complete');
|
2025-08-10 01:20:45 +05:30
|
|
|
} catch (e) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.error('logout-failed', scope: 'auth/state', error: e);
|
2025-08-10 01:20:45 +05:30
|
|
|
// Even if logout fails, clear local state
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
clearUser: true,
|
|
|
|
|
error: 'Logout error: $e',
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
2025-09-24 10:52:15 +05:30
|
|
|
_updateApiServiceToken(null);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
2025-09-28 23:18:24 +05:30
|
|
|
final current = _current;
|
|
|
|
|
if (current.token != null) {
|
|
|
|
|
final jwtUserInfo = TokenValidator.extractUserInfo(current.token!);
|
2025-08-10 01:20:45 +05:30
|
|
|
if (jwtUserInfo != null) {
|
2025-09-24 10:52:15 +05:30
|
|
|
final userFromJwt = _userFromJwtClaims(jwtUserInfo);
|
|
|
|
|
if (userFromJwt != null) {
|
|
|
|
|
DebugLogger.auth('Extracted user info from JWT token');
|
2025-09-28 23:18:24 +05:30
|
|
|
_update((current) => current.copyWith(user: userFromJwt));
|
2025-09-24 10:52:15 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// 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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.warning(
|
|
|
|
|
'user-data-load-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'error': e.toString()},
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
// Don't update state on user data load failure
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load complete user data from server
|
|
|
|
|
Future<void> _loadServerUserData() async {
|
|
|
|
|
try {
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-09-28 23:18:24 +05:30
|
|
|
final current = _current;
|
|
|
|
|
if (api != null && current.isAuthenticated) {
|
2025-08-10 01:20:45 +05:30
|
|
|
// Check if we already have user data from token validation
|
2025-09-28 23:18:24 +05:30
|
|
|
if (current.user != null) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.auth('user-data-present-from-token', scope: 'auth/state');
|
2025-08-10 01:20:45 +05:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final user = await api.getCurrentUser();
|
2025-09-28 23:18:24 +05:30
|
|
|
_update((current) => current.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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.warning(
|
|
|
|
|
'server-user-data-load-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'error': e.toString()},
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
// Don't update state on server data load failure - keep JWT data if available
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update API service with current token
|
2025-09-24 10:52:15 +05:30
|
|
|
void _updateApiServiceToken(String? token) {
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.warning(
|
|
|
|
|
'token-format-invalid',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'message': formatResult.message},
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
TokenValidationCache.cacheResult(token, formatResult);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If format is valid but token is expiring soon, try server validation
|
|
|
|
|
if (formatResult.isExpiringSoon) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.auth('token-expiring-soon', scope: 'auth/state');
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Server validation (async with timeout)
|
|
|
|
|
try {
|
2025-09-21 22:31:44 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
if (api == null) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.warning('token-validation-no-api', scope: 'auth/state');
|
2025-08-10 01:20:45 +05:30
|
|
|
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 &&
|
2025-09-28 23:18:24 +05:30
|
|
|
_current.isAuthenticated) {
|
|
|
|
|
_update((current) => current.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) {
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.warning(
|
|
|
|
|
'token-validation-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'error': e.toString()},
|
|
|
|
|
);
|
2025-08-10 01:20:45 +05:30
|
|
|
// 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 {
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-08-10 01:20:45 +05:30
|
|
|
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',
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-09-24 10:52:15 +05:30
|
|
|
|
|
|
|
|
User? _userFromJwtClaims(Map<String, dynamic> 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;
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|