Преглед на файлове

Add support for toggling email MFA (#174)

Neeraj Gupta преди 1 година
родител
ревизия
cf862d291d

+ 2 - 0
lib/app/view/app.dart

@@ -12,6 +12,7 @@ import "package:ente_auth/l10n/l10n.dart";
 import 'package:ente_auth/locale.dart';
 import "package:ente_auth/onboarding/view/onboarding_page.dart";
 import 'package:ente_auth/services/update_service.dart';
+import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/ui/settings/app_update_dialog.dart';
 import 'package:flutter/foundation.dart';
@@ -49,6 +50,7 @@ class _AppState extends State<App> {
       }
     });
     _signedInEvent = Bus.instance.on<SignedInEvent>().listen((event) {
+      UserService.instance.getUserDetailsV2().ignore();
       if (mounted) {
         setState(() {});
       }

+ 2 - 0
lib/l10n/arb/app_en.arb

@@ -87,6 +87,8 @@
   "importInstruction": "Please select a file that contains a list of your codes in the following format",
   "importCodeDelimiterInfo": "The codes can be separated by a comma or a new line",
   "selectFile": "Select file",
+  "emailVerificationToggle": "Email verification",
+  "authToChangeEmailVerificationSetting": "Please authenticate to change email verification",
   "authToViewYourRecoveryKey": "Please authenticate to view your recovery key",
   "authToChangeYourEmail": "Please authenticate to change your email",
   "authToChangeYourPassword": "Please authenticate to change your password",

+ 3 - 0
lib/models/api/user/srp.dart

@@ -87,6 +87,7 @@ class SrpAttributes {
   final int memLimit;
   final int opsLimit;
   final String kekSalt;
+  final bool isEmailMFAEnabled;
 
   SrpAttributes({
     required this.srpUserID,
@@ -94,6 +95,7 @@ class SrpAttributes {
     required this.memLimit,
     required this.opsLimit,
     required this.kekSalt,
+    required this.isEmailMFAEnabled,
   });
 
   factory SrpAttributes.fromMap(Map<String, dynamic> map) {
@@ -103,6 +105,7 @@ class SrpAttributes {
       memLimit: map['attributes']['memLimit'],
       opsLimit: map['attributes']['opsLimit'],
       kekSalt: map['attributes']['kekSalt'],
+      isEmailMFAEnabled: map['attributes']['isEmailMFAEnabled'],
     );
   }
 }

+ 37 - 0
lib/models/user_details.dart

@@ -1,3 +1,4 @@
+import 'dart:convert';
 import 'dart:math';
 
 import 'package:collection/collection.dart';
@@ -10,6 +11,7 @@ class UserDetails {
   final int sharedCollectionsCount;
   final Subscription subscription;
   final FamilyData? familyData;
+  final ProfileData? profileData;
 
   UserDetails(
     this.email,
@@ -18,6 +20,7 @@ class UserDetails {
     this.sharedCollectionsCount,
     this.subscription,
     this.familyData,
+      this.profileData,
   );
 
   bool isPartOfFamily() {
@@ -59,8 +62,10 @@ class UserDetails {
       (map['sharedCollectionsCount'] ?? 0) as int,
       Subscription.fromMap(map['subscription']),
       FamilyData.fromMap(map['familyData']),
+      ProfileData.fromJson(map['profileData']),
     );
   }
+
 }
 
 class FamilyMember {
@@ -80,7 +85,39 @@ class FamilyMember {
     );
   }
 }
+class ProfileData {
+  bool canDisableEmailMFA;
+  bool isEmailMFAEnabled;
+  bool isTwoFactorEnabled;
+
+  // Constructor with default values
+  ProfileData({
+    this.canDisableEmailMFA = false,
+    this.isEmailMFAEnabled = false,
+    this.isTwoFactorEnabled = false,
+  });
+
+  // Factory method to create ProfileData instance from JSON
+  factory ProfileData.fromJson(Map<String, dynamic>? json) {
+    if (json == null) null;
+
+    return ProfileData(
+      canDisableEmailMFA: json!['canDisableEmailMFA'] ?? false,
+      isEmailMFAEnabled: json['isEmailMFAEnabled'] ?? false,
+      isTwoFactorEnabled: json['isTwoFactorEnabled'] ?? false,
+    );
+  }
 
+  // Method to convert ProfileData instance to JSON
+  Map<String, dynamic> toJson() {
+    return {
+      'canDisableEmailMFA': canDisableEmailMFA,
+      'isEmailMFAEnabled': isEmailMFAEnabled,
+      'isTwoFactorEnabled': isTwoFactorEnabled,
+    };
+  }
+  String toJsonString() => json.encode(toJson());
+}
 class FamilyData {
   final List<FamilyMember>? members;
 

+ 30 - 6
lib/services/user_service.dart

@@ -45,6 +45,8 @@ import "package:uuid/uuid.dart";
 class UserService {
   static const keyHasEnabledTwoFactor = "has_enabled_two_factor";
   static const keyUserDetails = "user_details";
+  static const kCanDisableEmailMFA = "can_disable_email_mfa";
+  static const kIsEmailMFAEnabled = "is_email_mfa_enabled";
   final  SRP6GroupParameters kDefaultSrpGroup = SRP6StandardGroups.rfc5054_4096;
   final _dio = Network.instance.getDio();
   final _enteDio = Network.instance.enteDio;
@@ -131,10 +133,9 @@ class UserService {
 
 
   Future<UserDetails> getUserDetailsV2({
-    bool memoryCount = true,
-    bool shouldCache = false,
+    bool memoryCount = false,
+    bool shouldCache = true,
   }) async {
-    _logger.info("Fetching user details");
     try {
       final response = await _enteDio.get(
         "/users/details/v2",
@@ -144,14 +145,18 @@ class UserService {
       );
       final userDetails = UserDetails.fromMap(response.data);
       if (shouldCache) {
+        if(userDetails.profileData != null) {
+          _preferences.setBool(kIsEmailMFAEnabled, userDetails.profileData!.isEmailMFAEnabled);
+          _preferences.setBool(kCanDisableEmailMFA, userDetails.profileData!.canDisableEmailMFA);
+        }
         // handle email change from different client
         if (userDetails.email != _config.getEmail()) {
           setEmail(userDetails.email);
         }
       }
       return userDetails;
-    } on DioError catch (e) {
-      _logger.info(e);
+    } catch(e) {
+      _logger.warning("Failed to fetch", e);
       rethrow;
     }
   }
@@ -890,7 +895,26 @@ class UserService {
     }
   }
 
+  bool? canDisableEmailMFA() {
+    return _preferences.getBool(kCanDisableEmailMFA);;
+  }
+  bool hasEmailMFAEnabled() {
+    return _preferences.getBool(kIsEmailMFAEnabled) ?? true;
+  }
 
-
+  Future<void> updateEmailMFA(bool isEnabled) async {
+    try {
+      await _enteDio.put(
+        "/users/email-mfa",
+        data: {
+          "isEnabled": isEnabled,
+        },
+      );
+      _preferences.setBool(kIsEmailMFAEnabled, isEnabled);
+    } catch (e) {
+      _logger.severe("Failed to update email mfa",e);
+      rethrow;
+    }
+  }
 }
 

+ 13 - 7
lib/ui/account/login_page.dart

@@ -2,6 +2,7 @@ import 'package:email_validator/email_validator.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/errors.dart';
 import "package:ente_auth/l10n/l10n.dart";
+import 'package:ente_auth/models/api/user/srp.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/account/login_pwd_verification_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
@@ -61,22 +62,27 @@ class _LoginPageState extends State<LoginPage> {
         buttonText: context.l10n.logInLabel,
         onPressedFunction: () async {
           UserService.instance.setEmail(_email!);
+          SrpAttributes? attr;
+          bool isEmailVerificationEnabled = true;
           try {
-            final attr = await UserService.instance.getSrpAttributes(_email!);
+            attr = await UserService.instance.getSrpAttributes(_email!);
+            isEmailVerificationEnabled = attr.isEmailMFAEnabled;
+          } catch (e) {
+            if (e is! SrpSetupNotCompleteError) {
+              _logger.severe('Error getting SRP attributes', e);
+            }
+          }
+          if (attr != null && !isEmailVerificationEnabled) {
             Navigator.of(context).push(
               MaterialPageRoute(
                 builder: (BuildContext context) {
                   return LoginPasswordVerificationPage(
-                    srpAttributes: attr,
+                    srpAttributes: attr!,
                   );
                 },
               ),
             );
-          }
-          catch (e) {
-            if(e is! SrpSetupNotCompleteError) {
-              _logger.warning('Error getting SRP attributes', e);
-            }
+          } else {
             await UserService.instance
                 .sendOtt(context, _email!, isCreateAccountScreen: false);
           }

+ 48 - 0
lib/ui/settings/security_section_widget.dart

@@ -1,6 +1,9 @@
+import 'dart:async';
+
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/services/local_authentication_service.dart';
+import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/ui/account/sessions_page.dart';
 import 'package:ente_auth/ui/components/captioned_text_widget.dart';
@@ -8,6 +11,7 @@ import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
 import 'package:ente_auth/ui/components/menu_item_widget.dart';
 import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
 import 'package:ente_auth/ui/settings/common_settings.dart';
+import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 
 class SecuritySectionWidget extends StatefulWidget {
@@ -41,6 +45,15 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
   }
 
   Widget _getSectionOptions(BuildContext context) {
+    final bool? canDisableMFA = UserService.instance.canDisableEmailMFA();
+    if (canDisableMFA == null) {
+      // We don't know if the user can disable MFA yet, so we fetch the info
+      UserService.instance.getUserDetailsV2().then(
+            (value) => {
+              if (mounted) {setState(() {})}
+            },
+          );
+    }
     final l10n = context.l10n;
     final List<Widget> children = [];
     children.addAll([
@@ -64,6 +77,33 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
           },
         ),
       ),
+      if(canDisableMFA ?? false)
+        sectionOptionSpacing,
+      if(canDisableMFA ?? false)
+        MenuItemWidget(
+          captionedTextWidget:  CaptionedTextWidget(
+            title: l10n.emailVerificationToggle,
+          ),
+          trailingWidget: ToggleSwitchWidget(
+            value: () => UserService.instance.hasEmailMFAEnabled(),
+            onChanged: () async {
+              final hasAuthenticated = await LocalAuthenticationService
+                  .instance
+                  .requestLocalAuthentication(
+                context,
+                l10n.authToChangeEmailVerificationSetting,
+              );
+              final isEmailMFAEnabled =
+              UserService.instance.hasEmailMFAEnabled();
+              if (hasAuthenticated) {
+                await updateEmailMFA(!isEmailMFAEnabled);
+                if(mounted){
+                  setState(() {});
+                }
+              }
+            },
+          ),
+        ),
       sectionOptionSpacing,
       MenuItemWidget(
         captionedTextWidget: CaptionedTextWidget(
@@ -95,4 +135,12 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
       children: children,
     );
   }
+
+  Future<void> updateEmailMFA(bool isEnabled) async {
+    try {
+      await UserService.instance.updateEmailMFA(isEnabled);
+    } catch (e) {
+     showToast(context, context.l10n.somethingWentWrongMessage);
+    }
+  }
 }

+ 3 - 0
lib/ui/settings_page.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/theme/colors.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/ui/settings/about_section_widget.dart';
@@ -20,8 +21,10 @@ class SettingsPage extends StatelessWidget {
   final ValueNotifier<String?> emailNotifier;
   const SettingsPage({Key? key, required this.emailNotifier}) : super(key: key);
 
+
   @override
   Widget build(BuildContext context) {
+    UserService.instance.getUserDetailsV2().ignore();
     final enteColorScheme = getEnteColorScheme(context);
     return Scaffold(
       body: Container(

+ 1 - 1
pubspec.yaml

@@ -1,6 +1,6 @@
 name: ente_auth
 description: ente two-factor authenticator
-version: 1.0.41+41
+version: 1.0.43+43
 publish_to: none
 
 environment: