11import 'dart:async' ;
22import 'dart:convert' ;
33
4+ import 'package:cloud_firestore/cloud_firestore.dart' ;
45import 'package:firebase_core/firebase_core.dart' ;
56import 'package:flutter/foundation.dart' ;
67import 'package:flutter_riverpod/flutter_riverpod.dart' ;
@@ -11,6 +12,9 @@ import 'package:open_authenticator/model/migrator/firebase_auth/firebase_auth.da
1112import 'package:open_authenticator/model/migrator/firebase_options.dart' ;
1213import 'package:open_authenticator/model/settings/entry.dart' ;
1314import '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' ;
1418import 'package:open_authenticator/utils/result.dart' ;
1519import '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.
126205class _IdTokenException extends LocalizableException {
127206 /// Creates a new id token exception instance.
0 commit comments