diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 1e636f707..815d716bf 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -27,6 +27,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; class EmailAPI { @@ -160,6 +161,12 @@ class EmailAPI { String baseDownloadUrl, AccountRequest accountRequest ) async { + if (accountRequest.authenticationType == AuthenticationType.oidc && + accountRequest.token?.isExpired == true && + accountRequest.token?.refreshToken.isNotEmpty == true) { + throw DownloadAttachmentHasTokenExpiredException(accountRequest.token!.refreshToken); + } + String externalStorageDirPath; if (Platform.isAndroid) { externalStorageDirPath = await ExternalPath.getExternalStoragePublicDirectory(ExternalPath.DIRECTORY_DOWNLOADS); @@ -169,12 +176,16 @@ class EmailAPI { throw DeviceNotSupportedException(); } + final authentication = accountRequest.authenticationType == AuthenticationType.oidc + ? accountRequest.bearerToken + : accountRequest.basicAuth; + final taskIds = await Future.wait( attachments.map((attachment) async => await FlutterDownloader.enqueue( url: attachment.getDownloadUrl(baseDownloadUrl, accountId), savedDir: externalStorageDirPath, headers: { - HttpHeaders.authorizationHeader: accountRequest.basicAuth, + HttpHeaders.authorizationHeader: authentication, HttpHeaders.acceptHeader: DioClient.jmapHeader }, fileName: attachment.name, diff --git a/lib/features/email/domain/usecases/download_attachments_interactor.dart b/lib/features/email/domain/usecases/download_attachments_interactor.dart index add39dddf..8a99f4204 100644 --- a/lib/features/email/domain/usecases/download_attachments_interactor.dart +++ b/lib/features/email/domain/usecases/download_attachments_interactor.dart @@ -4,13 +4,26 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; class DownloadAttachmentsInteractor { final EmailRepository emailRepository; final CredentialRepository credentialRepository; + final AccountRepository _accountRepository; + final AuthenticationOIDCRepository _authenticationOIDCRepository; + final AuthorizationInterceptors _authorizationInterceptors; - DownloadAttachmentsInteractor(this.emailRepository, this.credentialRepository); + DownloadAttachmentsInteractor( + this.emailRepository, + this.credentialRepository, + this._accountRepository, + this._authenticationOIDCRepository, + this._authorizationInterceptors, + ); Stream> execute( List attachments, @@ -18,14 +31,34 @@ class DownloadAttachmentsInteractor { String baseDownloadUrl ) async* { try { - final taskIds = await Future.wait( - [credentialRepository.getUserName(), credentialRepository.getPassword()], - eagerError: true + final account = await _accountRepository.getCurrentAccount(); + + log('ExportAttachmentInteractor::execute(): account: $account'); + + final taskIds = await Future.wait([ + if (account.authenticationType == AuthenticationType.oidc) + _authenticationOIDCRepository.getStoredTokenOIDC(account.id) + else + ...[ + credentialRepository.getUserName(), + credentialRepository.getPassword() + ] + ], eagerError: true ).then((List responses) async { - final accountRequest = AccountRequest( - userName: responses.first, - password: responses.last, - authenticationType: AuthenticationType.basic); + AccountRequest accountRequest; + + if (account.authenticationType == AuthenticationType.oidc) { + final tokenOidc = responses.first as TokenOIDC; + accountRequest = AccountRequest( + token: tokenOidc.toToken(), + authenticationType: AuthenticationType.oidc); + } else { + accountRequest = AccountRequest( + userName: responses.first as UserName, + password: responses.last as Password, + authenticationType: AuthenticationType.basic); + } + return await emailRepository.downloadAttachments( attachments, accountId, @@ -33,8 +66,63 @@ class DownloadAttachmentsInteractor { accountRequest); }); + yield Right(DownloadAttachmentsSuccess(taskIds)); + } catch (exception) { + log('DownloadAttachmentsInteractor::execute(): $exception'); + if (exception is DownloadAttachmentHasTokenExpiredException) { + yield* _retryDownloadAttachments( + accountId, + baseDownloadUrl, + attachments, + exception.refreshToken); + } else { + yield Left(DownloadAttachmentsFailure(exception)); + } + } + } + + Stream> _retryDownloadAttachments( + AccountId accountId, + String baseDownloadUrl, + List attachments, + String refreshToken) async* { + log('DownloadAttachmentsInteractor::_retryDownloadAttachments(): $refreshToken'); + try { + final accountCurrent = await _accountRepository.getCurrentAccount(); + final oidcConfig = await _authenticationOIDCRepository.getStoredOidcConfiguration(); + final newTokenOIDC = await _authenticationOIDCRepository.refreshingTokensOIDC( + oidcConfig.clientId, + oidcConfig.redirectUrl, + oidcConfig.discoveryUrl, + oidcConfig.scopes, + refreshToken); + + await Future.wait([ + _authenticationOIDCRepository.persistTokenOIDC(newTokenOIDC), + _accountRepository.deleteCurrentAccount(accountCurrent.id), + _accountRepository.setCurrentAccount(Account( + newTokenOIDC.tokenIdHash, + AuthenticationType.oidc, + isSelected: true)) + ]); + + _authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: newTokenOIDC.toToken(), + newConfig: oidcConfig); + + final accountRequest = AccountRequest( + token: newTokenOIDC.toToken(), + authenticationType: AuthenticationType.oidc); + + final taskIds = await emailRepository.downloadAttachments( + attachments, + accountId, + baseDownloadUrl, + accountRequest); + yield Right(DownloadAttachmentsSuccess(taskIds)); } catch (e) { + logError('RefreshTokenOIDCInteractor::execute(): $e'); yield Left(DownloadAttachmentsFailure(e)); } } diff --git a/lib/features/email/presentation/email_bindings.dart b/lib/features/email/presentation/email_bindings.dart index 6a7d4102d..590bdc8f7 100644 --- a/lib/features/email/presentation/email_bindings.dart +++ b/lib/features/email/presentation/email_bindings.dart @@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_d import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; @@ -32,6 +33,7 @@ import 'package:tmail_ui_user/features/login/data/repository/credential_reposito import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/refresh_token_oidc_interactor.dart'; class EmailBindings extends BaseBindings { @@ -79,6 +81,9 @@ class EmailBindings extends BaseBindings { Get.lazyPut(() => DownloadAttachmentsInteractor( Get.find(), Get.find(), + Get.find(), + Get.find(), + Get.find(), )); Get.lazyPut(() => ExportAttachmentInteractor( Get.find(), @@ -92,6 +97,10 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => RefreshTokenOIDCInteractor( + Get.find(), + Get.find(), + )); } @override diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 952e7f472..6dc7d12b8 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -228,7 +228,7 @@ class EmailController extends BaseController { final status = await Permission.storage.status; switch (status) { case PermissionStatus.granted: - _downloadAttachmentsAction(context, attachments); + _downloadAttachmentsAction(attachments); break; case PermissionStatus.permanentlyDenied: _appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments); @@ -237,7 +237,7 @@ class EmailController extends BaseController { final requested = await Permission.storage.request(); switch (requested) { case PermissionStatus.granted: - _downloadAttachmentsAction(context, attachments); + _downloadAttachmentsAction(attachments); break; default: _appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments); @@ -246,11 +246,11 @@ class EmailController extends BaseController { } } } else { - _downloadAttachmentsAction(context, attachments); + _downloadAttachmentsAction(attachments); } } - void _downloadAttachmentsAction(BuildContext context, List attachments) async { + void _downloadAttachmentsAction(List attachments) async { final accountId = mailboxDashBoardController.accountId.value; if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(); @@ -258,7 +258,7 @@ class EmailController extends BaseController { } } - void _downloadAttachmentsFailure(Failure failure) { + void _downloadAttachmentsFailure(DownloadAttachmentsFailure failure) { if (currentContext != null) { _appToast.showErrorToast(AppLocalizations.of(currentContext!).attachment_download_failed); } diff --git a/lib/features/login/data/datasource/account_datasource.dart b/lib/features/login/data/datasource/account_datasource.dart index 1eef5031f..2e88d9a61 100644 --- a/lib/features/login/data/datasource/account_datasource.dart +++ b/lib/features/login/data/datasource/account_datasource.dart @@ -4,4 +4,6 @@ abstract class AccountDatasource { Future getCurrentAccount(); Future setCurrentAccount(Account newCurrentAccount); + + Future deleteCurrentAccount(String accountId); } \ No newline at end of file diff --git a/lib/features/login/data/datasource/authentication_oidc_datasource.dart b/lib/features/login/data/datasource/authentication_oidc_datasource.dart index 4744c7462..8e8efba06 100644 --- a/lib/features/login/data/datasource/authentication_oidc_datasource.dart +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -15,4 +15,11 @@ abstract class AuthenticationOIDCDataSource { Future persistAuthorityOidc(String authority); Future getStoredOidcConfiguration(); + + Future refreshingTokensOIDC( + String clientId, + String redirectUrl, + String discoveryUrl, + List scopes, + String refreshToken); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart index 1862f9aff..8e485e74d 100644 --- a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart @@ -62,4 +62,11 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { Future persistAuthorityOidc(String authority) { return _oidcConfigurationCacheManager.persistAuthorityOidc(authority); } + + @override + Future refreshingTokensOIDC(String clientId, String redirectUrl, + String discoveryUrl, List scopes, String refreshToken) { + return _oidcHttpClient.refreshingTokensOIDC( + clientId, redirectUrl, discoveryUrl, scopes, refreshToken); + } } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart b/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart index c1f1bd7f5..5416394f7 100644 --- a/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart @@ -19,4 +19,9 @@ class HiveAccountDatasourceImpl extends AccountDatasource { log('HiveAccountDatasourceImpl::setCurrentAccount(): $_accountCacheManager'); return _accountCacheManager.setSelectedAccount(newCurrentAccount); } + + @override + Future deleteCurrentAccount(String accountId) { + return _accountCacheManager.deleteSelectedAccount(accountId); + } } \ No newline at end of file diff --git a/lib/features/login/data/network/config/authorization_interceptors.dart b/lib/features/login/data/network/config/authorization_interceptors.dart index 0c6a14ed9..c4be31d45 100644 --- a/lib/features/login/data/network/config/authorization_interceptors.dart +++ b/lib/features/login/data/network/config/authorization_interceptors.dart @@ -49,6 +49,8 @@ class AuthorizationInterceptors extends InterceptorsWrapper { _token = newToken; } + OIDCConfiguration? get oidcConfig => _configOIDC; + @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { switch(_authenticationType) { diff --git a/lib/features/login/data/repository/account_repository_impl.dart b/lib/features/login/data/repository/account_repository_impl.dart index 62070b52c..aa4a6e077 100644 --- a/lib/features/login/data/repository/account_repository_impl.dart +++ b/lib/features/login/data/repository/account_repository_impl.dart @@ -19,4 +19,9 @@ class AccountRepositoryImpl extends AccountRepository { log('AccountRepositoryImpl::setCurrentAccount(): $newCurrentAccount'); return _accountDatasource.setCurrentAccount(newCurrentAccount); } + + @override + Future deleteCurrentAccount(String accountId) { + return _accountDatasource.deleteCurrentAccount(accountId); + } } \ No newline at end of file diff --git a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart index 688ea57ba..29f83edec 100644 --- a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -44,4 +44,20 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { Future persistAuthorityOidc(String authority) { return _oidcDataSource.persistAuthorityOidc(authority); } + + @override + Future refreshingTokensOIDC( + String clientId, + String redirectUrl, + String discoveryUrl, + List scopes, + String refreshToken + ) { + return _oidcDataSource.refreshingTokensOIDC( + clientId, + redirectUrl, + discoveryUrl, + scopes, + refreshToken); + } } \ No newline at end of file diff --git a/lib/features/login/domain/exceptions/authentication_exception.dart b/lib/features/login/domain/exceptions/authentication_exception.dart index b8b60a911..7c0ff3bea 100644 --- a/lib/features/login/domain/exceptions/authentication_exception.dart +++ b/lib/features/login/domain/exceptions/authentication_exception.dart @@ -35,4 +35,11 @@ class NotFoundAccessTokenException implements Exception { class AccessTokenInvalidException implements Exception { AccessTokenInvalidException(); +} + +class DownloadAttachmentHasTokenExpiredException implements Exception { + + final String refreshToken; + + DownloadAttachmentHasTokenExpiredException(this.refreshToken); } \ No newline at end of file diff --git a/lib/features/login/domain/repository/account_repository.dart b/lib/features/login/domain/repository/account_repository.dart index 8a83063f7..3ef4aca47 100644 --- a/lib/features/login/domain/repository/account_repository.dart +++ b/lib/features/login/domain/repository/account_repository.dart @@ -5,4 +5,6 @@ abstract class AccountRepository { Future getCurrentAccount(); Future setCurrentAccount(Account newCurrentAccount); + + Future deleteCurrentAccount(String accountId); } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_oidc_repository.dart b/lib/features/login/domain/repository/authentication_oidc_repository.dart index 5fa3e2037..05eb19410 100644 --- a/lib/features/login/domain/repository/authentication_oidc_repository.dart +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -15,4 +15,11 @@ abstract class AuthenticationOIDCRepository { Future persistAuthorityOidc(String authority); Future getStoredOidcConfiguration(); + + Future refreshingTokensOIDC( + String clientId, + String redirectUrl, + String discoveryUrl, + List scopes, + String refreshToken); } \ No newline at end of file diff --git a/lib/features/login/domain/state/refresh_token_oidc_state.dart b/lib/features/login/domain/state/refresh_token_oidc_state.dart new file mode 100644 index 000000000..55228f9d3 --- /dev/null +++ b/lib/features/login/domain/state/refresh_token_oidc_state.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:model/model.dart'; + +class RefreshTokenOIDCSuccess extends UIState { + + final TokenOIDC tokenOIDC; + + RefreshTokenOIDCSuccess(this.tokenOIDC); + + @override + List get props => [tokenOIDC]; +} + +class RefreshTokenOIDCFailure extends FeatureFailure { + final dynamic exception; + + RefreshTokenOIDCFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/refresh_token_oidc_interactor.dart b/lib/features/login/domain/usecases/refresh_token_oidc_interactor.dart new file mode 100644 index 000000000..2e342ad10 --- /dev/null +++ b/lib/features/login/domain/usecases/refresh_token_oidc_interactor.dart @@ -0,0 +1,40 @@ + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/refresh_token_oidc_state.dart'; + +class RefreshTokenOIDCInteractor { + + final AuthenticationOIDCRepository authenticationOIDCRepository; + final AccountRepository _accountRepository; + + RefreshTokenOIDCInteractor(this.authenticationOIDCRepository, this._accountRepository); + + Future> execute( + OIDCConfiguration config, + String refreshToken) async { + try { + final newTokenOIDC = await authenticationOIDCRepository.refreshingTokensOIDC( + config.clientId, + config.redirectUrl, + config.discoveryUrl, + config.scopes, + refreshToken); + + await Future.wait([ + _accountRepository.setCurrentAccount(Account( + newTokenOIDC.tokenId.hashCode.toString(), + AuthenticationType.oidc, + isSelected: true)), + authenticationOIDCRepository.persistTokenOIDC(newTokenOIDC), + ]); + return Right(RefreshTokenOIDCSuccess(newTokenOIDC)); + } catch (e) { + logError('RefreshTokenOIDCInteractor::execute(): $e'); + return Left(RefreshTokenOIDCFailure(e)); + } + } +} \ No newline at end of file