diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index 9533cb68a..e50d3ee29 100644 --- a/lib/models/user_details.dart +++ b/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? 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 toJson() { + return { + 'canDisableEmailMFA': canDisableEmailMFA, + 'isEmailMFAEnabled': isEmailMFAEnabled, + 'isTwoFactorEnabled': isTwoFactorEnabled, + }; + } + String toJsonString() => json.encode(toJson()); +} class FamilyData { final List? members; diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 4f4af1ae3..12bee120c 100644 --- a/lib/services/user_service.dart +++ b/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 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,19 @@ 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); } } + _logger.info("Successfully fetched user details"); return userDetails; - } on DioError catch (e) { - _logger.info(e); + } catch(e) { + _logger.warning("Failed to fetch", e); rethrow; } } @@ -890,7 +896,26 @@ class UserService { } } + bool canDisableEmailMFA() { + return _preferences.getBool(kCanDisableEmailMFA) ?? false; + } + bool hasEmailMFAEnabled() { + return _preferences.getBool(kIsEmailMFAEnabled) ?? true; + } - + Future 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; + } + } } diff --git a/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index 18b29c782..007599243 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/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,8 @@ class _SecuritySectionWidgetState extends State { } Widget _getSectionOptions(BuildContext context) { + final bool canDisableMFA = UserService.instance.canDisableEmailMFA(); + final l10n = context.l10n; final List children = []; children.addAll([ @@ -64,6 +70,33 @@ class _SecuritySectionWidgetState extends State { }, ), ), + if(canDisableMFA) + sectionOptionSpacing, + if(canDisableMFA) + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Email MFA", + ), + trailingWidget: ToggleSwitchWidget( + value: () => UserService.instance.hasEmailMFAEnabled(), + onChanged: () async { + final hasAuthenticated = await LocalAuthenticationService + .instance + .requestLocalAuthentication( + context, + "Authenticate to change your email MFA setting", + ); + final isEmailMFAEnabled = + UserService.instance.hasEmailMFAEnabled(); + if (hasAuthenticated) { + await updateEmailMFA(!isEmailMFAEnabled); + if(mounted){ + setState(() {}); + } + } + }, + ), + ), sectionOptionSpacing, MenuItemWidget( captionedTextWidget: CaptionedTextWidget( @@ -95,4 +128,12 @@ class _SecuritySectionWidgetState extends State { children: children, ); } + + Future updateEmailMFA(bool isEnabled) async { + try { + await UserService.instance.updateEmailMFA(isEnabled); + } catch (e) { + showToast(context, "Error updating email MFA"); + } + } } diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index d979377f4..74fd07eb4 100644 --- a/lib/ui/settings_page.dart +++ b/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 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(