diff --git a/env.file b/env.file index 27fb58814..f3ff169d6 100644 --- a/env.file +++ b/env.file @@ -6,6 +6,4 @@ APP_GRID_AVAILABLE=supported FCM_AVAILABLE=supported IOS_FCM=supported FORWARD_WARNING_MESSAGE= -PLATFORM=other -REGISTRATION_URL=http://localhost/ -SAAS_SERVER_URL=http://localhost/ \ No newline at end of file +PLATFORM=other \ No newline at end of file diff --git a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart index de92bdffa..9e4b60e79 100644 --- a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart +++ b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart @@ -15,7 +15,7 @@ extension OidcConfigurationExtensions on OIDCConfiguration { ? AppConfig.domainRedirectUrl + loginRedirectOidcWeb : '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb'; } else { - return authority == AppConfig.saasRegistrationUrl + return _isSaasAuthority(authority) ? OIDCConstant.twakeWorkplaceRedirectUrl : redirectOidcMobile; } @@ -29,11 +29,14 @@ extension OidcConfigurationExtensions on OIDCConfiguration { return '${AppConfig.domainRedirectUrl}/$logoutRedirectOidcWeb'; } } else { - return authority == AppConfig.saasRegistrationUrl + return _isSaasAuthority(authority) ? OIDCConstant.twakeWorkplaceRedirectUrl : redirectOidcMobile; + } } - } + + bool _isSaasAuthority(String authority) => + authority == AppConfig.saasRegistrationUrl; String get signInTWPUrl => ServicePath(authority) .withQueryParameters([ diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index 980cc98e7..2dd140624 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -6,6 +6,7 @@ import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cac import 'package:tmail_ui_user/features/login/data/datasource/login_datasource.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_login_datasource_impl.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/login_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; import 'package:tmail_ui_user/features/login/data/repository/login_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; @@ -26,6 +27,11 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_inte import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; +import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart'; +import 'package:tmail_ui_user/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart'; +import 'package:tmail_ui_user/features/starting_page/data/repository/saas_authentication_repository_impl.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; @@ -47,12 +53,15 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @override void bindingsDataSource() { Get.lazyPut(() => Get.find()); + Get.lazyPut( + () => Get.find()); } @override @@ -66,6 +75,10 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => SaasAuthenticationDataSourceImpl( + Get.find(), + Get.find(), + )); } @override @@ -98,11 +111,19 @@ class LoginBindings extends BaseBindings { Get.lazyPut(() => SaveLoginUsernameOnMobileInteractor(Get.find(),)); Get.lazyPut(() => GetAllRecentLoginUsernameOnMobileInteractor(Get.find())); Get.lazyPut(() => DNSLookupToGetJmapUrlInteractor(Get.find())); + Get.lazyPut(() => SignInTwakeWorkplaceInteractor( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); } @override void bindingsRepository() { Get.lazyPut(() => Get.find()); + Get.lazyPut( + () => Get.find()); } @override @@ -113,5 +134,7 @@ class LoginBindings extends BaseBindings { DataSourceType.network: Get.find(), } )); + Get.lazyPut(() => SaasAuthenticationRepositoryImpl( + Get.find())); } } \ No newline at end of file diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 770788166..e3e73d664 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -14,6 +14,7 @@ import 'package:model/account/password.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; import 'package:model/oidc/response/oidc_response.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; @@ -49,6 +50,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_m import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_workplace_state.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -70,6 +73,7 @@ class LoginController extends ReloadableController { final SaveLoginUsernameOnMobileInteractor _saveLoginUsernameOnMobileInteractor; final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor; final DNSLookupToGetJmapUrlInteractor _dnsLookupToGetJmapUrlInteractor; + final SignInTwakeWorkplaceInteractor _signInTwakeWorkplaceInteractor; final TextEditingController urlInputController = TextEditingController(); final TextEditingController usernameInputController = TextEditingController(); @@ -99,6 +103,7 @@ class LoginController extends ReloadableController { this._saveLoginUsernameOnMobileInteractor, this._getAllRecentLoginUsernameOnMobileInteractor, this._dnsLookupToGetJmapUrlInteractor, + this._signInTwakeWorkplaceInteractor, ); @override @@ -130,7 +135,8 @@ class LoginController extends ReloadableController { _handleCheckOIDCIsAvailableFailure(failure); } else if (failure is GetStoredOidcConfigurationFailure || failure is GetOIDCIsAvailableFailure || - failure is GetOIDCConfigurationFailure + failure is GetOIDCConfigurationFailure || + failure is SignInTwakeWorkplaceFailure ) { _handleCommonOIDCFailure(); } else if (failure is GetTokenOIDCFailure) { @@ -168,6 +174,12 @@ class LoginController extends ReloadableController { _loginSuccessAction(success); } else if (success is DNSLookupToGetJmapUrlSuccess) { _handleDNSLookupToGetJmapUrlSuccess(success); + } else if (success is SignInTwakeWorkplaceSuccess) { + _synchronizeTokenAndGetSession( + baseUri: success.baseUri, + tokenOIDC: success.tokenOIDC, + oidcConfiguration: success.oidcConfiguration, + ); } else { super.handleSuccessViewState(success); } @@ -180,7 +192,9 @@ class LoginController extends ReloadableController { _handleCheckOIDCIsAvailableFailure(failure); } else if (failure is GetStoredOidcConfigurationFailure || failure is GetOIDCConfigurationFailure || - failure is GetOIDCIsAvailableFailure) { + failure is GetOIDCIsAvailableFailure || + failure is SignInTwakeWorkplaceFailure + ) { _handleCommonOIDCFailure(); } else if (failure is GetTokenOIDCFailure) { _handleNoSuitableBrowserOIDC(failure) @@ -326,11 +340,41 @@ class LoginController extends ReloadableController { void _getOIDCConfigurationSuccess(GetOIDCConfigurationSuccess success) { if (PlatformInfo.isWeb) { _authenticateOidcOnBrowserAction(success.oidcConfiguration); + } else if (success.oidcConfiguration.authority == AppConfig.saasRegistrationUrl) { + _getTokenOIDCOnSaaSPlatform(success.oidcConfiguration); } else { _getTokenOIDCAction(success.oidcConfiguration); } } + void _getTokenOIDCOnSaaSPlatform(OIDCConfiguration oidcConfiguration) { + if (_currentBaseUrl != null) { + consumeState(_signInTwakeWorkplaceInteractor.execute( + baseUri: _currentBaseUrl!, + oidcConfiguration: oidcConfiguration, + )); + } else { + dispatchState(Left(GetTokenOIDCFailure(CanNotFoundBaseUrl()))); + } + } + + void _synchronizeTokenAndGetSession({ + required Uri baseUri, + required TokenOIDC tokenOIDC, + required OIDCConfiguration oidcConfiguration, + }) { + dynamicUrlInterceptors.setJmapUrl(baseUri.toString()); + dynamicUrlInterceptors.changeBaseUrl(baseUri.toString()); + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: tokenOIDC, + newConfig: oidcConfiguration); + authorizationIsolateInterceptors.setTokenAndAuthorityOidc( + newToken: tokenOIDC, + newConfig: oidcConfiguration); + + getSessionAction(); + } + void _getTokenOIDCAction(OIDCConfiguration config) { if (_currentBaseUrl != null) { consumeState(_getTokenOIDCInteractor.execute(_currentBaseUrl!, config)); @@ -357,15 +401,11 @@ class LoginController extends ReloadableController { } void _getTokenOIDCSuccess(GetTokenOIDCSuccess success) { - dynamicUrlInterceptors.setJmapUrl(_currentBaseUrl?.toString()); - dynamicUrlInterceptors.changeBaseUrl(_currentBaseUrl?.toString()); - authorizationInterceptors.setTokenAndAuthorityOidc( - newToken: success.tokenOIDC, - newConfig: success.configuration); - authorizationIsolateInterceptors.setTokenAndAuthorityOidc( - newToken: success.tokenOIDC, - newConfig: success.configuration); - getSessionAction(); + _synchronizeTokenAndGetSession( + baseUri: _currentBaseUrl!, + tokenOIDC: success.tokenOIDC, + oidcConfiguration: success.configuration, + ); } void _loginSuccessAction(AuthenticationUserSuccess success) { diff --git a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart index 55347ba9c..64b2e8595 100644 --- a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart +++ b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart @@ -46,11 +46,6 @@ class TwakeWelcomeController extends ReloadableController { void onClickSignIn(BuildContext context) { TipDialogHelper.loading(AppLocalizations.of(context).loadingPleaseWait); - if (AppConfig.saasJmapServerUrl.isEmpty) { - consumeState(Stream.value(Left(SignInTwakeWorkplaceFailure(CanNotFoundSaasServerUrl())))); - return; - } - final baseUri = Uri.tryParse(AppConfig.saasJmapServerUrl); if (baseUri == null) { @@ -71,11 +66,6 @@ class TwakeWelcomeController extends ReloadableController { void onSignUpTwakeWorkplace(BuildContext context) { TipDialogHelper.loading(AppLocalizations.of(context).loadingPleaseWait); - if (AppConfig.saasJmapServerUrl.isEmpty) { - consumeState(Stream.value(Left(SignUpTwakeWorkplaceFailure(CanNotFoundSaasServerUrl())))); - return; - } - final baseUri = Uri.tryParse(AppConfig.saasJmapServerUrl); if (baseUri == null) { diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index be4dcd9a4..ceb41963a 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -16,6 +16,8 @@ class AppConfig { static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions'; static const String saasPlatform = 'saas'; static const String linagoraPrivacyUrl = 'https://www.linagora.com/en/legal/privacy'; + static const String saasRegistrationUrl = 'https://sign-up.stg.lin-saas.com'; + static const String saasJmapServerUrl = 'https://jmap.stg.lin-saas.com'; static String get baseUrl => dotenv.get('SERVER_URL', fallback: ''); static String get domainRedirectUrl => dotenv.get('DOMAIN_REDIRECT_URL', fallback: ''); @@ -66,8 +68,4 @@ class AppConfig { static String get _platformEnv => dotenv.get('PLATFORM', fallback: 'other'); static bool get isSaasPlatForm => _platformEnv.toLowerCase() == saasPlatform; - - static String get saasRegistrationUrl => dotenv.get('SAAS_REGISTRATION_URL', fallback: ''); - - static String get saasJmapServerUrl => dotenv.get('SAAS_JMAP_SERVER_URL', fallback: ''); } \ No newline at end of file diff --git a/model/lib/oidc/token_oidc.dart b/model/lib/oidc/token_oidc.dart index a490a562b..dd853dcea 100644 --- a/model/lib/oidc/token_oidc.dart +++ b/model/lib/oidc/token_oidc.dart @@ -37,26 +37,19 @@ class TokenOIDC with EquatableMixin { throw AccessTokenIsNullException(); } - final refreshToken = queryParams['refresh_token']; - if (refreshToken == null || refreshToken.isEmpty) { - throw RefreshTokenIsNullException(); - } - - final idToken = queryParams['id_token']; - if (idToken == null || idToken.isEmpty) { - throw TokenIdIsNullException(); - } - + final refreshToken = queryParams['refresh_token'] ?? ''; + final idToken = queryParams['id_token'] ?? ''; final expiresIn = queryParams['expires_in']; - if (expiresIn == null || expiresIn.isEmpty) { - throw ExpiresTimeIsNullException(); - } + + final expiredTime = expiresIn == null + ? null + : DateTime.now().add(Duration(seconds: int.parse(expiresIn))); return TokenOIDC( accessToken, TokenId(idToken), refreshToken, - expiredTime: DateTime.now().add(Duration(seconds: int.parse(expiresIn))), + expiredTime: expiredTime, ); } diff --git a/model/test/oidc/token_oidc_test.dart b/model/test/oidc/token_oidc_test.dart index 8deaf6b5e..ce41efaa5 100644 --- a/model/test/oidc/token_oidc_test.dart +++ b/model/test/oidc/token_oidc_test.dart @@ -20,24 +20,6 @@ void main() { expect(() => TokenOIDC.fromUri(uriString), throwsA(isA())); }); - test('throws RefreshTokenIsNullException if refresh_token is missing', () { - const uriString = 'https://example.com/callback?access_token=valid_access&id_token=valid_id&expires_in=3600'; - - expect(() => TokenOIDC.fromUri(uriString), throwsA(isA())); - }); - - test('throws TokenIdIsNullException if id_token is missing', () { - const uriString = 'https://example.com/callback?access_token=valid_access&refresh_token=valid_refresh&expires_in=3600'; - - expect(() => TokenOIDC.fromUri(uriString), throwsA(isA())); - }); - - test('throws ExpiresTimeIsNullException if expires_in is missing', () { - const uriString = 'https://example.com/callback?access_token=valid_access&refresh_token=valid_refresh&id_token=valid_id'; - - expect(() => TokenOIDC.fromUri(uriString), throwsA(isA())); - }); - test('throws FormatException on invalid URL format', () { const uriString = '::Not valid URI::'; diff --git a/test/features/login/domain/extensions/oidc_configuration_extensions_test.dart b/test/features/login/domain/extensions/oidc_configuration_extensions_test.dart new file mode 100644 index 000000000..fcae78b94 --- /dev/null +++ b/test/features/login/domain/extensions/oidc_configuration_extensions_test.dart @@ -0,0 +1,50 @@ +import 'package:core/data/model/query/query_parameter.dart'; +import 'package:core/data/network/config/service_path.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; + + +void main() { + test('signInTWPUrl should return the correct URL with query parameters', () { + // Arrange + const authority = 'https://authority.example.com'; + const expectedUrl = '$authority?${OIDCConstant.postLoginRedirectUrlPathParams}=${OIDCConstant.twakeWorkplaceRedirectUrl}&app=${OIDCConstant.appParameter}'; + final service = ServicePath(authority); + + // Act + final actualUrl = service + .withQueryParameters([ + StringQueryParameter( + OIDCConstant.postLoginRedirectUrlPathParams, + OIDCConstant.twakeWorkplaceRedirectUrl, + ), + StringQueryParameter('app', OIDCConstant.appParameter), + ]) + .generateEndpointPath(); + + // Assert + expect(actualUrl, equals(expectedUrl)); + }); + + test('signUpTWPUrl should return the correct URL with query parameters', () { + // Arrange + const authority = 'https://authority.example.com'; + const expectedUrl = '$authority?${OIDCConstant.postRegisteredRedirectUrlPathParams}=${OIDCConstant.twakeWorkplaceRedirectUrl}&app=${OIDCConstant.appParameter}'; + final service = ServicePath(authority); + + // Act + final actualUrl = service + .withQueryParameters([ + StringQueryParameter( + OIDCConstant.postRegisteredRedirectUrlPathParams, + OIDCConstant.twakeWorkplaceRedirectUrl, + ), + StringQueryParameter('app', OIDCConstant.appParameter), + ]) + .generateEndpointPath(); + + // Assert + expect(actualUrl, equals(expectedUrl)); + }); +} diff --git a/test/features/login/presentation/login_controller_test.dart b/test/features/login/presentation/login_controller_test.dart index 9442b782c..f30e210f2 100644 --- a/test/features/login/presentation/login_controller_test.dart +++ b/test/features/login/presentation/login_controller_test.dart @@ -33,6 +33,7 @@ import 'package:tmail_ui_user/features/login/presentation/login_controller.dart' import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:uuid/uuid.dart'; @@ -62,6 +63,7 @@ import 'login_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -84,6 +86,7 @@ void main() { late MockSaveLoginUsernameOnMobileInteractor mockSaveLoginUsernameOnMobileInteractor; late MockGetAllRecentLoginUsernameOnMobileInteractor mockGetAllRecentLoginUsernameOnMobileInteractor; late MockDNSLookupToGetJmapUrlInteractor mockDNSLookupToGetJmapUrlInteractor; + late MockSignInTwakeWorkplaceInteractor mockSignInTwakeWorkplaceInteractor; late MockGetSessionInteractor mockGetSessionInteractor; late MockGetAuthenticatedAccountInteractor mockGetAuthenticatedAccountInteractor; late MockUpdateAccountCacheInteractor mockUpdateAccountCacheInteractor; @@ -118,6 +121,7 @@ void main() { mockSaveLoginUsernameOnMobileInteractor = MockSaveLoginUsernameOnMobileInteractor(); mockGetAllRecentLoginUsernameOnMobileInteractor = MockGetAllRecentLoginUsernameOnMobileInteractor(); mockDNSLookupToGetJmapUrlInteractor = MockDNSLookupToGetJmapUrlInteractor(); + mockSignInTwakeWorkplaceInteractor = MockSignInTwakeWorkplaceInteractor(); // mock reloadable controller mockGetSessionInteractor = MockGetSessionInteractor(); @@ -175,6 +179,7 @@ void main() { mockSaveLoginUsernameOnMobileInteractor, mockGetAllRecentLoginUsernameOnMobileInteractor, mockDNSLookupToGetJmapUrlInteractor, + mockSignInTwakeWorkplaceInteractor, );