712 lines
29 KiB
Dart
712 lines
29 KiB
Dart
// lib/auth_provider.dart
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/scheduler.dart'; // Added import for post-frame callback
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'dart:convert';
|
|
import 'package:bcrypt/bcrypt.dart'; // Import bcrypt
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // Import secure storage
|
|
|
|
import 'package:environment_monitoring_app/services/api_service.dart';
|
|
import 'package:environment_monitoring_app/services/base_api_service.dart';
|
|
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
|
import 'package:environment_monitoring_app/services/retry_service.dart';
|
|
import 'package:environment_monitoring_app/services/user_preferences_service.dart';
|
|
|
|
// Removed _CacheDataContainer class
|
|
// Removed _loadCacheDataFromIsolate function
|
|
|
|
class AuthProvider with ChangeNotifier {
|
|
late final ApiService _apiService;
|
|
late final DatabaseHelper _dbHelper;
|
|
late final ServerConfigService _serverConfigService;
|
|
late final RetryService _retryService;
|
|
final UserPreferencesService _userPreferencesService = UserPreferencesService();
|
|
|
|
// Initialize secure storage
|
|
final _secureStorage = const FlutterSecureStorage();
|
|
static const _passwordStorageKey = 'user_password';
|
|
|
|
// --- Session & Profile State ---
|
|
String? _jwtToken;
|
|
String? _userEmail;
|
|
Map<String, dynamic>? _profileData;
|
|
bool get isLoggedIn => _jwtToken != null;
|
|
String? get userEmail => _userEmail;
|
|
Map<String, dynamic>? get profileData => _profileData;
|
|
|
|
// --- App State ---
|
|
bool _isLoading = true; // Keep true initially
|
|
bool _isFirstLogin = true;
|
|
DateTime? _lastSyncTimestamp;
|
|
bool _isBackgroundLoading = false; // Added flag for background loading
|
|
bool get isLoading => _isLoading;
|
|
bool get isBackgroundLoading => _isBackgroundLoading;
|
|
bool get isFirstLogin => _isFirstLogin;
|
|
DateTime? get lastSyncTimestamp => _lastSyncTimestamp;
|
|
|
|
bool _isSessionExpired = false;
|
|
bool get isSessionExpired => _isSessionExpired;
|
|
|
|
// --- Cached Master Data ---
|
|
List<Map<String, dynamic>>? _allUsers;
|
|
List<Map<String, dynamic>>? _tarballStations;
|
|
List<Map<String, dynamic>>? _manualStations;
|
|
List<Map<String, dynamic>>? _tarballClassifications;
|
|
List<Map<String, dynamic>>? _riverManualStations;
|
|
List<Map<String, dynamic>>? _riverTriennialStations;
|
|
// --- ADDED: River Investigative Stations ---
|
|
List<Map<String, dynamic>>? _riverInvestigativeStations;
|
|
// --- END ADDED ---
|
|
List<Map<String, dynamic>>? _departments;
|
|
List<Map<String, dynamic>>? _companies;
|
|
List<Map<String, dynamic>>? _positions;
|
|
List<Map<String, dynamic>>? _airClients;
|
|
List<Map<String, dynamic>>? _airManualStations;
|
|
List<Map<String, dynamic>>? _states;
|
|
List<Map<String, dynamic>>? _appSettings;
|
|
List<Map<String, dynamic>>? _npeParameterLimits;
|
|
List<Map<String, dynamic>>? _marineParameterLimits;
|
|
List<Map<String, dynamic>>? _riverParameterLimits;
|
|
List<Map<String, dynamic>>? _apiConfigs;
|
|
List<Map<String, dynamic>>? _ftpConfigs;
|
|
List<Map<String, dynamic>>? _documents;
|
|
List<Map<String, dynamic>>? _pendingRetries;
|
|
|
|
// --- Getters for UI access ---
|
|
List<Map<String, dynamic>>? get allUsers => _allUsers;
|
|
List<Map<String, dynamic>>? get tarballStations => _tarballStations;
|
|
List<Map<String, dynamic>>? get manualStations => _manualStations;
|
|
List<Map<String, dynamic>>? get tarballClassifications => _tarballClassifications;
|
|
List<Map<String, dynamic>>? get riverManualStations => _riverManualStations;
|
|
List<Map<String, dynamic>>? get riverTriennialStations => _riverTriennialStations;
|
|
// --- ADDED: Getter for River Investigative Stations ---
|
|
List<Map<String, dynamic>>? get riverInvestigativeStations => _riverInvestigativeStations;
|
|
// --- END ADDED ---
|
|
List<Map<String, dynamic>>? get departments => _departments;
|
|
List<Map<String, dynamic>>? get companies => _companies;
|
|
List<Map<String, dynamic>>? get positions => _positions;
|
|
List<Map<String, dynamic>>? get airClients => _airClients;
|
|
List<Map<String, dynamic>>? get airManualStations => _airManualStations;
|
|
List<Map<String, dynamic>>? get states => _states;
|
|
List<Map<String, dynamic>>? get appSettings => _appSettings;
|
|
List<Map<String, dynamic>>? get npeParameterLimits => _npeParameterLimits;
|
|
List<Map<String, dynamic>>? get marineParameterLimits => _marineParameterLimits;
|
|
List<Map<String, dynamic>>? get riverParameterLimits => _riverParameterLimits;
|
|
List<Map<String, dynamic>>? get apiConfigs => _apiConfigs;
|
|
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
|
|
List<Map<String, dynamic>>? get documents => _documents;
|
|
List<Map<String, dynamic>>? get pendingRetries => _pendingRetries;
|
|
|
|
// --- SharedPreferences Keys ---
|
|
static const String tokenKey = 'jwt_token';
|
|
static const String userEmailKey = 'user_email';
|
|
static const String profileDataKey = 'user_profile_data';
|
|
static const String lastSyncTimestampKey = 'last_sync_timestamp';
|
|
static const String isFirstLoginKey = 'is_first_login';
|
|
static const String lastOnlineLoginKey = 'last_online_login';
|
|
|
|
AuthProvider({
|
|
required ApiService apiService,
|
|
required DatabaseHelper dbHelper,
|
|
required ServerConfigService serverConfigService,
|
|
required RetryService retryService,
|
|
}) : _apiService = apiService,
|
|
_dbHelper = dbHelper,
|
|
_serverConfigService = serverConfigService,
|
|
_retryService = retryService {
|
|
debugPrint('AuthProvider: Initializing...');
|
|
_initializeAndLoadData(); // Use the updated method name
|
|
}
|
|
|
|
Future<bool> isConnected() async {
|
|
final connectivityResult = await Connectivity().checkConnectivity();
|
|
return !connectivityResult.contains(ConnectivityResult.none);
|
|
}
|
|
|
|
// Updated method using SchedulerBinding instead of compute
|
|
Future<void> _initializeAndLoadData() async {
|
|
_isLoading = true;
|
|
notifyListeners(); // Notify UI about initial loading state
|
|
|
|
// 1. Perform quick SharedPreferences reads first.
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_jwtToken = prefs.getString(tokenKey);
|
|
_userEmail = prefs.getString(userEmailKey);
|
|
_isFirstLogin = prefs.getBool(isFirstLoginKey) ?? true;
|
|
|
|
final profileJson = prefs.getString(profileDataKey);
|
|
if (profileJson != null) {
|
|
try {
|
|
_profileData = jsonDecode(profileJson);
|
|
} catch (e) {
|
|
debugPrint("Failed to decode profile from prefs: $e");
|
|
prefs.remove(profileDataKey);
|
|
}
|
|
}
|
|
|
|
// --- START MODIFIED: Load server config with is_active check ---
|
|
// Load server config early
|
|
Map<String, dynamic>? activeApiConfig = await _serverConfigService.getActiveApiConfig();
|
|
if (activeApiConfig == null) {
|
|
debugPrint("AuthProvider: No active config in SharedPreferences. Checking local DB for 'is_active' flag...");
|
|
// Load configs directly from DB helper (since cache isn't loaded yet)
|
|
final allApiConfigs = await _dbHelper.loadApiConfigs();
|
|
if (allApiConfigs != null && allApiConfigs.isNotEmpty) {
|
|
try {
|
|
// Find the first config marked as active. Note: 'is_active' might be 1 or true.
|
|
activeApiConfig = allApiConfigs.firstWhere(
|
|
(c) => c['is_active'] == 1 || c['is_active'] == true,
|
|
orElse: () => allApiConfigs.first, // Fallback to just the first config if none are active
|
|
);
|
|
debugPrint("AuthProvider: Found active config in DB: ${activeApiConfig['config_name']}");
|
|
await _serverConfigService.setActiveApiConfig(activeApiConfig);
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: Error finding active config in DB. $e");
|
|
}
|
|
}
|
|
}
|
|
|
|
// If still no config (empty DB, first launch), set hardcoded default
|
|
if (activeApiConfig == null) {
|
|
debugPrint("AuthProvider: No active config found in DB. Setting default bootstrap URL.");
|
|
final initialConfig = {
|
|
'api_config_id': 0,
|
|
'config_name': 'Default Server',
|
|
'api_url': 'https://mms-apiv4.pstw.com.my/v1', // This is your bootstrap URL
|
|
};
|
|
await _serverConfigService.setActiveApiConfig(initialConfig);
|
|
}
|
|
// --- END MODIFIED ---
|
|
|
|
final lastSyncString = prefs.getString(lastSyncTimestampKey);
|
|
if (lastSyncString != null) {
|
|
try {
|
|
_lastSyncTimestamp = DateTime.parse(lastSyncString);
|
|
} catch (e) {
|
|
debugPrint("Error parsing last sync timestamp: $e");
|
|
prefs.remove(lastSyncTimestampKey);
|
|
}
|
|
}
|
|
|
|
// 2. Set isLoading to false *before* scheduling heavy work.
|
|
_isLoading = false;
|
|
notifyListeners(); // Let the UI build
|
|
|
|
// 3. Schedule heavy database load *after* the first frame.
|
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
|
debugPrint("AuthProvider: First frame built. Starting background cache load...");
|
|
_isBackgroundLoading = true; // Indicate background activity
|
|
notifyListeners(); // Show a secondary loading indicator if needed
|
|
|
|
try {
|
|
// Call the original cache loading method here
|
|
await _loadDataFromCache();
|
|
debugPrint("AuthProvider: Background cache load complete.");
|
|
|
|
// After loading cache, check session status and potentially sync
|
|
if (_jwtToken != null) {
|
|
debugPrint('AuthProvider: Session loaded.');
|
|
await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded();
|
|
// Decide whether to call checkAndTransition or validateAndRefresh here
|
|
await checkAndTransitionToOnlineSession(); // Example: Check if transition needed
|
|
await validateAndRefreshSession(); // Example: Validate if already online
|
|
} else {
|
|
debugPrint('AuthProvider: No active session. App is in offline mode.');
|
|
}
|
|
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: Error during background cache load: $e");
|
|
// Handle error appropriately
|
|
} finally {
|
|
_isBackgroundLoading = false; // Background load finished
|
|
notifyListeners(); // Update UI
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Checks if the session is offline and attempts to transition to an online session by performing a silent re-login.
|
|
/// Returns true if a successful transition occurred.
|
|
Future<bool> checkAndTransitionToOnlineSession() async {
|
|
// Condition 1: Check connectivity
|
|
if (!(await isConnected())) {
|
|
debugPrint("AuthProvider: No internet connection. Skipping transition check.");
|
|
return false;
|
|
}
|
|
|
|
// Condition 2: Check if currently in an offline session state
|
|
final bool inOfflineSession = _jwtToken != null && _jwtToken!.startsWith("offline-session-");
|
|
if (!inOfflineSession) {
|
|
// Already online or logged out, no transition needed.
|
|
// --- START: FIX FOR DOUBLE SYNC ---
|
|
// Removed the redundant syncAllData call from here.
|
|
if(_jwtToken != null) {
|
|
debugPrint("AuthProvider: Session is already online. Skipping transition sync.");
|
|
// Consider calling validateAndRefreshSession() here instead if needed,
|
|
// but avoid a full syncAllData().
|
|
}
|
|
// --- END: FIX FOR DOUBLE SYNC ---
|
|
return false;
|
|
}
|
|
|
|
// Read password from secure storage
|
|
final String? password = await _secureStorage.read(key: _passwordStorageKey);
|
|
if (password == null || _userEmail == null) {
|
|
debugPrint("AuthProvider: In offline session, but no password in secure storage for auto-relogin. Manual login required.");
|
|
return false;
|
|
}
|
|
|
|
debugPrint("AuthProvider: Internet detected in offline session. Attempting silent re-login for $_userEmail...");
|
|
|
|
try {
|
|
final result = await _apiService.login(_userEmail!, password);
|
|
|
|
if (result['success'] == true) {
|
|
debugPrint("AuthProvider: Silent re-login successful. Transitioning to online session.");
|
|
final String token = result['data']['token'];
|
|
final Map<String, dynamic> profile = result['data']['profile'];
|
|
|
|
// Use existing login method to set up session and trigger sync.
|
|
await login(token, profile, password); // This call includes syncAllData
|
|
|
|
notifyListeners(); // Ensure UI updates after state change
|
|
return true;
|
|
} else {
|
|
// Silent login failed
|
|
debugPrint("AuthProvider: Silent re-login failed: ${result['message']}");
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: Silent re-login exception: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> validateAndRefreshSession() async {
|
|
if (!(await isConnected())) {
|
|
debugPrint('AuthProvider: No connection, skipping session validation.');
|
|
return;
|
|
}
|
|
|
|
if (_isSessionExpired) {
|
|
debugPrint('AuthProvider: Session is marked as expired, manual login required.');
|
|
return;
|
|
}
|
|
|
|
if (_jwtToken == null || _jwtToken!.startsWith("offline-session-")) {
|
|
return; // No online session to validate
|
|
}
|
|
|
|
try {
|
|
await _apiService.validateToken();
|
|
debugPrint('AuthProvider: Session token is valid.');
|
|
} on SessionExpiredException {
|
|
debugPrint('AuthProvider: Session validation failed (token expired). Attempting silent re-login.');
|
|
final bool reauthenticated = await attemptSilentRelogin();
|
|
|
|
if (!reauthenticated) {
|
|
debugPrint('AuthProvider: Silent re-login failed. Switching to session-expired offline mode.');
|
|
_isSessionExpired = true;
|
|
notifyListeners();
|
|
} else {
|
|
debugPrint('AuthProvider: Silent re-login successful. Session restored.');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('AuthProvider: An error occurred during session validation: $e');
|
|
}
|
|
}
|
|
|
|
Future<bool> attemptSilentRelogin() async {
|
|
if (!(await isConnected())) {
|
|
debugPrint("AuthProvider: No internet for silent relogin.");
|
|
return false;
|
|
}
|
|
|
|
final String? password = await _secureStorage.read(key: _passwordStorageKey);
|
|
if (password == null || _userEmail == null) {
|
|
debugPrint("AuthProvider: No cached credentials in secure storage for silent relogin.");
|
|
return false;
|
|
}
|
|
|
|
debugPrint("AuthProvider: Session may be expired. Attempting silent re-login for $_userEmail...");
|
|
try {
|
|
final result = await _apiService.login(_userEmail!, password);
|
|
if (result['success'] == true) {
|
|
debugPrint("AuthProvider: Silent re-login successful.");
|
|
final String token = result['data']['token'];
|
|
final Map<String, dynamic> profile = result['data']['profile'];
|
|
// Critical: Call the main login function to update token, profile, hash, etc.
|
|
// BUT prevent it from triggering another full sync immediately if called during syncAllData
|
|
await _updateSessionInternals(token, profile, password); // Use helper to avoid sync loop
|
|
_isSessionExpired = false;
|
|
notifyListeners();
|
|
return true;
|
|
} else {
|
|
debugPrint("AuthProvider: Silent re-login failed: ${result['message']}");
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: Silent re-login exception: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Helper to update session without triggering sync, used by attemptSilentRelogin
|
|
Future<void> _updateSessionInternals(String token, Map<String, dynamic> profile, String password) async {
|
|
_jwtToken = token;
|
|
_userEmail = profile['email'];
|
|
|
|
await _secureStorage.write(key: _passwordStorageKey, value: password);
|
|
|
|
final Map<String, dynamic> profileWithToken = Map.from(profile);
|
|
profileWithToken['token'] = token;
|
|
_profileData = profileWithToken;
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(tokenKey, token);
|
|
await prefs.setString(userEmailKey, _userEmail!);
|
|
await prefs.setString(profileDataKey, jsonEncode(_profileData));
|
|
await prefs.setString(lastOnlineLoginKey, DateTime.now().toIso8601String()); // Update last online time
|
|
await _dbHelper.saveProfile(_profileData!);
|
|
|
|
try {
|
|
debugPrint("AuthProvider: (Re-login) Hashing and caching password for offline login.");
|
|
final String hashedPassword = await compute(hashPassword, password);
|
|
await _dbHelper.upsertUserWithCredentials(
|
|
profile: profile,
|
|
passwordHash: hashedPassword,
|
|
);
|
|
debugPrint("AuthProvider: (Re-login) Credentials cached successfully.");
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: (Re-login) Failed to cache password hash: $e");
|
|
}
|
|
// DO NOT call syncAllData here to prevent loops when called from syncAllData's catch block.
|
|
}
|
|
|
|
|
|
Future<void> syncAllData({bool forceRefresh = false}) async {
|
|
if (!(await isConnected())) {
|
|
debugPrint("AuthProvider: Device is OFFLINE. Skipping sync.");
|
|
return;
|
|
}
|
|
|
|
if (_isSessionExpired) {
|
|
debugPrint("AuthProvider: Skipping sync, session is expired. Manual login required.");
|
|
throw Exception('Session expired. Please log in again to sync.');
|
|
}
|
|
|
|
if (_jwtToken == null || _jwtToken!.startsWith("offline-session-")) {
|
|
debugPrint("AuthProvider: Skipping sync, session is offline or null.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
debugPrint("AuthProvider: Device is ONLINE. Starting delta sync.");
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final String? lastSync = forceRefresh ? null : prefs.getString(lastSyncTimestampKey);
|
|
|
|
final result = await _apiService.syncAllData(lastSyncTimestamp: lastSync);
|
|
|
|
if (result['success']) {
|
|
debugPrint("AuthProvider: Delta sync successful.");
|
|
final newSyncTimestamp = DateTime.now().toUtc().toIso8601String();
|
|
await prefs.setString(lastSyncTimestampKey, newSyncTimestamp);
|
|
_lastSyncTimestamp = DateTime.parse(newSyncTimestamp);
|
|
|
|
if (_isFirstLogin) {
|
|
await setIsFirstLogin(false);
|
|
debugPrint("AuthProvider: First successful sync complete. isFirstLogin flag set to false.");
|
|
}
|
|
|
|
await _loadDataFromCache(); // Reload data after successful sync
|
|
notifyListeners();
|
|
} else {
|
|
debugPrint("AuthProvider: Delta sync failed logically. Message: ${result['message']}");
|
|
throw Exception('Data sync failed. Please check the logs.');
|
|
}
|
|
} on SessionExpiredException {
|
|
debugPrint("AuthProvider: Session expired during sync. Attempting silent re-login...");
|
|
final bool reauthenticated = await attemptSilentRelogin();
|
|
if (reauthenticated) {
|
|
debugPrint("AuthProvider: Re-login successful. Retrying sync...");
|
|
await syncAllData(forceRefresh: forceRefresh); // Retry the sync
|
|
} else {
|
|
debugPrint("AuthProvider: Re-login failed after session expired during sync. Switching to offline mode.");
|
|
_isSessionExpired = true;
|
|
notifyListeners();
|
|
throw Exception('Session expired. App is now in offline mode.');
|
|
}
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: A general error occurred during sync: $e");
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<void> syncRegistrationData() async {
|
|
if (!(await isConnected())) {
|
|
debugPrint("AuthProvider: Device is OFFLINE. Skipping registration data sync.");
|
|
return;
|
|
}
|
|
|
|
debugPrint("AuthProvider: Fetching data for registration screen...");
|
|
final result = await _apiService.syncRegistrationData();
|
|
|
|
if (result['success']) {
|
|
await _loadDataFromCache();
|
|
notifyListeners();
|
|
debugPrint("AuthProvider: Registration data loaded and UI notified.");
|
|
} else {
|
|
debugPrint("AuthProvider: Registration data sync failed.");
|
|
}
|
|
}
|
|
|
|
Future<void> refreshProfile() async {
|
|
if (!(await isConnected())) {
|
|
debugPrint("AuthProvider: Device is OFFLINE. Skipping profile refresh.");
|
|
return;
|
|
}
|
|
if (_isSessionExpired || _jwtToken == null || _jwtToken!.startsWith("offline-session-")) {
|
|
debugPrint("AuthProvider: Skipping profile refresh, session is offline, expired, or null.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final result = await _apiService.refreshProfile();
|
|
if (result['success']) {
|
|
await setProfileData(result['data']);
|
|
}
|
|
} on SessionExpiredException {
|
|
debugPrint("AuthProvider: Session expired during profile refresh. Attempting silent re-login...");
|
|
await attemptSilentRelogin(); // Attempt re-login but don't retry refresh automatically here
|
|
} catch (e) {
|
|
debugPrint("AuthProvider: Error during profile refresh: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> proactiveTokenRefresh() async {
|
|
if (!(await isConnected())) {
|
|
debugPrint('AuthProvider: No connection, skipping proactive token refresh.');
|
|
return;
|
|
}
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final lastOnlineLoginString = prefs.getString(lastOnlineLoginKey);
|
|
|
|
if (lastOnlineLoginString == null) {
|
|
debugPrint('AuthProvider: No last online login timestamp found, skipping proactive refresh.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final lastOnlineLogin = DateTime.parse(lastOnlineLoginString);
|
|
if (DateTime.now().difference(lastOnlineLogin).inHours >= 24) {
|
|
debugPrint('AuthProvider: Session is older than 24 hours. Attempting proactive silent re-login.');
|
|
await attemptSilentRelogin();
|
|
} else {
|
|
debugPrint('AuthProvider: Session is fresh (< 24 hours old). No proactive refresh needed.');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('AuthProvider: Error during proactive token refresh check: $e');
|
|
}
|
|
}
|
|
|
|
// This method performs the actual DB reads
|
|
Future<void> _loadDataFromCache() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final profileJson = prefs.getString(profileDataKey);
|
|
if (profileJson != null && _profileData == null) {
|
|
try { _profileData = jsonDecode(profileJson); } catch(e) { /*...*/ }
|
|
}
|
|
_profileData ??= await _dbHelper.loadProfile();
|
|
|
|
_allUsers = await _dbHelper.loadUsers();
|
|
_tarballStations = await _dbHelper.loadTarballStations();
|
|
_manualStations = await _dbHelper.loadManualStations();
|
|
_tarballClassifications = await _dbHelper.loadTarballClassifications();
|
|
_riverManualStations = await _dbHelper.loadRiverManualStations();
|
|
_riverTriennialStations = await _dbHelper.loadRiverTriennialStations();
|
|
// --- MODIFIED: Load River Investigative Stations ---
|
|
_riverInvestigativeStations = await _dbHelper.loadRiverInvestigativeStations();
|
|
// --- END MODIFIED ---
|
|
_departments = await _dbHelper.loadDepartments();
|
|
_companies = await _dbHelper.loadCompanies();
|
|
_positions = await _dbHelper.loadPositions();
|
|
_airClients = await _dbHelper.loadAirClients();
|
|
_airManualStations = await _dbHelper.loadAirManualStations();
|
|
_states = await _dbHelper.loadStates();
|
|
_appSettings = await _dbHelper.loadAppSettings();
|
|
_npeParameterLimits = await _dbHelper.loadNpeParameterLimits();
|
|
_marineParameterLimits = await _dbHelper.loadMarineParameterLimits();
|
|
_riverParameterLimits = await _dbHelper.loadRiverParameterLimits();
|
|
_documents = await _dbHelper.loadDocuments();
|
|
_apiConfigs = await _dbHelper.loadApiConfigs();
|
|
_ftpConfigs = await _dbHelper.loadFtpConfigs();
|
|
_pendingRetries = await _retryService.getPendingTasks(); // Use service here is okay
|
|
debugPrint("AuthProvider: All master data loaded from local DB cache (background/sync).");
|
|
}
|
|
|
|
|
|
Future<void> refreshPendingTasks() async {
|
|
_pendingRetries = await _retryService.getPendingTasks();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> login(String token, Map<String, dynamic> profile, String password) async {
|
|
// Call the internal helper first
|
|
await _updateSessionInternals(token, profile, password);
|
|
|
|
// Now proceed with post-login actions that *don't* belong in the helper
|
|
debugPrint('AuthProvider: Login successful. Session and profile persisted.');
|
|
// --- THIS IS THE LINE THAT FIXES THE TICKING ---
|
|
await _userPreferencesService.applyAndSaveDefaultPreferencesIfNeeded();
|
|
// ---
|
|
// The main sync triggered by a direct user login
|
|
await syncAllData(forceRefresh: true);
|
|
// Notify listeners *after* sync is attempted (or throws)
|
|
notifyListeners();
|
|
}
|
|
|
|
|
|
Future<bool> loginOffline(String email, String password) async {
|
|
debugPrint("AuthProvider: Attempting offline login for user $email.");
|
|
|
|
try {
|
|
final String? storedHash = await _dbHelper.getUserPasswordHashByEmail(email);
|
|
|
|
if (storedHash == null || storedHash.isEmpty) {
|
|
debugPrint("AuthProvider DEBUG: Offline login failed for $email because NO HASH was found in local storage.");
|
|
return false;
|
|
}
|
|
|
|
debugPrint("AuthProvider: Verifying password against stored hash...");
|
|
final bool passwordMatches = await compute(verifyPassword, CheckPasswordParams(password, storedHash));
|
|
|
|
if (passwordMatches) {
|
|
debugPrint("AuthProvider: Offline password verification successful.");
|
|
final Map<String, dynamic>? cachedProfile = await _dbHelper.loadProfileByEmail(email);
|
|
if (cachedProfile == null) {
|
|
debugPrint("AuthProvider DEBUG: Offline login failed because profile data was missing, even though password matched.");
|
|
return false;
|
|
}
|
|
|
|
_jwtToken = "offline-session-${DateTime.now().millisecondsSinceEpoch}";
|
|
_userEmail = email;
|
|
|
|
await _secureStorage.write(key: _passwordStorageKey, value: password);
|
|
|
|
final Map<String, dynamic> profileWithToken = Map.from(cachedProfile);
|
|
profileWithToken['token'] = _jwtToken;
|
|
_profileData = profileWithToken;
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(tokenKey, _jwtToken!);
|
|
await prefs.setString(userEmailKey, _userEmail!);
|
|
await prefs.setString(profileDataKey, jsonEncode(_profileData));
|
|
|
|
// Load cache data immediately after offline login succeeds
|
|
// This doesn't need the post-frame callback as it's triggered by user action
|
|
await _loadDataFromCache();
|
|
notifyListeners();
|
|
return true;
|
|
} else {
|
|
debugPrint("AuthProvider DEBUG: Offline login failed because password did not match stored hash (Hash Mismatch).");
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
debugPrint("AuthProvider DEBUG: An unexpected error occurred during offline login process: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> setProfileData(Map<String, dynamic> data) async {
|
|
final String? existingToken = _profileData?['token'];
|
|
_profileData = data;
|
|
if (_profileData != null && existingToken != null) {
|
|
_profileData!['token'] = existingToken;
|
|
}
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(profileDataKey, jsonEncode(_profileData));
|
|
await _dbHelper.saveProfile(_profileData!);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> setIsFirstLogin(bool value) async {
|
|
_isFirstLogin = value;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(isFirstLoginKey, value);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> logout() async {
|
|
debugPrint('AuthProvider: Initiating logout...');
|
|
_jwtToken = null;
|
|
_userEmail = null;
|
|
_profileData = null;
|
|
_lastSyncTimestamp = null;
|
|
_isFirstLogin = true;
|
|
_isSessionExpired = false;
|
|
|
|
await _secureStorage.delete(key: _passwordStorageKey);
|
|
|
|
// Clear cached data
|
|
_allUsers = null;
|
|
_tarballStations = null;
|
|
_manualStations = null;
|
|
_tarballClassifications = null;
|
|
_riverManualStations = null;
|
|
_riverTriennialStations = null;
|
|
// --- MODIFIED: Clear River Investigative Stations ---
|
|
_riverInvestigativeStations = null;
|
|
// --- END MODIFIED ---
|
|
_departments = null;
|
|
_companies = null;
|
|
_positions = null;
|
|
_airClients = null;
|
|
_airManualStations = null;
|
|
_states = null;
|
|
_appSettings = null;
|
|
_npeParameterLimits = null;
|
|
_marineParameterLimits = null;
|
|
_riverParameterLimits = null;
|
|
_documents = null;
|
|
_apiConfigs = null;
|
|
_ftpConfigs = null;
|
|
_pendingRetries = null;
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(tokenKey);
|
|
await prefs.remove(userEmailKey);
|
|
await prefs.remove(profileDataKey);
|
|
await prefs.remove(lastSyncTimestampKey);
|
|
await prefs.remove(lastOnlineLoginKey);
|
|
await prefs.remove('default_preferences_saved'); // Also clear user prefs flag
|
|
await prefs.setBool(isFirstLoginKey, true);
|
|
|
|
debugPrint('AuthProvider: All session and cached data cleared.');
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<Map<String, dynamic>> resetPassword(String email) {
|
|
// Assuming _apiService has a method for this, otherwise implement it.
|
|
// This looks correct based on your previous code structure.
|
|
return _apiService.post('auth/forgot-password', {'email': email});
|
|
}
|
|
}
|
|
|
|
// These remain unchanged as they are used by compute for password hashing/checking
|
|
class CheckPasswordParams {
|
|
final String password;
|
|
final String hash;
|
|
CheckPasswordParams(this.password, this.hash);
|
|
}
|
|
|
|
String hashPassword(String password) {
|
|
return BCrypt.hashpw(password, BCrypt.gensalt());
|
|
}
|
|
|
|
bool verifyPassword(CheckPasswordParams params) {
|
|
return BCrypt.checkpw(params.password, params.hash);
|
|
} |