diff --git a/lib/controller/permission_controller.dart b/lib/controller/permission_controller.dart index 4db0cab..8442cb7 100644 --- a/lib/controller/permission_controller.dart +++ b/lib/controller/permission_controller.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/helpers/services/permission_service.dart'; +import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/model/user_permission.dart'; import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:on_field_work/model/projects_model.dart'; @@ -51,7 +51,7 @@ class PermissionController extends GetxController { Future loadData(String token) async { try { isLoading.value = true; - final userData = await PermissionService.fetchAllUserData(token); + final userData = await AuthService.fetchAllUserData(token); _updateState(userData); await _storeData(); logSafe("Data loaded and state updated successfully."); diff --git a/lib/controller/tenant/tenant_selection_controller.dart b/lib/controller/tenant/tenant_selection_controller.dart index c5074be..3dd88a3 100644 --- a/lib/controller/tenant/tenant_selection_controller.dart +++ b/lib/controller/tenant/tenant_selection_controller.dart @@ -1,13 +1,12 @@ import 'package:get/get.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/helpers/services/tenant_service.dart'; +import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/model/tenant/tenant_list_model.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/controller/permission_controller.dart'; class TenantSelectionController extends GetxController { - final TenantService _tenantService = TenantService(); // Tenant list final tenants = [].obs; @@ -32,7 +31,7 @@ class TenantSelectionController extends GetxController { isLoading.value = true; isAutoSelecting.value = true; // show splash during auto-selection try { - final data = await _tenantService.getTenants(); + final data = await AuthService.getTenants(); if (data == null || data.isEmpty) { tenants.clear(); logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning); @@ -87,7 +86,7 @@ class TenantSelectionController extends GetxController { try { isLoading.value = true; - final success = await _tenantService.selectTenant(tenantId); + final success = await AuthService.selectTenant(tenantId); if (!success) { showAppSnackbar( title: "Error", @@ -99,7 +98,7 @@ class TenantSelectionController extends GetxController { // Update tenant & persist final selectedTenant = tenants.firstWhere((t) => t.id == tenantId); - TenantService.setSelectedTenant(selectedTenant); + AuthService.setSelectedTenant(selectedTenant); selectedTenantId.value = tenantId; await LocalStorage.setRecentTenantId(tenantId); @@ -131,6 +130,6 @@ class TenantSelectionController extends GetxController { /// Clear tenant selection void _clearSelection() { selectedTenantId.value = null; - TenantService.currentTenant = null; + AuthService.currentTenant = null; } } diff --git a/lib/controller/tenant/tenant_switch_controller.dart b/lib/controller/tenant/tenant_switch_controller.dart index 1ecaa8a..4ae0f86 100644 --- a/lib/controller/tenant/tenant_switch_controller.dart +++ b/lib/controller/tenant/tenant_switch_controller.dart @@ -1,13 +1,12 @@ import 'package:get/get.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/helpers/services/tenant_service.dart'; +import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/model/tenant/tenant_list_model.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/controller/permission_controller.dart'; class TenantSwitchController extends GetxController { - final TenantService _tenantService = TenantService(); final tenants = [].obs; final isLoading = false.obs; @@ -23,7 +22,7 @@ class TenantSwitchController extends GetxController { Future loadTenants() async { isLoading.value = true; try { - final data = await _tenantService.getTenants(); + final data = await AuthService.getTenants(); if (data == null || data.isEmpty) { tenants.clear(); logSafe("⚠️ No tenants available for switching.", level: LogLevel.warning); @@ -33,7 +32,7 @@ class TenantSwitchController extends GetxController { tenants.value = data.map((e) => Tenant.fromJson(e)).toList(); // Keep current tenant as selected - selectedTenantId.value = TenantService.currentTenant?.id; + selectedTenantId.value = AuthService.currentTenant?.id; } catch (e, st) { logSafe("❌ Exception in loadTenants", level: LogLevel.error, error: e, stackTrace: st); showAppSnackbar( @@ -48,11 +47,11 @@ class TenantSwitchController extends GetxController { /// Switch to a different tenant and navigate fully Future switchTenant(String tenantId) async { - if (TenantService.currentTenant?.id == tenantId) return; + if (AuthService.currentTenant?.id == tenantId) return; isLoading.value = true; try { - final success = await _tenantService.selectTenant(tenantId); + final success = await AuthService.selectTenant(tenantId); if (!success) { logSafe("❌ Tenant switch failed: $tenantId", level: LogLevel.warning); showAppSnackbar( @@ -64,7 +63,7 @@ class TenantSwitchController extends GetxController { } final selectedTenant = tenants.firstWhere((t) => t.id == tenantId); - TenantService.setSelectedTenant(selectedTenant); + AuthService.setSelectedTenant(selectedTenant); selectedTenantId.value = tenantId; // Persist recent tenant diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index 04520e1..36777b9 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -1,25 +1,47 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:get/get.dart'; +import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/utils/encryption_helper.dart'; +import 'package:on_field_work/model/tenant/tenant_list_model.dart'; +import 'package:on_field_work/model/user_permission.dart'; +import 'package:on_field_work/model/employees/employee_info.dart'; +import 'package:on_field_work/model/projects_model.dart'; + +// Enum for standardizing HTTP methods within the service +enum _HttpMethod { get, post } class AuthService { static const String _baseUrl = ApiEndpoints.baseUrl; - static const Map _headers = { + static const Map _defaultHeaders = { 'Content-Type': 'application/json', }; + // AuthService properties static bool isLoggedIn = false; -/* -------------------------------------------------------------------------- / -/ Logout API / -/ -------------------------------------------------------------------------- */ + // TenantService properties + static Tenant? currentTenant; + + // PermissionService properties + static final Map> _userDataCache = {}; + +/* -------------------------------------------------------------------------- */ +/* AUTH METHODS                                */ +/* -------------------------------------------------------------------------- */ + + /// Logs the user out by calling the logout API. static Future logoutApi(String refreshToken, String fcmToken) async { try { final body = {"refreshToken": refreshToken, "fcmToken": fcmToken}; - final response = await _post("/auth/logout", body); + final response = await _networkRequest( + path: "/auth/logout", + method: _HttpMethod.post, + body: body, + ); if (response != null && response['statusCode'] == 200) { logSafe("✅ Logout API successful"); @@ -35,10 +57,7 @@ class AuthService { } } -/* -------------------------------------------------------------------------- / -/ Public Methods / -/ -------------------------------------------------------------------------- */ - + /// Registers or updates the Firebase Cloud Messaging token. static Future registerDeviceToken(String fcmToken) async { final token = await LocalStorage.getJwtToken(); if (token == null || token.isEmpty) { @@ -48,23 +67,36 @@ class AuthService { } final body = {"fcmToken": fcmToken}; - final data = await _post("/auth/set/device-token", body, authToken: token); + final response = await _networkRequest( + path: "/auth/set/device-token", + method: _HttpMethod.post, + body: body, + authToken: token, + ); - if (data != null && data['success'] == true) { + if (response != null && response['success'] == true) { logSafe("✅ Device token registered successfully."); return true; } - logSafe("⚠️ Failed to register device token: ${data?['message']}", + logSafe("⚠️ Failed to register device token: ${response?['message']}", level: LogLevel.warning); return false; } + /// Handles user login with email/password. + /// Returns error map on failure, or null on success. static Future?> loginUser( Map data) async { logSafe("Attempting login..."); - final responseData = await _post("/auth/app/login", data); - if (responseData == null) + final responseData = await _networkRequest( + path: "/auth/app/login", + method: _HttpMethod.post, + body: data, + ); + + if (responseData == null) { return {"error": "Network error. Please check your connection."}; + } if (responseData['data'] != null) { await _handleLoginSuccess(responseData['data']); @@ -76,9 +108,10 @@ class AuthService { return {"error": responseData['message'] ?? "Unexpected error occurred"}; } + /// Refreshes the JWT access token using the refresh token. static Future refreshToken() async { - final accessToken = LocalStorage.getJwtToken(); - final refreshToken = LocalStorage.getRefreshToken(); + final accessToken = await LocalStorage.getJwtToken(); + final refreshToken = await LocalStorage.getRefreshToken(); if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) { logSafe("Missing access or refresh token.", level: LogLevel.warning); @@ -86,14 +119,19 @@ class AuthService { } final body = {"token": accessToken, "refreshToken": refreshToken}; - final data = await _post("/auth/refresh-token", body); - if (data != null && data['success'] == true) { + final data = await _networkRequest( + path: "/auth/refresh-token", + method: _HttpMethod.post, + body: body, + ); + + if (data != null && data['success'] == true && data['data'] != null) { await LocalStorage.setJwtToken(data['data']['token']); await LocalStorage.setRefreshToken(data['data']['refreshToken']); await LocalStorage.setLoggedInUser(true); logSafe("Token refreshed successfully."); - final newFcmToken = LocalStorage.getFcmToken(); + final newFcmToken = await LocalStorage.getFcmToken(); if (newFcmToken?.isNotEmpty ?? false) { await registerDeviceToken(newFcmToken!); } @@ -104,35 +142,29 @@ class AuthService { return false; } + /// Initiates the forgot password process. static Future?> forgotPassword(String email) => - _wrapErrorHandling(() => _post("/auth/forgot-password", {"email": email}), + _wrapErrorHandling( + () => _networkRequest( + path: "/auth/forgot-password", + method: _HttpMethod.post, + body: {"email": email}, + ), successCondition: (data) => data['success'] == true, defaultError: "Failed to send reset link."); - static Future?> requestDemo( - Map demoData) => - _wrapErrorHandling(() => _post("/market/inquiry", demoData), - successCondition: (data) => data['success'] == true, - defaultError: "Failed to submit demo request."); - - static Future>?> getIndustries() async { - final data = await _get("/market/industries"); - if (data != null && data['success'] == true) { - return List>.from(data['data']); - } - return null; - } - + /// Generates an MPIN for the user. static Future?> generateMpin({ required String employeeId, required String mpin, }) => _wrapErrorHandling( () async { - final token = LocalStorage.getJwtToken(); - return _post( - "/auth/generate-mpin", - {"employeeId": employeeId, "mpin": mpin}, + final token = await LocalStorage.getJwtToken(); + return _networkRequest( + path: "/auth/generate-mpin", + method: _HttpMethod.post, + body: {"employeeId": employeeId, "mpin": mpin}, authToken: token, ); }, @@ -140,6 +172,7 @@ class AuthService { defaultError: "Failed to generate MPIN.", ); + /// Verifies the MPIN for quick login. static Future?> verifyMpin({ required String mpin, required String mpinToken, @@ -147,12 +180,14 @@ class AuthService { }) => _wrapErrorHandling( () async { - final employeeInfo = LocalStorage.getEmployeeInfo(); - if (employeeInfo == null) return null; + final employeeInfo = await LocalStorage.getEmployeeInfo(); + if (employeeInfo == null) return null; // Fails immediately if info is missing final token = await LocalStorage.getJwtToken(); - return _post( - "/auth/login-mpin", - { + + final responseData = await _networkRequest( + path: "/auth/login-mpin", + method: _HttpMethod.post, + body: { "employeeId": employeeInfo.id, "mpin": mpin, "mpinToken": mpinToken, @@ -160,21 +195,41 @@ class AuthService { }, authToken: token, ); + + // Handle token updates from MPIN login success if necessary, + // though typically refresh or a separate login handles this. + if (responseData?['data'] != null) { + await _handleLoginSuccess(responseData!['data']); + } + + return responseData; }, successCondition: (data) => data['success'] == true, defaultError: "MPIN verification failed.", ); + /// Generates an OTP for login/verification. static Future?> generateOtp(String email) => - _wrapErrorHandling(() => _post("/auth/send-otp", {"email": email}), + _wrapErrorHandling( + () => _networkRequest( + path: "/auth/send-otp", + method: _HttpMethod.post, + body: {"email": email}, + ), successCondition: (data) => data['success'] == true, defaultError: "Failed to generate OTP."); + /// Verifies the OTP and completes the login process. static Future?> verifyOtp({ required String email, required String otp, }) async { - final data = await _post("/auth/login-otp", {"email": email, "otp": otp}); + final data = await _networkRequest( + path: "/auth/login-otp", + method: _HttpMethod.post, + body: {"email": email, "otp": otp}, + ); + if (data != null && data['data'] != null) { await _handleLoginSuccess(data['data']); return null; @@ -182,61 +237,297 @@ class AuthService { return {"error": data?['message'] ?? "OTP verification failed."}; } - /* -------------------------------------------------------------------------- */ - /* Private Utilities */ - /* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* MARKET/OTHER METHODS                          */ +/* -------------------------------------------------------------------------- */ - static Future?> _post( - String path, - Map body, { + /// Submits a demo request to the market endpoint. + static Future?> requestDemo( + Map demoData) => + _wrapErrorHandling( + () => _networkRequest( + path: "/market/inquiry", + method: _HttpMethod.post, + body: demoData, + ), + successCondition: (data) => data['success'] == true, + defaultError: "Failed to submit demo request."); + + /// Fetches the list of available industries. + static Future>?> getIndustries() async { + final data = await _networkRequest( + path: "/market/industries", + method: _HttpMethod.get, + ); + if (data != null && data['success'] == true && data['data'] is List) { + return List>.from(data['data']); + } + return null; + } + +/* -------------------------------------------------------------------------- */ +/* TENANT METHODS                                */ +/* -------------------------------------------------------------------------- */ + + static void setSelectedTenant(Tenant tenant) { + currentTenant = tenant; + } + + static bool get isTenantSelected => currentTenant != null; + + /// Fetches the list of tenants the user belongs to. + static Future>?> getTenants( + {bool hasRetried = false}) async { + final token = await LocalStorage.getJwtToken(); + if (token == null) { + await _handleUnauthorized(); + return null; + } + + final data = await _networkRequest( + path: "/auth/get/user/tenants", + method: _HttpMethod.get, + authToken: token, + ); + + if (data != null && data['success'] == true && data['data'] is List) { + return List>.from(data['data']); + } + + // Handle 401 Unauthorized via refreshToken/retry logic + if (data?['statusCode'] == 401 && !hasRetried) { + final refreshed = await refreshToken(); + if (refreshed) return getTenants(hasRetried: true); + } + + // Fallback on all other failures + if (data != null && data['statusCode'] != 401) { + _handleApiError( + data['statusCode'], data, "Fetching tenants"); + } else if (data?['statusCode'] == 401 && hasRetried) { + await _handleUnauthorized(); + } + return null; + } + + /// Selects a specific tenant, updating the JWT and refresh tokens. + static Future selectTenant(String tenantId, + {bool hasRetried = false}) async { + final token = await LocalStorage.getJwtToken(); + if (token == null) { + await _handleUnauthorized(); + return false; + } + + final data = await _networkRequest( + path: "/auth/select-tenant/$tenantId", + method: _HttpMethod.post, + authToken: token, + ); + + if (data != null && data['success'] == true && data['data'] != null) { + await LocalStorage.setJwtToken(data['data']['token']); + await LocalStorage.setRefreshToken(data['data']['refreshToken']); + logSafe("✅ Tenant selected successfully. Tokens updated."); + + // Refresh project controller data + try { + final projectController = Get.find(); + projectController.clearProjects(); + projectController.fetchProjects(); + } catch (_) { + logSafe("⚠️ ProjectController not found while refreshing projects"); + } + + // Re-register FCM token with new tenant context + final fcmToken = await LocalStorage.getFcmToken(); + if (fcmToken?.isNotEmpty ?? false) { + await registerDeviceToken(fcmToken!); + } + + return true; + } + + // Handle 401 Unauthorized via refreshToken/retry logic + if (data?['statusCode'] == 401 && !hasRetried) { + final refreshed = await refreshToken(); + if (refreshed) return selectTenant(tenantId, hasRetried: true); + await _handleUnauthorized(); + } + + // Fallback on all other failures + if (data != null) { + _handleApiError(data['statusCode'], data, "Selecting tenant"); + } + return false; + } + +/* -------------------------------------------------------------------------- */ +/* PERMISSION/USER METHODS                        */ +/* -------------------------------------------------------------------------- */ + + /// Fetches all user-related data (permissions, employee info, projects). + static Future> fetchAllUserData( + String token, { + bool hasRetried = false, + }) async { + logSafe("Fetching user data..."); + + final cached = _userDataCache[token]; + if (cached != null) { + logSafe("User data cache hit."); + return cached; + } + + final data = await _networkRequest( + path: "/user/profile", + method: _HttpMethod.get, + authToken: token, + ); + + if (data != null && data['success'] == true && data['data'] is Map) { + final responseData = data['data'] as Map; + + final result = { + 'permissions': _parsePermissions(responseData['featurePermissions']), + 'employeeInfo': await _parseEmployeeInfo(responseData['employeeInfo']), + 'projects': _parseProjectsInfo(responseData['projects']), + }; + + _userDataCache[token] = result; + logSafe("User data fetched and decrypted successfully."); + return result; + } + + // Handle 401 Unauthorized via refreshToken/retry logic + if (data?['statusCode'] == 401 && !hasRetried) { + final refreshed = await refreshToken(); + final newToken = await LocalStorage.getJwtToken(); + if (refreshed && newToken != null) { + return fetchAllUserData(newToken, hasRetried: true); + } + } + + // Handle failure and unauthorized + if (data?['statusCode'] == 401 || data?['statusCode'] == 403 || data == null) { + await _handleUnauthorized(); + throw Exception('Unauthorized or Network Error. Token refresh failed.'); + } + + final errorMsg = data['message'] ?? 'Unknown error'; + logSafe("Failed to fetch user data: $errorMsg", level: LogLevel.warning); + throw Exception('Failed to fetch user data: $errorMsg'); + } + +/* -------------------------------------------------------------------------- */ +/* Private Utilities                              */ +/* -------------------------------------------------------------------------- */ + + /// Global handler for unauthorized access, clears tokens and redirects. + static Future _handleUnauthorized() async { + logSafe( + "Clearing tokens and redirecting to login due to unauthorized access.", + level: LogLevel.warning); + await LocalStorage.removeToken('jwt_token'); + await LocalStorage.removeToken('refresh_token'); + await LocalStorage.setLoggedInUser(false); + isLoggedIn = false; + Get.offAllNamed('/auth/login-option'); + } + + /// Parses raw permission list into a list of UserPermission models. + static List _parsePermissions(List? permissions) { + logSafe("Parsing user permissions..."); + if (permissions == null) return []; + return permissions + .map((perm) => UserPermission.fromJson({'id': perm})) + .toList(); + } + + /// Parses raw employee info, stores it locally, and returns the model. + static Future _parseEmployeeInfo( + Map? data) async { + logSafe("Parsing employee info..."); + if (data == null) throw Exception("Employee data missing"); + final employeeInfo = EmployeeInfo.fromJson(data); + await LocalStorage.setEmployeeInfo(employeeInfo); + return employeeInfo; + } + + /// Parses raw projects list into a list of ProjectInfo models. + static List _parseProjectsInfo(List? projects) { + logSafe("Parsing projects info..."); + if (projects == null) return []; + return projects.map((proj) => ProjectInfo.fromJson(proj)).toList(); + } + + /// Internal utility to report API errors. + static void _handleApiError( + int statusCode, Map data, String context) { + final message = data['message'] ?? 'Unknown error'; + final level = statusCode >= 500 ? LogLevel.error : LogLevel.warning; + logSafe("❌ $context failed: $message [Status: $statusCode]", level: level); + } + + + /// General network request handler for both GET and POST. + static Future?> _networkRequest({ + required String path, + required _HttpMethod method, + Map? body, String? authToken, }) async { - try { - final headers = { - ..._headers, - if (authToken?.isNotEmpty ?? false) - 'Authorization': 'Bearer $authToken', - }; - final response = await http.post(Uri.parse("$_baseUrl$path"), - headers: headers, body: jsonEncode(body)); + final uri = Uri.parse("$_baseUrl$path"); + final headers = { + ..._defaultHeaders, + if (authToken?.isNotEmpty ?? false) 'Authorization': 'Bearer $authToken', + }; - final decrypted = decryptResponse(response.body); // <-- Decrypt here - if (decrypted is Map) { - return {"statusCode": response.statusCode, ...decrypted}; - } else { - return {"statusCode": response.statusCode, "data": decrypted}; + http.Response? response; + try { + logSafe( + "➡️ ${method.name.toUpperCase()} $_baseUrl$path${body != null ? '\nBody: ${jsonEncode(body)}' : ''}", + level: LogLevel.info); + + if (method == _HttpMethod.post) { + response = await http.post(uri, headers: headers, body: jsonEncode(body)); + } else { // GET + response = await http.get(uri, headers: headers); } + + if (response.body.isEmpty || response.body.trim().isEmpty) { + logSafe("❌ Empty response for $path", level: LogLevel.error); + // Special case for unauthorized response with no body (e.g., gateway issue) + if (response.statusCode == 401) { + await _handleUnauthorized(); + } + return {"statusCode": response.statusCode, "success": false, "message": "Empty response body"}; + } + + final decrypted = decryptResponse(response.body); + + if (decrypted == null) { + logSafe("❌ Response decryption failed for $path", level: LogLevel.error); + return {"statusCode": response.statusCode, "success": false, "message": "Failed to decrypt response"}; + } + + final Map result = decrypted is Map + ? decrypted + : {"data": decrypted}; // Wrap non-map responses + + logSafe( + "⬅️ Response: ${jsonEncode(result)} [Status: ${response.statusCode}]", + level: LogLevel.info); + + return {"statusCode": response.statusCode, ...result}; + } catch (e, st) { - _handleError("$path POST error", e, st); - return null; - } - } - - static Future?> _get( - String path, { - String? authToken, - }) async { - try { - final headers = { - ..._headers, - if (authToken?.isNotEmpty ?? false) - 'Authorization': 'Bearer $authToken', - }; - final response = - await http.get(Uri.parse("$_baseUrl$path"), headers: headers); - - final decrypted = decryptResponse(response.body); // <-- Decrypt here - if (decrypted is Map) { - return {"statusCode": response.statusCode, ...decrypted}; - } else { - return {"statusCode": response.statusCode, "data": decrypted}; - } - } catch (e, st) { - _handleError("$path GET error", e, st); + _handleError("$path ${method.name.toUpperCase()} error", e, st); return null; } } + /// Utility to wrap simple API calls with error-to-UI message mapping. static Future?> _wrapErrorHandling( Future?> Function() request, { required bool Function(Map data) successCondition, @@ -247,10 +538,12 @@ class AuthService { return {"error": data?['message'] ?? defaultError}; } + /// Generic error logging helper. static void _handleError(String message, Object error, StackTrace st) { logSafe(message, level: LogLevel.error, error: error, stackTrace: st); } + /// Common logic for storing tokens and login state upon successful authentication. static Future _handleLoginSuccess(Map data) async { await LocalStorage.setJwtToken(data['token']); await LocalStorage.setLoggedInUser(true); @@ -268,4 +561,4 @@ class AuthService { } isLoggedIn = true; } -} +} \ No newline at end of file diff --git a/lib/helpers/services/permission_service.dart b/lib/helpers/services/permission_service.dart deleted file mode 100644 index bab696b..0000000 --- a/lib/helpers/services/permission_service.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:get/get.dart'; -import 'package:http/http.dart' as http; - -import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/model/user_permission.dart'; -import 'package:on_field_work/model/employees/employee_info.dart'; -import 'package:on_field_work/model/projects_model.dart'; -import 'package:on_field_work/helpers/services/storage/local_storage.dart'; -import 'package:on_field_work/helpers/services/auth_service.dart'; -import 'package:on_field_work/helpers/services/api_endpoints.dart'; -import 'package:on_field_work/helpers/utils/encryption_helper.dart'; - -class PermissionService { -static final Map> _userDataCache = {}; -static const String _baseUrl = ApiEndpoints.baseUrl; - -static Future> fetchAllUserData( -String token, { -bool hasRetried = false, -}) async { -logSafe("Fetching user data..."); - -final cached = _userDataCache[token]; -if (cached != null) { - logSafe("User data cache hit."); - return cached; -} - -final uri = Uri.parse("$_baseUrl/user/profile"); -final headers = {'Authorization': 'Bearer $token'}; - -try { - final response = await http.get(uri, headers: headers); - final statusCode = response.statusCode; - - if (response.body.isEmpty || response.body.trim().isEmpty) { - logSafe("❌ Empty user data response — auto logout"); - await _handleUnauthorized(); - throw Exception("Empty user data response"); - } - - final decrypted = decryptResponse(response.body); - if (decrypted == null) { - logSafe("❌ Failed to decrypt user data — auto logout", level: LogLevel.error); - await _handleUnauthorized(); - throw Exception("Decryption failed for user data"); - } - - final data = decrypted is Map ? decrypted['data'] ?? decrypted : null; - if (data == null || data is! Map) { - logSafe("❌ Decrypted user data is invalid — auto logout", level: LogLevel.error); - await _handleUnauthorized(); - throw Exception("Invalid decrypted user data"); - } - - if (statusCode == 200) { - final result = { - 'permissions': _parsePermissions(data['featurePermissions']), - 'employeeInfo': _parseEmployeeInfo(data['employeeInfo']), - 'projects': _parseProjectsInfo(data['projects']), - }; - - _userDataCache[token] = result; - logSafe("User data fetched and decrypted successfully."); - return result; - } - - if (statusCode == 401 && !hasRetried) { - logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning); - - final refreshed = await AuthService.refreshToken(); - if (refreshed) { - final newToken = await LocalStorage.getJwtToken(); - if (newToken != null && newToken.isNotEmpty) { - return fetchAllUserData(newToken, hasRetried: true); - } - } - - await _handleUnauthorized(); - logSafe("Token refresh failed. Redirecting to login.", level: LogLevel.warning); - throw Exception('Unauthorized. Token refresh failed.'); - } - - final errorMsg = data['message'] ?? 'Unknown error'; - logSafe("Failed to fetch user data: $errorMsg", level: LogLevel.warning); - throw Exception('Failed to fetch user data: $errorMsg'); -} catch (e, stacktrace) { - logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace); - rethrow; -} - - -} - -static Future _handleUnauthorized() async { -logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning); -await LocalStorage.removeToken('jwt_token'); -await LocalStorage.removeToken('refresh_token'); -await LocalStorage.setLoggedInUser(false); -Get.offAllNamed('/auth/login-option'); -} - -static List _parsePermissions(List? permissions) { -logSafe("Parsing user permissions..."); -if (permissions == null) return []; -return permissions.map((perm) => UserPermission.fromJson({'id': perm})).toList(); -} - -static EmployeeInfo _parseEmployeeInfo(Map? data) { -logSafe("Parsing employee info..."); -if (data == null) throw Exception("Employee data missing"); -return EmployeeInfo.fromJson(data); -} - -static List _parseProjectsInfo(List? projects) { -logSafe("Parsing projects info..."); -if (projects == null) return []; -return projects.map((proj) => ProjectInfo.fromJson(proj)).toList(); -} -} \ No newline at end of file diff --git a/lib/helpers/services/tenant_service.dart b/lib/helpers/services/tenant_service.dart deleted file mode 100644 index 337e823..0000000 --- a/lib/helpers/services/tenant_service.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:get/get.dart'; -import 'package:on_field_work/controller/project_controller.dart'; -import 'package:on_field_work/helpers/services/api_endpoints.dart'; -import 'package:on_field_work/helpers/services/storage/local_storage.dart'; -import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/helpers/services/auth_service.dart'; -import 'package:on_field_work/model/tenant/tenant_list_model.dart'; -import 'package:on_field_work/helpers/utils/encryption_helper.dart'; - -abstract class ITenantService { -Future>?> getTenants({bool hasRetried = false}); -Future selectTenant(String tenantId, {bool hasRetried = false}); -} - -class TenantService implements ITenantService { -static const String _baseUrl = ApiEndpoints.baseUrl; -static const Map _headers = { -'Content-Type': 'application/json', -}; - -static Tenant? currentTenant; - -static void setSelectedTenant(Tenant tenant) { -currentTenant = tenant; -} - -static bool get isTenantSelected => currentTenant != null; - -static Future> _authorizedHeaders() async { -final token = await LocalStorage.getJwtToken(); -if (token == null || token.isEmpty) throw Exception('Missing JWT token'); -return {..._headers, 'Authorization': 'Bearer $token'}; -} - -static void _handleApiError(http.Response response, dynamic data, String context) { -final message = data['message'] ?? 'Unknown error'; -final level = response.statusCode >= 500 ? LogLevel.error : LogLevel.warning; -logSafe("❌ $context failed: $message [Status: ${response.statusCode}]", level: level); -} - -static void _logException(dynamic e, dynamic st, String context) { -logSafe("❌ $context exception", level: LogLevel.error, error: e, stackTrace: st); -} - -@override -Future>?> getTenants({bool hasRetried = false}) async { -try { -final headers = await _authorizedHeaders(); -final response = await http.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers); - - if (response.body.isEmpty || response.body.trim().isEmpty) { - logSafe("❌ Empty tenant response — auto logout"); - await LocalStorage.logout(); - return null; - } - - final decrypted = decryptResponse(response.body); - if (decrypted == null) { - logSafe("❌ Tenant response decryption failed — auto logout"); - await LocalStorage.logout(); - return null; - } - - final data = decrypted is Map ? decrypted : null; - if (data == null) { - logSafe("❌ Decrypted tenant data is not valid JSON — auto logout"); - await LocalStorage.logout(); - return null; - } - - if (response.statusCode == 200 && data['success'] == true) { - final list = data['data']; - if (list is! List) return null; - return List>.from(list); - } - - if (response.statusCode == 401 && !hasRetried) { - final refreshed = await AuthService.refreshToken(); - if (refreshed) return getTenants(hasRetried: true); - return null; - } - - _handleApiError(response, data, "Fetching tenants"); - return null; -} catch (e, st) { - _logException(e, st, "Get Tenants API"); - return null; -} - - -} - -@override -Future selectTenant(String tenantId, {bool hasRetried = false}) async { -try { -final headers = await _authorizedHeaders(); -logSafe("➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers", level: LogLevel.info); - - final response = await http.post(Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"), headers: headers); - final decrypted = decryptResponse(response.body); - if (decrypted == null) { - logSafe("❌ Tenant selection response decryption failed", level: LogLevel.error); - return false; - } - - final data = decrypted is Map ? decrypted : null; - if (data == null) { - logSafe("❌ Decrypted tenant selection data is not valid JSON", level: LogLevel.error); - return false; - } - - logSafe("⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]", level: LogLevel.info); - - if (response.statusCode == 200 && data['success'] == true) { - await LocalStorage.setJwtToken(data['data']['token']); - await LocalStorage.setRefreshToken(data['data']['refreshToken']); - logSafe("✅ Tenant selected successfully. Tokens updated."); - - try { - final projectController = Get.find(); - projectController.clearProjects(); - projectController.fetchProjects(); - } catch (_) { - logSafe("⚠️ ProjectController not found while refreshing projects"); - } - - final fcmToken = LocalStorage.getFcmToken(); - if (fcmToken?.isNotEmpty ?? false) { - final success = await AuthService.registerDeviceToken(fcmToken!); - logSafe(success ? "✅ FCM token registered after tenant selection." : "⚠️ Failed to register FCM token.", level: success ? LogLevel.info : LogLevel.warning); - } - - return true; - } - - if (response.statusCode == 401 && !hasRetried) { - logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...", level: LogLevel.warning); - final refreshed = await AuthService.refreshToken(); - if (refreshed) return selectTenant(tenantId, hasRetried: true); - logSafe("❌ Token refresh failed while selecting tenant.", level: LogLevel.error); - return false; - } - - _handleApiError(response, data, "Selecting tenant"); - return false; -} catch (e, st) { - _logException(e, st, "Select Tenant API"); - return false; -} - - -} -} \ No newline at end of file diff --git a/lib/routes.dart b/lib/routes.dart index 44f8287..905d44d 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/services/auth_service.dart'; -import 'package:on_field_work/helpers/services/tenant_service.dart'; import 'package:on_field_work/view/auth/forgot_password_screen.dart'; import 'package:on_field_work/view/auth/login_screen.dart'; import 'package:on_field_work/view/auth/register_account_screen.dart'; @@ -32,7 +31,7 @@ class AuthMiddleware extends GetMiddleware { if (route != '/auth/login-option') { return const RouteSettings(name: '/auth/login-option'); } - } else if (!TenantService.isTenantSelected) { + } else if (!AuthService.isTenantSelected) { if (route != '/select-tenant') { return const RouteSettings(name: '/select-tenant'); } diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 983848e..a3f3039 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -8,7 +8,7 @@ import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/images.dart'; import 'package:on_field_work/view/layouts/user_profile_right_bar.dart'; -import 'package:on_field_work/helpers/services/tenant_service.dart'; +import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; class Layout extends StatefulWidget { @@ -106,7 +106,7 @@ class _LayoutState extends State with UIMixin { } Widget _buildHeaderContent(bool isMobile) { - final selectedTenant = TenantService.currentTenant; + final selectedTenant = AuthService.currentTenant; return Padding( padding: const EdgeInsets.fromLTRB(10, 45, 10, 0), diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 9d19155..7db5881 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -11,7 +11,7 @@ import 'package:on_field_work/helpers/widgets/avatar.dart'; import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:on_field_work/controller/auth/mpin_controller.dart'; import 'package:on_field_work/view/employees/employee_profile_screen.dart'; -import 'package:on_field_work/helpers/services/tenant_service.dart'; +import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:on_field_work/view/tenant/tenant_selection_screen.dart'; import 'package:on_field_work/controller/tenant/tenant_switch_controller.dart'; import 'package:on_field_work/helpers/theme/theme_editor_widget.dart'; @@ -285,7 +285,7 @@ class _UserProfileBarState extends State ); } - final selectedTenant = TenantService.currentTenant; + final selectedTenant = AuthService.currentTenant; final sortedTenants = List.of(tenants); if (selectedTenant != null) {