TF-2387 Handle sign in SAAS platform
This commit is contained in:
@@ -8,4 +8,8 @@ class OIDCConstant {
|
|||||||
static const authResponseKey = "auth_info";
|
static const authResponseKey = "auth_info";
|
||||||
|
|
||||||
static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId;
|
static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId;
|
||||||
|
|
||||||
|
static String get saasClientId => PlatformInfo.isMobile
|
||||||
|
? mobileOidcClientId
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
@@ -45,3 +45,7 @@ class CanNotFoundPassword implements Exception {}
|
|||||||
class CanNotAuthenticationInfoOnWeb implements Exception {}
|
class CanNotAuthenticationInfoOnWeb implements Exception {}
|
||||||
|
|
||||||
class NotFoundAuthenticationInfoCache implements Exception {}
|
class NotFoundAuthenticationInfoCache implements Exception {}
|
||||||
|
|
||||||
|
class CanNotFoundSaasServerUrl implements Exception {}
|
||||||
|
|
||||||
|
class SaasServerUriIsNull implements Exception {}
|
||||||
@@ -9,6 +9,7 @@ import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart'
|
|||||||
import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart';
|
import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart';
|
import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/widgets/dns_lookup_input_form.dart';
|
import 'package:tmail_ui_user/features/login/presentation/widgets/dns_lookup_input_form.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/widgets/horizontal_progress_loading_button.dart';
|
import 'package:tmail_ui_user/features/login/presentation/widgets/horizontal_progress_loading_button.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/widgets/login_back_button.dart';
|
import 'package:tmail_ui_user/features/login/presentation/widgets/login_back_button.dart';
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:model/oidc/token_oidc.dart';
|
||||||
|
|
||||||
|
abstract class SaasAuthenticationDataSource {
|
||||||
|
Future<TokenOIDC> signIn(OIDCConfiguration oidcConfiguration);
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:model/oidc/token_oidc.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
|
||||||
|
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
|
||||||
|
|
||||||
|
class SaasAuthenticationDataSourceImpl extends SaasAuthenticationDataSource {
|
||||||
|
|
||||||
|
final AuthenticationClientBase _authenticationClient;
|
||||||
|
final ExceptionThrower _exceptionThrower;
|
||||||
|
|
||||||
|
SaasAuthenticationDataSourceImpl(
|
||||||
|
this._authenticationClient,
|
||||||
|
this._exceptionThrower,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TokenOIDC> signIn(OIDCConfiguration oidcConfiguration) {
|
||||||
|
return Future.sync(() async {
|
||||||
|
return await _authenticationClient.getTokenOIDC(
|
||||||
|
oidcConfiguration.clientId,
|
||||||
|
oidcConfiguration.redirectUrl,
|
||||||
|
oidcConfiguration.discoveryUrl,
|
||||||
|
oidcConfiguration.scopes,
|
||||||
|
);
|
||||||
|
}).catchError(_exceptionThrower.throwException);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:model/oidc/token_oidc.dart';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart';
|
||||||
|
|
||||||
|
class SaasAuthenticationRepositoryImpl extends SaasAuthenticationRepository {
|
||||||
|
|
||||||
|
final SaasAuthenticationDataSource _saasAuthenticationDataSource;
|
||||||
|
|
||||||
|
SaasAuthenticationRepositoryImpl(this._saasAuthenticationDataSource);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TokenOIDC> signIn(OIDCConfiguration oidcConfiguration) {
|
||||||
|
return _saasAuthenticationDataSource.signIn(oidcConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:model/oidc/token_oidc.dart';
|
||||||
|
|
||||||
|
abstract class SaasAuthenticationRepository {
|
||||||
|
Future<TokenOIDC> signIn(OIDCConfiguration oidcConfiguration);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:core/presentation/state/failure.dart';
|
||||||
|
import 'package:core/presentation/state/success.dart';
|
||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:model/oidc/token_oidc.dart';
|
||||||
|
|
||||||
|
class SignInSaasLoading extends LoadingState {}
|
||||||
|
|
||||||
|
class SignInSaasSuccess extends Success {
|
||||||
|
final TokenOIDC tokenOIDC;
|
||||||
|
final Uri baseUri;
|
||||||
|
final OIDCConfiguration oidcConfiguration;
|
||||||
|
|
||||||
|
SignInSaasSuccess(this.tokenOIDC, this.baseUri, this.oidcConfiguration);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [tokenOIDC, baseUri, oidcConfiguration];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignInSaasFailure extends FeatureFailure {
|
||||||
|
SignInSaasFailure(dynamic exception) : super(exception: exception);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:core/presentation/state/failure.dart';
|
||||||
|
import 'package:core/presentation/state/success.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:model/account/authentication_type.dart';
|
||||||
|
import 'package:model/account/personal_account.dart';
|
||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:model/oidc/token_oidc.dart';
|
||||||
|
import 'package:rich_text_composer/views/commons/logger.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/exceptions/login_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';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_saas_state.dart';
|
||||||
|
|
||||||
|
class SignInSaasInteractor {
|
||||||
|
final SaasAuthenticationRepository _saasRepository;
|
||||||
|
final AuthenticationOIDCRepository _authenticationOIDCRepository;
|
||||||
|
final AccountRepository _accountRepository;
|
||||||
|
final CredentialRepository _credentialRepository;
|
||||||
|
|
||||||
|
SignInSaasInteractor(
|
||||||
|
this._saasRepository,
|
||||||
|
this._authenticationOIDCRepository,
|
||||||
|
this._accountRepository,
|
||||||
|
this._credentialRepository
|
||||||
|
);
|
||||||
|
|
||||||
|
Stream<Either<Failure, Success>> execute({
|
||||||
|
required Uri baseUri,
|
||||||
|
required OIDCConfiguration oidcConfiguration
|
||||||
|
}) async* {
|
||||||
|
try {
|
||||||
|
yield Right<Failure, Success>(SignInSaasLoading());
|
||||||
|
|
||||||
|
final tokenOIDC = await _saasRepository.signIn(oidcConfiguration);
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
_credentialRepository.saveBaseUrl(baseUri),
|
||||||
|
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
|
||||||
|
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await _accountRepository.setCurrentAccount(
|
||||||
|
PersonalAccount(
|
||||||
|
tokenOIDC.tokenIdHash,
|
||||||
|
AuthenticationType.oidc,
|
||||||
|
isSelected: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
yield Right<Failure, Success>(SignInSaasSuccess(tokenOIDC, baseUri, oidcConfiguration));
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
logError('SignInSaasInteractor::execute():PlatformException ${e.message} - ${e.stacktrace}');
|
||||||
|
if (NoSuitableBrowserForOIDCException.verifyException(e)) {
|
||||||
|
yield Left<Failure, Success>(SignInSaasFailure(NoSuitableBrowserForOIDCException()));
|
||||||
|
} else {
|
||||||
|
yield Left<Failure, Success>(SignInSaasFailure(e));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError('SignInSaasInteractor::execute():Exception = $e');
|
||||||
|
yield Left<Failure, Success>(SignInSaasFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,60 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:tmail_ui_user/features/base/base_bindings.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.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';
|
||||||
|
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_saas_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart';
|
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart';
|
||||||
|
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
|
||||||
|
|
||||||
|
class TwakeWelcomeBindings extends BaseBindings {
|
||||||
|
|
||||||
class TwakeWelcomeBindings extends Bindings {
|
|
||||||
@override
|
@override
|
||||||
void dependencies() {
|
void bindingsController() {
|
||||||
Get.lazyPut(() => TwakeWelcomeController());
|
Get.lazyPut(() => TwakeWelcomeController(
|
||||||
|
Get.find<SignInSaasInteractor>(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void bindingsInteractor() {
|
||||||
|
Get.lazyPut(() => SignInSaasInteractor(
|
||||||
|
Get.find<SaasAuthenticationRepository>(),
|
||||||
|
Get.find<AuthenticationOIDCRepository>(),
|
||||||
|
Get.find<AccountRepository>(),
|
||||||
|
Get.find<CredentialRepository>(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void bindingsDataSourceImpl() {
|
||||||
|
Get.lazyPut(() => SaasAuthenticationDataSourceImpl(
|
||||||
|
Get.find<AuthenticationClientBase>(),
|
||||||
|
Get.find<RemoteExceptionThrower>(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void bindingsDataSource() {
|
||||||
|
Get.lazyPut<SaasAuthenticationDataSource>(
|
||||||
|
() => Get.find<SaasAuthenticationDataSourceImpl>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void bindingsRepository() {
|
||||||
|
Get.lazyPut<SaasAuthenticationRepository>(
|
||||||
|
() => Get.find<SaasAuthenticationRepositoryImpl>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void bindingsRepositoryImpl() {
|
||||||
|
Get.lazyPut(() => SaasAuthenticationRepositoryImpl(
|
||||||
|
Get.find<SaasAuthenticationDataSource>(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+66
-2
@@ -1,12 +1,25 @@
|
|||||||
import 'package:tmail_ui_user/features/base/base_controller.dart';
|
import 'package:core/presentation/state/success.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
||||||
|
import 'package:model/oidc/oidc_configuration.dart';
|
||||||
|
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/login_form_type.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/login/presentation/model/login_arguments.dart';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_saas_state.dart';
|
||||||
|
import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_saas_interactor.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/app_routes.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_navigation.dart';
|
||||||
|
import 'package:tmail_ui_user/main/routes/route_utils.dart';
|
||||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||||
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
||||||
|
|
||||||
class TwakeWelcomeController extends BaseController {
|
class TwakeWelcomeController extends ReloadableController {
|
||||||
|
|
||||||
|
final SignInSaasInteractor _signInSaasInteractor;
|
||||||
|
|
||||||
|
TwakeWelcomeController(this._signInSaasInteractor);
|
||||||
|
|
||||||
void handleUseCompanyServer() {
|
void handleUseCompanyServer() {
|
||||||
popAndPush(
|
popAndPush(
|
||||||
@@ -17,4 +30,55 @@ class TwakeWelcomeController extends BaseController {
|
|||||||
void onClickPrivacyPolicy() {
|
void onClickPrivacyPolicy() {
|
||||||
AppUtils.launchLink(AppConfig.linagoraPrivacyUrl);
|
AppUtils.launchLink(AppConfig.linagoraPrivacyUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onClickSignIn() {
|
||||||
|
if (AppConfig.saasServerUrl.isEmpty) {
|
||||||
|
consumeState(Stream.value(Left(SignInSaasFailure(CanNotFoundSaasServerUrl()))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final baseUri = Uri.tryParse(AppConfig.saasServerUrl);
|
||||||
|
|
||||||
|
if (baseUri == null) {
|
||||||
|
consumeState(Stream.value(Left(SignInSaasFailure(SaasServerUriIsNull()))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeState(_signInSaasInteractor.execute(
|
||||||
|
baseUri: baseUri,
|
||||||
|
oidcConfiguration: OIDCConfiguration(
|
||||||
|
authority: AppConfig.registrationUrl,
|
||||||
|
clientId: OIDCConstant.saasClientId,
|
||||||
|
scopes: AppConfig.oidcScopes
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleSuccessViewState(Success success) {
|
||||||
|
if (success is SignInSaasSuccess) {
|
||||||
|
_handleSignInSaasSuccess(success);
|
||||||
|
} else {
|
||||||
|
super.handleSuccessViewState(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleReloaded(Session session) {
|
||||||
|
popAndPush(
|
||||||
|
RouteUtils.generateNavigationRoute(AppRoutes.dashboard),
|
||||||
|
arguments: session);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSignInSaasSuccess(SignInSaasSuccess success) {
|
||||||
|
dynamicUrlInterceptors.setJmapUrl(success.baseUri.toString());
|
||||||
|
dynamicUrlInterceptors.changeBaseUrl(success.baseUri.toString());
|
||||||
|
authorizationInterceptors.setTokenAndAuthorityOidc(
|
||||||
|
newToken: success.tokenOIDC,
|
||||||
|
newConfig: success.oidcConfiguration);
|
||||||
|
authorizationIsolateInterceptors.setTokenAndAuthorityOidc(
|
||||||
|
newToken: success.tokenOIDC,
|
||||||
|
newConfig: success.oidcConfiguration);
|
||||||
|
getSessionAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart';
|
|||||||
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart';
|
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_view_style.dart';
|
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_view_style.dart';
|
||||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
|
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||||
|
|
||||||
class TwakeWelcomeView extends GetWidget<TwakeWelcomeController> {
|
class TwakeWelcomeView extends GetWidget<TwakeWelcomeController> {
|
||||||
|
|
||||||
@@ -31,8 +32,13 @@ class TwakeWelcomeView extends GetWidget<TwakeWelcomeController> {
|
|||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '',
|
signInTitle: AppConfig.isSaasPlatForm
|
||||||
createTwakeIdTitle: AppLocalizations.of(context).createTwakeId,
|
? AppLocalizations.of(context).signIn.capitalizeFirst ?? ''
|
||||||
|
: null,
|
||||||
|
onSignInOnTap: controller.onClickSignIn,
|
||||||
|
createTwakeIdTitle: AppConfig.isSaasPlatForm
|
||||||
|
? AppLocalizations.of(context).createTwakeId
|
||||||
|
: null,
|
||||||
useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer,
|
useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer,
|
||||||
description: AppLocalizations.of(context).descriptionWelcomeTo,
|
description: AppLocalizations.of(context).descriptionWelcomeTo,
|
||||||
privacyPolicy: AppLocalizations.of(context).privacyPolicy,
|
privacyPolicy: AppLocalizations.of(context).privacyPolicy,
|
||||||
|
|||||||
@@ -66,4 +66,8 @@ class AppConfig {
|
|||||||
static String get _platformEnv => dotenv.get('PLATFORM', fallback: 'other');
|
static String get _platformEnv => dotenv.get('PLATFORM', fallback: 'other');
|
||||||
|
|
||||||
static bool get isSaasPlatForm => _platformEnv.toLowerCase() == saasPlatform;
|
static bool get isSaasPlatForm => _platformEnv.toLowerCase() == saasPlatform;
|
||||||
|
|
||||||
|
static String get registrationUrl => dotenv.get('REGISTRATION_URL', fallback: '');
|
||||||
|
|
||||||
|
static String get saasServerUrl => dotenv.get('SAAS_SERVER_URL', fallback: '');
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,6 @@ class OIDCConfiguration with EquatableMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get clientIdHash => clientId.hashCode.toString();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
authority,
|
authority,
|
||||||
|
|||||||
Reference in New Issue
Block a user