Add support for toggling email MFA (#174)

This commit is contained in:
Neeraj Gupta 2023-08-03 16:49:39 +05:30 committed by GitHub
commit cf862d291d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 14 deletions

View file

@ -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(() {});
}

View file

@ -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",

View file

@ -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'],
);
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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(

View file

@ -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: