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';
|
2025-11-22 21:53:14 +05:30
|
|
|
import '../services/optimized_storage_service.dart';
|
2025-08-10 01:20:45 +05:30
|
|
|
import 'token_validator.dart';
|
|
|
|
|
import 'auth_cache_manager.dart';
|
2025-08-20 22:15:26 +05:30
|
|
|
import '../utils/debug_logger.dart';
|
2025-11-22 21:53:14 +05:30
|
|
|
import '../utils/user_avatar_utils.dart';
|
2025-12-05 13:01:17 +05:30
|
|
|
import '../../features/tools/providers/tools_providers.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,
|
2025-11-12 13:23:58 +05:30
|
|
|
credentialError, // Invalid credentials - need re-login
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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-11-12 13:23:58 +05:30
|
|
|
// Prevent infinite retry loops
|
|
|
|
|
int _retryCount = 0;
|
|
|
|
|
static const int _maxRetries = 3;
|
|
|
|
|
DateTime? _lastRetryTime;
|
|
|
|
|
|
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}) {
|
2025-11-22 21:53:14 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
|
|
|
|
if (next.user != null && next.isAuthenticated) {
|
|
|
|
|
// Persist user and avatar asynchronously without blocking state update
|
|
|
|
|
unawaited(_persistUserWithAvatar(next, storage));
|
|
|
|
|
} else if (!next.isAuthenticated) {
|
2025-11-25 00:08:51 +05:30
|
|
|
unawaited(
|
|
|
|
|
storage.saveLocalUser(null).onError((error, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'Failed to clear local user on logout',
|
|
|
|
|
scope: 'auth/persistence',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
unawaited(
|
|
|
|
|
storage.saveLocalUserAvatar(null).onError((error, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'Failed to clear local user avatar on logout',
|
|
|
|
|
scope: 'auth/persistence',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
2025-11-22 21:53:14 +05:30
|
|
|
}
|
2025-09-28 23:18:24 +05:30
|
|
|
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-11-22 21:53:14 +05:30
|
|
|
Future<void> _persistUserWithAvatar(
|
|
|
|
|
AuthState authState,
|
|
|
|
|
OptimizedStorageService storage,
|
|
|
|
|
) async {
|
|
|
|
|
try {
|
|
|
|
|
final api = ref.read(apiServiceProvider);
|
|
|
|
|
final user = authState.user!;
|
|
|
|
|
final resolvedAvatar = resolveUserProfileImageUrl(
|
|
|
|
|
api,
|
|
|
|
|
deriveUserProfileImage(user),
|
|
|
|
|
);
|
|
|
|
|
final userWithAvatar =
|
|
|
|
|
resolvedAvatar != null && resolvedAvatar != user.profileImage
|
2025-11-25 00:08:51 +05:30
|
|
|
? user.copyWith(profileImage: resolvedAvatar)
|
|
|
|
|
: user;
|
2025-11-22 21:53:14 +05:30
|
|
|
await storage.saveLocalUser(userWithAvatar);
|
|
|
|
|
if (resolvedAvatar != null) {
|
|
|
|
|
await storage.saveLocalUserAvatar(resolvedAvatar);
|
|
|
|
|
}
|
|
|
|
|
} catch (error, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'Failed to persist user with avatar',
|
|
|
|
|
scope: 'auth/persistence',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-11-22 21:53:14 +05:30
|
|
|
try {
|
|
|
|
|
final cachedUser = await storage.getLocalUser();
|
|
|
|
|
if (cachedUser != null) {
|
|
|
|
|
// Restore cached avatar as well
|
|
|
|
|
final cachedAvatar = await storage.getLocalUserAvatar();
|
|
|
|
|
final userWithAvatar =
|
|
|
|
|
cachedAvatar != null &&
|
|
|
|
|
cachedAvatar.isNotEmpty &&
|
|
|
|
|
cachedUser.profileImage != cachedAvatar
|
|
|
|
|
? cachedUser.copyWith(profileImage: cachedAvatar)
|
|
|
|
|
: cachedUser;
|
|
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(user: userWithAvatar),
|
|
|
|
|
cache: true,
|
|
|
|
|
);
|
|
|
|
|
DebugLogger.auth('Restored user from cache');
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
2025-10-01 23:44:22 +05:30
|
|
|
// Update API service with token and kick off dependent background work
|
2025-08-10 01:20:45 +05:30
|
|
|
_updateApiServiceToken(token);
|
2025-10-01 23:44:22 +05:30
|
|
|
_preloadDefaultModel();
|
2025-08-10 01:20:45 +05:30
|
|
|
_loadUserData();
|
2025-10-02 21:09:01 +05:30
|
|
|
_prefetchConversations();
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
);
|
|
|
|
|
|
2025-10-01 23:44:22 +05:30
|
|
|
// Update API service with token and kick off dependent background work
|
2025-08-16 15:51:27 +05:30
|
|
|
_updateApiServiceToken(tokenStr);
|
2025-10-01 23:44:22 +05:30
|
|
|
_preloadDefaultModel();
|
2025-08-16 15:51:27 +05:30
|
|
|
|
|
|
|
|
// Load user data in background (consistent with credentials method)
|
|
|
|
|
_loadUserData();
|
2025-10-02 21:09:01 +05:30
|
|
|
_prefetchConversations();
|
2025-08-16 15:51:27 +05:30
|
|
|
|
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');
|
|
|
|
|
}
|
2025-10-30 14:28:00 +05:30
|
|
|
} catch (e, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'api-key-login-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
error: e,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
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
|
|
|
);
|
2025-10-30 14:28:00 +05:30
|
|
|
rethrow;
|
2025-08-16 15:51:27 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-10-01 23:44:22 +05:30
|
|
|
_preloadDefaultModel();
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
// Load user data in background
|
|
|
|
|
_loadUserData();
|
2025-10-02 21:09:01 +05:30
|
|
|
_prefetchConversations();
|
2025-08-10 01:20:45 +05:30
|
|
|
|
2025-08-20 22:15:26 +05:30
|
|
|
DebugLogger.auth('Login successful');
|
2025-08-10 01:20:45 +05:30
|
|
|
return true;
|
2025-10-30 14:28:00 +05:30
|
|
|
} catch (e, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'login-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
error: e,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
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
|
|
|
);
|
2025-10-30 14:28:00 +05:30
|
|
|
rethrow;
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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-10-30 14:28:00 +05:30
|
|
|
} catch (e, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'silent-login-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
error: e,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
String errorMessage = e.toString();
|
2025-08-10 01:20:45 +05:30
|
|
|
|
2025-11-12 13:23:58 +05:30
|
|
|
// Don't clear credentials on connection errors - only clear on actual auth failures
|
|
|
|
|
// Check if this is a genuine auth failure vs network issue
|
|
|
|
|
final isNetworkError =
|
|
|
|
|
e.toString().contains('SocketException') ||
|
|
|
|
|
e.toString().contains('Connection') ||
|
|
|
|
|
e.toString().contains('timeout') ||
|
|
|
|
|
e.toString().contains('NetworkImage');
|
|
|
|
|
|
|
|
|
|
if (!isNetworkError &&
|
|
|
|
|
(e.toString().contains('401') ||
|
|
|
|
|
e.toString().contains('403') ||
|
|
|
|
|
e.toString().contains('authentication') ||
|
|
|
|
|
e.toString().contains('unauthorized'))) {
|
|
|
|
|
// Only clear credentials if this is a real auth failure, not a network issue
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-10-30 14:28:00 +05:30
|
|
|
try {
|
2025-11-12 13:23:58 +05:30
|
|
|
DebugLogger.auth('Clearing invalid credentials after auth failure');
|
2025-10-30 14:28:00 +05:30
|
|
|
await storage.deleteSavedCredentials();
|
|
|
|
|
} catch (deleteError, deleteStack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'silent-login-credential-clear-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
error: deleteError,
|
|
|
|
|
stackTrace: deleteStack,
|
|
|
|
|
);
|
|
|
|
|
errorMessage =
|
|
|
|
|
'$errorMessage. Also failed to clear saved '
|
|
|
|
|
'credentials; please clear Conduit credentials from '
|
|
|
|
|
'system settings.';
|
|
|
|
|
}
|
2025-11-12 13:23:58 +05:30
|
|
|
|
|
|
|
|
// Set credential error status to trigger login page
|
|
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.credentialError,
|
|
|
|
|
error: errorMessage,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
} else if (isNetworkError) {
|
|
|
|
|
DebugLogger.auth(
|
|
|
|
|
'Silent login failed due to network error - keeping credentials',
|
|
|
|
|
);
|
|
|
|
|
errorMessage = 'Connection issue - please check your network';
|
|
|
|
|
|
|
|
|
|
// Set general error status to trigger connection issue page
|
|
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: errorMessage,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return false;
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
2025-11-12 13:23:58 +05:30
|
|
|
// Unknown error type - treat as connection issue but keep credentials
|
|
|
|
|
if (errorMessage.trim().isEmpty) {
|
|
|
|
|
errorMessage = 'Connection issue - please try again shortly';
|
|
|
|
|
}
|
|
|
|
|
DebugLogger.auth(
|
|
|
|
|
'Silent login failed with unknown error - keeping credentials',
|
|
|
|
|
);
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
2025-11-12 13:23:58 +05:30
|
|
|
status: AuthStatus.error,
|
2025-10-30 14:28:00 +05:30
|
|
|
error: errorMessage,
|
2025-09-28 23:18:24 +05:30
|
|
|
isLoading: false,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 13:23:58 +05:30
|
|
|
/// Reset retry counter (called when user manually retries)
|
|
|
|
|
void resetRetryCounter() {
|
|
|
|
|
_retryCount = 0;
|
|
|
|
|
_lastRetryTime = null;
|
|
|
|
|
DebugLogger.auth('Retry counter reset for manual retry');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle auth issues (called by API service)
|
|
|
|
|
/// This shows connection issue page instead of logging out
|
|
|
|
|
void onAuthIssue() {
|
|
|
|
|
DebugLogger.auth('Auth issue detected - showing connection issue page');
|
|
|
|
|
// Don't clear token or user data - just set error state
|
|
|
|
|
// The router will show connection issue page
|
|
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: 'Connection issue - please check your connection',
|
|
|
|
|
clearError: false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle token invalidation (called by API service for explicit token expiry)
|
|
|
|
|
/// This is only used when we need to clear the token for re-login attempts
|
2025-08-10 01:20:45 +05:30
|
|
|
Future<void> onTokenInvalidated() async {
|
2025-11-12 13:23:58 +05:30
|
|
|
// Prevent infinite retry loops
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
if (_lastRetryTime != null &&
|
|
|
|
|
now.difference(_lastRetryTime!).inSeconds < 5) {
|
|
|
|
|
_retryCount++;
|
|
|
|
|
if (_retryCount >= _maxRetries) {
|
|
|
|
|
DebugLogger.auth(
|
|
|
|
|
'Max retry attempts reached - stopping silent re-login',
|
|
|
|
|
);
|
|
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.error,
|
|
|
|
|
error: 'Connection issue - please retry manually',
|
|
|
|
|
clearError: false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
// Reset after 30 seconds to allow manual retry
|
|
|
|
|
Future.delayed(const Duration(seconds: 30), () {
|
|
|
|
|
_retryCount = 0;
|
|
|
|
|
_lastRetryTime = null;
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Reset counter if enough time has passed
|
|
|
|
|
_retryCount = 0;
|
|
|
|
|
}
|
|
|
|
|
_lastRetryTime = now;
|
|
|
|
|
|
2025-08-29 12:58:56 +05:30
|
|
|
// Avoid spamming logs if multiple requests invalidate at once
|
|
|
|
|
final reloginInProgress = _silentLoginFuture != null;
|
|
|
|
|
if (!reloginInProgress) {
|
2025-11-12 13:23:58 +05:30
|
|
|
DebugLogger.auth(
|
|
|
|
|
'Auth token invalidated - attempting silent re-login (attempt ${_retryCount + 1}/$_maxRetries)',
|
|
|
|
|
);
|
2025-08-29 12:58:56 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
|
2025-09-21 22:31:44 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-10-30 14:28:00 +05:30
|
|
|
try {
|
|
|
|
|
await storage.deleteAuthToken();
|
2025-11-12 13:23:58 +05:30
|
|
|
DebugLogger.auth('Cleared invalidated token from secure storage');
|
|
|
|
|
} catch (e, stack) {
|
2025-10-30 14:28:00 +05:30
|
|
|
DebugLogger.error(
|
|
|
|
|
'token-delete-failed',
|
|
|
|
|
scope: 'auth/state',
|
2025-11-12 13:23:58 +05:30
|
|
|
error: e,
|
2025-10-30 14:28:00 +05:30
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-11-12 13:23:58 +05:30
|
|
|
_updateApiServiceToken(null);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.tokenExpired,
|
2025-11-12 13:23:58 +05:30
|
|
|
error: 'Session expired - please sign in again',
|
2025-09-28 23:18:24 +05:30
|
|
|
clearToken: true,
|
|
|
|
|
clearUser: true,
|
2025-11-12 13:23:58 +05:30
|
|
|
isLoading: false,
|
2025-09-28 23:18:24 +05:30
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Attempt silent re-login if credentials are available
|
|
|
|
|
final hasCredentials = await storage.getSavedCredentials() != null;
|
2025-11-12 13:23:58 +05:30
|
|
|
if (hasCredentials && !reloginInProgress) {
|
|
|
|
|
DebugLogger.auth('Attempting silent re-login after token invalidation');
|
|
|
|
|
final success = await silentLogin();
|
|
|
|
|
if (success) {
|
|
|
|
|
// Reset retry counter on success
|
|
|
|
|
_retryCount = 0;
|
|
|
|
|
_lastRetryTime = null;
|
2025-08-29 12:58:56 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 13:44:05 +05:30
|
|
|
/// Logout user and clear all data including server configs and custom headers
|
2025-08-10 01:20:45 +05:30
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 13:44:05 +05:30
|
|
|
// Clear all local auth data (including server configs with custom headers)
|
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
|
|
|
|
2025-10-10 22:08:23 +05:30
|
|
|
// Clear active server to force return to server connection page
|
|
|
|
|
await storage.setActiveServerId(null);
|
2025-11-22 21:53:14 +05:30
|
|
|
|
2025-11-12 13:44:05 +05:30
|
|
|
// Invalidate all auth-related providers to clear cached data
|
2025-10-10 22:08:23 +05:30
|
|
|
ref.invalidate(activeServerProvider);
|
2025-11-12 13:44:05 +05:30
|
|
|
ref.invalidate(serverConfigsProvider);
|
2025-12-05 13:01:17 +05:30
|
|
|
ref.invalidate(toolsListProvider);
|
2025-11-22 21:53:14 +05:30
|
|
|
|
2025-11-12 13:44:05 +05:30
|
|
|
// Clear auth cache manager
|
|
|
|
|
_cacheManager.clearAuthCache();
|
2025-10-10 22:08:23 +05:30
|
|
|
|
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-11-22 21:53:14 +05:30
|
|
|
DebugLogger.auth(
|
|
|
|
|
'Logout complete - all data cleared including server configs and custom headers',
|
|
|
|
|
);
|
2025-10-30 14:28:00 +05:30
|
|
|
} catch (e, stack) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'logout-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
error: e,
|
|
|
|
|
stackTrace: stack,
|
|
|
|
|
);
|
|
|
|
|
// Even if logout fails, clear local state where possible
|
2025-10-10 22:08:23 +05:30
|
|
|
final storage = ref.read(optimizedStorageServiceProvider);
|
2025-11-12 13:44:05 +05:30
|
|
|
try {
|
|
|
|
|
await storage.clearAuthData();
|
|
|
|
|
} catch (clearError) {
|
|
|
|
|
DebugLogger.error(
|
|
|
|
|
'logout-clear-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
error: clearError,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-10 22:08:23 +05:30
|
|
|
await storage.setActiveServerId(null);
|
|
|
|
|
ref.invalidate(activeServerProvider);
|
2025-11-12 13:44:05 +05:30
|
|
|
ref.invalidate(serverConfigsProvider);
|
|
|
|
|
_cacheManager.clearAuthCache();
|
2025-10-10 22:08:23 +05:30
|
|
|
|
2025-09-28 23:18:24 +05:30
|
|
|
_update(
|
|
|
|
|
(current) => current.copyWith(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
clearToken: true,
|
|
|
|
|
clearUser: true,
|
2025-10-30 14:28:00 +05:30
|
|
|
error:
|
2025-11-12 13:44:05 +05:30
|
|
|
'Logout error: $e. Some data may remain stored; '
|
|
|
|
|
'please clear app data from your device settings if needed.',
|
2025-09-28 23:18:24 +05:30
|
|
|
),
|
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
|
|
|
}
|
2025-10-01 23:44:22 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Preload the default model as soon as authentication succeeds.
|
|
|
|
|
void _preloadDefaultModel() {
|
|
|
|
|
Future.microtask(() async {
|
|
|
|
|
if (!ref.mounted) return;
|
|
|
|
|
try {
|
|
|
|
|
await ref.read(defaultModelProvider.future);
|
|
|
|
|
DebugLogger.auth('Default model preload requested');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (!ref.mounted) return;
|
|
|
|
|
DebugLogger.warning(
|
|
|
|
|
'default-model-preload-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'error': e.toString()},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-02 21:09:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Prime the conversations list so navigation drawers show real data after login.
|
|
|
|
|
void _prefetchConversations() {
|
2025-10-04 16:04:49 +05:30
|
|
|
Future.microtask(() {
|
2025-10-02 21:09:01 +05:30
|
|
|
if (!ref.mounted) return;
|
|
|
|
|
try {
|
|
|
|
|
refreshConversationsCache(ref, includeFolders: true);
|
2025-10-04 16:04:49 +05:30
|
|
|
DebugLogger.auth('Conversations prefetch scheduled');
|
2025-10-02 21:09:01 +05:30
|
|
|
} catch (e) {
|
|
|
|
|
if (!ref.mounted) return;
|
|
|
|
|
DebugLogger.warning(
|
|
|
|
|
'conversation-prefetch-failed',
|
|
|
|
|
scope: 'auth/state',
|
|
|
|
|
data: {'error': e.toString()},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
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
|
|
|
}
|