From c5d1512375451340f9a159b91322c72bdbee8141 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 18 Feb 2021 18:58:47 +0100 Subject: [PATCH] Apply password policy on all forms --- lib/helpers/account_helper.dart | 23 ++++++-- lib/models/api_request.dart | 4 ++ .../res_check_password_reset_token.dart | 19 ++++++ lib/ui/dialogs/input_new_password_dialog.dart | 39 ++++++++---- .../dialogs/input_user_password_dialog.dart | 2 +- lib/ui/routes/create_account_route.dart | 8 ++- lib/ui/routes/password_reset_route.dart | 14 ++++- .../settings/account_security_settings.dart | 15 ++++- lib/ui/widgets/new_password_input_widget.dart | 59 +++++++++++-------- lib/utils/input_utils.dart | 2 +- 10 files changed, 135 insertions(+), 50 deletions(-) create mode 100644 lib/models/res_check_password_reset_token.dart diff --git a/lib/helpers/account_helper.dart b/lib/helpers/account_helper.dart index 3ae238d..3f1362a 100644 --- a/lib/helpers/account_helper.dart +++ b/lib/helpers/account_helper.dart @@ -4,6 +4,7 @@ import 'package:comunic/helpers/websocket_helper.dart'; import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/authentication_details.dart'; import 'package:comunic/models/new_account.dart'; +import 'package:comunic/models/res_check_password_reset_token.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Account helper @@ -125,6 +126,11 @@ class AccountHelper { .execWithThrow()) .getObject()["exists"]; + /// Get current user email address + static Future getCurrentAccountEmailAddress() async => + (await APIRequest.withLogin("account/mail") + .execWithThrowGetObject())["mail"]; + /// Check out whether security questions have been set for an account or not /// /// Throws in case of failure @@ -161,10 +167,19 @@ class AccountHelper { /// Check a password reset token /// /// Throws in case failure - static Future validatePasswordResetToken(String token) async => - await APIRequest.withoutLogin("account/check_password_reset_token") - .addString("token", token) - .execWithThrow(); + static Future validatePasswordResetToken( + String token) async { + final response = + await APIRequest.withoutLogin("account/check_password_reset_token") + .addString("token", token) + .execWithThrowGetObject(); + + return ResCheckPasswordToken( + firstName: response["first_name"], + lastName: response["last_name"], + email: response["mail"], + ); + } /// Change account password using password reset token /// diff --git a/lib/models/api_request.dart b/lib/models/api_request.dart index c3b3b9c..c263e60 100644 --- a/lib/models/api_request.dart +++ b/lib/models/api_request.dart @@ -88,6 +88,10 @@ class APIRequest { /// Execute the request, throws an exception in case of failure Future execWithThrow() async => (await exec()).assertOk(); + /// Execute the request, throws an exception in case of failure + Future> execWithThrowGetObject() async => + (await execWithThrow()).getObject(); + /// Execute the request with files Future execWithFiles() async => APIHelper().execWithFiles(this); diff --git a/lib/models/res_check_password_reset_token.dart b/lib/models/res_check_password_reset_token.dart new file mode 100644 index 0000000..f3bd53a --- /dev/null +++ b/lib/models/res_check_password_reset_token.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +/// Check password reset token result +/// +/// @author Pierre Hubert + +class ResCheckPasswordToken { + final String firstName; + final String lastName; + final String email; + + const ResCheckPasswordToken({ + @required this.firstName, + @required this.lastName, + @required this.email, + }) : assert(firstName != null), + assert(lastName != null), + assert(email != null); +} diff --git a/lib/ui/dialogs/input_new_password_dialog.dart b/lib/ui/dialogs/input_new_password_dialog.dart index d727d7e..85e7766 100644 --- a/lib/ui/dialogs/input_new_password_dialog.dart +++ b/lib/ui/dialogs/input_new_password_dialog.dart @@ -1,5 +1,5 @@ import 'package:comunic/ui/widgets/dialogs/auto_sized_dialog_content_widget.dart'; -import 'package:comunic/utils/input_utils.dart'; +import 'package:comunic/ui/widgets/new_password_input_widget.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:flutter/material.dart'; @@ -8,27 +8,44 @@ import 'package:flutter/material.dart'; /// @author Pierre HUBERT /// Ask the user to enter a new password -Future showInputNewPassword(BuildContext context) async { +Future showInputNewPassword({ + @required BuildContext context, + @required UserInfoForPassword userInfo, +}) async { + assert(context != null); + assert(userInfo != null); return await showDialog( - context: context, builder: (c) => _InputNewPasswordDialog()); + context: context, + builder: (c) => _InputNewPasswordDialog( + userInfo: userInfo, + )); } class _InputNewPasswordDialog extends StatefulWidget { + final UserInfoForPassword userInfo; + + const _InputNewPasswordDialog({ + Key key, + @required this.userInfo, + }) : assert(userInfo != null), + super(key: key); + @override __InputNewPasswordDialogState createState() => __InputNewPasswordDialogState(); } class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> { - final _controller1 = TextEditingController(); + final _controller1 = GlobalKey(); final _controller2 = TextEditingController(); final _focusScopeNode = FocusScopeNode(); - String get _password => _controller1.text; + String get _password => _controller1.currentState.value; - bool get _input1Valid => validatePassword(_password); + bool get _input1Valid => + _controller1.currentState != null && _controller1.currentState.valid; - bool get _input2Valid => _controller1.text == _controller2.text; + bool get _input2Valid => _controller1.currentState.value == _controller2.text; bool get _isValid => _input1Valid && _input2Valid; @@ -65,12 +82,10 @@ class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> { child: Column( children: [ // Input 1 - _buildPasswordField( - controller: _controller1, + NewPasswordInputWidget( + key: _controller1, + user: widget.userInfo, label: tr("Your new password"), - errorText: _controller1.text.isNotEmpty && !_input1Valid - ? tr("Invalid password!") - : null, textInputAction: TextInputAction.next, onSubmitted: () => _focusScopeNode.nextFocus(), ), diff --git a/lib/ui/dialogs/input_user_password_dialog.dart b/lib/ui/dialogs/input_user_password_dialog.dart index 01e2489..44414ee 100644 --- a/lib/ui/dialogs/input_user_password_dialog.dart +++ b/lib/ui/dialogs/input_user_password_dialog.dart @@ -30,7 +30,7 @@ class __InputUserPasswordDialogState String get _currPass => _controller.text; bool get _canSubmit => - validatePassword(_controller.text) && _status != _Status.CHECKING; + legacyValidatePassword(_controller.text) && _status != _Status.CHECKING; void _setStatus(_Status s) => setState(() => _status = s); diff --git a/lib/ui/routes/create_account_route.dart b/lib/ui/routes/create_account_route.dart index 998e7b4..eba23ea 100644 --- a/lib/ui/routes/create_account_route.dart +++ b/lib/ui/routes/create_account_route.dart @@ -139,9 +139,11 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> { label: tr("Password"), onEdited: _updateUI, icon: Icon(Icons.lock), - email: _emailController.text, - firstName: _firstNameController.text, - lastName: _lastNameController.text, + user: UserInfoForPassword( + firstName: _firstNameController.text, + lastName: _lastNameController.text, + email: _emailController.text, + ), ), // Verify password diff --git a/lib/ui/routes/password_reset_route.dart b/lib/ui/routes/password_reset_route.dart index bc5c254..c3f2574 100644 --- a/lib/ui/routes/password_reset_route.dart +++ b/lib/ui/routes/password_reset_route.dart @@ -1,6 +1,8 @@ import 'package:comunic/helpers/account_helper.dart'; +import 'package:comunic/models/res_check_password_reset_token.dart'; import 'package:comunic/ui/dialogs/input_new_password_dialog.dart'; import 'package:comunic/ui/widgets/async_screen_widget.dart'; +import 'package:comunic/ui/widgets/new_password_input_widget.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; @@ -47,12 +49,13 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> { final _key = GlobalKey(); var _status = _Status.BEFORE_CHANGE; + ResCheckPasswordToken _tokenInfo; void _setStatus(_Status s) => setState(() => _status = s); Future _validateToken() async { _status = _Status.BEFORE_CHANGE; - await AccountHelper.validatePasswordResetToken(widget.token); + _tokenInfo = await AccountHelper.validatePasswordResetToken(widget.token); } @override @@ -108,7 +111,14 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> { void _changePassword() async { try { // Ask for new password - final newPass = await showInputNewPassword(context); + final newPass = await showInputNewPassword( + context: context, + userInfo: UserInfoForPassword( + firstName: _tokenInfo.firstName, + lastName: _tokenInfo.lastName, + email: _tokenInfo.email, + ), + ); if (newPass == null) return; _setStatus(_Status.WHILE_CHANGE); diff --git a/lib/ui/routes/settings/account_security_settings.dart b/lib/ui/routes/settings/account_security_settings.dart index 0306234..3682897 100644 --- a/lib/ui/routes/settings/account_security_settings.dart +++ b/lib/ui/routes/settings/account_security_settings.dart @@ -1,10 +1,13 @@ import 'package:comunic/helpers/account_helper.dart'; import 'package:comunic/helpers/settings_helper.dart'; +import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/models/security_settings.dart'; import 'package:comunic/ui/dialogs/input_new_password_dialog.dart'; import 'package:comunic/ui/dialogs/input_user_password_dialog.dart'; import 'package:comunic/ui/widgets/dialogs/auto_sized_dialog_content_widget.dart'; +import 'package:comunic/ui/widgets/new_password_input_widget.dart'; import 'package:comunic/ui/widgets/settings/header_spacer_section.dart'; +import 'package:comunic/utils/account_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; @@ -55,11 +58,21 @@ class _AccountSecuritySettingsScreenState /// Change current user password void _changePassword() async { try { + final currEmail = await AccountHelper.getCurrentAccountEmailAddress(); + final currUser = await UsersHelper().getSingleWithThrow(userID()); + final currPassword = await showUserPasswordDialog(context); if (currPassword == null) return; - final newPassword = await showInputNewPassword(context); + final newPassword = await showInputNewPassword( + context: context, + userInfo: UserInfoForPassword( + firstName: currUser.firstName, + lastName: currUser.lastName, + email: currEmail, + ), + ); if (newPassword == null) return; diff --git a/lib/ui/widgets/new_password_input_widget.dart b/lib/ui/widgets/new_password_input_widget.dart index 8100535..6d47273 100644 --- a/lib/ui/widgets/new_password_input_widget.dart +++ b/lib/ui/widgets/new_password_input_widget.dart @@ -1,6 +1,5 @@ import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/models/server_config.dart'; -import 'package:comunic/models/user.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; @@ -9,26 +8,38 @@ import 'package:flutter/material.dart'; /// /// @author Pierre Hubert -class NewPasswordInputWidget extends StatefulWidget { - final Widget icon; - final VoidCallback onEdited; - final String label; - - final User user; +class UserInfoForPassword { final String firstName; final String lastName; final String email; + const UserInfoForPassword({ + @required this.firstName, + @required this.lastName, + @required this.email, + }); +} + +class NewPasswordInputWidget extends StatefulWidget { + final Widget icon; + final VoidCallback onEdited; + final VoidCallback onSubmitted; + final TextInputAction textInputAction; + final String label; + + final UserInfoForPassword user; + const NewPasswordInputWidget({ Key key, this.icon, this.onEdited, + this.onSubmitted, + this.textInputAction, @required this.label, - this.user, - this.firstName, - this.lastName, - this.email, - }) : super(key: key); + @required this.user, + }) : assert(label != null), + assert(user != null), + super(key: key); @override NewPasswordInputWidgetState createState() => NewPasswordInputWidgetState(); @@ -73,6 +84,9 @@ class NewPasswordInputWidgetState extends State { controller: _controller, obscureText: true, onChanged: (s) => _onChanged(), + onSubmitted: + widget.onSubmitted == null ? null : (s) => widget.onSubmitted(), + textInputAction: widget.textInputAction, decoration: InputDecoration( errorText: _errorMessage, errorMaxLines: 3, @@ -92,28 +106,21 @@ class NewPasswordInputWidgetState extends State { // Mandatory checks if (!_policy.allowMailInPassword && - (widget.email ?? "").isNotEmpty && - (widget.email.toLowerCase().contains(value.toLowerCase()) || - value.toLowerCase().contains(widget.email.toLowerCase()))) { - return tr("Your password must not contains part of your email address!"); - } - - if (!_policy.allowMailInPassword && - (widget.email ?? "").isNotEmpty && - (widget.email.toLowerCase().contains(value.toLowerCase()) || - value.toLowerCase().contains(widget.email.toLowerCase()))) { + (widget.user.email ?? "").isNotEmpty && + (widget.user.email.toLowerCase().contains(value.toLowerCase()) || + value.toLowerCase().contains(widget.user.email.toLowerCase()))) { return tr("Your password must not contains part of your email address!"); } if (!_policy.allowNameInPassword && - (widget.firstName ?? "").isNotEmpty && - value.toLowerCase().contains(widget.firstName.toLowerCase())) { + (widget.user.firstName ?? "").isNotEmpty && + value.toLowerCase().contains(widget.user.firstName.toLowerCase())) { return tr("Your password must not contains your first name!"); } if (!_policy.allowNameInPassword && - (widget.lastName ?? "").isNotEmpty && - value.toLowerCase().contains(widget.lastName.toLowerCase())) { + (widget.user.lastName ?? "").isNotEmpty && + value.toLowerCase().contains(widget.user.lastName.toLowerCase())) { return tr("Your password must not contains your last name!"); } diff --git a/lib/utils/input_utils.dart b/lib/utils/input_utils.dart index 3e443c3..dda67da 100644 --- a/lib/utils/input_utils.dart +++ b/lib/utils/input_utils.dart @@ -3,7 +3,7 @@ /// @author Pierre HUBERT /// Check out whether a password is valid or not -bool validatePassword(String s) => s.length > 3; +bool legacyValidatePassword(String s) => s.length > 3; /// Check out whether a given email address is valid or not