diff --git a/lib/helpers/account_helper.dart b/lib/helpers/account_helper.dart index 935b49c..f628385 100644 --- a/lib/helpers/account_helper.dart +++ b/lib/helpers/account_helper.dart @@ -158,6 +158,24 @@ class AccountHelper { .execWithThrow()) .getObject()["reset_token"]; + /// 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(); + + /// Change account password using password reset token + /// + /// Throws an exception in case of failure + static Future changeAccountPassword( + String token, String password) async => + await APIRequest.withoutLogin("account/reset_user_passwd") + .addString("token", token) + .addString("password", password) + .execWithThrow(); + /// Get current user ID from the server Future _downloadCurrentUserID() async { final response = await APIRequest( diff --git a/lib/ui/routes/password_reset_route.dart b/lib/ui/routes/password_reset_route.dart new file mode 100644 index 0000000..bc5c254 --- /dev/null +++ b/lib/ui/routes/password_reset_route.dart @@ -0,0 +1,129 @@ +import 'package:comunic/helpers/account_helper.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/safe_state.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Reset user password route +/// +/// @author Pierre HUBERT + +class PasswordResetRoute extends StatelessWidget { + final String token; + + const PasswordResetRoute({ + Key key, + @required this.token, + }) : assert(token != null), + super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(tr("Change your password")), + ), + body: _PasswordResetBody(token: token), + ); + } +} + +class _PasswordResetBody extends StatefulWidget { + final String token; + + const _PasswordResetBody({Key key, @required this.token}) + : assert(token != null), + super(key: key); + + @override + __PasswordResetBodyState createState() => __PasswordResetBodyState(); +} + +enum _Status { BEFORE_CHANGE, WHILE_CHANGE, AFTER_CHANGE } + +class __PasswordResetBodyState extends SafeState<_PasswordResetBody> { + final _key = GlobalKey(); + + var _status = _Status.BEFORE_CHANGE; + + void _setStatus(_Status s) => setState(() => _status = s); + + Future _validateToken() async { + _status = _Status.BEFORE_CHANGE; + await AccountHelper.validatePasswordResetToken(widget.token); + } + + @override + Widget build(BuildContext context) { + return AsyncScreenWidget( + key: _key, + onReload: _validateToken, // The first time, we validate the token + onBuild: _buildBody, + errorMessage: tr( + "Could not validate your password reset token! Maybe it has expired now..."), + ); + } + + Widget _buildBody() { + // The user has not requested yet to change his password + if (_status == _Status.BEFORE_CHANGE) + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(tr("You can choose a new password.")), + OutlineButton( + onPressed: _changePassword, + child: Text(tr("Choose a new password")), + ) + ], + )); + + // Password is being changed + if (_status == _Status.WHILE_CHANGE) return buildCenteredProgressBar(); + + // Password was successfully changed! + if (_status == _Status.AFTER_CHANGE) + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + tr("Congratulations! Your password has now been successfully changed!"), + textAlign: TextAlign.center, + ), + OutlineButton( + onPressed: _quitScreen, + child: Text(tr("Login")), + ) + ], + )); + + throw Exception("Unreachable statement!"); + } + + /// Ask the user to specify his new password + void _changePassword() async { + try { + // Ask for new password + final newPass = await showInputNewPassword(context); + if (newPass == null) return; + _setStatus(_Status.WHILE_CHANGE); + + // Apply it + await AccountHelper.changeAccountPassword(widget.token, newPass); + _setStatus(_Status.AFTER_CHANGE); + } catch (e, s) { + print("Could not change user password! $e\n$s"); + showSimpleSnack(context, tr("Could not change your password!")); + + // We start everything again + _key.currentState.refresh(); + } + } + + /// Go back to login screen + void _quitScreen() => Navigator.of(context).pop(); +} diff --git a/lib/ui/routes/reset_password_route.dart b/lib/ui/routes/reset_password_route.dart index 944d646..0747cf4 100644 --- a/lib/ui/routes/reset_password_route.dart +++ b/lib/ui/routes/reset_password_route.dart @@ -1,4 +1,5 @@ import 'package:comunic/helpers/account_helper.dart'; +import 'package:comunic/ui/routes/password_reset_route.dart'; import 'package:comunic/ui/widgets/dialogs/cancel_dialog_button.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/utils/input_utils.dart'; @@ -80,6 +81,8 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> { case _SelectedOption.SECURITY_QUESTIONS: return _buildSecurityQuestionsScreen(); } + + throw Exception("Unreachable statement!"); } Widget _buildEnterEmailAddressScreen() { @@ -225,7 +228,8 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> { _setLoading(true); try { final token = await AccountHelper.checkAnswers(_emailAddress, _answers); - print(token); + + _useResetToken(token); } catch (e, s) { print("Could not submit security answers! $e\n$s"); showSimpleSnack( @@ -233,6 +237,10 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> { } _setLoading(false); } + + /// Call this method whenever the user managed to get a password reset token + void _useResetToken(String token) => Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (c) => PasswordResetRoute(token: token))); } class _Spacer extends StatelessWidget {