From 85bc997776e46d59ed4500b2a7b882e884912642 Mon Sep 17 00:00:00 2001 From: Aliaksei Tratseuski Date: Wed, 26 Jun 2024 01:54:56 +0400 Subject: [PATCH] feat: db versioning and better logging --- .vscode/launch.json | 4 +- lib/config/config.dart | 18 + lib/config/hive_config.dart | 316 +++++++++++------- .../api_maps/rest_maps/rest_api_map.dart | 19 +- lib/utils/app_logger.dart | 26 ++ lib/utils/secure_storage.dart | 48 +++ 6 files changed, 293 insertions(+), 138 deletions(-) create mode 100644 lib/config/config.dart create mode 100644 lib/utils/app_logger.dart create mode 100644 lib/utils/secure_storage.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 2b83ac77..aea512cb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,7 +31,7 @@ ] }, { - "name": "debug (production)", + "name": "debug (production flavor)", "request": "launch", "type": "dart", "args": [ @@ -41,7 +41,7 @@ }, { - "name": "debug (nightly)", + "name": "debug (nightly flavor)", "request": "launch", "type": "dart", "args": [ diff --git a/lib/config/config.dart b/lib/config/config.dart new file mode 100644 index 00000000..0b76d915 --- /dev/null +++ b/lib/config/config.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +/// internal app configuration +const config = InternalConfig( + shouldDebugPrint: kDebugMode, +); + +class InternalConfig { + const InternalConfig({ + required this.shouldDebugPrint, + }); + + final bool shouldDebugPrint; + + // example of other possible fields + // final String appVersion; + // +} diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 261c6450..28b51de9 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -1,7 +1,3 @@ -import 'dart:convert'; - -import 'package:flutter/services.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart'; @@ -12,144 +8,210 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/hive/wizards_data/server_installation_wizard_data.dart'; +import 'package:selfprivacy/utils/app_logger.dart'; import 'package:selfprivacy/utils/platform_adapter.dart'; +import 'package:selfprivacy/utils/secure_storage.dart'; class HiveConfig { + static final log = const AppLogger(name: 'hive_config').log; + + /// bump on schema changes + static const version = 2; + static Future init() async { final String? storagePath = PlatformAdapter.storagePath; - print('HiveConfig: Custom storage path: $storagePath'); + log('set custom storage path to: "$storagePath"'); + await Hive.initFlutter(storagePath); - Hive.registerAdapter(UserAdapter()); - Hive.registerAdapter(ServerHostingDetailsAdapter()); - Hive.registerAdapter(ServerDomainAdapter()); - Hive.registerAdapter(BackupsCredentialAdapter()); - Hive.registerAdapter(ServerProviderVolumeAdapter()); - Hive.registerAdapter(BackblazeBucketAdapter()); - Hive.registerAdapter(ServerProviderCredentialAdapter()); - Hive.registerAdapter(DnsProviderCredentialAdapter()); - Hive.registerAdapter(ServerAdapter()); - Hive.registerAdapter(DnsProviderTypeAdapter()); - Hive.registerAdapter(ServerProviderTypeAdapter()); - Hive.registerAdapter(UserTypeAdapter()); - Hive.registerAdapter(BackupsProviderTypeAdapter()); - Hive.registerAdapter(ServerInstallationWizardDataAdapter()); - await Hive.openBox(BNames.appSettingsBox); + registerAdapters(); + await decryptBoxes(); + await performMigrations(); + } + static void registerAdapters() { try { - final HiveAesCipher cipher = HiveAesCipher( - await getEncryptedKey(BNames.serverInstallationEncryptionKey), + Hive.registerAdapter(UserAdapter()); + Hive.registerAdapter(ServerHostingDetailsAdapter()); + Hive.registerAdapter(ServerDomainAdapter()); + Hive.registerAdapter(BackupsCredentialAdapter()); + Hive.registerAdapter(ServerProviderVolumeAdapter()); + Hive.registerAdapter(BackblazeBucketAdapter()); + Hive.registerAdapter(ServerProviderCredentialAdapter()); + Hive.registerAdapter(DnsProviderCredentialAdapter()); + Hive.registerAdapter(ServerAdapter()); + Hive.registerAdapter(DnsProviderTypeAdapter()); + Hive.registerAdapter(ServerProviderTypeAdapter()); + Hive.registerAdapter(UserTypeAdapter()); + Hive.registerAdapter(BackupsProviderTypeAdapter()); + Hive.registerAdapter(ServerInstallationWizardDataAdapter()); + log('successfully registered every adapter'); + } catch (error, stackTrace) { + log( + 'error registering adapters', + error: error, + stackTrace: stackTrace, ); - - await Hive.openBox(BNames.serverInstallationBox, - encryptionCipher: cipher); - await Hive.openBox(BNames.resourcesBox, encryptionCipher: cipher); - await Hive.openBox(BNames.wizardDataBox, encryptionCipher: cipher); - - final Box resourcesBox = Hive.box(BNames.resourcesBox); - if (resourcesBox.isEmpty) { - final Box serverInstallationBox = - Hive.box(BNames.serverInstallationBox); - - final String? serverProviderKey = - serverInstallationBox.get(BNames.hetznerKey); - final String? serverLocation = - serverInstallationBox.get(BNames.serverLocation); - final String? dnsProviderKey = - serverInstallationBox.get(BNames.cloudFlareKey); - final BackupsCredential? backblazeCredential = - serverInstallationBox.get(BNames.backblazeCredential); - final ServerDomain? serverDomain = - serverInstallationBox.get(BNames.serverDomain); - final ServerHostingDetails? serverDetails = - serverInstallationBox.get(BNames.serverDetails); - final BackblazeBucket? backblazeBucket = - serverInstallationBox.get(BNames.backblazeBucket); - final String? serverType = - serverInstallationBox.get(BNames.serverTypeIdentifier); - final ServerProviderType? serverProvider = - serverInstallationBox.get(BNames.serverProvider); - final DnsProviderType? dnsProvider = - serverInstallationBox.get(BNames.dnsProvider); - - if (serverProviderKey != null && - (serverProvider != null || - (serverDetails != null && - serverDetails.provider != ServerProviderType.unknown))) { - final ServerProviderCredential serverProviderCredential = - ServerProviderCredential( - tokenId: null, - token: serverProviderKey, - provider: serverProvider ?? serverDetails!.provider, - associatedServerIds: - serverDetails != null ? [serverDetails.id] : [], - ); - - await resourcesBox - .put(BNames.serverProviderTokens, [serverProviderCredential]); - } - - if (dnsProviderKey != null && - (dnsProvider != null || - (serverDomain != null && - serverDomain.provider != DnsProviderType.unknown))) { - final DnsProviderCredential dnsProviderCredential = - DnsProviderCredential( - tokenId: null, - token: dnsProviderKey, - provider: dnsProvider ?? serverDomain!.provider, - associatedDomainNames: - serverDomain != null ? [serverDomain.domainName] : [], - ); - - await resourcesBox - .put(BNames.dnsProviderTokens, [dnsProviderCredential]); - } - - if (backblazeCredential != null) { - await resourcesBox - .put(BNames.backupsProviderTokens, [backblazeCredential]); - } - - if (backblazeBucket != null) { - await resourcesBox.put(BNames.backblazeBucket, backblazeBucket); - } - - if (serverDetails != null && serverDomain != null) { - await resourcesBox.put(BNames.servers, [ - Server( - domain: serverDomain, - hostingDetails: serverDetails.copyWith( - serverLocation: serverLocation, - serverType: serverType, - ), - ), - ]); - } - } - } on PlatformException catch (e) { - print('HiveConfig: Error while opening boxes: $e'); rethrow; } } - static Future getEncryptedKey(final String encKey) async { - const FlutterSecureStorage secureStorage = FlutterSecureStorage(); - try { - final bool hasEncryptionKey = - await secureStorage.containsKey(key: encKey); - if (!hasEncryptionKey) { - final List key = Hive.generateSecureKey(); - await secureStorage.write(key: encKey, value: base64UrlEncode(key)); - } + static Future getCipher() async { + List? key = await SecureStorage.getKey(); + if (key == null) { + key = Hive.generateSecureKey(); + await SecureStorage.setKey(key); + } + return HiveAesCipher(key); + } - final String? string = await secureStorage.read(key: encKey); - return base64Url.decode(string!); - } on PlatformException catch (e) { - print('HiveConfig: Error while getting encryption key: $e'); + static Future decryptBoxes() async { + try { + // load encrypted boxes into memory + final HiveAesCipher cipher = await getCipher(); + + await Hive.openBox( + BNames.serverInstallationBox, + encryptionCipher: cipher, + ); + await Hive.openBox( + BNames.resourcesBox, + encryptionCipher: cipher, + ); + await Hive.openBox( + BNames.wizardDataBox, + encryptionCipher: cipher, + ); + log('successfully decrypted boxes'); + } catch (error, stackTrace) { + log( + 'error initializing encrypted boxes', + error: error, + stackTrace: stackTrace, + ); rethrow; } } + + // migrations + + static Future performMigrations() async { + try { + // perform migration check + final localSettingsBox = await Hive.openBox(BNames.appSettingsBox); + + // if it is an initial app launch, we do not need to perform any migrations + final savedVersion = localSettingsBox.isEmpty + ? version + // if box was initialized, but database version was not introduced in + // it yet, it means that we have initial value + : await localSettingsBox.get(BNames.databaseVersion, defaultValue: 1); + + /// launch migrations based on version + if (savedVersion < version) { + if (savedVersion < 2) { + await migrateFrom1To2(); + } + + /// add new migrations here, like: + /// if (version < 3) {...}, etc. + + /// update saved version after successfull migraions + await localSettingsBox.put(BNames.databaseVersion, version); + } + } catch (error, stackTrace) { + log( + 'error running db migrations', + error: error, + stackTrace: stackTrace, + ); + rethrow; + } + } + + /// introduce and populate resourcesBox + static Future migrateFrom1To2() async { + final Box resourcesBox = Hive.box(BNames.resourcesBox); + if (resourcesBox.isEmpty) { + final Box serverInstallationBox = Hive.box(BNames.serverInstallationBox); + + final String? serverProviderKey = + serverInstallationBox.get(BNames.hetznerKey); + final String? serverLocation = + serverInstallationBox.get(BNames.serverLocation); + final String? dnsProviderKey = + serverInstallationBox.get(BNames.cloudFlareKey); + final BackupsCredential? backblazeCredential = + serverInstallationBox.get(BNames.backblazeCredential); + final ServerDomain? serverDomain = + serverInstallationBox.get(BNames.serverDomain); + final ServerHostingDetails? serverDetails = + serverInstallationBox.get(BNames.serverDetails); + final BackblazeBucket? backblazeBucket = + serverInstallationBox.get(BNames.backblazeBucket); + final String? serverType = + serverInstallationBox.get(BNames.serverTypeIdentifier); + final ServerProviderType? serverProvider = + serverInstallationBox.get(BNames.serverProvider); + final DnsProviderType? dnsProvider = + serverInstallationBox.get(BNames.dnsProvider); + + if (serverProviderKey != null && + (serverProvider != null || + (serverDetails != null && + serverDetails.provider != ServerProviderType.unknown))) { + final ServerProviderCredential serverProviderCredential = + ServerProviderCredential( + tokenId: null, + token: serverProviderKey, + provider: serverProvider ?? serverDetails!.provider, + associatedServerIds: serverDetails != null ? [serverDetails.id] : [], + ); + + await resourcesBox + .put(BNames.serverProviderTokens, [serverProviderCredential]); + } + + if (dnsProviderKey != null && + (dnsProvider != null || + (serverDomain != null && + serverDomain.provider != DnsProviderType.unknown))) { + final DnsProviderCredential dnsProviderCredential = + DnsProviderCredential( + tokenId: null, + token: dnsProviderKey, + provider: dnsProvider ?? serverDomain!.provider, + associatedDomainNames: + serverDomain != null ? [serverDomain.domainName] : [], + ); + + await resourcesBox + .put(BNames.dnsProviderTokens, [dnsProviderCredential]); + } + + if (backblazeCredential != null) { + await resourcesBox + .put(BNames.backupsProviderTokens, [backblazeCredential]); + } + + if (backblazeBucket != null) { + await resourcesBox.put(BNames.backblazeBucket, backblazeBucket); + } + + if (serverDetails != null && serverDomain != null) { + await resourcesBox.put(BNames.servers, [ + Server( + domain: serverDomain, + hostingDetails: serverDetails.copyWith( + serverLocation: serverLocation, + serverType: serverType, + ), + ), + ]); + } + } + log('successfully migration of db from 1 to 2 version'); + } } /// Mappings for the different boxes and their keys @@ -157,6 +219,9 @@ class BNames { /// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding] static String appSettingsBox = 'appSettings'; + /// An integer with last saved version of the database + static String databaseVersion = 'databaseVersion'; + /// A boolean field of [appSettingsBox] box. static String darkThemeModeOn = 'isDarkModeOn'; @@ -169,9 +234,6 @@ class BNames { /// A string field static String appLocale = 'appLocale'; - /// Encryption key to decrypt [serverInstallationBox] box. - static String serverInstallationEncryptionKey = 'key'; - /// Server installation box. Contains server details and provider tokens. static String serverInstallationBox = 'appConfig'; diff --git a/lib/logic/api_maps/rest_maps/rest_api_map.dart b/lib/logic/api_maps/rest_maps/rest_api_map.dart index 5426248f..73c44c1e 100644 --- a/lib/logic/api_maps/rest_maps/rest_api_map.dart +++ b/lib/logic/api_maps/rest_maps/rest_api_map.dart @@ -1,19 +1,25 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; -import 'package:pretty_dio_logger/pretty_dio_logger.dart'; +// import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/models/console_log.dart'; +import 'package:selfprivacy/utils/app_logger.dart'; abstract class RestApiMap { + static final log = const AppLogger(name: 'rest_api_map').log; + Future getClient({final BaseOptions? customOptions}) async { final Dio dio = Dio(customOptions ?? (await options)); if (hasLogger) { - dio.interceptors.add(PrettyDioLogger()); + // dio.interceptors.add( + // PrettyDioLogger( + // logPrint: (final object) => log('$object'), + // ), + // ); } dio.interceptors.add(ConsoleInterceptor()); dio.httpClientAdapter = IOHttpClientAdapter( @@ -29,11 +35,7 @@ abstract class RestApiMap { dio.interceptors.add( InterceptorsWrapper( onError: (final DioException e, final ErrorInterceptorHandler handler) { - print(e.requestOptions.path); - print(e.requestOptions.data); - - print(e.message); - print(e.response); + log('got dio error', error: e); return handler.next(e); }, @@ -103,7 +105,6 @@ class ConsoleInterceptor extends InterceptorsWrapper { final ErrorInterceptorHandler handler, ) async { final Response? response = err.response; - log(err.toString()); addConsoleLog( ManualConsoleLog.warning( diff --git a/lib/utils/app_logger.dart b/lib/utils/app_logger.dart new file mode 100644 index 00000000..b03c01f7 --- /dev/null +++ b/lib/utils/app_logger.dart @@ -0,0 +1,26 @@ +import 'dart:developer' as developer; + +import 'package:selfprivacy/config/config.dart'; + +class AppLogger { + const AppLogger({required this.name}); + final String name; + + // TODO: research other possible options, which could support both + // throttling and output formatting + void log( + final String message, { + final Object? error, + final StackTrace? stackTrace, + }) { + if (config.shouldDebugPrint) { + developer.log( + message, + error: error, + stackTrace: stackTrace, + time: DateTime.now(), + name: name, + ); + } + } +} diff --git a/lib/utils/secure_storage.dart b/lib/utils/secure_storage.dart new file mode 100644 index 00000000..a51562c5 --- /dev/null +++ b/lib/utils/secure_storage.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:selfprivacy/utils/app_logger.dart'; + +class SecureStorage { + static final log = const AppLogger(name: 'secure_storage').log; + + static const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + static String keyName = 'key'; + + static Future getKey() async { + try { + final bool hasEncryptionKey = + await secureStorage.containsKey(key: keyName); + if (!hasEncryptionKey) { + return null; + } + final String? string = await secureStorage.read(key: keyName); + log('successfully got encryption key: $string'); + + return base64Url.decode(string!); + } catch (error, stackTrace) { + log( + 'error reading encryption key', + error: error, + stackTrace: stackTrace, + ); + rethrow; + } + } + + static Future setKey(final List key) async { + try { + final value = base64UrlEncode(key); + await secureStorage.write(key: keyName, value: value); + log('successfully saved encryption key: $value'); + } catch (error, stackTrace) { + log( + 'error saving new encryption key', + error: error, + stackTrace: stackTrace, + ); + rethrow; + } + } +}