Przeglądaj źródła

Add 2FA code entry screen

Vishnu 4 lat temu
rodzic
commit
e8f3d7bcce

+ 46 - 4
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/ott_verification_page.dart';
 import 'package:photos/ui/password_entry_page.dart';
 import 'package:photos/ui/password_entry_page.dart';
 import 'package:photos/ui/password_reentry_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/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
 
@@ -92,13 +93,18 @@ class UserService {
       );
       );
       await dialog.hide();
       await dialog.hide();
       if (response != null && response.statusCode == 200) {
       if (response != null && response.statusCode == 200) {
-        await _saveConfiguration(response);
         showToast("email verification successful!");
         showToast("email verification successful!");
         var page;
         var page;
-        if (Configuration.instance.getEncryptedToken() != null) {
-          page = PasswordReentryPage();
+        final String twoFASessionID = response.data["twoFactorSessionID"];
+        if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
+          page = TwoFactorAuthenticationPage(twoFASessionID);
         } else {
         } else {
-          page = PasswordEntryPage();
+          await _saveConfiguration(response);
+          if (Configuration.instance.getEncryptedToken() != null) {
+            page = PasswordReentryPage();
+          } else {
+            page = PasswordEntryPage();
+          }
         }
         }
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
@@ -192,6 +198,42 @@ class UserService {
     }
     }
   }
   }
 
 
+  Future<void> 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<void> _saveConfiguration(Response response) async {
   Future<void> _saveConfiguration(Response response) async {
     await Configuration.instance.setUserID(response.data["id"]);
     await Configuration.instance.setUserID(response.data["id"]);
     if (response.data["encryptedToken"] != null) {
     if (response.data["encryptedToken"] != null) {

+ 134 - 0
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<TwoFactorAuthenticationPage> {
+  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<void> _verifyTwoFactorCode(String code) async {
+    await UserService.instance.verifyTwoFactor(context, widget.sessionID, code);
+  }
+}

+ 17 - 0
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<TwoFactorRecoveryPage> {
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+       child: null,
+    );
+  }
+}

+ 7 - 0
pubspec.lock

@@ -641,6 +641,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.11.1"
     version: "0.11.1"
+  pinput:
+    dependency: "direct main"
+    description:
+      name: pinput
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
   platform:
   platform:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 1 - 0
pubspec.yaml

@@ -88,6 +88,7 @@ dependencies:
   image_editor: ^1.0.0
   image_editor: ^1.0.0
   syncfusion_flutter_sliders: ^19.1.67-beta
   syncfusion_flutter_sliders: ^19.1.67-beta
   syncfusion_flutter_core: ^19.1.67
   syncfusion_flutter_core: ^19.1.67
+  pinput: ^1.2.0
 
 
 dev_dependencies:
 dev_dependencies:
   flutter_test:
   flutter_test: