[auth][mob] Add recovery support for passkey

This commit is contained in:
Neeraj Gupta
2024-03-11 17:06:15 +05:30
parent 9a8e76b494
commit 27c1b66c08
24 changed files with 267 additions and 66 deletions

View File

@@ -0,0 +1,78 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'intl/messages_all.dart';
// **************************************************************************
// Generator: Flutter Intl IDE plugin
// Made by Localizely
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
class S {
S();
static S? _current;
static S get current {
assert(_current != null,
'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.');
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance;
return instance;
});
}
static S of(BuildContext context) {
final instance = S.maybeOf(context);
assert(instance != null,
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?');
return instance!;
}
static S? maybeOf(BuildContext context) {
return Localizations.of<S>(context, S);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<S> load(Locale locale) => S.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}

View File

@@ -144,7 +144,8 @@
"enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
"lostDeviceTitle": "Lost device?",
"twoFactorAuthTitle": "Two-factor authentication",
"passkeyAuthTitle": "Passkey authentication",
"passkeyAuthTitle": "Passkey verification",
"verifyPasskey": "Verify passkey",
"recoverAccount": "Recover account",
"enterRecoveryKeyHint": "Enter your recovery key",
"recover": "Recover",
@@ -407,7 +408,7 @@
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
"waitingForBrowserRequest": "Waiting for browser request...",
"launchPasskeyUrlAgain": "Launch passkey URL again",
"waitingForVerification": "Waiting for verification...",
"passkey": "Passkey",
"developerSettingsWarning":"Are you sure that you want to modify Developer settings?",
"developerSettings": "Developer settings",

View File

@@ -0,0 +1,13 @@
enum TwoFactorType { totp, passkey }
// ToString for TwoFactorType
String twoFactorTypeToString(TwoFactorType type) {
switch (type) {
case TwoFactorType.totp:
return "totp";
case TwoFactorType.passkey:
return "passkey";
default:
return type.name;
}
}

View File

@@ -17,6 +17,28 @@ class PasskeyService {
return response.data!["accountsToken"] as String;
}
Future<bool> isPasskeyRecoveryEnabled() async {
final response = await _enteDio.get(
"/users/two-factor/recovery-status",
);
return response.data!["isPasskeyRecoveryEnabled"] as bool;
}
Future<void> configurePasskeyRecovery(
String secret,
String userEncryptedSecret,
String userSecretNonce,
) async {
await _enteDio.post(
"/users/two-factor/passkeys/configure-recovery",
data: {
"secret": secret,
"userSecretCipher": userEncryptedSecret,
"userSecretNonce": userSecretNonce,
},
);
}
Future<void> openPasskeyPage(BuildContext context) async {
try {
final jwtToken = await getJwtToken();

View File

@@ -11,6 +11,7 @@ import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/events/user_details_changed_event.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/account/two_factor.dart';
import 'package:ente_auth/models/api/user/srp.dart';
import 'package:ente_auth/models/delete_account.dart';
import 'package:ente_auth/models/key_attributes.dart';
@@ -762,7 +763,11 @@ class UserService {
}
}
Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
Future<void> recoverTwoFactor(
BuildContext context,
String sessionID,
TwoFactorType type,
) async {
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
@@ -770,6 +775,7 @@ class UserService {
_config.getHttpEndpoint() + "/users/two-factor/recover",
queryParameters: {
"sessionID": sessionID,
"twoFactorType": twoFactorTypeToString(type),
},
);
if (response.statusCode == 200) {
@@ -778,6 +784,7 @@ class UserService {
MaterialPageRoute(
builder: (BuildContext context) {
return TwoFactorRecoveryPage(
type,
sessionID,
response.data["encryptedSecret"],
response.data["secretDecryptionNonce"],
@@ -788,6 +795,7 @@ class UserService {
);
}
} on DioError catch (e) {
await dialog.hide();
_logger.severe(e);
if (e.response != null && e.response!.statusCode == 404) {
showToast(context, context.l10n.sessionExpired);
@@ -809,6 +817,7 @@ class UserService {
);
}
} catch (e) {
await dialog.hide();
_logger.severe(e);
// ignore: unawaited_futures
showErrorDialog(
@@ -823,6 +832,7 @@ class UserService {
Future<void> removeTwoFactor(
BuildContext context,
TwoFactorType type,
String sessionID,
String recoveryKey,
String encryptedSecret,
@@ -857,11 +867,15 @@ class UserService {
return;
}
try {
final secretValue = type == TwoFactorType.passkey
? utf8.decode(base64.decode(secret))
: secret;
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/two-factor/remove",
data: {
"sessionID": sessionID,
"secret": secret,
"secret": secretValue,
"twoFactorType": twoFactorTypeToString(type),
},
);
if (response.statusCode == 200) {
@@ -881,6 +895,7 @@ class UserService {
);
}
} on DioError catch (e) {
await dialog.hide();
_logger.severe(e);
if (e.response != null && e.response!.statusCode == 404) {
showToast(context, "Session expired");
@@ -902,6 +917,7 @@ class UserService {
);
}
} catch (e) {
await dialog.hide();
_logger.severe(e);
// ignore: unawaited_futures
showErrorDialog(

View File

@@ -1,9 +1,11 @@
import 'dart:convert';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/account/two_factor.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@@ -99,30 +101,50 @@ class _PasskeyPageState extends State<PasskeyPage> {
}
Widget _getBody() {
final l10n = context.l10n;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.waitingForBrowserRequest,
style: const TextStyle(
height: 1.4,
fontSize: 16,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
context.l10n.waitingForVerification,
style: const TextStyle(
height: 1.4,
fontSize: 16,
),
),
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32),
child: ElevatedButton(
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
onPressed: launchPasskey,
child: Text(l10n.launchPasskeyUrlAgain),
const SizedBox(height: 16),
ButtonWidget(
buttonType: ButtonType.primary,
labelText: context.l10n.verifyPasskey,
onTap: () => launchPasskey(),
),
),
],
const Padding(padding: EdgeInsets.all(30)),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
UserService.instance.recoverTwoFactor(
context,
widget.sessionID,
TwoFactorType.passkey,
);
},
child: Container(
padding: const EdgeInsets.all(10),
child: Center(
child: Text(
context.l10n.recoverAccount,
style: const TextStyle(
decoration: TextDecoration.underline,
fontSize: 12,
),
),
),
),
),
],
),
),
);
}

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:ente_auth/core/configuration.dart';
@@ -21,6 +22,8 @@ import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
class SecuritySectionWidget extends StatefulWidget {
const SecuritySectionWidget({Key? key}) : super(key: key);
@@ -32,6 +35,7 @@ class SecuritySectionWidget extends StatefulWidget {
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
final _config = Configuration.instance;
late bool _hasLoggedIn;
final Logger _logger = Logger('SecuritySectionWidget');
@override
void initState() {
@@ -75,7 +79,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () => PasskeyService.instance.openPasskeyPage(context),
onTap: () async => await onPasskeyClick(context),
),
sectionOptionSpacing,
MenuItemWidget(
@@ -159,6 +163,32 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
);
}
Future<void> onPasskeyClick(BuildContext buildContext) async {
try {
final isPassKeyResetEnabled =
await PasskeyService.instance.isPasskeyRecoveryEnabled();
if (!isPassKeyResetEnabled) {
final Uint8List recoveryKey = Configuration.instance.getRecoveryKey();
final resetSecret = const Uuid().v4().toString();
final bytes = utf8.encode(resetSecret);
final base64Str = base64.encode(bytes);
final encryptionResult = CryptoUtil.encryptSync(
CryptoUtil.base642bin(base64Str),
recoveryKey,
);
await PasskeyService.instance.configurePasskeyRecovery(
resetSecret,
CryptoUtil.bin2base64(encryptionResult.encryptedData!),
CryptoUtil.bin2base64(encryptionResult.nonce!),
);
}
PasskeyService.instance.openPasskeyPage(buildContext).ignore();
} catch (e, s) {
_logger.severe("failed to open passkey page", e, s);
await showGenericErrorDialog(context: context);
}
}
Future<void> updateEmailMFA(bool enableEmailMFA) async {
try {
final UserDetails details =

View File

@@ -1,4 +1,5 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/account/two_factor.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
import 'package:flutter/material.dart';
@@ -129,7 +130,11 @@ class _TwoFactorAuthenticationPageState
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
UserService.instance.recoverTwoFactor(context, widget.sessionID);
UserService.instance.recoverTwoFactor(
context,
widget.sessionID,
TwoFactorType.totp,
);
},
child: Container(
padding: const EdgeInsets.all(10),

View File

@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/account/two_factor.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/material.dart';
@@ -9,8 +10,10 @@ class TwoFactorRecoveryPage extends StatefulWidget {
final String sessionID;
final String encryptedSecret;
final String secretDecryptionNonce;
final TwoFactorType type;
const TwoFactorRecoveryPage(
this.type,
this.sessionID,
this.encryptedSecret,
this.secretDecryptionNonce, {
@@ -72,6 +75,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
? () async {
await UserService.instance.removeTwoFactor(
context,
widget.type,
widget.sessionID,
_recoveryKey.text,
widget.encryptedSecret,

View File

@@ -29,12 +29,14 @@ class MessageLookup extends MessageLookupByLibrary {
"contacts": MessageLookupByLibrary.simpleMessage("Contacts"),
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"modifyYourQueryOrTrySearchingFor":
MessageLookupByLibrary.simpleMessage(
"Modify your query, or try searching for"),

View File

@@ -562,6 +562,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Geteiltes Album löschen?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Dieses Album wird für alle gelöscht\n\nDu wirst den Zugriff auf geteilte Fotos in diesem Album, die anderen gehören, verlieren"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Alle abwählen"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Entwickelt um zu bewahren"),
@@ -872,6 +873,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("Standortname"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Ein Standort-Tag gruppiert alle Fotos, die in einem Radius eines Fotos aufgenommen wurden"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Sperren"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Um den Sperrbildschirm zu aktivieren, legen Sie bitte den Geräte-Passcode oder die Bildschirmsperre in den Systemeinstellungen fest."),

View File

@@ -548,6 +548,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Delete shared album?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"The album will be deleted for everyone\n\nYou will lose access to shared photos in this album that are owned by others"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Deselect all"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Designed to outlive"),
@@ -797,8 +798,6 @@ class MessageLookup extends MessageLookupByLibrary {
"Kindly help us with this information"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"lastUpdated": MessageLookupByLibrary.simpleMessage("Last updated"),
"launchPasskeyUrlAgain":
MessageLookupByLibrary.simpleMessage("Launch passkey URL again"),
"leave": MessageLookupByLibrary.simpleMessage("Leave"),
"leaveAlbum": MessageLookupByLibrary.simpleMessage("Leave album"),
"leaveFamily": MessageLookupByLibrary.simpleMessage("Leave family"),
@@ -848,6 +847,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("Location name"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"A location tag groups all photos that were taken within some radius of a photo"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Lock"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"To enable lockscreen, please setup device passcode or screen lock in your system settings."),

View File

@@ -487,6 +487,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("¿Borrar álbum compartido?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"El álbum se eliminará para todos\n\nPerderás el acceso a las fotos compartidas en este álbum que son propiedad de otros"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Deseleccionar todo"),
"designedToOutlive":
@@ -761,6 +762,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Nombre de la ubicación"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Una etiqueta de ubicación agrupa todas las fotos que fueron tomadas dentro de un radio de una foto"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Bloquear"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Para activar la pantalla de bloqueo, por favor configure el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes de su sistema."),

View File

@@ -562,6 +562,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Supprimer l\'album partagé ?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"L\'album sera supprimé pour tout le monde\n\nVous perdrez l\'accès aux photos partagées dans cet album qui sont détenues par d\'autres personnes"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Tout déselectionner"),
"designedToOutlive":
@@ -870,6 +871,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("Nom du lieu"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Un tag d\'emplacement regroupe toutes les photos qui ont été prises dans un certain rayon d\'une photo"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Verrouiller"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Pour activer l\'écran de verrouillage, veuillez configurer le code d\'accès de l\'appareil ou le verrouillage de l\'écran dans les paramètres de votre système."),

View File

@@ -542,6 +542,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Eliminare l\'album condiviso?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"L\'album verrà eliminato per tutti\n\nPerderai l\'accesso alle foto condivise in questo album che sono di proprietà di altri"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Deseleziona tutti"),
"designedToOutlive":
@@ -838,6 +839,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Nome della località"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Un tag di localizzazione raggruppa tutte le foto scattate entro il raggio di una foto"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Blocca"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Per abilitare la schermata di blocco, configura il codice di accesso del dispositivo o il blocco schermo nelle impostazioni di sistema."),

View File

@@ -29,12 +29,14 @@ class MessageLookup extends MessageLookupByLibrary {
"contacts": MessageLookupByLibrary.simpleMessage("Contacts"),
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
"Edits to location will only be seen within Ente"),
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"modifyYourQueryOrTrySearchingFor":
MessageLookupByLibrary.simpleMessage(
"Modify your query, or try searching for"),

View File

@@ -564,6 +564,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Gedeeld album verwijderen?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Het album wordt verwijderd voor iedereen\n\nJe verliest de toegang tot gedeelde foto\'s in dit album die eigendom zijn van anderen"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll":
MessageLookupByLibrary.simpleMessage("Alles deselecteren"),
"designedToOutlive": MessageLookupByLibrary.simpleMessage(
@@ -880,6 +881,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("Locatie naam"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Een locatie tag groept alle foto\'s die binnen een bepaalde straal van een foto zijn genomen"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Vergrendel"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Om vergrendelscherm in te schakelen, moet u een toegangscode of schermvergrendeling instellen in uw systeeminstellingen."),

View File

@@ -41,6 +41,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Vi er lei oss for at du forlater oss. Gi oss gjerne en tilbakemelding så vi kan forbedre oss."),
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
"This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
MessageLookupByLibrary.simpleMessage(
@@ -57,6 +58,7 @@ class MessageLookup extends MessageLookupByLibrary {
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
"Vær vennlig og hjelp oss med denne informasjonen"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"modifyYourQueryOrTrySearchingFor":
MessageLookupByLibrary.simpleMessage(
"Modify your query, or try searching for"),

View File

@@ -78,6 +78,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Inna, niewymieniona wyżej przyczyna"),
"deleteRequestSLAText": MessageLookupByLibrary.simpleMessage(
"Twoje żądanie zostanie przetworzone w ciągu 72 godzin."),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"doThisLater": MessageLookupByLibrary.simpleMessage("Spróbuj później"),
"editLocation": MessageLookupByLibrary.simpleMessage("Edit location"),
"editsToLocationWillOnlyBeSeenWithinEnte":
@@ -116,6 +117,7 @@ class MessageLookup extends MessageLookupByLibrary {
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
"kindlyHelpUsWithThisInformation":
MessageLookupByLibrary.simpleMessage("Pomóż nam z tą informacją"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"logInLabel": MessageLookupByLibrary.simpleMessage("Zaloguj się"),
"moderateStrength": MessageLookupByLibrary.simpleMessage("Umiarkowana"),
"modifyYourQueryOrTrySearchingFor":

View File

@@ -558,6 +558,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Excluir álbum compartilhado?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que pertencem aos outros"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar todos"),
"designedToOutlive":
MessageLookupByLibrary.simpleMessage("Feito para ter logenvidade"),
@@ -820,8 +821,6 @@ class MessageLookup extends MessageLookupByLibrary {
"language": MessageLookupByLibrary.simpleMessage("Idioma"),
"lastUpdated":
MessageLookupByLibrary.simpleMessage("Última atualização"),
"launchPasskeyUrlAgain": MessageLookupByLibrary.simpleMessage(
"Iniciar a URL de chave de acesso novamente"),
"leave": MessageLookupByLibrary.simpleMessage("Sair"),
"leaveAlbum": MessageLookupByLibrary.simpleMessage("Sair do álbum"),
"leaveFamily": MessageLookupByLibrary.simpleMessage("Sair da família"),
@@ -873,6 +872,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("Nome do Local"),
"locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage(
"Uma tag em grupo de todas as fotos que foram tiradas dentro de algum raio de uma foto"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("Bloquear"),
"lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage(
"Para ativar o bloqueio de tela, por favor ative um método de autenticação nas configurações do sistema do seu dispositivo."),

View File

@@ -465,6 +465,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deleteSharedAlbum": MessageLookupByLibrary.simpleMessage("要删除共享相册吗?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"将为所有人删除相册\n\n您将无法访问此相册中他人拥有的共享照片"),
"descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"),
"deselectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"designedToOutlive": MessageLookupByLibrary.simpleMessage("经久耐用"),
"details": MessageLookupByLibrary.simpleMessage("详情"),
@@ -673,8 +674,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("请帮助我们了解这个信息"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"lastUpdated": MessageLookupByLibrary.simpleMessage("最后更新"),
"launchPasskeyUrlAgain":
MessageLookupByLibrary.simpleMessage("再次启动 通行密钥 URL"),
"leave": MessageLookupByLibrary.simpleMessage("离开"),
"leaveAlbum": MessageLookupByLibrary.simpleMessage("离开相册"),
"leaveFamily": MessageLookupByLibrary.simpleMessage("离开家庭计划"),
@@ -717,6 +716,7 @@ class MessageLookup extends MessageLookupByLibrary {
"locationName": MessageLookupByLibrary.simpleMessage("地点名称"),
"locationTagFeatureDescription":
MessageLookupByLibrary.simpleMessage("位置标签将在照片的某个半径范围内拍摄的所有照片进行分组"),
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
"lockButtonLabel": MessageLookupByLibrary.simpleMessage("锁定"),
"lockScreenEnablePreSteps":
MessageLookupByLibrary.simpleMessage("要启用锁屏,请在系统设置中设置设备密码或屏幕锁定。"),

View File

@@ -6919,16 +6919,6 @@ class S {
);
}
/// `Descriptions`
String get descriptions {
return Intl.message(
'Descriptions',
name: 'descriptions',
desc: '',
args: [],
);
}
/// `File types and names`
String get fileTypesAndNames {
return Intl.message(
@@ -6949,16 +6939,6 @@ class S {
);
}
/// `Locations`
String get locations {
return Intl.message(
'Locations',
name: 'locations',
desc: '',
args: [],
);
}
/// `Moments`
String get moments {
return Intl.message(
@@ -8328,16 +8308,6 @@ class S {
);
}
/// `Launch passkey URL again`
String get launchPasskeyUrlAgain {
return Intl.message(
'Launch passkey URL again',
name: 'launchPasskeyUrlAgain',
desc: '',
args: [],
);
}
/// `Passkey`
String get passkey {
return Intl.message(
@@ -8427,6 +8397,26 @@ class S {
args: [],
);
}
/// `Locations`
String get locations {
return Intl.message(
'Locations',
name: 'locations',
desc: '',
args: [],
);
}
/// `Descriptions`
String get descriptions {
return Intl.message(
'Descriptions',
name: 'descriptions',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -1188,7 +1188,6 @@
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
"cleanUncategorized": "Clean Uncategorized",
"waitingForVerification": "Waiting for verification...",
"launchPasskeyUrlAgain": "Launch passkey URL again",
"passkey": "Passkey",
"passkeyAuthTitle": "Passkey verification",
"verifyPasskey": "Verify passkey",

View File

@@ -912,11 +912,14 @@ class UserService {
return;
}
try {
final secretValue = type == TwoFactorType.passkey
? utf8.decode(base64.decode(secret))
: secret;
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/two-factor/remove",
data: {
"sessionID": sessionID,
"secret": utf8.decode(base64.decode(secret)),
"secret": secretValue,
"twoFactorType": twoFactorTypeToString(type),
},
);