diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 29018458e..fc7d339e6 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -13,6 +13,7 @@ import 'package:photos/models/set_recovery_key_request.dart'; import 'package:photos/ui/ott_verification_page.dart'; import 'package:photos/ui/password_entry_page.dart'; import 'package:photos/ui/password_reentry_page.dart'; +import 'package:photos/ui/two_factor_authentication_page.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; @@ -92,13 +93,18 @@ class UserService { ); await dialog.hide(); if (response != null && response.statusCode == 200) { - await _saveConfiguration(response); showToast("email verification successful!"); var page; - if (Configuration.instance.getEncryptedToken() != null) { - page = PasswordReentryPage(); + final String twoFASessionID = response.data["twoFactorSessionID"]; + if (twoFASessionID != null && twoFASessionID.isNotEmpty) { + page = TwoFactorAuthenticationPage(twoFASessionID); } else { - page = PasswordEntryPage(); + await _saveConfiguration(response); + if (Configuration.instance.getEncryptedToken() != null) { + page = PasswordReentryPage(); + } else { + page = PasswordEntryPage(); + } } Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( @@ -192,6 +198,42 @@ class UserService { } } + Future verifyTwoFactor( + BuildContext context, String sessionID, String code) async { + final dialog = createProgressDialog(context, "authenticating..."); + await dialog.show(); + try { + final response = await _dio.post( + _config.getHttpEndpoint() + "/users/two-factor/verify", + data: { + "sessionID": sessionID, + "code": code, + }, + ); + await dialog.hide(); + if (response != null && response.statusCode == 200) { + showToast("authentication successful!"); + await _saveConfiguration(response); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) { + return PasswordReentryPage(); + }, + ), + (route) => route.isFirst, + ); + } else { + showErrorDialog( + context, "oops", "authentication failed, please try again"); + } + } catch (e) { + await dialog.hide(); + _logger.severe(e); + showErrorDialog( + context, "oops", "authentication failed, please try again"); + } + } + Future _saveConfiguration(Response response) async { await Configuration.instance.setUserID(response.data["id"]); if (response.data["encryptedToken"] != null) { diff --git a/lib/ui/two_factor_authentication_page.dart b/lib/ui/two_factor_authentication_page.dart new file mode 100644 index 000000000..62ffe09a8 --- /dev/null +++ b/lib/ui/two_factor_authentication_page.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/services/user_service.dart'; +import 'package:photos/ui/common_elements.dart'; +import 'package:photos/ui/two_factor_recovery_page.dart'; +import 'package:photos/utils/dialog_util.dart'; +import 'package:pinput/pin_put/pin_put.dart'; + +class TwoFactorAuthenticationPage extends StatefulWidget { + final String sessionID; + + const TwoFactorAuthenticationPage(this.sessionID, {Key key}) + : super(key: key); + + @override + _TwoFactorAuthenticationPageState createState() => + _TwoFactorAuthenticationPageState(); +} + +class _TwoFactorAuthenticationPageState + extends State { + final _pinController = TextEditingController(); + final _pinPutDecoration = BoxDecoration( + border: Border.all(color: Color.fromRGBO(45, 194, 98, 1.0)), + borderRadius: BorderRadius.circular(15.0), + ); + String _code = ""; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + "two-factor authentication", + ), + ), + body: _getBody(), + ); + } + + Widget _getBody() { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + "enter the 6-digit code from\nyour authenticator app", + style: TextStyle( + height: 1.4, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + Padding(padding: EdgeInsets.all(32)), + Padding( + padding: const EdgeInsets.fromLTRB(40, 0, 40, 0), + child: PinPut( + fieldsCount: 6, + onSubmit: (String code) { + _verifyTwoFactorCode(code); + }, + onChanged: (String pin) { + setState(() { + _code = pin; + }); + }, + controller: _pinController, + submittedFieldDecoration: _pinPutDecoration.copyWith( + borderRadius: BorderRadius.circular(20.0), + ), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration.copyWith( + borderRadius: BorderRadius.circular(5.0), + border: Border.all( + color: Color.fromRGBO(45, 194, 98, 0.5), + ), + ), + inputDecoration: InputDecoration( + focusedBorder: InputBorder.none, + border: InputBorder.none, + counterText: '', + ), + ), + ), + Padding(padding: EdgeInsets.all(24)), + Container( + padding: const EdgeInsets.fromLTRB(80, 0, 80, 0), + width: double.infinity, + height: 64, + child: button( + "authenticate", + fontSize: 18, + onPressed: _code.length == 6 + ? () async { + _verifyTwoFactorCode(_code); + } + : null, + ), + ), + Padding(padding: EdgeInsets.all(30)), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return TwoFactorRecoveryPage(); + }, + ), + ); + }, + child: Container( + padding: EdgeInsets.all(10), + child: Center( + child: Text( + "lost device?", + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + ), + ), + ), + ), + ), + ], + ); + } + + Future _verifyTwoFactorCode(String code) async { + await UserService.instance.verifyTwoFactor(context, widget.sessionID, code); + } +} diff --git a/lib/ui/two_factor_recovery_page.dart b/lib/ui/two_factor_recovery_page.dart new file mode 100644 index 000000000..ed70fd026 --- /dev/null +++ b/lib/ui/two_factor_recovery_page.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class TwoFactorRecoveryPage extends StatefulWidget { + TwoFactorRecoveryPage({Key key}) : super(key: key); + + @override + _TwoFactorRecoveryPageState createState() => _TwoFactorRecoveryPageState(); +} + +class _TwoFactorRecoveryPageState extends State { + @override + Widget build(BuildContext context) { + return Container( + child: null, + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index c11ca511a..c2c7a2eb5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -641,6 +641,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.11.1" + pinput: + dependency: "direct main" + description: + name: pinput + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index de8791592..19fee167c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: image_editor: ^1.0.0 syncfusion_flutter_sliders: ^19.1.67-beta syncfusion_flutter_core: ^19.1.67 + pinput: ^1.2.0 dev_dependencies: flutter_test: