From 80f28463ec167556fca6b87fad4f3026d3a8e7e5 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 21 Jul 2023 18:07:26 -0300 Subject: [PATCH 01/11] fix(recovery): Implement server type id requesting on recovery finish --- .../server_installation_cubit.dart | 3 ++ lib/logic/models/server_basic_info.dart | 4 ++ .../server_providers/digital_ocean.dart | 52 +++++++++++++++++++ .../providers/server_providers/hetzner.dart | 48 +++++++++++++++++ .../server_providers/server_provider.dart | 7 +++ 5 files changed, 114 insertions(+) diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 3f00a5b5..968444e8 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -721,12 +721,15 @@ class ServerInstallationCubit extends Cubit { await repository.saveIsServerResetedSecondTime(true); await repository.saveHasFinalChecked(true); await repository.saveIsRecoveringServer(false); + final serverInfo = await ProvidersController.currentServerProvider! + .getServerInfo(state.serverDetails!.id); final User mainUser = await repository.getMainUser(); await repository.saveRootUser(mainUser); final ServerInstallationRecovery updatedState = (state as ServerInstallationRecovery).copyWith( backblazeCredential: backblazeCredential, rootUser: mainUser, + serverTypeIdentificator: serverInfo.data?.serverTypeId ?? '', ); emit(updatedState.finish()); } diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart index 3037a2d5..dc7b9165 100644 --- a/lib/logic/models/server_basic_info.dart +++ b/lib/logic/models/server_basic_info.dart @@ -3,12 +3,14 @@ class ServerBasicInfo { required this.id, required this.name, required this.reverseDns, + required this.serverTypeId, required this.ip, required this.created, }); final int id; final String name; final String reverseDns; + final String serverTypeId; final String ip; final DateTime created; } @@ -22,6 +24,7 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo { id: serverBasicInfo.id, name: serverBasicInfo.name, reverseDns: serverBasicInfo.reverseDns, + serverTypeId: serverBasicInfo.serverTypeId, ip: serverBasicInfo.ip, created: serverBasicInfo.created, isIpValid: isIpValid, @@ -32,6 +35,7 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo { required super.id, required super.name, required super.reverseDns, + required super.serverTypeId, required super.ip, required super.created, required this.isIpValid, diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index bca7bd97..7b613698 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -81,6 +81,7 @@ class DigitalOceanServerProvider extends ServerProvider { created: DateTime.now(), ip: ipv4, name: server['name'], + serverTypeId: server['region']['slug'], ); }, ).toList(); @@ -88,6 +89,57 @@ class DigitalOceanServerProvider extends ServerProvider { return GenericResult(success: true, data: servers); } + @override + Future> getServerInfo( + final int serverId, + ) async { + ServerBasicInfo? server; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: server, + code: result.code, + message: result.message, + ); + } + + final rawServers = result.data; + for (final rawServer in rawServers) { + String? ipv4; + if (rawServer['networks']['v4'].isNotEmpty) { + for (final v4 in rawServer['networks']['v4']) { + if (v4['type'].toString() == 'public') { + ipv4 = v4['ip_address'].toString(); + } + } + } + + try { + server = ServerBasicInfo( + id: rawServer['id'], + reverseDns: rawServer['name'], + created: DateTime.now(), + ip: ipv4!, + name: rawServer['name'], + serverTypeId: rawServer['region']['slug'], + ); + } catch (e) { + print(e); + continue; + } + } + + if (server == null) { + return GenericResult( + success: false, + data: server, + ); + } + + return GenericResult(success: true, data: server); + } + @override Future> launchInstallation( final LaunchInstallationData installationData, diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index 140c1542..db9ddc03 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -77,6 +77,7 @@ class HetznerServerProvider extends ServerProvider { ip: hetznerServer.publicNet.ipv4!.ip, reverseDns: hetznerServer.publicNet.ipv4!.reverseDns, created: hetznerServer.created, + serverTypeId: hetznerServer.name, ); } catch (e) { continue; @@ -88,6 +89,53 @@ class HetznerServerProvider extends ServerProvider { return GenericResult(success: true, data: servers); } + @override + Future> getServerInfo( + final int serverId, + ) async { + ServerBasicInfo? server; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: server, + code: result.code, + message: result.message, + ); + } + + final List hetznerServers = result.data; + for (final hetznerServer in hetznerServers) { + if (hetznerServer.publicNet.ipv4 == null || + hetznerServer.id != serverId) { + continue; + } + + try { + server = ServerBasicInfo( + id: hetznerServer.id, + name: hetznerServer.name, + ip: hetznerServer.publicNet.ipv4!.ip, + reverseDns: hetznerServer.publicNet.ipv4!.reverseDns, + created: hetznerServer.created, + serverTypeId: hetznerServer.serverType.name, + ); + } catch (e) { + print(e); + continue; + } + } + + if (server == null) { + return GenericResult( + success: false, + data: server, + ); + } + + return GenericResult(success: true, data: server); + } + @override Future> launchInstallation( final LaunchInstallationData installationData, diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart index f71ce09a..4182d9fc 100644 --- a/lib/logic/providers/server_providers/server_provider.dart +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -24,6 +24,13 @@ abstract class ServerProvider { /// Only with public IPv4 addresses. Future>> getServers(); + /// Returns actual [ServerBasicInfo] of the + /// requested server entry assigned + /// to the authorized user. + /// + /// Only with public IPv4 address. + Future> getServerInfo(final int serverId); + /// Tries to launch installation of SelfPrivacy on /// the requested server entry for the authorized account. /// Depending on a server provider, the algorithm From 7b0207434512d4544cf8392c0ce314061724266f Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 21 Jul 2023 19:48:00 -0300 Subject: [PATCH 02/11] fix(recovery): Replace server basic info request method with server type info request method --- .../server_installation_cubit.dart | 9 +- .../server_providers/digital_ocean.dart | 84 +++++++++++++------ .../providers/server_providers/hetzner.dart | 73 ++++++++++------ .../server_providers/server_provider.dart | 6 +- 4 files changed, 117 insertions(+), 55 deletions(-) diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 968444e8..c669cf75 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -721,15 +721,18 @@ class ServerInstallationCubit extends Cubit { await repository.saveIsServerResetedSecondTime(true); await repository.saveHasFinalChecked(true); await repository.saveIsRecoveringServer(false); - final serverInfo = await ProvidersController.currentServerProvider! - .getServerInfo(state.serverDetails!.id); + final serverType = await ProvidersController.currentServerProvider! + .getServerType(state.serverDetails!.id); + await repository.saveServerType(serverType.data!); + await ProvidersController.currentServerProvider! + .trySetServerLocation(serverType.data!.location.identifier); final User mainUser = await repository.getMainUser(); await repository.saveRootUser(mainUser); final ServerInstallationRecovery updatedState = (state as ServerInstallationRecovery).copyWith( backblazeCredential: backblazeCredential, rootUser: mainUser, - serverTypeIdentificator: serverInfo.data?.serverTypeId ?? '', + serverTypeIdentificator: serverType.data!.identifier, ); emit(updatedState.finish()); } diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index 7b613698..4082abe5 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -90,54 +90,90 @@ class DigitalOceanServerProvider extends ServerProvider { } @override - Future> getServerInfo( - final int serverId, - ) async { - ServerBasicInfo? server; + Future> getServerType(final int serverId) async { + ServerType? serverType; + dynamic server; final result = await _adapter.api().getServers(); if (result.data.isEmpty || !result.success) { return GenericResult( success: result.success, - data: server, + data: serverType, code: result.code, message: result.message, ); } - final rawServers = result.data; + final List rawServers = result.data; for (final rawServer in rawServers) { - String? ipv4; if (rawServer['networks']['v4'].isNotEmpty) { for (final v4 in rawServer['networks']['v4']) { if (v4['type'].toString() == 'public') { - ipv4 = v4['ip_address'].toString(); + server = rawServer; } } } - - try { - server = ServerBasicInfo( - id: rawServer['id'], - reverseDns: rawServer['name'], - created: DateTime.now(), - ip: ipv4!, - name: rawServer['name'], - serverTypeId: rawServer['region']['slug'], - ); - } catch (e) { - print(e); - continue; - } } if (server == null) { + const String msg = 'getServerType: no server!'; + print(msg); return GenericResult( success: false, - data: server, + data: serverType, + message: msg, ); } - return GenericResult(success: true, data: server); + final rawLocationsResult = await getAvailableLocations(); + if (rawLocationsResult.data.isEmpty || !rawLocationsResult.success) { + return GenericResult( + success: rawLocationsResult.success, + data: serverType, + code: rawLocationsResult.code, + message: rawLocationsResult.message, + ); + } + + ServerProviderLocation? location; + for (final rawLocation in rawLocationsResult.data) { + if (rawLocation.identifier == server['region']['slug']) { + location = rawLocation; + } + } + + if (location == null) { + const String msg = 'getServerType: no location!'; + print(msg); + return GenericResult( + success: false, + data: serverType, + message: msg, + ); + } + + ServerType? type; + final rawSize = DigitalOceanServerType.fromJson(server['size']); + for (final rawRegion in rawSize.regions) { + if (rawRegion == server['region']['slug']) { + type = ServerType( + title: rawSize.description, + identifier: rawSize.slug, + ram: rawSize.memory / 1024, + cores: rawSize.vcpus, + disk: DiskSize(byte: rawSize.disk * 1024 * 1024 * 1024), + price: Price( + value: rawSize.priceMonthly, + currency: currency, + ), + location: location, + ); + } + } + + return GenericResult( + success: true, + data: type, + ); } @override diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index db9ddc03..e902a788 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -90,15 +90,14 @@ class HetznerServerProvider extends ServerProvider { } @override - Future> getServerInfo( - final int serverId, - ) async { - ServerBasicInfo? server; + Future> getServerType(final int serverId) async { + ServerType? serverType; + HetznerServerInfo? server; final result = await _adapter.api().getServers(); if (result.data.isEmpty || !result.success) { return GenericResult( success: result.success, - data: server, + data: serverType, code: result.code, message: result.message, ); @@ -106,34 +105,60 @@ class HetznerServerProvider extends ServerProvider { final List hetznerServers = result.data; for (final hetznerServer in hetznerServers) { - if (hetznerServer.publicNet.ipv4 == null || - hetznerServer.id != serverId) { - continue; - } - - try { - server = ServerBasicInfo( - id: hetznerServer.id, - name: hetznerServer.name, - ip: hetznerServer.publicNet.ipv4!.ip, - reverseDns: hetznerServer.publicNet.ipv4!.reverseDns, - created: hetznerServer.created, - serverTypeId: hetznerServer.serverType.name, - ); - } catch (e) { - print(e); - continue; + if (hetznerServer.publicNet.ipv4 != null || + hetznerServer.id == serverId) { + server = hetznerServer; + break; } } if (server == null) { + const String msg = 'getServerType: no server!'; + print(msg); return GenericResult( success: false, - data: server, + data: serverType, + message: msg, ); } - return GenericResult(success: true, data: server); + double? priceValue; + for (final price in server.serverType.prices) { + if (price.location == server.location.name) { + priceValue = price.monthly; + } + } + + if (priceValue == null) { + const String msg = 'getServerType: no price!'; + print(msg); + return GenericResult( + success: false, + data: serverType, + message: msg, + ); + } + + return GenericResult( + success: true, + data: ServerType( + title: server.serverType.description, + identifier: server.serverType.name, + ram: server.serverType.memory.toDouble(), + cores: server.serverType.cores, + disk: DiskSize(byte: server.serverType.disk * 1024 * 1024 * 1024), + price: Price( + value: priceValue, + currency: currency, + ), + location: ServerProviderLocation( + title: server.location.city, + description: server.location.description, + flag: server.location.flag, + identifier: server.location.name, + ), + ), + ); } @override diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart index 4182d9fc..cfcf3f5e 100644 --- a/lib/logic/providers/server_providers/server_provider.dart +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -24,12 +24,10 @@ abstract class ServerProvider { /// Only with public IPv4 addresses. Future>> getServers(); - /// Returns actual [ServerBasicInfo] of the + /// Returns actual [ServerType] of the /// requested server entry assigned /// to the authorized user. - /// - /// Only with public IPv4 address. - Future> getServerInfo(final int serverId); + Future> getServerType(final int serverId); /// Tries to launch installation of SelfPrivacy on /// the requested server entry for the authorized account. From 69822f24ca0ee05fe74e437c7f315e526101e415 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 21 Jul 2023 19:50:20 -0300 Subject: [PATCH 03/11] chore: Remove unused server basic info attribute --- lib/logic/models/server_basic_info.dart | 4 ---- lib/logic/providers/server_providers/digital_ocean.dart | 1 - lib/logic/providers/server_providers/hetzner.dart | 1 - 3 files changed, 6 deletions(-) diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart index dc7b9165..3037a2d5 100644 --- a/lib/logic/models/server_basic_info.dart +++ b/lib/logic/models/server_basic_info.dart @@ -3,14 +3,12 @@ class ServerBasicInfo { required this.id, required this.name, required this.reverseDns, - required this.serverTypeId, required this.ip, required this.created, }); final int id; final String name; final String reverseDns; - final String serverTypeId; final String ip; final DateTime created; } @@ -24,7 +22,6 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo { id: serverBasicInfo.id, name: serverBasicInfo.name, reverseDns: serverBasicInfo.reverseDns, - serverTypeId: serverBasicInfo.serverTypeId, ip: serverBasicInfo.ip, created: serverBasicInfo.created, isIpValid: isIpValid, @@ -35,7 +32,6 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo { required super.id, required super.name, required super.reverseDns, - required super.serverTypeId, required super.ip, required super.created, required this.isIpValid, diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index 4082abe5..da246b18 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -81,7 +81,6 @@ class DigitalOceanServerProvider extends ServerProvider { created: DateTime.now(), ip: ipv4, name: server['name'], - serverTypeId: server['region']['slug'], ); }, ).toList(); diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index e902a788..ec15801e 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -77,7 +77,6 @@ class HetznerServerProvider extends ServerProvider { ip: hetznerServer.publicNet.ipv4!.ip, reverseDns: hetznerServer.publicNet.ipv4!.reverseDns, created: hetznerServer.created, - serverTypeId: hetznerServer.name, ); } catch (e) { continue; From 08f32586156eed103dab79a4cb08e7cda88846fa Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sat, 22 Jul 2023 11:15:43 -0300 Subject: [PATCH 04/11] feat(graphql): Implement Accept-Language header for GraphQL API map --- .../graphql_maps/graphql_api_map.dart | 10 +++- lib/logic/get_it/api_config.dart | 8 +++ lib/main.dart | 60 ++++++++++--------- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/lib/logic/api_maps/graphql_maps/graphql_api_map.dart b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart index 2c11c127..f91fc7a7 100644 --- a/lib/logic/api_maps/graphql_maps/graphql_api_map.dart +++ b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart @@ -71,6 +71,7 @@ abstract class GraphQLApiMap { 'https://api.$rootAddress/graphql', httpClient: ioClient, parser: ResponseLoggingParser(), + defaultHeaders: {'Accept-Language': _locale}, ); final String token = _getApiToken(); @@ -101,7 +102,12 @@ abstract class GraphQLApiMap { 'ws://api.$rootAddress/graphql', config: SocketClientConfig( autoReconnect: true, - headers: token.isEmpty ? null : {'Authorization': 'Bearer $token'}, + headers: token.isEmpty + ? null + : { + 'Authorization': 'Bearer $token', + 'Accept-Language': _locale, + }, ), ); @@ -111,6 +117,8 @@ abstract class GraphQLApiMap { ); } + String get _locale => getIt.get().localeCode ?? 'en'; + String _getApiToken() { String token = ''; final serverDetails = getIt().serverDetails; diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 18c8a02c..ac889dcc 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -9,6 +9,7 @@ class ApiConfigModel { final Box _box = Hive.box(BNames.serverInstallationBox); ServerHostingDetails? get serverDetails => _serverDetails; + String? get localeCode => _localeCode; String? get serverProviderKey => _serverProviderKey; String? get serverLocation => _serverLocation; String? get serverType => _serverType; @@ -20,6 +21,7 @@ class ApiConfigModel { ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; + String? _localeCode; String? _serverProviderKey; String? _serverLocation; String? _dnsProviderKey; @@ -31,6 +33,10 @@ class ApiConfigModel { ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; + Future setLocaleCode(final String value) async { + _localeCode = value; + } + Future storeServerProviderType(final ServerProviderType value) async { await _box.put(BNames.serverProvider, value); _serverProvider = value; @@ -82,6 +88,7 @@ class ApiConfigModel { } void clear() { + _localeCode = null; _serverProviderKey = null; _dnsProvider = null; _serverLocation = null; @@ -95,6 +102,7 @@ class ApiConfigModel { } void init() { + _localeCode = 'en'; _serverProviderKey = _box.get(BNames.hetznerKey); _serverLocation = _box.get(BNames.serverLocation); _dnsProviderKey = _box.get(BNames.cloudFlareKey); diff --git a/lib/main.dart b/lib/main.dart index e6358ba7..7ba0114a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -71,34 +71,38 @@ class SelfprivacyApp extends StatelessWidget { builder: ( final BuildContext context, final AppSettingsState appSettings, - ) => - MaterialApp.router( - routeInformationParser: _appRouter.defaultRouteParser(), - routerDelegate: _appRouter.delegate(), - scaffoldMessengerKey: - getIt.get().scaffoldMessengerKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - title: 'SelfPrivacy', - theme: lightThemeData, - darkTheme: darkThemeData, - themeMode: appSettings.isAutoDarkModeOn - ? ThemeMode.system - : appSettings.isDarkModeOn - ? ThemeMode.dark - : ThemeMode.light, - builder: (final BuildContext context, final Widget? widget) { - Widget error = const Text('...rendering error...'); - if (widget is Scaffold || widget is Navigator) { - error = Scaffold(body: Center(child: error)); - } - ErrorWidget.builder = - (final FlutterErrorDetails errorDetails) => error; - return widget!; - }, - ), + ) { + getIt.get().setLocaleCode( + context.locale.languageCode, + ); + return MaterialApp.router( + routeInformationParser: _appRouter.defaultRouteParser(), + routerDelegate: _appRouter.delegate(), + scaffoldMessengerKey: + getIt.get().scaffoldMessengerKey, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + title: 'SelfPrivacy', + theme: lightThemeData, + darkTheme: darkThemeData, + themeMode: appSettings.isAutoDarkModeOn + ? ThemeMode.system + : appSettings.isDarkModeOn + ? ThemeMode.dark + : ThemeMode.light, + builder: (final BuildContext context, final Widget? widget) { + Widget error = const Text('...rendering error...'); + if (widget is Scaffold || widget is Navigator) { + error = Scaffold(body: Center(child: error)); + } + ErrorWidget.builder = + (final FlutterErrorDetails errorDetails) => error; + return widget!; + }, + ); + }, ), ), ); From 27aaf9383875eff37c86885773230e28c18e1142 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sat, 22 Jul 2023 11:26:48 -0300 Subject: [PATCH 05/11] chore: Rename '_getApiToken' to 'get _token' for GraphQL API map --- lib/logic/api_maps/graphql_maps/graphql_api_map.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/logic/api_maps/graphql_maps/graphql_api_map.dart b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart index f91fc7a7..6a00f5b6 100644 --- a/lib/logic/api_maps/graphql_maps/graphql_api_map.dart +++ b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart @@ -74,13 +74,11 @@ abstract class GraphQLApiMap { defaultHeaders: {'Accept-Language': _locale}, ); - final String token = _getApiToken(); - final Link graphQLLink = RequestLoggingLink().concat( isWithToken ? AuthLink( getToken: () async => - customToken == '' ? 'Bearer $token' : customToken, + customToken == '' ? 'Bearer $_token' : customToken, ).concat(httpLink) : httpLink, ); @@ -96,16 +94,14 @@ abstract class GraphQLApiMap { } Future getSubscriptionClient() async { - final String token = _getApiToken(); - final WebSocketLink webSocketLink = WebSocketLink( 'ws://api.$rootAddress/graphql', config: SocketClientConfig( autoReconnect: true, - headers: token.isEmpty + headers: _token.isEmpty ? null : { - 'Authorization': 'Bearer $token', + 'Authorization': 'Bearer $_token', 'Accept-Language': _locale, }, ), @@ -119,7 +115,7 @@ abstract class GraphQLApiMap { String get _locale => getIt.get().localeCode ?? 'en'; - String _getApiToken() { + String get _token { String token = ''; final serverDetails = getIt().serverDetails; if (serverDetails != null) { From c3752673f7fd311756ea587f9afbbbdd91426d28 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 20 Jul 2023 18:35:13 -0300 Subject: [PATCH 06/11] feat(backups): Implement modal for copying backups encryption key --- assets/translations/en.json | 6 +- assets/translations/ru.json | 4 +- lib/ui/pages/backups/backup_details.dart | 29 ++++++ .../backups/copy_encryption_key_modal.dart | 88 +++++++++++++++++++ 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 lib/ui/pages/backups/copy_encryption_key_modal.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index fc7e1eb2..f1ef524f 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -34,7 +34,8 @@ "apply": "Apply", "done": "Done", "continue": "Continue", - "alert": "Alert" + "alert": "Alert", + "copied_to_clipboard": "Copied to clipboard!" }, "more_page": { "configuration_wizard": "Setup wizard", @@ -196,6 +197,7 @@ "autobackup_custom_hint": "Enter custom period in minutes", "autobackup_set_period": "Set period", "autobackup_period_set": "Period set", + "backups_encryption_key": "Encryption key", "pending_jobs": "Currently running backup jobs", "snapshots_title": "Snapshot list" }, @@ -536,4 +538,4 @@ "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", "cubit_statuses": "Cubit loading statuses" } -} +} \ No newline at end of file diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 12c161b9..e906e838 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -34,6 +34,7 @@ "done": "Готово", "continue": "Продолжить", "alert": "Уведомление", + "copied_to_clipboard": "Скопировано в буфер обмена!", "app_name": "SelfPrivacy" }, "more_page": { @@ -197,6 +198,7 @@ "autobackup_custom_hint": "Введите период в минутах", "autobackup_set_period": "Установить период", "autobackup_period_set": "Период установлен", + "backups_encryption_key": "Ключ шифрования", "snapshots_title": "Список снимков" }, "storage": { @@ -536,4 +538,4 @@ "ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.", "ignore_tls": "Не проверять сертификаты TLS" } -} +} \ No newline at end of file diff --git a/lib/ui/pages/backups/backup_details.dart b/lib/ui/pages/backups/backup_details.dart index e3fe427b..54136412 100644 --- a/lib/ui/pages/backups/backup_details.dart +++ b/lib/ui/pages/backups/backup_details.dart @@ -16,6 +16,7 @@ import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart'; +import 'package:selfprivacy/ui/pages/backups/copy_encryption_key_modal.dart'; import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/extensions/duration.dart'; @@ -144,6 +145,34 @@ class BackupDetailsPage extends StatelessWidget { : 'backup.autobackup_period_never'.tr(), ), ), + ListTile( + onTap: preventActions + ? null + : () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + isScrollControlled: true, + builder: (final BuildContext context) => + DraggableScrollableSheet( + expand: false, + maxChildSize: 0.6, + minChildSize: 0.3, + initialChildSize: 0.3, + builder: (final context, final scrollController) => + CopyEncryptionKeyModal( + scrollController: scrollController, + ), + ), + ); + }, + leading: const Icon( + Icons.key_outlined, + ), + title: Text( + 'backup.backups_encryption_key'.tr(), + ), + ), const SizedBox(height: 16), if (backupJobs.isNotEmpty) Column( diff --git a/lib/ui/pages/backups/copy_encryption_key_modal.dart b/lib/ui/pages/backups/copy_encryption_key_modal.dart new file mode 100644 index 00000000..5421b2a8 --- /dev/null +++ b/lib/ui/pages/backups/copy_encryption_key_modal.dart @@ -0,0 +1,88 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; + +class CopyEncryptionKeyModal extends StatefulWidget { + const CopyEncryptionKeyModal({ + required this.scrollController, + super.key, + }); + + final ScrollController scrollController; + + @override + State createState() => _CopyEncryptionKeyModalState(); +} + +class _CopyEncryptionKeyModalState extends State { + bool isKeyVisible = false; + @override + Widget build(final BuildContext context) { + final String encryptionKey = + context.watch().state.backblazeBucket!.encryptionKey; + return ListView( + controller: widget.scrollController, + padding: const EdgeInsets.all(16), + children: [ + const SizedBox(height: 16), + Text( + 'backup.backups_encryption_key'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon( + isKeyVisible + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState( + () { + isKeyVisible = !isKeyVisible; + }, + ); + }, + ), + IconButton( + icon: const Icon(Icons.copy_all_outlined), + onPressed: () { + getIt() + .showSnackBar('basis.copied_to_clipboard'.tr()); + Clipboard.setData( + ClipboardData( + text: encryptionKey, + ), + ); + }, + ), + ], + ), + Flexible( + child: isKeyVisible + ? SelectableText( + encryptionKey, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ) + : Text( + ''.padLeft(encryptionKey.length, '●'), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ), + ], + ); + } +} From 3b1e71d7712b824d30722948f7b3cd20a959b21e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 25 Jul 2023 18:39:58 +0300 Subject: [PATCH 07/11] fix: Add a workaround for the case when we don't have sreverTypeId --- .../server_installation_repository.dart | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 717b7535..5b39463b 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -76,21 +76,44 @@ class ServerInstallationRepository { if (box.get(BNames.hasFinalChecked, defaultValue: false)) { TlsOptions.verifyCertificate = true; - return ServerInstallationFinished( - installationDialoguePopUp: null, - providerApiToken: providerApiToken!, - serverTypeIdentificator: serverTypeIdentificator!, - dnsApiToken: dnsApiToken!, - serverDomain: serverDomain!, - backblazeCredential: backblazeCredential!, - serverDetails: serverDetails!, - rootUser: box.get(BNames.rootUser), - isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), - isServerResetedFirstTime: - box.get(BNames.isServerResetedFirstTime, defaultValue: false), - isServerResetedSecondTime: - box.get(BNames.isServerResetedSecondTime, defaultValue: false), - ); + if (serverTypeIdentificator == null && serverDetails != null) { + final finalServerType = await ProvidersController.currentServerProvider! + .getServerType(serverDetails.id); + await saveServerType(finalServerType.data!); + await ProvidersController.currentServerProvider! + .trySetServerLocation(finalServerType.data!.location.identifier); + return ServerInstallationFinished( + installationDialoguePopUp: null, + providerApiToken: providerApiToken!, + serverTypeIdentificator: finalServerType.data!.identifier, + dnsApiToken: dnsApiToken!, + serverDomain: serverDomain!, + backblazeCredential: backblazeCredential!, + serverDetails: serverDetails, + rootUser: box.get(BNames.rootUser), + isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), + isServerResetedFirstTime: + box.get(BNames.isServerResetedFirstTime, defaultValue: false), + isServerResetedSecondTime: + box.get(BNames.isServerResetedSecondTime, defaultValue: false), + ); + } else { + return ServerInstallationFinished( + installationDialoguePopUp: null, + providerApiToken: providerApiToken!, + serverTypeIdentificator: serverTypeIdentificator!, + dnsApiToken: dnsApiToken!, + serverDomain: serverDomain!, + backblazeCredential: backblazeCredential!, + serverDetails: serverDetails!, + rootUser: box.get(BNames.rootUser), + isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), + isServerResetedFirstTime: + box.get(BNames.isServerResetedFirstTime, defaultValue: false), + isServerResetedSecondTime: + box.get(BNames.isServerResetedSecondTime, defaultValue: false), + ); + } } if (box.get(BNames.isRecoveringServer, defaultValue: false) && From cfcfd5d70859f779a4f73d39b6185d82fddc36d0 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 25 Jul 2023 22:25:08 +0300 Subject: [PATCH 08/11] feat(backups): Update the UI of the encryption key modal --- assets/translations/en.json | 6 +- lib/ui/pages/backups/backup_details.dart | 9 +- lib/ui/pages/backups/change_period_modal.dart | 3 - .../backups/copy_encryption_key_modal.dart | 108 +++++++++++------- 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index f1ef524f..b442b683 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -198,6 +198,10 @@ "autobackup_set_period": "Set period", "autobackup_period_set": "Period set", "backups_encryption_key": "Encryption key", + "backups_encryption_key_subtitle": "Keep it in a safe place.", + "backups_encryption_key_copy": "Copy the encryption key", + "backups_encryption_key_show": "Show the encryption key", + "backups_encryption_key_description": "This key is used to encrypt your backups. If you lose it, you will not be able to restore your backups. Keep it in a safe place, as it will be useful if you ever need to restore from backups manually.", "pending_jobs": "Currently running backup jobs", "snapshots_title": "Snapshot list" }, @@ -538,4 +542,4 @@ "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", "cubit_statuses": "Cubit loading statuses" } -} \ No newline at end of file +} diff --git a/lib/ui/pages/backups/backup_details.dart b/lib/ui/pages/backups/backup_details.dart index 54136412..44995577 100644 --- a/lib/ui/pages/backups/backup_details.dart +++ b/lib/ui/pages/backups/backup_details.dart @@ -156,9 +156,9 @@ class BackupDetailsPage extends StatelessWidget { builder: (final BuildContext context) => DraggableScrollableSheet( expand: false, - maxChildSize: 0.6, - minChildSize: 0.3, - initialChildSize: 0.3, + maxChildSize: 0.9, + minChildSize: 0.5, + initialChildSize: 0.7, builder: (final context, final scrollController) => CopyEncryptionKeyModal( scrollController: scrollController, @@ -172,6 +172,9 @@ class BackupDetailsPage extends StatelessWidget { title: Text( 'backup.backups_encryption_key'.tr(), ), + subtitle: Text( + 'backup.backups_encryption_key_subtitle'.tr(), + ), ), const SizedBox(height: 16), if (backupJobs.isNotEmpty) diff --git a/lib/ui/pages/backups/change_period_modal.dart b/lib/ui/pages/backups/change_period_modal.dart index f3fb2ce3..dbee981e 100644 --- a/lib/ui/pages/backups/change_period_modal.dart +++ b/lib/ui/pages/backups/change_period_modal.dart @@ -20,9 +20,6 @@ class ChangeAutobackupsPeriodModal extends StatefulWidget { class _ChangeAutobackupsPeriodModalState extends State { - // This is a modal with radio buttons to select the autobackup period - // Period might be none, selected from predefined list or custom - // Store in state the selected period Duration? selectedPeriod; static const List autobackupPeriods = [ diff --git a/lib/ui/pages/backups/copy_encryption_key_modal.dart b/lib/ui/pages/backups/copy_encryption_key_modal.dart index 5421b2a8..7b9ce40f 100644 --- a/lib/ui/pages/backups/copy_encryption_key_modal.dart +++ b/lib/ui/pages/backups/copy_encryption_key_modal.dart @@ -20,6 +20,7 @@ class CopyEncryptionKeyModal extends StatefulWidget { class _CopyEncryptionKeyModalState extends State { bool isKeyVisible = false; + @override Widget build(final BuildContext context) { final String encryptionKey = @@ -35,52 +36,77 @@ class _CopyEncryptionKeyModalState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon( - isKeyVisible - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - onPressed: () { - setState( - () { - isKeyVisible = !isKeyVisible; - }, - ); - }, - ), - IconButton( - icon: const Icon(Icons.copy_all_outlined), - onPressed: () { - getIt() - .showSnackBar('basis.copied_to_clipboard'.tr()); - Clipboard.setData( - ClipboardData( - text: encryptionKey, - ), - ); - }, - ), - ], + Text( + 'backup.backups_encryption_key_description'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, ), - Flexible( - child: isKeyVisible - ? SelectableText( + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.surfaceVariant, + ), + padding: const EdgeInsets.all(16), + child: Stack( + children: [ + SelectableText( encryptionKey, style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ) - : Text( - ''.padLeft(encryptionKey.length, '●'), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), + Positioned.fill( + child: InkWell( + onTap: () { + setState( + () { + isKeyVisible = !isKeyVisible; + }, + ); + }, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isKeyVisible ? 0 : 1, + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.visibility_outlined), + const SizedBox(width: 8), + Text( + 'backup.backups_encryption_key_show'.tr(), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ], + )), + ), + ), + ), + ], + )), + const SizedBox(height: 8), + FilledButton.icon( + onPressed: () { + getIt() + .showSnackBar('basis.copied_to_clipboard'.tr()); + Clipboard.setData( + ClipboardData( + text: encryptionKey, + ), + ); + }, + icon: const Icon(Icons.copy_all_outlined), + label: Text('backup.backups_encryption_key_copy'.tr()), ), ], ); From bbc619deed692dfd5b9b706d375722d8b9f77abd Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 25 Jul 2023 22:43:28 +0300 Subject: [PATCH 09/11] feat(backups): Show the user that the key is copied --- .../backups/copy_encryption_key_modal.dart | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/ui/pages/backups/copy_encryption_key_modal.dart b/lib/ui/pages/backups/copy_encryption_key_modal.dart index 7b9ce40f..289ea184 100644 --- a/lib/ui/pages/backups/copy_encryption_key_modal.dart +++ b/lib/ui/pages/backups/copy_encryption_key_modal.dart @@ -1,7 +1,8 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; @@ -20,6 +21,14 @@ class CopyEncryptionKeyModal extends StatefulWidget { class _CopyEncryptionKeyModalState extends State { bool isKeyVisible = false; + bool copiedToClipboard = false; + Timer? copyToClipboardTimer; + + @override + void dispose() { + copyToClipboardTimer?.cancel(); + super.dispose(); + } @override Widget build(final BuildContext context) { @@ -97,8 +106,23 @@ class _CopyEncryptionKeyModalState extends State { const SizedBox(height: 8), FilledButton.icon( onPressed: () { - getIt() - .showSnackBar('basis.copied_to_clipboard'.tr()); + setState( + () { + copiedToClipboard = true; + }, + ); + // Make a timer to reset the copyToClipboardTime + setState(() { + copyToClipboardTimer?.cancel(); + copyToClipboardTimer = Timer( + const Duration(seconds: 5), + () { + setState(() { + copiedToClipboard = false; + }); + }, + ); + }); Clipboard.setData( ClipboardData( text: encryptionKey, @@ -106,7 +130,11 @@ class _CopyEncryptionKeyModalState extends State { ); }, icon: const Icon(Icons.copy_all_outlined), - label: Text('backup.backups_encryption_key_copy'.tr()), + label: Text( + copiedToClipboard + ? 'basis.copied_to_clipboard'.tr() + : 'backup.backups_encryption_key_copy'.tr(), + ), ), ], ); From 5df1d676f67bbbb06a1b6f75c3ade895f35898f8 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 25 Jul 2023 22:45:44 +0300 Subject: [PATCH 10/11] style: Remove misleading code comment --- lib/ui/pages/backups/copy_encryption_key_modal.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ui/pages/backups/copy_encryption_key_modal.dart b/lib/ui/pages/backups/copy_encryption_key_modal.dart index 289ea184..aff629f7 100644 --- a/lib/ui/pages/backups/copy_encryption_key_modal.dart +++ b/lib/ui/pages/backups/copy_encryption_key_modal.dart @@ -111,7 +111,6 @@ class _CopyEncryptionKeyModalState extends State { copiedToClipboard = true; }, ); - // Make a timer to reset the copyToClipboardTime setState(() { copyToClipboardTimer?.cancel(); copyToClipboardTimer = Timer( From 4dae3404fadd8ce4105886f4755e4e332618d9d3 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 25 Jul 2023 18:56:47 -0300 Subject: [PATCH 11/11] fix(ui): Prevent service moving if volume is null for some reason --- lib/ui/pages/services/service_page.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ui/pages/services/service_page.dart b/lib/ui/pages/services/service_page.dart index 0b713e79..e1e00ec0 100644 --- a/lib/ui/pages/services/service_page.dart +++ b/lib/ui/pages/services/service_page.dart @@ -139,7 +139,9 @@ class _ServicePageState extends State { ), style: Theme.of(context).textTheme.bodyMedium, ), - enabled: !serviceDisabled && !serviceLocked, + enabled: !serviceDisabled && + !serviceLocked && + service.storageUsage.volume != null, ), if (service.canBeBackedUp) ListTile(