Add support for toggling email MFA (#174)
This commit is contained in:
commit
cf862d291d
9 changed files with 139 additions and 14 deletions
|
@ -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(() {});
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,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:
|
||||
|
|
Loading…
Add table
Reference in a new issue