From 41dc77103f8a98b7076a9bee7469f387c6bf2d83 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 22 Dec 2022 22:45:06 +0400 Subject: [PATCH 1/2] feat: Implement error handling on server deletion Notify users when errors occured and handle application state accordingly --- assets/translations/en.json | 2 + assets/translations/ru.json | 2 + .../digital_ocean/digital_ocean.dart | 48 +++++++++++++++--- .../server_providers/hetzner/hetzner.dart | 49 ++++++++++++------- .../server_providers/server_provider.dart | 4 +- .../server_installation_cubit.dart | 6 ++- .../server_installation_repository.dart | 25 ++++++++-- 7 files changed, 107 insertions(+), 29 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index daa4544f..1b596cba 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -392,6 +392,8 @@ "generation_error": "Couldn't generate a recovery key. {}" }, "modals": { + "dns_removal_error": "Couldn't remove DNS records.", + "server_deletion_error": "Couldn't delete active server.", "server_validators_error": "Couldn't fetch available servers.", "already_exists": "Such server already exists.", "unexpected_error": "Unexpected error during placement from the provider side.", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index a5cc6634..8764580a 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -392,6 +392,8 @@ "generation_error": "Не удалось сгенерировать ключ. {}" }, "modals": { + "dns_removal_error": "Невозможно удалить DNS записи.", + "server_deletion_error": "Невозможно удалить сервер.", "server_validators_error": "Не удалось получить список серверов.", "already_exists": "Такой сервер уже существует.", "unexpected_error": "Непредвиденная ошибка со стороны провайдера.", diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart index c7928653..2898b609 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart @@ -431,17 +431,41 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { } @override - Future deleteServer({ + Future> deleteServer({ required final String domainName, }) async { final Dio client = await getClient(); - final ServerBasicInfo serverToRemove = (await getServers()).firstWhere( - (final el) => el.name == domainName, - ); - final ServerVolume volumeToRemove = (await getVolumes()).firstWhere( - (final el) => el.serverId == serverToRemove.id, - ); + final servers = await getServers(); + final ServerBasicInfo serverToRemove; + try { + serverToRemove = servers.firstWhere( + (final el) => el.name == domainName, + ); + } catch (e) { + print(e); + return APIGenericResult( + data: false, + success: false, + message: e.toString(), + ); + } + + final volumes = await getVolumes(); + final ServerVolume volumeToRemove; + try { + volumeToRemove = volumes.firstWhere( + (final el) => el.serverId == serverToRemove.id, + ); + } catch (e) { + print(e); + return APIGenericResult( + data: false, + success: false, + message: e.toString(), + ); + } + final List laterFutures = []; await detachVolume(volumeToRemove); @@ -453,9 +477,19 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { await Future.wait(laterFutures); } catch (e) { print(e); + return APIGenericResult( + success: false, + data: false, + message: e.toString(), + ); } finally { close(client); } + + return APIGenericResult( + success: true, + data: true, + ); } @override diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart index f0e032e8..d7a13b95 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart @@ -479,31 +479,46 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi { } @override - Future deleteServer({ + Future> deleteServer({ required final String domainName, }) async { final Dio client = await getClient(); + try { + final String hostname = getHostnameFromDomain(domainName); - final String hostname = getHostnameFromDomain(domainName); + final Response serversReponse = await client.get('/servers'); + final List servers = serversReponse.data['servers']; + final Map server = + servers.firstWhere((final el) => el['name'] == hostname); + final List volumes = server['volumes']; + final List laterFutures = []; - final Response serversReponse = await client.get('/servers'); - final List servers = serversReponse.data['servers']; - final Map server = servers.firstWhere((final el) => el['name'] == hostname); - final List volumes = server['volumes']; - final List laterFutures = []; + for (final volumeId in volumes) { + await client.post('/volumes/$volumeId/actions/detach'); + } + await Future.delayed(const Duration(seconds: 10)); - for (final volumeId in volumes) { - await client.post('/volumes/$volumeId/actions/detach'); + for (final volumeId in volumes) { + laterFutures.add(client.delete('/volumes/$volumeId')); + } + laterFutures.add(client.delete('/servers/${server['id']}')); + + await Future.wait(laterFutures); + } catch (e) { + print(e); + return APIGenericResult( + success: false, + data: false, + message: e.toString(), + ); + } finally { + close(client); } - await Future.delayed(const Duration(seconds: 10)); - for (final volumeId in volumes) { - laterFutures.add(client.delete('/volumes/$volumeId')); - } - laterFutures.add(client.delete('/servers/${server['id']}')); - - await Future.wait(laterFutures); - close(client); + return APIGenericResult( + success: true, + data: true, + ); } @override diff --git a/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart b/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart index 05fb5e61..c858d67b 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart @@ -31,7 +31,9 @@ abstract class ServerProviderApi extends ApiMap { Future restart(); Future powerOn(); - Future deleteServer({required final String domainName}); + Future> deleteServer({ + required final String domainName, + }); Future> createServer({ required final String dnsApiToken, required final User rootUser, diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 4da57479..5638b765 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -756,7 +756,11 @@ class ServerInstallationCubit extends Cubit { closeTimer(); if (state.serverDetails != null) { - await repository.deleteServer(state.serverDomain!); + final bool deletionResult = + await repository.deleteServer(state.serverDomain!); + if (!deletionResult) { + return; + } } await repository.deleteServerRelatedRecords(); emit( diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index f1aeadf5..5d45e7b9 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -759,13 +759,26 @@ class ServerInstallationRepository { await box.put(BNames.hasFinalChecked, value); } - Future deleteServer(final ServerDomain serverDomain) async { - await ApiController.currentServerProviderApiFactory! + Future deleteServer(final ServerDomain serverDomain) async { + final APIGenericResult deletionResult = await ApiController + .currentServerProviderApiFactory! .getServerProvider() .deleteServer( domainName: serverDomain.domainName, ); + if (!deletionResult.success) { + getIt() + .showSnackBar('modals.server_validators_error'.tr()); + return false; + } + + if (!deletionResult.data) { + getIt() + .showSnackBar('modals.server_deletion_error'.tr()); + return false; + } + await box.put(BNames.hasFinalChecked, false); await box.put(BNames.isServerStarted, false); await box.put(BNames.isServerResetedFirstTime, false); @@ -773,9 +786,15 @@ class ServerInstallationRepository { await box.put(BNames.isLoading, false); await box.put(BNames.serverDetails, null); - await ApiController.currentDnsProviderApiFactory! + final APIGenericResult removalResult = await ApiController + .currentDnsProviderApiFactory! .getDnsProvider() .removeSimilarRecords(domain: serverDomain); + + if (!removalResult.success) { + getIt().showSnackBar('modals.dns_removal_error'.tr()); + } + return true; } Future deleteServerRelatedRecords() async { From f64f741a76796bbe672556321834dbd0574716b5 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Fri, 23 Dec 2022 00:17:48 +0400 Subject: [PATCH 2/2] fix: Manage server deletion for Digital Ocean --- .../digital_ocean/digital_ocean.dart | 23 ++++--------------- .../server_providers/hetzner/hetzner.dart | 18 +-------------- lib/utils/network_utils.dart | 17 ++++++++++++++ 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart index 2898b609..4beb53f7 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart @@ -18,6 +18,7 @@ import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/utils/extensions/string_extensions.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { @@ -325,23 +326,6 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { return success; } - static String getHostnameFromDomain(final String domain) { - // Replace all non-alphanumeric characters with an underscore - String hostname = - domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); - if (hostname.endsWith('-')) { - hostname = hostname.substring(0, hostname.length - 1); - } - if (hostname.startsWith('-')) { - hostname = hostname.substring(1); - } - if (hostname.isEmpty) { - hostname = 'selfprivacy-server'; - } - - return hostname; - } - @override Future> createServer({ required final String dnsApiToken, @@ -436,11 +420,12 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { }) async { final Dio client = await getClient(); + final String hostname = getHostnameFromDomain(domainName); final servers = await getServers(); final ServerBasicInfo serverToRemove; try { serverToRemove = servers.firstWhere( - (final el) => el.name == domainName, + (final el) => el.name == hostname, ); } catch (e) { print(e); @@ -473,7 +458,7 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { try { laterFutures.add(deleteVolume(volumeToRemove)); - laterFutures.add(client.delete('/droplets/$serverToRemove.id')); + laterFutures.add(client.delete('/droplets/${serverToRemove.id}')); await Future.wait(laterFutures); } catch (e) { print(e); diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart index d7a13b95..7f51c768 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart @@ -19,6 +19,7 @@ import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/utils/extensions/string_extensions.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ServerProviderApi with VolumeProviderApi { @@ -461,23 +462,6 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi { ); } - static String getHostnameFromDomain(final String domain) { - // Replace all non-alphanumeric characters with an underscore - String hostname = - domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); - if (hostname.endsWith('-')) { - hostname = hostname.substring(0, hostname.length - 1); - } - if (hostname.startsWith('-')) { - hostname = hostname.substring(1); - } - if (hostname.isEmpty) { - hostname = 'selfprivacy-server'; - } - - return hostname; - } - @override Future> deleteServer({ required final String domainName, diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index 8b75728f..fc06ecb8 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -133,3 +133,20 @@ DnsRecord? extractDkimRecord(final List records) { return dkimRecord; } + +String getHostnameFromDomain(final String domain) { + // Replace all non-alphanumeric characters with an underscore + String hostname = + domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); + if (hostname.endsWith('-')) { + hostname = hostname.substring(0, hostname.length - 1); + } + if (hostname.startsWith('-')) { + hostname = hostname.substring(1); + } + if (hostname.isEmpty) { + hostname = 'selfprivacy-server'; + } + + return hostname; +}