import 'package:logging/logging.dart'; import 'package:pshared/api/errors/authorization_failed.dart'; import 'package:pshared/api/requests/login.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/api/responses/account.dart'; import 'package:pshared/api/responses/login.dart'; import 'package:pshared/api/responses/login_pending.dart'; import 'package:pshared/config/web.dart'; import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/auth/login_outcome.dart'; import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/models/session_identifier.dart'; import 'package:pshared/service/authorization/circuit_breaker.dart'; import 'package:pshared/service/authorization/retry_helper.dart'; import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/authorization/token.dart'; import 'package:pshared/service/device_id.dart'; import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/http/requests.dart' as httpr; /// AuthorizationService provides centralized authorization management /// with token refresh, retry logic, and circuit breaker patterns class AuthorizationService { static final _logger = Logger('service.authorization.auth_service'); static Future login(String service, LoginData login) async { _logger.fine('Logging in ${login.login} with ${login.locale} locale'); final deviceId = await DeviceIdManager.getDeviceId(); final response = await httpr.getPOSTResponse( service, '/login', LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(), ); if (response.containsKey('refreshToken')) { return LoginOutcome.completed((await completeLogin(response)).account.toDomain()); } if (response.containsKey('pendingToken')) { final pending = PendingLogin.fromResponse( PendingLoginResponse.fromJson(response), session: SessionIdentifier(clientId: Constants.clientId, deviceId: deviceId), ); return LoginOutcome.pending(pending); } throw AuthenticationFailedException('Unexpected login response', Exception(response.toString())); } static Future _updateAccessToken(AccountResponse response) async { await AuthorizationStorage.updateToken(response.accessToken); } static Future _updateTokens(LoginResponse response) async { await _updateAccessToken(response); return AuthorizationStorage.updateRefreshToken(response.refreshToken); } static Future _completeLogin(Map response) async { final LoginResponse lr = LoginResponse.fromJson(response); await _updateTokens(lr); return lr; } static Future completeLogin(Map response) => _completeLogin(response); static Future restore() async { return (await TokenService.refreshAccessToken()).account.toDomain(); } static Future logout() async { return AuthorizationStorage.removeTokens(); } // Original AuthorizationService methods - keeping the interface unchanged static Future> getGETResponse(String service, String url) async { final token = await TokenService.getAccessTokenSafe(); return httpr.getGETResponse(service, url, authToken: token); } static Future> getPOSTResponse(String service, String url, Map body) async { final token = await TokenService.getAccessTokenSafe(); return httpr.getPOSTResponse(service, url, body, authToken: token); } static Future> getPUTResponse(String service, String url, Map body) async { final token = await TokenService.getAccessTokenSafe(); return httpr.getPUTResponse(service, url, body, authToken: token); } static Future> getPATCHResponse(String service, String url, Map body) async { final token = await TokenService.getAccessTokenSafe(); return httpr.getPATCHResponse(service, url, body, authToken: token); } static Future> getDELETEResponse(String service, String url, Map body) async { final token = await TokenService.getAccessTokenSafe(); return httpr.getDELETEResponse(service, url, body, authToken: token); } static Future getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List bytes) async { final token = await TokenService.getAccessTokenSafe(); final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: token); if (res == null) { throw Exception('Upload failed'); } return res.url; } static Future isAuthorizationStored() async => AuthorizationStorage.isAuthorizationStored(); /// Execute an operation with automatic token management and retry logic static Future executeWithAuth( Future Function() operation, String description, { int? maxRetries, }) async => AuthCircuitBreaker.execute(() async => RetryHelper.withExponentialBackoff( operation, maxRetries: maxRetries ?? 3, initialDelay: Duration(milliseconds: 100), maxDelay: Duration(seconds: 5), shouldRetry: (error) => RetryHelper.isRetryableError(error), )); /// Handle 401 unauthorized errors with automatic token recovery static Future handleUnauthorized( Future Function() operation, String description, ) async { _logger.warning('Handling unauthorized error with token recovery: $description'); return executeWithAuth( () async { try { // Attempt token recovery first await TokenService.handleUnauthorized(); // Retry the original operation return await operation(); } catch (e) { _logger.severe('Token recovery failed', e); throw AuthenticationFailedException('Token recovery failed', toException(e)); } }, 'unauthorized recovery: $description', ); } }