diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index c5335433f..f7b4bab56 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -79,6 +79,13 @@ class AuthRepository { return sendRequest(url: url, data: credentials, secret: secret); } + Future logout({@required Credentials credentials}) async { + return webClient.post( + '${credentials.url}/logout', + credentials.token, + ); + } + Future oauthLogin( {@required String idToken, @required String accessToken, @@ -87,7 +94,6 @@ class AuthRepository { @required String platform}) async { final credentials = { 'id_token': idToken, - //'access_token': accessToken, 'provider': 'google', }; url = formatApiUrl(url) + '/oauth_login'; diff --git a/lib/redux/app/app_middleware.dart b/lib/redux/app/app_middleware.dart index d0f9c12a5..67e0890d2 100644 --- a/lib/redux/app/app_middleware.dart +++ b/lib/redux/app/app_middleware.dart @@ -236,7 +236,7 @@ Middleware _createLoadState( store.dispatch(RefreshData( completer: Completer() ..future.catchError((Object error) { - store.dispatch(UserLogout(action.context)); + store.dispatch(UserLogout()); }))); if (uiState.currentRoute != LoginScreen.route && @@ -303,11 +303,11 @@ Middleware _createLoadState( } }).catchError((Object error) { print('Error (app_middleware - refresh): $error'); - store.dispatch(UserLogout(action.context)); + store.dispatch(UserLogout()); }); store.dispatch(RefreshData(completer: completer, clearData: true)); } else { - store.dispatch(UserLogout(action.context)); + store.dispatch(UserLogout()); } } diff --git a/lib/redux/auth/auth_actions.dart b/lib/redux/auth/auth_actions.dart index 6b64fa40e..cf9a594ce 100644 --- a/lib/redux/auth/auth_actions.dart +++ b/lib/redux/auth/auth_actions.dart @@ -92,11 +92,18 @@ class RecoverPasswordFailure implements StopLoading { final Object error; } -class UserLogout implements PersistData, PersistUI { - UserLogout(this.context, {this.navigate = true}); +class UserLogout implements PersistData, PersistUI {} - final BuildContext context; - final bool navigate; +class UserLogoutAll implements StartLoading { + const UserLogoutAll({this.completer}); + final Completer completer; +} + +class UserLogoutAllSuccess implements StopLoading {} + +class UserLogoutAllFailure implements StopLoading { + const UserLogoutAllFailure(this.error); + final Object error; } class UserSignUpRequest implements StartLoading { diff --git a/lib/redux/auth/auth_middleware.dart b/lib/redux/auth/auth_middleware.dart index a1aa21369..29fd213f6 100644 --- a/lib/redux/auth/auth_middleware.dart +++ b/lib/redux/auth/auth_middleware.dart @@ -20,6 +20,7 @@ List> createStoreAuthMiddleware([ AuthRepository repository = const AuthRepository(), ]) { final userLogout = _createUserLogout(); + final userLogoutAll = _createUserLogoutAll(repository); final loginRequest = _createLoginRequest(repository); final oauthLoginRequest = _createOAuthLoginRequest(repository); final signUpRequest = _createSignUpRequest(repository); @@ -33,6 +34,7 @@ List> createStoreAuthMiddleware([ return [ TypedMiddleware(userLogout), + TypedMiddleware(userLogoutAll), TypedMiddleware(loginRequest), TypedMiddleware(oauthLoginRequest), TypedMiddleware(signUpRequest), @@ -57,15 +59,35 @@ Middleware _createUserLogout() { next(action); - if (action.navigate) { - navigatorKey.currentState.pushNamedAndRemoveUntil( - LoginScreen.route, (Route route) => false); - } + navigatorKey.currentState.pushNamedAndRemoveUntil( + LoginScreen.route, (Route route) => false); store.dispatch(UpdateCurrentRoute(LoginScreen.route)); }; } +Middleware _createUserLogoutAll(AuthRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as UserLogoutAll; + + repository + .logout(credentials: store.state.credentials) + .then((dynamic response) { + print('## DONE MIDDLE'); + + store.dispatch(UserLogoutAllSuccess()); + store.dispatch(UserLogout()); + }).catchError((Object error) { + if (action.completer != null) { + //action.completer.completeError(error); + } + store.dispatch(UserLogoutAllFailure(error)); + }); + + next(action); + }; +} + Middleware _createLoginRequest(AuthRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as UserLoginRequest; diff --git a/lib/ui/app/desktop_session_timeout.dart b/lib/ui/app/desktop_session_timeout.dart index f66fd0677..8b7269b7b 100644 --- a/lib/ui/app/desktop_session_timeout.dart +++ b/lib/ui/app/desktop_session_timeout.dart @@ -44,7 +44,7 @@ class _DesktopSessionTimeoutState extends State { state.userCompanyState.lastUpdated; if (sessionLength > sessionTimeout) { - store.dispatch(UserLogout(context)); + store.dispatch(UserLogout()); } else if (sessionLength > (sessionTimeout - (1000 * 60 * 2))) { setState(() { _isWarned = true; diff --git a/lib/ui/app/dialogs/error_dialog.dart b/lib/ui/app/dialogs/error_dialog.dart index 3ee5c3f9b..5bcac0788 100644 --- a/lib/ui/app/dialogs/error_dialog.dart +++ b/lib/ui/app/dialogs/error_dialog.dart @@ -38,7 +38,7 @@ class ErrorDialog extends StatelessWidget { confirmCallback( context: context, callback: () { - store.dispatch(UserLogout(context)); + store.dispatch(UserLogout()); }); }), TextButton( diff --git a/lib/ui/app/menu_drawer_vm.dart b/lib/ui/app/menu_drawer_vm.dart index 898fd0fe1..b80473c47 100644 --- a/lib/ui/app/menu_drawer_vm.dart +++ b/lib/ui/app/menu_drawer_vm.dart @@ -73,7 +73,7 @@ class MenuDrawerVM { message: AppLocalization.of(context).logout, context: context, callback: () async { - store.dispatch(UserLogout(context)); + store.dispatch(UserLogout()); if (store.state.user.isConnectedToGoogle) { GoogleOAuth.signOut(); } diff --git a/lib/ui/app/mobile_session_timeout.dart b/lib/ui/app/mobile_session_timeout.dart index 3f78b1376..4ce037f6e 100644 --- a/lib/ui/app/mobile_session_timeout.dart +++ b/lib/ui/app/mobile_session_timeout.dart @@ -43,10 +43,7 @@ class _MobileSessionTimeoutState extends State { state.userCompanyState.lastUpdated; if (sessionLength > sessionTimeout) { - store.dispatch(UserLogout(context, navigate: false)); - WidgetsBinding.instance.addPostFrameCallback((duration) { - WebUtils.reloadBrowser(); - }); + store.dispatch(UserLogout()); } }, ); diff --git a/lib/ui/settings/account_management_vm.dart b/lib/ui/settings/account_management_vm.dart index fac4398e4..e0936e3df 100644 --- a/lib/ui/settings/account_management_vm.dart +++ b/lib/ui/settings/account_management_vm.dart @@ -70,7 +70,7 @@ class AccountManagementVM { final context = navigatorKey.currentContext; final state = store.state; if (companyLength == 1) { - store.dispatch(UserLogout(context)); + store.dispatch(UserLogout()); if (state.user.isConnectedToGoogle) { GoogleOAuth.disconnect(); } diff --git a/lib/ui/settings/device_settings_list.dart b/lib/ui/settings/device_settings_list.dart index e616e1cca..9f32eef15 100644 --- a/lib/ui/settings/device_settings_list.dart +++ b/lib/ui/settings/device_settings_list.dart @@ -37,6 +37,11 @@ class _DeviceSettingsState extends State { final viewModel = widget.viewModel; final state = viewModel.state; final prefState = state.prefState; + final countSessions = state.tokenState.list + .map((tokenId) => state.tokenState.map[tokenId]) + .where( + (token) => token.isSystem && token.createdUserId == state.user.id) + .length; return Scaffold( appBar: AppBar( @@ -247,6 +252,15 @@ class _DeviceSettingsState extends State { }, ); }), + ListTile( + leading: Icon(Icons.logout), + title: Text(localization.endAllSessions), + subtitle: Text(countSessions == 1 + ? localization.countSession + : localization.countSession + .replaceFirst(':count', '$countSessions')), + onTap: () => viewModel.onLogoutTap(context), + ), ], ) ], diff --git a/lib/ui/settings/device_settings_list_vm.dart b/lib/ui/settings/device_settings_list_vm.dart index 45ed19cfe..5332b7daf 100644 --- a/lib/ui/settings/device_settings_list_vm.dart +++ b/lib/ui/settings/device_settings_list_vm.dart @@ -5,6 +5,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; import 'package:invoiceninja_flutter/ui/app/app_builder.dart'; @@ -34,6 +35,7 @@ class DeviceSettingsVM { DeviceSettingsVM({ @required this.state, @required this.onRefreshTap, + @required this.onLogoutTap, @required this.onDarkModeChanged, @required this.onLayoutChanged, @required this.onRequireAuthenticationChanged, @@ -71,6 +73,11 @@ class DeviceSettingsVM { return DeviceSettingsVM( state: store.state, onRefreshTap: (BuildContext context) => _refreshData(context), + onLogoutTap: (BuildContext context) { + final completer = snackBarCompleter( + context, AppLocalization.of(context).endedAllSessions); + store.dispatch(UserLogoutAll(completer: completer)); + }, onDarkModeChanged: (BuildContext context, bool value) async { store.dispatch(UpdateUserPreferences(enableDarkMode: value)); AppBuilder.of(context).rebuild(); @@ -151,6 +158,7 @@ class DeviceSettingsVM { final AppState state; final Function(BuildContext) onRefreshTap; + final Function(BuildContext) onLogoutTap; final Function(BuildContext, bool) onDarkModeChanged; final Function(BuildContext, AppLayout) onLayoutChanged; final Function(BuildContext, AppSidebarMode) onMenuModeChanged; diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 6b399fcec..2541299f4 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -15,6 +15,10 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'ended_all_sessions': 'Successfully ended all sessions', + 'end_all_sessions': 'End All Sessions', + 'count_session': '1 Session', + 'count_sessions': ':count Sessions', 'invoice_created': 'Invoice Created', 'quote_created': 'Quote Created', 'credit_created': 'Credit Created', @@ -60306,6 +60310,22 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['credit_created'] ?? _localizedValues['en']['credit_created']; + String get endAllSessions => + _localizedValues[localeCode]['end_all_sessions'] ?? + _localizedValues['en']['end_all_sessions']; + + String get countSession => + _localizedValues[localeCode]['count_session'] ?? + _localizedValues['en']['count_session']; + + String get countSessions => + _localizedValues[localeCode]['count_sessions'] ?? + _localizedValues['en']['count_sessions']; + + String get endedAllSessions => + _localizedValues[localeCode]['ended_all_sessions'] ?? + _localizedValues['en']['ended_all_sessions']; + String lookup(String key) { final lookupKey = toSnakeCase(key);