diff --git a/lib/helpers/settings_helper.dart b/lib/helpers/settings_helper.dart index 2b05e79..f5a1080 100644 --- a/lib/helpers/settings_helper.dart +++ b/lib/helpers/settings_helper.dart @@ -5,6 +5,7 @@ import 'package:comunic/models/account_image_settings.dart'; import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/general_settings.dart'; import 'package:comunic/models/new_emoji.dart'; +import 'package:comunic/models/security_settings.dart'; /// Settings helper /// @@ -161,4 +162,36 @@ class SettingsHelper { .addString("oldPassword", oldPassword) .addString("newPassword", newPassword) .execWithThrow(); + + /// Retrieve security settings of the user + /// + /// This method throws in case of failure + static Future getSecuritySettings(String password) async { + final response = + (await APIRequest(uri: "settings/get_security", needLogin: true) + .addString("password", password) + .execWithThrow()) + .getObject(); + + return SecuritySettings( + securityQuestion1: response["security_question_1"], + securityAnswer1: response["security_answer_1"], + securityQuestion2: response["security_question_2"], + securityAnswer2: response["security_answer_2"], + ); + } + + /// Apply new security settings to the user + /// + /// Throws in case of failure + static Future setSecuritySettings( + String password, SecuritySettings newSettings) async { + await APIRequest(uri: "settings/set_security", needLogin: true) + .addString("password", password) + .addString("security_question_1", newSettings.securityQuestion1) + .addString("security_answer_1", newSettings.securityAnswer1) + .addString("security_question_2", newSettings.securityQuestion2) + .addString("security_answer_2", newSettings.securityAnswer2) + .execWithThrow(); + } } diff --git a/lib/models/security_settings.dart b/lib/models/security_settings.dart new file mode 100644 index 0000000..157f01f --- /dev/null +++ b/lib/models/security_settings.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +/// Security settings of the user +/// +/// @author Pierre HUBERT + +class SecuritySettings { + final String securityQuestion1; + final String securityAnswer1; + final String securityQuestion2; + final String securityAnswer2; + + const SecuritySettings({ + @required this.securityQuestion1, + @required this.securityAnswer1, + @required this.securityQuestion2, + @required this.securityAnswer2, + }) : assert(securityQuestion1 != null), + assert(securityAnswer1 != null), + assert(securityQuestion2 != null), + assert(securityAnswer2 != null); +} diff --git a/lib/ui/routes/account_settings/account_security_settings.dart b/lib/ui/routes/account_settings/account_security_settings.dart index c73fc26..946003a 100644 --- a/lib/ui/routes/account_settings/account_security_settings.dart +++ b/lib/ui/routes/account_settings/account_security_settings.dart @@ -1,6 +1,8 @@ import 'package:comunic/helpers/settings_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/auto_sized_dialog_content_widget.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; @@ -41,6 +43,12 @@ class __AccountSecuritySettingsScreenBodyState title: tr("Change password"), onTap: _changePassword, ), + SettingsTile( + title: tr("Change your security questions"), + subtitle: tr( + "Your security questions can be used to recover an access to your account when you loose your password..."), + onTap: _changeSecurityQuestions, + ), ], ) ], @@ -67,4 +75,118 @@ class __AccountSecuritySettingsScreenBodyState showSimpleSnack(context, tr("Could not update password!")); } } + + /// Change security questions + void _changeSecurityQuestions() async { + try { + final password = await showUserPasswordDialog(context); + + if (password == null) return; + + final settings = await SettingsHelper.getSecuritySettings(password); + + final newSettings = await showDialog( + context: context, + builder: (c) => _SecurityQuestionsDialog(settings: settings)); + + if (newSettings == null) return; + + await SettingsHelper.setSecuritySettings(password, newSettings); + + showSimpleSnack(context, + tr("You security questions have been successfully updated!")); + } catch (e, stack) { + print("Could not update security questions!$e\n$stack"); + showSimpleSnack(context, tr("Could not update security questions!")); + } + } +} + +class _SecurityQuestionsDialog extends StatefulWidget { + final SecuritySettings settings; + + const _SecurityQuestionsDialog({Key key, @required this.settings}) + : assert(settings != null), + super(key: key); + + @override + __SecurityQuestionsDialogState createState() => + __SecurityQuestionsDialogState(); +} + +class __SecurityQuestionsDialogState extends State<_SecurityQuestionsDialog> { + TextEditingController _controllerQuestion1; + TextEditingController _controllerAnswer1; + TextEditingController _controllerQuestion2; + TextEditingController _controllerAnswer2; + + SecuritySettings get _newSettings => SecuritySettings( + securityQuestion1: _controllerQuestion1.text, + securityAnswer1: _controllerAnswer1.text, + securityQuestion2: _controllerQuestion2.text, + securityAnswer2: _controllerAnswer2.text, + ); + + @override + void initState() { + _controllerQuestion1 = + TextEditingController(text: widget.settings.securityQuestion1); + _controllerAnswer1 = + TextEditingController(text: widget.settings.securityAnswer1); + _controllerQuestion2 = + TextEditingController(text: widget.settings.securityQuestion2); + _controllerAnswer2 = + TextEditingController(text: widget.settings.securityAnswer2); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(tr("Update security questions")), + content: AutoSizeDialogContentWidget(child: _buildContent()), + actions: [ + MaterialButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(tr("Cancel").toUpperCase()), + ), + MaterialButton( + onPressed: () => Navigator.of(context).pop(_newSettings), + child: Text(tr("Update").toUpperCase()), + ) + ], + ); + } + + Widget _buildContent() { + return Column( + children: [ + Text(tr( + "Note: Your two questions and answers MUST be completed in order to be able to recover your account using your security questions!")), + _buildTextField( + controller: _controllerQuestion1, label: tr("Question 1")), + _buildTextField(controller: _controllerAnswer1, label: tr("Answer 1")), + _buildTextField( + controller: _controllerQuestion2, label: tr("Question 2")), + _buildTextField(controller: _controllerAnswer2, label: tr("Answer 2")), + ], + ); + } + + Widget _buildTextField({ + @required TextEditingController controller, + @required String label, + }) { + return TextField( + controller: controller, + onChanged: (s) => setState(() {}), + decoration: InputDecoration( + alignLabelWithHint: true, + labelText: label, + errorText: controller.text.isNotEmpty && controller.text.length < 5 + ? tr("Unsafe value!") + : null, + ), + ); + } }