Skip to content

Commit be6d125

Browse files
committed
chore: Improved the migrator.
1 parent 52f5927 commit be6d125

13 files changed

Lines changed: 215 additions & 149 deletions

File tree

lib/i18n/de/error.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"passwordMismatch": "Das neue Passwort für den Krypto-Speicher ist nicht korrekt.",
8181
"migrator": {
8282
"message": "Beim Migrieren Ihrer Daten ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
83+
"noFirebaseUser": "Kein Firebase-Benutzer verfügbar. Bitte versuchen Sie es später erneut oder kontaktieren Sie uns.",
8384
"idToken": "ID-Token konnte nicht abgerufen werden. Bitte versuche es später erneut.",
8485
"httpError": "Migration fehlgeschlagen. HTTP-Fehler $statusCode: $body.",
8586
"migrationCancelled": "Migration abgebrochen.",

lib/i18n/en/error.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"passwordMismatch": "The new crypto store password is incorrect.",
8181
"migrator": {
8282
"message": "An error occurred while migrating your data. Please try again.",
83+
"noFirebaseUser": "No Firebase user available. Please try again later or contact us.",
8384
"idToken": "Could not get ID token. Please try again later.",
8485
"httpError": "Migration failed. HTTP Error $statusCode : $body.",
8586
"migrationCancelled": "Migration cancelled.",

lib/i18n/fr/error.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"passwordMismatch": "Le mot de passe du magasin de chiffrement est incorrect.",
8181
"migrator": {
8282
"message": "Une erreur est survenue lors de la migration de vos données. Veuillez réessayer.",
83+
"noFirebaseUser": "Aucun utilisateur Firebase disponible. Veuillez réessayer plus tard ou nous contacter.",
8384
"idToken": "Impossible de récupérer le jeton d'identification. Veuillez réessayer plus tard.",
8485
"httpError": "Migration échouée. Erreur HTTP $statusCode : $body.",
8586
"migrationCancelled": "Migration annulée.",

lib/i18n/it/error.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"passwordMismatch": "La password del store di crittografia è errata",
8181
"migrator": {
8282
"message": "Si è verificato un errore durante la migrazione dei tuoi dati. Per favore, riprova.",
83+
"noFirebaseUser": "Nessun utente Firebase disponibile. Riprova più tardi o contattaci.",
8384
"idToken": "Impossibile ottenere il token di identificazione. Riprova più tardi.",
8485
"httpError": "Migrazione fallita. Errore HTTP $statusCode: $body.",
8586
"migrationCancelled": "Migrazione annullata.",

lib/i18n/pt/error.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"passwordMismatch": "A senha do armazenamento de criptografia está incorreta.",
8181
"migrator": {
8282
"message": "Ocorreu um erro ao migrar os seus dados. Por favor, tente novamente.",
83+
"noFirebaseUser": "Nenhum utilizador Firebase disponível. Por favor, tente novamente mais tarde ou contacte-nos.",
8384
"idToken": "Não foi possível obter o token de identificação. Tente novamente mais tarde.",
8485
"httpError": "Migração falhou. Erro HTTP $statusCode : $body.",
8586
"migrationCancelled": "Migração cancelada.",

lib/model/migrator/firebase_auth/rest.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FirebaseAuthRest extends FirebaseAuth {
3939

4040
@override
4141
Future<void> initialize() async {
42-
super.initialize();
42+
await super.initialize();
4343
_methodChannel.setMethodCallHandler(_handlePlatformCall);
4444
String? userData = await SimpleSecureStorage.read(_kUserData);
4545
if (userData == null) {

lib/model/migrator/migrator.dart

Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:convert';
33

4+
import 'package:cloud_firestore/cloud_firestore.dart';
45
import 'package:firebase_core/firebase_core.dart';
56
import 'package:flutter/foundation.dart';
67
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -11,6 +12,9 @@ import 'package:open_authenticator/model/migrator/firebase_auth/firebase_auth.da
1112
import 'package:open_authenticator/model/migrator/firebase_options.dart';
1213
import 'package:open_authenticator/model/settings/entry.dart';
1314
import 'package:open_authenticator/model/settings/storage_type.dart';
15+
import 'package:open_authenticator/model/totp/json.dart';
16+
import 'package:open_authenticator/model/totp/repository.dart';
17+
import 'package:open_authenticator/model/totp/totp.dart';
1418
import 'package:open_authenticator/utils/result.dart';
1519
import 'package:open_authenticator/utils/shared_preferences_with_prefix.dart';
1620

@@ -49,42 +53,17 @@ class Migrator extends AsyncNotifier<MigrationState> {
4953
if (newStorageType == .shared) {
5054
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
5155
await FirebaseAuth.instance.initialize();
52-
await Future.delayed(const Duration(seconds: 1));
53-
User? user = FirebaseAuth.instance.currentUser;
54-
String? idToken = await user?.getIdToken(forceRefresh: true);
55-
if (idToken == null) {
56-
throw _IdTokenException();
57-
}
58-
http.Response response = await http.post(
59-
Uri.https(
60-
'europe-west1-open-authenticator-by-skyost.cloudfunctions.net',
61-
'/migrate',
62-
),
63-
headers: {
64-
'Content-Type': 'application/json',
65-
},
66-
body: jsonEncode({
67-
'userId': user!.uid,
68-
'idToken': idToken,
69-
'debug': kDebugMode,
70-
}),
71-
);
72-
Map<String, dynamic> json = {'success': false};
73-
try {
74-
json = jsonDecode(response.body);
75-
} on FormatException catch (_) {
76-
throw _HttpErrorException(response: response);
77-
}
78-
if (!json['success']) {
79-
String errorCode = json['data']?['errorCode'] ?? 'migration';
80-
if (errorCode == 'userAlreadyExists') {
81-
await markMigrated();
82-
return const ResultSuccess(value: .shared);
56+
await Future.delayed(const Duration(seconds: 2));
57+
Result totpsMigrationResult = await _migrateFirebaseTotps();
58+
Result userMigrationResult = await _migrateFirebaseUser();
59+
if (totpsMigrationResult is! ResultSuccess || userMigrationResult is! ResultSuccess) {
60+
if (totpsMigrationResult is ResultError) {
61+
Error.throwWithStackTrace(totpsMigrationResult.exception, totpsMigrationResult.stackTrace);
8362
}
84-
throw _MigrationRequestException(
85-
errorCode: errorCode,
86-
message: json['data']?['message'],
87-
);
63+
if (userMigrationResult is ResultError) {
64+
Error.throwWithStackTrace(userMigrationResult.exception, userMigrationResult.stackTrace);
65+
}
66+
return (totpsMigrationResult is! ResultSuccess ? userMigrationResult : totpsMigrationResult).to((_) => null);
8867
}
8968
}
9069
await markMigrated();
@@ -100,6 +79,97 @@ class Migrator extends AsyncNotifier<MigrationState> {
10079
}
10180
}
10281

82+
/// Migrates the Firestore stored TOTPs to the new database.
83+
Future<Result> _migrateFirebaseTotps({
84+
String userDataDocumentName = 'userData',
85+
String totpsCollectionName = 'totps',
86+
String updatedAtFieldName = 'updated',
87+
}) async {
88+
try {
89+
User? user = FirebaseAuth.instance.currentUser;
90+
if (user == null) {
91+
throw _NoFirebaseUserException();
92+
}
93+
DocumentReference<Map<String, dynamic>> userDataDocument = FirebaseFirestore.instance.collection(user.uid).doc(userDataDocumentName);
94+
CollectionReference totpsCollection = userDataDocument.collection(totpsCollectionName);
95+
QuerySnapshot result = await totpsCollection.orderBy(Totp.kIssuerKey).get();
96+
List<QueryDocumentSnapshot> docs = result.docs;
97+
List<Totp> totps = [];
98+
int now = DateTime.now().millisecondsSinceEpoch;
99+
for (QueryDocumentSnapshot doc in docs) {
100+
dynamic data = doc.data();
101+
if (data is! Map<String, Object?>) {
102+
continue;
103+
}
104+
int updatedAt = data[updatedAtFieldName] is Timestamp ? (data[updatedAtFieldName] as Timestamp).toDate().millisecondsSinceEpoch : now;
105+
Totp? totp = JsonTotp.tryFromJson({
106+
...data,
107+
Totp.kUpdatedAtKey: updatedAt,
108+
});
109+
if (totp != null) {
110+
totps.add(totp);
111+
}
112+
}
113+
await ref.read(totpRepositoryProvider.notifier).addTotps(totps);
114+
return const ResultSuccess();
115+
} catch (ex, stackTrace) {
116+
return ResultError(
117+
exception: ex,
118+
stackTrace: stackTrace,
119+
);
120+
}
121+
}
122+
123+
/// Migrates the Firebase user to the new backend.
124+
Future<Result<StorageType>> _migrateFirebaseUser() async {
125+
try {
126+
User? user = FirebaseAuth.instance.currentUser;
127+
if (user == null) {
128+
throw _NoFirebaseUserException();
129+
}
130+
String? idToken = await user.getIdToken(forceRefresh: true);
131+
if (idToken == null) {
132+
throw _IdTokenException();
133+
}
134+
http.Response response = await http.post(
135+
Uri.https(
136+
'europe-west1-open-authenticator-by-skyost.cloudfunctions.net',
137+
'/migrate',
138+
),
139+
headers: {
140+
'Content-Type': 'application/json',
141+
},
142+
body: jsonEncode({
143+
'userId': user.uid,
144+
'idToken': idToken,
145+
'debug': kDebugMode,
146+
}),
147+
);
148+
Map<String, dynamic> json = {'success': false};
149+
try {
150+
json = jsonDecode(response.body);
151+
} on FormatException catch (_) {
152+
throw _HttpErrorException(response: response);
153+
}
154+
if (!json['success']) {
155+
String errorCode = json['data']?['errorCode'] ?? 'migration';
156+
if (errorCode == 'userAlreadyExists') {
157+
return const ResultSuccess();
158+
}
159+
throw _MigrationRequestException(
160+
errorCode: errorCode,
161+
message: json['data']?['message'],
162+
);
163+
}
164+
return const ResultSuccess();
165+
} catch (ex, stackTrace) {
166+
return ResultError(
167+
exception: ex,
168+
stackTrace: stackTrace,
169+
);
170+
}
171+
}
172+
103173
/// Marks the migration as done.
104174
Future<void> markMigrated() async {
105175
SharedPreferencesWithPrefix preferences = await ref.read(sharedPreferencesProvider.future);
@@ -122,6 +192,15 @@ enum MigrationState {
122192
done,
123193
}
124194

195+
/// Thrown when the Firebase user could not be retrieved.
196+
class _NoFirebaseUserException extends LocalizableException {
197+
/// Creates a new no firebase user exception instance.
198+
_NoFirebaseUserException()
199+
: super(
200+
localizedErrorMessage: translations.error.migrator.noFirebaseUser,
201+
);
202+
}
203+
125204
/// Thrown when the ID token could not be retrieved.
126205
class _IdTokenException extends LocalizableException {
127206
/// Creates a new id token exception instance.

lib/pages/settings/entries/change_backend_url.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,9 @@ class ChangeBackendUrlSettingsEntryWidget extends ConsumerWidget with FTileMixin
3434
ref,
3535
.localOnly,
3636
logout: true,
37-
handleResult: false,
37+
handleResult: (result) => result is! ResultSuccess,
3838
);
39-
if (!context.mounted) {
40-
return;
41-
}
42-
if (result is! ResultSuccess) {
43-
context.handleResult(result);
39+
if (!context.mounted || result is! ResultSuccess) {
4440
return;
4541
}
4642
String currentUrl = await showWaitingOverlay(

lib/utils/account.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class AccountUtils {
2222
context,
2323
waitingDialogMessage: translations.authentication.logIn.waitingLoginMessage,
2424
action: action,
25+
handleResult: (result) => result is! ResultSuccess,
2526
);
2627
}
2728

@@ -48,15 +49,15 @@ class AccountUtils {
4849
context,
4950
waitingDialogMessage: unlink ? null : translations.authentication.logIn.waitingLoginMessage,
5051
action: result.action,
52+
handleResult: (result) => result is! ResultSuccess,
5153
);
5254
}
5355

5456
/// Prompts the user to choose an authentication provider, use it to re-authenticate and delete its account.
5557
static Future<Result> tryDeleteAccount(
5658
BuildContext context,
57-
WidgetRef ref, {
58-
bool handleResult = true,
59-
}) async {
59+
WidgetRef ref,
60+
) async {
6061
bool confirm = await ConfirmationDialog.ask(
6162
context,
6263
title: translations.authentication.deleteConfirmationDialog.title,
@@ -78,6 +79,7 @@ class AccountUtils {
7879
return _tryTo(
7980
context,
8081
action: () => ref.read(userProvider.notifier).deleteUser(),
82+
handleResult: (_) => true,
8183
);
8284
}
8385

@@ -86,13 +88,14 @@ class AccountUtils {
8688
BuildContext context, {
8789
required Future<Result> Function() action,
8890
String? waitingDialogMessage,
91+
bool Function(Result result)? handleResult,
8992
}) async {
9093
Result result = await showWaitingOverlay(
9194
context,
9295
future: action(),
9396
message: waitingDialogMessage,
9497
);
95-
if (context.mounted && result is! ResultSuccess) {
98+
if ((handleResult?.call(result) ?? true) && context.mounted) {
9699
context.handleResult(result);
97100
}
98101
return result;

lib/utils/email_confirmation.dart

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,25 @@ class EmailConfirmationUtils {
1717
static Future<Result> askForConfirmation(
1818
BuildContext context,
1919
WidgetRef ref, {
20-
bool handleResult = true,
20+
bool Function(Result result)? handleResult,
2121
}) async {
22+
Result result;
2223
_ConfirmAction? confirmAction = await _ConfirmActionPickerDialog.openDialog(context);
2324
if (confirmAction == null || !context.mounted) {
24-
return const ResultCancelled();
25+
result = const ResultCancelled();
26+
} else {
27+
result = await switch (confirmAction) {
28+
.tryConfirm => _tryConfirm(context, ref),
29+
.cancelConfirmation => _tryCancelConfirmation(context, ref),
30+
};
2531
}
26-
switch (confirmAction) {
27-
case _ConfirmAction.tryConfirm:
28-
Result result = await _tryConfirm(context, ref);
29-
if (handleResult && context.mounted) {
30-
context.handleResult(
31-
result,
32-
successMessage: result.valueOrNull?.localizedMessage,
33-
);
34-
}
35-
return result;
36-
case _ConfirmAction.cancelConfirmation:
37-
Result result = await _tryCancelConfirmation(context, ref);
38-
if (handleResult && context.mounted) {
39-
context.handleResult(result);
40-
}
41-
return result;
32+
if ((handleResult?.call(result) ?? true) && context.mounted) {
33+
context.handleResult(
34+
result,
35+
successMessage: confirmAction == .tryConfirm ? (result as ResultSuccess<RedirectResult>).valueOrNull?.localizedMessage : null,
36+
);
4237
}
38+
return result;
4339
}
4440

4541
/// Tries to cancel the confirmation.

0 commit comments

Comments
 (0)