1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-22 21:09:21 +00:00

Apply password policy on all forms

This commit is contained in:
Pierre HUBERT 2021-02-18 18:58:47 +01:00
parent 16ec9a8e00
commit c5d1512375
10 changed files with 135 additions and 50 deletions

View File

@ -4,6 +4,7 @@ import 'package:comunic/helpers/websocket_helper.dart';
import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/authentication_details.dart'; import 'package:comunic/models/authentication_details.dart';
import 'package:comunic/models/new_account.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'; import 'package:shared_preferences/shared_preferences.dart';
/// Account helper /// Account helper
@ -125,6 +126,11 @@ class AccountHelper {
.execWithThrow()) .execWithThrow())
.getObject()["exists"]; .getObject()["exists"];
/// Get current user email address
static Future<String> getCurrentAccountEmailAddress() async =>
(await APIRequest.withLogin("account/mail")
.execWithThrowGetObject())["mail"];
/// Check out whether security questions have been set for an account or not /// Check out whether security questions have been set for an account or not
/// ///
/// Throws in case of failure /// Throws in case of failure
@ -161,10 +167,19 @@ class AccountHelper {
/// Check a password reset token /// Check a password reset token
/// ///
/// Throws in case failure /// Throws in case failure
static Future<void> validatePasswordResetToken(String token) async => static Future<ResCheckPasswordToken> validatePasswordResetToken(
await APIRequest.withoutLogin("account/check_password_reset_token") String token) async {
.addString("token", token) final response =
.execWithThrow(); 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 /// Change account password using password reset token
/// ///

View File

@ -88,6 +88,10 @@ class APIRequest {
/// Execute the request, throws an exception in case of failure /// Execute the request, throws an exception in case of failure
Future<APIResponse> execWithThrow() async => (await exec()).assertOk(); Future<APIResponse> execWithThrow() async => (await exec()).assertOk();
/// Execute the request, throws an exception in case of failure
Future<Map<String, dynamic>> execWithThrowGetObject() async =>
(await execWithThrow()).getObject();
/// Execute the request with files /// Execute the request with files
Future<APIResponse> execWithFiles() async => APIHelper().execWithFiles(this); Future<APIResponse> execWithFiles() async => APIHelper().execWithFiles(this);

View File

@ -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);
}

View File

@ -1,5 +1,5 @@
import 'package:comunic/ui/widgets/dialogs/auto_sized_dialog_content_widget.dart'; 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:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -8,27 +8,44 @@ import 'package:flutter/material.dart';
/// @author Pierre HUBERT /// @author Pierre HUBERT
/// Ask the user to enter a new password /// Ask the user to enter a new password
Future<String> showInputNewPassword(BuildContext context) async { Future<String> showInputNewPassword({
@required BuildContext context,
@required UserInfoForPassword userInfo,
}) async {
assert(context != null);
assert(userInfo != null);
return await showDialog( return await showDialog(
context: context, builder: (c) => _InputNewPasswordDialog()); context: context,
builder: (c) => _InputNewPasswordDialog(
userInfo: userInfo,
));
} }
class _InputNewPasswordDialog extends StatefulWidget { class _InputNewPasswordDialog extends StatefulWidget {
final UserInfoForPassword userInfo;
const _InputNewPasswordDialog({
Key key,
@required this.userInfo,
}) : assert(userInfo != null),
super(key: key);
@override @override
__InputNewPasswordDialogState createState() => __InputNewPasswordDialogState createState() =>
__InputNewPasswordDialogState(); __InputNewPasswordDialogState();
} }
class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> { class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> {
final _controller1 = TextEditingController(); final _controller1 = GlobalKey<NewPasswordInputWidgetState>();
final _controller2 = TextEditingController(); final _controller2 = TextEditingController();
final _focusScopeNode = FocusScopeNode(); 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; bool get _isValid => _input1Valid && _input2Valid;
@ -65,12 +82,10 @@ class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
// Input 1 // Input 1
_buildPasswordField( NewPasswordInputWidget(
controller: _controller1, key: _controller1,
user: widget.userInfo,
label: tr("Your new password"), label: tr("Your new password"),
errorText: _controller1.text.isNotEmpty && !_input1Valid
? tr("Invalid password!")
: null,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onSubmitted: () => _focusScopeNode.nextFocus(), onSubmitted: () => _focusScopeNode.nextFocus(),
), ),

View File

@ -30,7 +30,7 @@ class __InputUserPasswordDialogState
String get _currPass => _controller.text; String get _currPass => _controller.text;
bool get _canSubmit => bool get _canSubmit =>
validatePassword(_controller.text) && _status != _Status.CHECKING; legacyValidatePassword(_controller.text) && _status != _Status.CHECKING;
void _setStatus(_Status s) => setState(() => _status = s); void _setStatus(_Status s) => setState(() => _status = s);

View File

@ -139,9 +139,11 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
label: tr("Password"), label: tr("Password"),
onEdited: _updateUI, onEdited: _updateUI,
icon: Icon(Icons.lock), icon: Icon(Icons.lock),
email: _emailController.text, user: UserInfoForPassword(
firstName: _firstNameController.text, firstName: _firstNameController.text,
lastName: _lastNameController.text, lastName: _lastNameController.text,
email: _emailController.text,
),
), ),
// Verify password // Verify password

View File

@ -1,6 +1,8 @@
import 'package:comunic/helpers/account_helper.dart'; 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/dialogs/input_new_password_dialog.dart';
import 'package:comunic/ui/widgets/async_screen_widget.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/ui/widgets/safe_state.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
@ -47,12 +49,13 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
final _key = GlobalKey<AsyncScreenWidgetState>(); final _key = GlobalKey<AsyncScreenWidgetState>();
var _status = _Status.BEFORE_CHANGE; var _status = _Status.BEFORE_CHANGE;
ResCheckPasswordToken _tokenInfo;
void _setStatus(_Status s) => setState(() => _status = s); void _setStatus(_Status s) => setState(() => _status = s);
Future<void> _validateToken() async { Future<void> _validateToken() async {
_status = _Status.BEFORE_CHANGE; _status = _Status.BEFORE_CHANGE;
await AccountHelper.validatePasswordResetToken(widget.token); _tokenInfo = await AccountHelper.validatePasswordResetToken(widget.token);
} }
@override @override
@ -108,7 +111,14 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
void _changePassword() async { void _changePassword() async {
try { try {
// Ask for new password // 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; if (newPass == null) return;
_setStatus(_Status.WHILE_CHANGE); _setStatus(_Status.WHILE_CHANGE);

View File

@ -1,10 +1,13 @@
import 'package:comunic/helpers/account_helper.dart'; import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/settings_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/models/security_settings.dart';
import 'package:comunic/ui/dialogs/input_new_password_dialog.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/dialogs/input_user_password_dialog.dart';
import 'package:comunic/ui/widgets/dialogs/auto_sized_dialog_content_widget.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/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/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -55,11 +58,21 @@ class _AccountSecuritySettingsScreenState
/// Change current user password /// Change current user password
void _changePassword() async { void _changePassword() async {
try { try {
final currEmail = await AccountHelper.getCurrentAccountEmailAddress();
final currUser = await UsersHelper().getSingleWithThrow(userID());
final currPassword = await showUserPasswordDialog(context); final currPassword = await showUserPasswordDialog(context);
if (currPassword == null) return; 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; if (newPassword == null) return;

View File

@ -1,6 +1,5 @@
import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/server_config.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/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,26 +8,38 @@ import 'package:flutter/material.dart';
/// ///
/// @author Pierre Hubert /// @author Pierre Hubert
class NewPasswordInputWidget extends StatefulWidget { class UserInfoForPassword {
final Widget icon;
final VoidCallback onEdited;
final String label;
final User user;
final String firstName; final String firstName;
final String lastName; final String lastName;
final String email; 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({ const NewPasswordInputWidget({
Key key, Key key,
this.icon, this.icon,
this.onEdited, this.onEdited,
this.onSubmitted,
this.textInputAction,
@required this.label, @required this.label,
this.user, @required this.user,
this.firstName, }) : assert(label != null),
this.lastName, assert(user != null),
this.email, super(key: key);
}) : super(key: key);
@override @override
NewPasswordInputWidgetState createState() => NewPasswordInputWidgetState(); NewPasswordInputWidgetState createState() => NewPasswordInputWidgetState();
@ -73,6 +84,9 @@ class NewPasswordInputWidgetState extends State<NewPasswordInputWidget> {
controller: _controller, controller: _controller,
obscureText: true, obscureText: true,
onChanged: (s) => _onChanged(), onChanged: (s) => _onChanged(),
onSubmitted:
widget.onSubmitted == null ? null : (s) => widget.onSubmitted(),
textInputAction: widget.textInputAction,
decoration: InputDecoration( decoration: InputDecoration(
errorText: _errorMessage, errorText: _errorMessage,
errorMaxLines: 3, errorMaxLines: 3,
@ -92,28 +106,21 @@ class NewPasswordInputWidgetState extends State<NewPasswordInputWidget> {
// Mandatory checks // Mandatory checks
if (!_policy.allowMailInPassword && if (!_policy.allowMailInPassword &&
(widget.email ?? "").isNotEmpty && (widget.user.email ?? "").isNotEmpty &&
(widget.email.toLowerCase().contains(value.toLowerCase()) || (widget.user.email.toLowerCase().contains(value.toLowerCase()) ||
value.toLowerCase().contains(widget.email.toLowerCase()))) { value.toLowerCase().contains(widget.user.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()))) {
return tr("Your password must not contains part of your email address!"); return tr("Your password must not contains part of your email address!");
} }
if (!_policy.allowNameInPassword && if (!_policy.allowNameInPassword &&
(widget.firstName ?? "").isNotEmpty && (widget.user.firstName ?? "").isNotEmpty &&
value.toLowerCase().contains(widget.firstName.toLowerCase())) { value.toLowerCase().contains(widget.user.firstName.toLowerCase())) {
return tr("Your password must not contains your first name!"); return tr("Your password must not contains your first name!");
} }
if (!_policy.allowNameInPassword && if (!_policy.allowNameInPassword &&
(widget.lastName ?? "").isNotEmpty && (widget.user.lastName ?? "").isNotEmpty &&
value.toLowerCase().contains(widget.lastName.toLowerCase())) { value.toLowerCase().contains(widget.user.lastName.toLowerCase())) {
return tr("Your password must not contains your last name!"); return tr("Your password must not contains your last name!");
} }

View File

@ -3,7 +3,7 @@
/// @author Pierre HUBERT /// @author Pierre HUBERT
/// Check out whether a password is valid or not /// 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 /// Check out whether a given email address is valid or not