diff --git a/android/gradle/wrapper/gradle-wrapper 2.jar b/android/gradle/wrapper/gradle-wrapper 2.jar new file mode 100755 index 000000000..13372aef5 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper 2.jar differ diff --git a/android/gradlew 2 b/android/gradlew 2 new file mode 100755 index 000000000..9d82f7891 --- /dev/null +++ b/android/gradlew 2 @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew 2.bat b/android/gradlew 2.bat new file mode 100755 index 000000000..aec99730b --- /dev/null +++ b/android/gradlew 2.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/core/lib/core.dart b/core/lib/core.dart index 941aa289a..e2bf9ef10 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -77,7 +77,6 @@ export 'data/constants/constant.dart'; // Network export 'data/network/config/authorization_interceptors.dart'; export 'data/network/config/dynamic_url_interceptors.dart'; -export 'data/network/config/endpoint.dart'; export 'data/network/config/service_path.dart'; export 'data/network/dio_client.dart'; export 'data/network/download/download_client.dart'; @@ -95,4 +94,5 @@ export 'data/local/database_client.dart'; export 'data/local/database_manager.dart'; // Model -export 'data/model/data_source_type.dart'; \ No newline at end of file +export 'data/model/source_type/data_source_type.dart'; +export 'data/model/query/query_parameter.dart'; \ No newline at end of file diff --git a/core/lib/data/model/query/query_parameter.dart b/core/lib/data/model/query/query_parameter.dart new file mode 100644 index 000000000..e6eb36ad4 --- /dev/null +++ b/core/lib/data/model/query/query_parameter.dart @@ -0,0 +1,19 @@ + +abstract class QueryParameter { + final String queryName; + final T queryValue; + + QueryParameter(this.queryName, this.queryValue); +} + +class BooleanQueryParameter extends QueryParameter { + BooleanQueryParameter(String queryName, bool queryValue) : super(queryName, queryValue); +} + +class StringQueryParameter extends QueryParameter { + StringQueryParameter(String queryName, String queryValue) : super(queryName, queryValue); +} + +class IntQueryParameter extends QueryParameter { + IntQueryParameter(String queryName, int queryValue) : super(queryName, queryValue); +} diff --git a/core/lib/data/model/data_source_type.dart b/core/lib/data/model/source_type/data_source_type.dart similarity index 100% rename from core/lib/data/model/data_source_type.dart rename to core/lib/data/model/source_type/data_source_type.dart diff --git a/core/lib/data/network/config/endpoint.dart b/core/lib/data/network/config/endpoint.dart deleted file mode 100644 index b99292dae..000000000 --- a/core/lib/data/network/config/endpoint.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:core/data/network/config/service_path.dart'; - -class Endpoint { - static final ServicePath authentication = ServicePath('/api/login'); -} diff --git a/core/lib/data/network/config/service_path.dart b/core/lib/data/network/config/service_path.dart index db9fd6b0d..68163010a 100644 --- a/core/lib/data/network/config/service_path.dart +++ b/core/lib/data/network/config/service_path.dart @@ -1,19 +1,6 @@ + class ServicePath { final String path; ServicePath(this.path); -} - -extension ServicePathExtension on ServicePath { - String generateEndpointPath() { - return '$path'; - } - - String generateAuthenticationUrl(Uri? baseUrl) { - if (baseUrl == null) { - return generateEndpointPath(); - } else { - return baseUrl.origin + generateEndpointPath(); - } - } } \ No newline at end of file diff --git a/core/lib/presentation/extensions/url_extension.dart b/core/lib/presentation/extensions/url_extension.dart index 86d0efb31..05b094b8c 100644 --- a/core/lib/presentation/extensions/url_extension.dart +++ b/core/lib/presentation/extensions/url_extension.dart @@ -5,13 +5,16 @@ extension URLExtension on String { static final String prefixUrlHttp = 'http://'; String formatURLValid() { - if (startsWith(prefixUrlHttps)) { - return this; - } else if (startsWith(prefixUrlHttp)) { - return kReleaseMode ? replaceAll(prefixUrlHttp, prefixUrlHttps) : this; - } else { - return '$prefixUrlHttps${this}'; + if (isNotEmpty) { + if (startsWith(prefixUrlHttps)) { + return this; + } else if (startsWith(prefixUrlHttp)) { + return kReleaseMode ? replaceAll(prefixUrlHttp, prefixUrlHttps) : this; + } else { + return '$prefixUrlHttps${this}'; + } } + return ''; } String removePrefix() { diff --git a/lib/features/login/data/datasource/authentication_oidc_datasource.dart b/lib/features/login/data/datasource/authentication_oidc_datasource.dart new file mode 100644 index 000000000..3b80f5259 --- /dev/null +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -0,0 +1,6 @@ + +import 'package:model/model.dart'; + +abstract class AuthenticationOIDCDataSource { + Future checkOIDCIsAvailable(OIDCRequest oidcRequest); +} \ 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 new file mode 100644 index 000000000..df52f433d --- /dev/null +++ b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart @@ -0,0 +1,20 @@ +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; + +class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { + + final OIDCHttpClient _oidcHttpClient; + + AuthenticationOIDCDataSourceImpl(this._oidcHttpClient); + + @override + Future checkOIDCIsAvailable(OIDCRequest oidcRequest) { + return Future.sync(() async { + final oidcResponse = await _oidcHttpClient.checkOIDCIsAvailable(oidcRequest); + return oidcResponse != null; + }).catchError((error) { + throw error; + }); + } +} \ No newline at end of file diff --git a/lib/features/login/data/extensions/service_path_extension.dart b/lib/features/login/data/extensions/service_path_extension.dart new file mode 100644 index 000000000..e66330792 --- /dev/null +++ b/lib/features/login/data/extensions/service_path_extension.dart @@ -0,0 +1,29 @@ + +import 'package:core/core.dart'; +import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; + +extension ServicePathExtension on ServicePath { + String generateEndpointPath() { + return path; + } + + ServicePath generateOIDCPath(Uri baseUrl) { + return ServicePath(baseUrl.origin + Endpoint.oidc + path); + } + + ServicePath withQueryParameters(List queryParameters) { + if (queryParameters.isEmpty) { + return this; + } + return ServicePath('$path?${queryParameters + .map((query) => '${query.queryName}=${query.queryValue}').join('&')}'); + } + + ServicePath withPathParameter([String? pathParameter]) { + if (pathParameter == null || pathParameter.isEmpty) { + return this; + } + + return ServicePath('$path/$pathParameter'); + } +} \ No newline at end of file diff --git a/lib/features/login/data/network/endpoint.dart b/lib/features/login/data/network/endpoint.dart new file mode 100644 index 000000000..b1f527519 --- /dev/null +++ b/lib/features/login/data/network/endpoint.dart @@ -0,0 +1,7 @@ +import 'package:core/data/network/config/service_path.dart'; + +class Endpoint { + static const String oidc = '/oidc'; + + static final ServicePath webFinger = ServicePath('/.well-known/webfinger'); +} diff --git a/lib/features/login/data/network/oidc_http_client.dart b/lib/features/login/data/network/oidc_http_client.dart new file mode 100644 index 000000000..b4cde1600 --- /dev/null +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -0,0 +1,32 @@ + +import 'dart:convert'; + +import 'package:core/core.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; +import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; + +class OIDCHttpClient { + + final DioClient _dioClient; + + OIDCHttpClient(this._dioClient); + + Future checkOIDCIsAvailable(OIDCRequest oidcRequest) async { + final result = await _dioClient.get( + Endpoint.webFinger + .generateOIDCPath(Uri.parse(oidcRequest.baseUrl)) + .withQueryParameters([ + StringQueryParameter('resource', oidcRequest.resourceUrl), + StringQueryParameter('rel', oidcRequest.relUrl), + ]) + .generateEndpointPath() + ); + log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result'); + if (result is Map) { + return OIDCResponse.fromJson(result); + } else { + return OIDCResponse.fromJson(jsonDecode(result)); + } + } +} \ 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 new file mode 100644 index 000000000..b6c982e51 --- /dev/null +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -0,0 +1,14 @@ +import 'package:model/oidc/request/oidc_request.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; + +class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { + final AuthenticationOIDCDataSource _oidcDataSource; + + AuthenticationOIDCRepositoryImpl(this._oidcDataSource); + + @override + Future checkOIDCIsAvailable(OIDCRequest oidcRequest) { + return _oidcDataSource.checkOIDCIsAvailable(oidcRequest); + } +} \ 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 new file mode 100644 index 000000000..0ec51e61f --- /dev/null +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -0,0 +1,6 @@ + +import 'package:model/oidc/request/oidc_request.dart'; + +abstract class AuthenticationOIDCRepository { + Future checkOIDCIsAvailable(OIDCRequest oidcRequest); +} \ No newline at end of file diff --git a/lib/features/login/domain/state/check_oidc_is_available_state.dart b/lib/features/login/domain/state/check_oidc_is_available_state.dart new file mode 100644 index 000000000..3a26181b4 --- /dev/null +++ b/lib/features/login/domain/state/check_oidc_is_available_state.dart @@ -0,0 +1,18 @@ +import 'package:core/core.dart'; + +class CheckOIDCIsAvailableSuccess extends UIState { + + CheckOIDCIsAvailableSuccess(); + + @override + List get props => []; +} + +class CheckOIDCIsAvailableFailure extends FeatureFailure { + final dynamic exception; + + CheckOIDCIsAvailableFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/check_oidc_is_available_interactor.dart b/lib/features/login/domain/usecases/check_oidc_is_available_interactor.dart new file mode 100644 index 000000000..f067ab944 --- /dev/null +++ b/lib/features/login/domain/usecases/check_oidc_is_available_interactor.dart @@ -0,0 +1,24 @@ +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; + +class CheckOIDCIsAvailableInteractor { + final AuthenticationOIDCRepository _oidcRepository; + + CheckOIDCIsAvailableInteractor(this._oidcRepository); + + Future> execute(OIDCRequest oidcRequest) async { + try { + final result = await _oidcRepository.checkOIDCIsAvailable(oidcRequest); + log('CheckOIDCIsAvailableInteractor::execute(): result: $result'); + return result + ? Right(CheckOIDCIsAvailableSuccess()) + : Left(CheckOIDCIsAvailableFailure(null)); + } catch (e) { + log('CheckOIDCIsAvailableInteractor::execute(): ERROR: $e'); + return Left(CheckOIDCIsAvailableFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/base_login_view.dart b/lib/features/login/presentation/base_login_view.dart index d03ab12f3..2af56b079 100644 --- a/lib/features/login/presentation/base_login_view.dart +++ b/lib/features/login/presentation/base_login_view.dart @@ -42,6 +42,8 @@ abstract class BaseLoginView extends GetWidget { (success) { if (loginController.loginFormType.value == LoginFormType.credentialForm || kIsWeb) { return AppLocalizations.of(context).loginInputCredentialMessage; + } else if (loginController.loginFormType.value == LoginFormType.ssoForm) { + return AppLocalizations.of(context).loginInputSSOMessage; } return AppLocalizations.of(context).loginInputUrlMessage; })) diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index 95c2821f1..ef85aaac6 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -3,33 +3,48 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; +import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; import 'package:tmail_ui_user/features/login/data/repository/authentication_repository_impl.dart'; import 'package:tmail_ui_user/features/login/data/repository/credential_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; class LoginBindings extends BaseBindings { + @override + void dependencies() { + Get.lazyPut(() => OIDCHttpClient(Get.find())); + super.dependencies(); + } + @override void bindingsController() { Get.lazyPut(() => LoginController( Get.find(), Get.find(), Get.find(), + Get.find(), )); } @override void bindingsDataSource() { Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override void bindingsDataSourceImpl() { Get.lazyPut(() => AuthenticationDataSourceImpl()); + Get.lazyPut(() => AuthenticationOIDCDataSourceImpl(Get.find())); } @override @@ -38,17 +53,22 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => CheckOIDCIsAvailableInteractor( + Get.find(), + )); } @override void bindingsRepository() { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override void bindingsRepositoryImpl() { Get.lazyPut(() => CredentialRepositoryImpl(Get.find())); Get.lazyPut(() => AuthenticationRepositoryImpl(Get.find())); + Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(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 67e1b7160..d4bae0319 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -5,7 +5,9 @@ import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -17,6 +19,7 @@ class LoginController extends GetxController { final AuthenticationInteractor _authenticationInteractor; final DynamicUrlInterceptors _dynamicUrlInterceptors; final AuthorizationInterceptors _authorizationInterceptors; + final CheckOIDCIsAvailableInteractor _checkOIDCIsAvailableInteractor; final TextEditingController urlInputController = TextEditingController(); @@ -24,6 +27,7 @@ class LoginController extends GetxController { this._authenticationInteractor, this._dynamicUrlInterceptors, this._authorizationInterceptors, + this._checkOIDCIsAvailableInteractor, ); var loginState = LoginState(Right(LoginInitAction())).obs; @@ -33,20 +37,48 @@ class LoginController extends GetxController { String? _userNameText; String? _passwordText; - void setUrlText(String url) => _urlText = url.formatURLValid(); + void setUrlText(String url) => _urlText = url.trim().formatURLValid(); void setUserNameText(String userName) => _userNameText = userName; void setPasswordText(String password) => _passwordText = password; - Uri? _parseUri(String? url) => url != null ? Uri.parse(url.trim()) : null; + Uri? _parseUri(String? url) => url != null && url.trim().isNotEmpty + ? Uri.parse(url.trim()) + : null; - UserName? _parseUserName(String? userName) => userName != null ? UserName(userName.trim()) : null; + UserName? _parseUserName(String? userName) => userName != null && userName.trim().isNotEmpty + ? UserName(userName.trim()) + : null; - Password? _parsePassword(String? password) => password != null ? Password(password.trim()) : null; + Password? _parsePassword(String? password) => password != null && password.trim().isNotEmpty + ? Password(password.trim()) + : null; void handleNextInUrlInputFormPress() { - completeInputUrlAction(); + _checkOIDCIsAvailable(); + } + + void _checkOIDCIsAvailable() async { + final baseUri = BuildUtils.isWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + + if (baseUri == null) { + loginState.value = LoginState(Left(LoginMissUrlAction())); + } else { + loginState.value = LoginState(Right(LoginLoadingAction())); + await _checkOIDCIsAvailableInteractor + .execute(OIDCRequest(baseUrl: baseUri.path)) + .then((response) => response.fold( + (failure) => _showFormLoginWithCredentialAction(), + (success) => success is CheckOIDCIsAvailableSuccess + ? _showFormLoginWithSSOAction(success) + : _showFormLoginWithCredentialAction())); + } + } + + void _showFormLoginWithSSOAction(CheckOIDCIsAvailableSuccess success) { + loginState.value = LoginState(Right(success)); + loginFormType.value = LoginFormType.ssoForm; } void handleBackInCredentialForm() { @@ -54,23 +86,27 @@ class LoginController extends GetxController { loginFormType.value = LoginFormType.baseUrlForm; } - void completeInputUrlAction() { + void _showFormLoginWithCredentialAction() { loginState.value = LoginState(Right(InputUrlCompletion())); loginFormType.value = LoginFormType.credentialForm; } void handleLoginPressed() { - final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); - final userName = _parseUserName(_userNameText); - final password = _parsePassword(_passwordText); - if (baseUri != null && userName != null && password != null) { - _loginAction(baseUri, userName, password); - } else if (baseUri == null) { - loginState.value = LoginState(Left(LoginMissUrlAction())); - } else if (userName == null) { - loginState.value = LoginState(Left(LoginMissUsernameAction())); - } else if (password == null) { - loginState.value = LoginState(Left(LoginMissPasswordAction())); + if (loginFormType.value == LoginFormType.ssoForm) { + + } else { + final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + final userName = _parseUserName(_userNameText); + final password = _parsePassword(_passwordText); + if (baseUri != null && userName != null && password != null) { + _loginAction(baseUri, userName, password); + } else if (baseUri == null) { + loginState.value = LoginState(Left(LoginMissUrlAction())); + } else if (userName == null) { + loginState.value = LoginState(Left(LoginMissUsernameAction())); + } else if (password == null) { + loginState.value = LoginState(Left(LoginMissPasswordAction())); + } } } diff --git a/lib/features/login/presentation/login_form_type.dart b/lib/features/login/presentation/login_form_type.dart index a5eb9d79b..dd8804698 100644 --- a/lib/features/login/presentation/login_form_type.dart +++ b/lib/features/login/presentation/login_form_type.dart @@ -1,3 +1,3 @@ enum LoginFormType { - none, baseUrlForm, credentialForm + none, baseUrlForm, credentialForm, ssoForm } \ No newline at end of file diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index 840519cc2..abbb2371a 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -26,9 +26,11 @@ class LoginView extends BaseLoginView { children: [ Center(child: SingleChildScrollView(child: _buildCenterForm(context), scrollDirection: Axis.vertical)), Obx(() { - return loginController.loginFormType.value == LoginFormType.credentialForm - ? _buildBackButton(context) - : const SizedBox.shrink(); + if (loginController.loginFormType.value == LoginFormType.credentialForm + || loginController.loginFormType.value == LoginFormType.ssoForm) { + return _buildBackButton(context); + } + return const SizedBox.shrink(); }) ] ) @@ -36,9 +38,11 @@ class LoginView extends BaseLoginView { children: [ _buildCenterForm(context), Obx(() { - return loginController.loginFormType.value == LoginFormType.credentialForm - ? _buildBackButton(context) - : const SizedBox.shrink(); + if (loginController.loginFormType.value == LoginFormType.credentialForm + || loginController.loginFormType.value == LoginFormType.ssoForm) { + return _buildBackButton(context); + } + return const SizedBox.shrink(); }) ] ), @@ -78,15 +82,23 @@ class LoginView extends BaseLoginView { Obx(() { switch (controller.loginFormType.value) { case LoginFormType.baseUrlForm: - return _supportScrollForm(context) - ? _buildNextButton(context) - : _buildExpandedButton(context, _buildNextButton(context)); + return Obx(() => loginController.loginState.value.viewState.fold( + (failure) => _buildNextButtonInContext(context), + (success) => success is LoginLoadingAction + ? _buildLoadingCircularProgress() + : _buildNextButtonInContext(context))); case LoginFormType.credentialForm: return Obx(() => loginController.loginState.value.viewState.fold( (failure) => _buildLoginButtonInContext(context), (success) => success is LoginLoadingAction ? _buildLoadingCircularProgress() : _buildLoginButtonInContext(context))); + case LoginFormType.ssoForm: + return Obx(() => loginController.loginState.value.viewState.fold( + (failure) => _buildLoginButtonInContext(context), + (success) => success is LoginLoadingAction + ? _buildLoadingCircularProgress() + : _buildLoginButtonInContext(context))); default: return const SizedBox.shrink(); } @@ -166,6 +178,12 @@ class LoginView extends BaseLoginView { ); } + Widget _buildNextButtonInContext(BuildContext context) { + return _supportScrollForm(context) + ? _buildNextButton(context) + : _buildExpandedButton(context, _buildNextButton(context)); + } + Widget _buildLoginButtonInContext(BuildContext context) { return _supportScrollForm(context) ? buildLoginButton(context) diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index cb3d04fe7..d40cb9e80 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1387,4 +1387,10 @@ class AppLocalizations { name: 'thisEmailAddressInvalid', ); } + + String get loginInputSSOMessage { + return Intl.message( + 'Sign-in with my SSO account', + name: 'loginInputSSOMessage'); + } } \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index bb119c9d4..60723ef97 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -71,4 +71,9 @@ export 'upload/upload_response.dart'; export 'autocomplete/auto_complete_pattern.dart'; // Identity -export 'identity/identity_request_dto.dart'; \ No newline at end of file +export 'identity/identity_request_dto.dart'; + +// OIDC +export 'oidc/response/oidc_response.dart'; +export 'oidc/response/oidc_link_dto.dart'; +export 'oidc/request/oidc_request.dart'; \ No newline at end of file diff --git a/model/lib/oidc/converter/uri_converter.dart b/model/lib/oidc/converter/uri_converter.dart new file mode 100644 index 000000000..1952f2066 --- /dev/null +++ b/model/lib/oidc/converter/uri_converter.dart @@ -0,0 +1,12 @@ + +import 'package:json_annotation/json_annotation.dart'; + +class UriConverter implements JsonConverter { + const UriConverter(); + + @override + Uri fromJson(String json) => Uri.parse(json); + + @override + String toJson(Uri uri) => uri.path; +} \ No newline at end of file diff --git a/model/lib/oidc/request/oidc_request.dart b/model/lib/oidc/request/oidc_request.dart new file mode 100644 index 000000000..f47a1512a --- /dev/null +++ b/model/lib/oidc/request/oidc_request.dart @@ -0,0 +1,14 @@ + +import 'package:equatable/equatable.dart'; + +class OIDCRequest with EquatableMixin { + + final String baseUrl; + final String resourceUrl = 'https://gateway.upn.integration-open-paas.org'; + final String relUrl = 'http://openid.net/specs/connect/1.0/issuer'; + + OIDCRequest({required this.baseUrl}); + + @override + List get props => [baseUrl, resourceUrl, relUrl]; +} \ No newline at end of file diff --git a/model/lib/oidc/response/oidc_link_dto.dart b/model/lib/oidc/response/oidc_link_dto.dart new file mode 100644 index 000000000..574e1b914 --- /dev/null +++ b/model/lib/oidc/response/oidc_link_dto.dart @@ -0,0 +1,23 @@ + +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:model/oidc/converter/uri_converter.dart'; + +part 'oidc_link_dto.g.dart'; + +@UriConverter() +@JsonSerializable() +class OIDCLinkDto with EquatableMixin { + + final Uri rel; + final Uri href; + + OIDCLinkDto(this.rel, this.href); + + factory OIDCLinkDto.fromJson(Map json) => _$OIDCLinkDtoFromJson(json); + + Map toJson() => _$OIDCLinkDtoToJson(this); + + @override + List get props => [rel, href]; +} \ No newline at end of file diff --git a/model/lib/oidc/response/oidc_response.dart b/model/lib/oidc/response/oidc_response.dart new file mode 100644 index 000000000..1be55d83a --- /dev/null +++ b/model/lib/oidc/response/oidc_response.dart @@ -0,0 +1,24 @@ + +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:model/oidc/converter/uri_converter.dart'; +import 'package:model/oidc/response/oidc_link_dto.dart'; + +part 'oidc_response.g.dart'; + +@UriConverter() +@JsonSerializable() +class OIDCResponse with EquatableMixin { + + final String subject; + final List links; + + OIDCResponse(this.subject, this.links); + + factory OIDCResponse.fromJson(Map json) => _$OIDCResponseFromJson(json); + + Map toJson() => _$OIDCResponseToJson(this); + + @override + List get props => [subject, links]; +} \ No newline at end of file