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 _defaultHeaders = { 'Content-Type': 'application/json', }; // AuthService properties static bool isLoggedIn = false; // 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 _networkRequest( path: "/auth/logout", method: _HttpMethod.post, body: body, ); if (response != null && response['statusCode'] == 200) { logSafe("✅ Logout API successful"); return true; } logSafe("⚠️ Logout API failed: ${response?['message']}", level: LogLevel.warning); return false; } catch (e, st) { _handleError("Logout API error", e, st); return false; } } /// Registers or updates the Firebase Cloud Messaging token. static Future registerDeviceToken(String fcmToken) async { final token = await LocalStorage.getJwtToken(); if (token == null || token.isEmpty) { logSafe("❌ Cannot register device token: missing JWT token", level: LogLevel.warning); return false; } final body = {"fcmToken": fcmToken}; final response = await _networkRequest( path: "/auth/set/device-token", method: _HttpMethod.post, body: body, authToken: token, ); if (response != null && response['success'] == true) { logSafe("✅ Device token registered successfully."); return true; } 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 _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']); return null; } if (responseData['statusCode'] == 401) { return {"password": "Invalid email or password"}; } return {"error": responseData['message'] ?? "Unexpected error occurred"}; } /// Refreshes the JWT access token using the refresh token. static Future refreshToken() async { 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); return false; } final body = {"token": accessToken, "refreshToken": refreshToken}; 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 = await LocalStorage.getFcmToken(); if (newFcmToken?.isNotEmpty ?? false) { await registerDeviceToken(newFcmToken!); } return true; } logSafe("Refresh token failed: ${data?['message']}", level: LogLevel.warning); return false; } /// Initiates the forgot password process. static Future?> forgotPassword(String email) => _wrapErrorHandling( () => _networkRequest( path: "/auth/forgot-password", method: _HttpMethod.post, body: {"email": email}, ), successCondition: (data) => data['success'] == true, defaultError: "Failed to send reset link."); /// Generates an MPIN for the user. static Future?> generateMpin({ required String employeeId, required String mpin, }) => _wrapErrorHandling( () async { final token = await LocalStorage.getJwtToken(); return _networkRequest( path: "/auth/generate-mpin", method: _HttpMethod.post, body: {"employeeId": employeeId, "mpin": mpin}, authToken: token, ); }, successCondition: (data) => data['success'] == true, defaultError: "Failed to generate MPIN.", ); /// Verifies the MPIN for quick login. static Future?> verifyMpin({ required String mpin, required String mpinToken, required String fcmToken, }) => _wrapErrorHandling( () async { final employeeInfo = await LocalStorage.getEmployeeInfo(); if (employeeInfo == null) return null; // Fails immediately if info is missing final token = await LocalStorage.getJwtToken(); final responseData = await _networkRequest( path: "/auth/login-mpin", method: _HttpMethod.post, body: { "employeeId": employeeInfo.id, "mpin": mpin, "mpinToken": mpinToken, "fcmToken": fcmToken, }, 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( () => _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 _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; } return {"error": data?['message'] ?? "OTP verification failed."}; } /* -------------------------------------------------------------------------- */ /* MARKET/OTHER METHODS                          */ /* -------------------------------------------------------------------------- */ /// 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 { final uri = Uri.parse("$_baseUrl$path"); final headers = { ..._defaultHeaders, if (authToken?.isNotEmpty ?? false) 'Authorization': 'Bearer $authToken', }; 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 ${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, required String defaultError, }) async { final data = await request(); if (data != null && successCondition(data)) return null; 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); if (data['refreshToken'] != null) { await LocalStorage.setRefreshToken(data['refreshToken']); } if (data['mpinToken']?.isNotEmpty ?? false) { await LocalStorage.setMpinToken(data['mpinToken']); await LocalStorage.setIsMpin(true); } else { await LocalStorage.setIsMpin(false); await LocalStorage.removeMpinToken(); } isLoggedIn = true; } }