diff --git a/assets/images/logos/cloudflare.svg b/assets/images/logos/cloudflare.svg index 7099a7e9..03a60465 100644 --- a/assets/images/logos/cloudflare.svg +++ b/assets/images/logos/cloudflare.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/translations/en.json b/assets/translations/en.json index 653e33f8..b45e6ac2 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -279,6 +279,8 @@ "no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon." }, "initializing": { + "server_provider_description": "A place where your data and SelfPrivacy services will reside:", + "dns_provider_description": "A service which lets your IP point towards domain names:", "connect_to_server": "Let's start with a server.", "select_provider": "Pick any provider from the following list, they all support SelfPrivacy", "select_provider_notice": "By 'Relatively small' we mean a machine with 2 cores of CPU and 2 gigabytes of RAM.", @@ -313,8 +315,10 @@ "choose_server_type_storage": "{} GB of system storage", "choose_server_type_payment_per_month": "{} per month", "no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.", - "cloudflare_bad_key_error": "DNS Provider API key is invalid", + "dns_provider_bad_key_error": "API key is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid", + "connect_to_dns": "Connect the DNS provider", + "connect_to_dns_provider_text": "With API token SelfPrivacy will manage all DNS entries", "select_dns": "Now let's select a DNS provider", "manage_domain_dns": "To manage your domain's DNS", "use_this_domain": "Use this domain?", @@ -444,6 +448,7 @@ "modals": { "dns_removal_error": "Couldn't remove DNS records.", "server_deletion_error": "Couldn't delete active server.", + "volume_creation_error": "Couldn't create volume.", "server_validators_error": "Couldn't fetch available servers.", "already_exists": "Such server already exists.", "unexpected_error": "Unexpected error during placement from the provider side.", @@ -488,7 +493,7 @@ "required": "Required", "already_exist": "Already exists", "invalid_format": "Invalid format", - "invalid_format_password": "Must not contain empty characters", + "invalid_format_password": "Password must not contain spaces", "invalid_format_ssh": "Must follow the SSH key format", "root_name": "Cannot be 'root'", "length_not_equal": "Length is [], should be {}", @@ -502,10 +507,12 @@ "subtitle": "These settings are for debugging only. Don't change them unless you know what you're doing.", "server_setup": "Server setup", "use_staging_acme": "Use staging ACME server", - "use_staging_acme_description": "Rebuild your app to change this value.", + "use_staging_acme_description": "Applies when setting up a new server.", + "ignore_tls": "Do not verify TLS certificates", + "ignore_tls_description": "App will not verify TLS certificates when connecting to the server.", "routing": "App routing", "reset_onboarding": "Reset onboarding switch", "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 b4bdd938..9b0c93ad 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -273,6 +273,7 @@ "no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунты. Единая авторизация для всех сервисов ещё не реализована." }, "initializing": { + "dns_provider_description": "Это позволит связать ваш домен с IP адресом:", "connect_to_server": "Начнём с сервера.", "select_provider": "Ниже подборка провайдеров, которых поддерживает SelfPrivacy", "select_provider_notice": "Под 'Небольшим сервером' имеется ввиду сервер с двумя потоками процессора и двумя гигабайтами оперативной памяти.", @@ -307,8 +308,10 @@ "choose_server_type_storage": "{} GB системного хранилища", "choose_server_type_payment_per_month": "{} в месяц", "no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...", - "cloudflare_bad_key_error": "API ключ неверен", + "dns_provider_bad_key_error": "API ключ неверен", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", + "connect_to_dns": "Подключите DNS провайдера", + "connect_to_dns_provider_text": "С помощью API токена приложение SelfPrivacy настроит DNS записи", "manage_domain_dns": "Для управления DNS вашего домена", "use_this_domain": "Используем этот домен?", "use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом", @@ -469,10 +472,10 @@ "required": "Обязательное поле", "already_exist": "Уже существует", "invalid_format": "Неверный формат", - "invalid_format_password": "Должен не содержать пустые символы", + "invalid_format_password": "Пароль не должен содержать пробелы", "invalid_format_ssh": "Должен следовать формату SSH ключей", "root_name": "Имя пользователя не может быть 'root'", "length_not_equal": "Длина строки [], должна быть равна {}", "length_longer": "Длина строки [], должна быть меньше либо равна {}" } -} \ No newline at end of file +} diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 44b03f26..afaae80b 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -18,10 +18,9 @@ class HiveConfig { Hive.registerAdapter(BackblazeCredentialAdapter()); Hive.registerAdapter(BackblazeBucketAdapter()); Hive.registerAdapter(ServerVolumeAdapter()); - - Hive.registerAdapter(DnsProviderAdapter()); - Hive.registerAdapter(ServerProviderAdapter()); Hive.registerAdapter(UserTypeAdapter()); + Hive.registerAdapter(DnsProviderTypeAdapter()); + Hive.registerAdapter(ServerProviderTypeAdapter()); await Hive.openBox(BNames.appSettingsBox); @@ -35,8 +34,8 @@ class HiveConfig { final Box deprecatedUsers = Hive.box(BNames.usersDeprecated); if (deprecatedUsers.isNotEmpty) { final Box users = Hive.box(BNames.usersBox); - users.addAll(deprecatedUsers.values.toList()); - deprecatedUsers.clear(); + await users.addAll(deprecatedUsers.values.toList()); + await deprecatedUsers.clear(); } await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); diff --git a/lib/logic/api_maps/api_generic_result.dart b/lib/logic/api_maps/generic_result.dart similarity index 85% rename from lib/logic/api_maps/api_generic_result.dart rename to lib/logic/api_maps/generic_result.dart index 81e1760a..5ce31561 100644 --- a/lib/logic/api_maps/api_generic_result.dart +++ b/lib/logic/api_maps/generic_result.dart @@ -1,5 +1,5 @@ -class APIGenericResult { - APIGenericResult({ +class GenericResult { + GenericResult({ required this.success, required this.data, this.message, diff --git a/lib/logic/api_maps/graphql_maps/api_map.dart b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart similarity index 95% rename from lib/logic/api_maps/graphql_maps/api_map.dart rename to lib/logic/api_maps/graphql_maps/graphql_api_map.dart index 34e39b7a..2c11c127 100644 --- a/lib/logic/api_maps/graphql_maps/api_map.dart +++ b/lib/logic/api_maps/graphql_maps/graphql_api_map.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:http/io_client.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/staging_options.dart'; +import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/models/message.dart'; void _logToAppConsole(final T objectToLog) { @@ -53,10 +53,10 @@ class ResponseLoggingParser extends ResponseParser { } } -abstract class ApiMap { +abstract class GraphQLApiMap { Future getClient() async { IOClient? ioClient; - if (StagingOptions.stagingAcme || !StagingOptions.verifyCertificate) { + if (TlsOptions.stagingAcme || !TlsOptions.verifyCertificate) { final HttpClient httpClient = HttpClient(); httpClient.badCertificateCallback = ( final cert, diff --git a/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart index 84550cc2..fe3ec7f0 100644 --- a/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart @@ -3,6 +3,7 @@ import 'package:gql/ast.dart'; import 'package:graphql/client.dart' as graphql; import 'package:selfprivacy/utils/scalars.dart'; import 'schema.graphql.dart'; +import 'services.graphql.dart'; class Fragment$basicMutationReturnFields { Fragment$basicMutationReturnFields({ diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql b/lib/logic/api_maps/graphql_maps/schema/schema.graphql index 89bc30c8..a4394ee6 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql @@ -76,7 +76,8 @@ type DeviceApiTokenMutationReturn implements MutationReturnInterface { enum DnsProvider { CLOUDFLARE, - DESEC + DESEC, + DIGITALOCEAN } type DnsRecord { diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart index 9325f5cb..710b305f 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart @@ -1096,7 +1096,7 @@ class _CopyWithStubImpl$Input$UserMutationInput _res; } -enum Enum$DnsProvider { CLOUDFLARE, DESEC, $unknown } +enum Enum$DnsProvider { CLOUDFLARE, DESEC, DIGITALOCEAN, $unknown } String toJson$Enum$DnsProvider(Enum$DnsProvider e) { switch (e) { @@ -1104,6 +1104,8 @@ String toJson$Enum$DnsProvider(Enum$DnsProvider e) { return r'CLOUDFLARE'; case Enum$DnsProvider.DESEC: return r'DESEC'; + case Enum$DnsProvider.DIGITALOCEAN: + return r'DIGITALOCEAN'; case Enum$DnsProvider.$unknown: return r'$unknown'; } @@ -1115,6 +1117,8 @@ Enum$DnsProvider fromJson$Enum$DnsProvider(String value) { return Enum$DnsProvider.CLOUDFLARE; case r'DESEC': return Enum$DnsProvider.DESEC; + case r'DIGITALOCEAN': + return Enum$DnsProvider.DIGITALOCEAN; default: return Enum$DnsProvider.$unknown; } diff --git a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql index 35df3749..f1012815 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql @@ -80,7 +80,6 @@ query SystemDnsProvider { } } - query GetApiTokens { api { devices { diff --git a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart index f41e841f..a80b783c 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'disk_volumes.graphql.dart'; import 'package:gql/ast.dart'; import 'package:graphql/client.dart' as graphql; import 'package:selfprivacy/utils/scalars.dart'; import 'schema.graphql.dart'; +import 'services.graphql.dart'; class Fragment$basicMutationReturnFields { Fragment$basicMutationReturnFields({ diff --git a/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart index 64738ad8..f834457e 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'disk_volumes.graphql.dart'; import 'package:gql/ast.dart'; import 'package:graphql/client.dart' as graphql; import 'schema.graphql.dart'; +import 'services.graphql.dart'; class Fragment$basicMutationReturnFields { Fragment$basicMutationReturnFields({ diff --git a/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart index 616788d8..d23b0112 100644 --- a/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'disk_volumes.graphql.dart'; import 'package:gql/ast.dart'; import 'package:graphql/client.dart' as graphql; import 'package:selfprivacy/utils/scalars.dart'; diff --git a/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart index 02cde074..d8ef0287 100644 --- a/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'disk_volumes.graphql.dart'; import 'package:gql/ast.dart'; import 'package:graphql/client.dart' as graphql; import 'schema.graphql.dart'; +import 'services.graphql.dart'; class Fragment$basicMutationReturnFields { Fragment$basicMutationReturnFields({ diff --git a/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart b/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart index c14aa98d..8ed73a5d 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart @@ -1,6 +1,6 @@ part of 'server_api.dart'; -mixin JobsApi on ApiMap { +mixin JobsApi on GraphQLApiMap { Future> getServerJobs() async { QueryResult response; List jobsList = []; @@ -22,13 +22,13 @@ mixin JobsApi on ApiMap { return jobsList; } - Future> removeApiJob(final String uid) async { + Future> removeApiJob(final String uid) async { try { final GraphQLClient client = await getClient(); final variables = Variables$Mutation$RemoveJob(jobId: uid); final mutation = Options$Mutation$RemoveJob(variables: variables); final response = await client.mutate$RemoveJob(mutation); - return APIGenericResult( + return GenericResult( data: response.parsedData?.removeJob.success ?? false, success: true, code: response.parsedData?.removeJob.code ?? 0, @@ -36,7 +36,7 @@ mixin JobsApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: false, success: false, code: 0, diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart index 65e77b98..f6fd5201 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart @@ -1,6 +1,6 @@ part of 'server_api.dart'; -mixin ServerActionsApi on ApiMap { +mixin ServerActionsApi on GraphQLApiMap { Future _commonBoolRequest(final Function graphQLMethod) async { QueryResult response; bool result = false; diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart index 51da63ac..91d4eeed 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart @@ -1,7 +1,7 @@ import 'package:graphql/client.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/api_map.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/graphql_api_map.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_api.graphql.dart'; @@ -24,7 +24,7 @@ import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/ssh_settings.dart'; import 'package:selfprivacy/logic/models/system_settings.dart'; -export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; +export 'package:selfprivacy/logic/api_maps/generic_result.dart'; part 'jobs_api.dart'; part 'server_actions_api.dart'; @@ -32,7 +32,7 @@ part 'services_api.dart'; part 'users_api.dart'; part 'volume_api.dart'; -class ServerApi extends ApiMap +class ServerApi extends GraphQLApiMap with VolumeApi, JobsApi, ServerActionsApi, ServicesApi, UsersApi { ServerApi({ this.hasLogger = false, @@ -69,9 +69,9 @@ class ServerApi extends ApiMap return apiVersion; } - Future getServerProviderType() async { + Future getServerProviderType() async { QueryResult response; - ServerProvider providerType = ServerProvider.unknown; + ServerProviderType providerType = ServerProviderType.unknown; try { final GraphQLClient client = await getClient(); @@ -79,7 +79,7 @@ class ServerApi extends ApiMap if (response.hasException) { print(response.exception.toString()); } - providerType = ServerProvider.fromGraphQL( + providerType = ServerProviderType.fromGraphQL( response.parsedData!.system.provider.provider, ); } catch (e) { @@ -88,9 +88,9 @@ class ServerApi extends ApiMap return providerType; } - Future getDnsProviderType() async { + Future getDnsProviderType() async { QueryResult response; - DnsProvider providerType = DnsProvider.unknown; + DnsProviderType providerType = DnsProviderType.unknown; try { final GraphQLClient client = await getClient(); @@ -98,7 +98,7 @@ class ServerApi extends ApiMap if (response.hasException) { print(response.exception.toString()); } - providerType = DnsProvider.fromGraphQL( + providerType = DnsProviderType.fromGraphQL( response.parsedData!.system.domainInfo.provider, ); } catch (e) { @@ -205,7 +205,7 @@ class ServerApi extends ApiMap return settings; } - Future> getRecoveryTokenStatus() async { + Future> getRecoveryTokenStatus() async { RecoveryKeyStatus? key; QueryResult response; String? error; @@ -222,18 +222,18 @@ class ServerApi extends ApiMap print(e); } - return APIGenericResult( + return GenericResult( success: error == null, data: key, message: error, ); } - Future> generateRecoveryToken( + Future> generateRecoveryToken( final DateTime? expirationDate, final int? numberOfUses, ) async { - APIGenericResult key; + GenericResult key; QueryResult response; try { @@ -254,19 +254,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - key = APIGenericResult( + key = GenericResult( success: false, data: '', message: response.exception.toString(), ); } - key = APIGenericResult( + key = GenericResult( success: true, data: response.parsedData!.getNewRecoveryApiKey.key!, ); } catch (e) { print(e); - key = APIGenericResult( + key = GenericResult( success: false, data: '', message: e.toString(), @@ -299,8 +299,8 @@ class ServerApi extends ApiMap return records; } - Future>> getApiTokens() async { - APIGenericResult> tokens; + Future>> getApiTokens() async { + GenericResult> tokens; QueryResult response; try { @@ -309,7 +309,7 @@ class ServerApi extends ApiMap if (response.hasException) { final message = response.exception.toString(); print(message); - tokens = APIGenericResult>( + tokens = GenericResult>( success: false, data: [], message: message, @@ -323,13 +323,13 @@ class ServerApi extends ApiMap ApiToken.fromGraphQL(device), ) .toList(); - tokens = APIGenericResult>( + tokens = GenericResult>( success: true, data: parsed, ); } catch (e) { print(e); - tokens = APIGenericResult>( + tokens = GenericResult>( success: false, data: [], message: e.toString(), @@ -339,8 +339,8 @@ class ServerApi extends ApiMap return tokens; } - Future> deleteApiToken(final String name) async { - APIGenericResult returnable; + Future> deleteApiToken(final String name) async { + GenericResult returnable; QueryResult response; try { @@ -357,19 +357,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - returnable = APIGenericResult( + returnable = GenericResult( success: false, data: null, message: response.exception.toString(), ); } - returnable = APIGenericResult( + returnable = GenericResult( success: true, data: null, ); } catch (e) { print(e); - returnable = APIGenericResult( + returnable = GenericResult( success: false, data: null, message: e.toString(), @@ -379,8 +379,8 @@ class ServerApi extends ApiMap return returnable; } - Future> createDeviceToken() async { - APIGenericResult token; + Future> createDeviceToken() async { + GenericResult token; QueryResult response; try { @@ -392,19 +392,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - token = APIGenericResult( + token = GenericResult( success: false, data: '', message: response.exception.toString(), ); } - token = APIGenericResult( + token = GenericResult( success: true, data: response.parsedData!.getNewDeviceApiKey.key!, ); } catch (e) { print(e); - token = APIGenericResult( + token = GenericResult( success: false, data: '', message: e.toString(), @@ -416,10 +416,10 @@ class ServerApi extends ApiMap Future isHttpServerWorking() async => (await getApiVersion()) != null; - Future> authorizeDevice( + Future> authorizeDevice( final DeviceToken deviceToken, ) async { - APIGenericResult token; + GenericResult token; QueryResult response; try { @@ -441,19 +441,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - token = APIGenericResult( + token = GenericResult( success: false, data: '', message: response.exception.toString(), ); } - token = APIGenericResult( + token = GenericResult( success: true, data: response.parsedData!.authorizeWithNewDeviceApiKey.token!, ); } catch (e) { print(e); - token = APIGenericResult( + token = GenericResult( success: false, data: '', message: e.toString(), @@ -463,10 +463,10 @@ class ServerApi extends ApiMap return token; } - Future> useRecoveryToken( + Future> useRecoveryToken( final DeviceToken deviceToken, ) async { - APIGenericResult token; + GenericResult token; QueryResult response; try { @@ -488,19 +488,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - token = APIGenericResult( + token = GenericResult( success: false, data: '', message: response.exception.toString(), ); } - token = APIGenericResult( + token = GenericResult( success: true, data: response.parsedData!.useRecoveryApiKey.token!, ); } catch (e) { print(e); - token = APIGenericResult( + token = GenericResult( success: false, data: '', message: e.toString(), diff --git a/lib/logic/api_maps/graphql_maps/server_api/services_api.dart b/lib/logic/api_maps/graphql_maps/server_api/services_api.dart index adfe806f..1632533b 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/services_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/services_api.dart @@ -1,6 +1,6 @@ part of 'server_api.dart'; -mixin ServicesApi on ApiMap { +mixin ServicesApi on GraphQLApiMap { Future> getAllServices() async { QueryResult response; List services = []; @@ -20,7 +20,7 @@ mixin ServicesApi on ApiMap { return services; } - Future> enableService( + Future> enableService( final String serviceId, ) async { try { @@ -28,7 +28,7 @@ mixin ServicesApi on ApiMap { final variables = Variables$Mutation$EnableService(serviceId: serviceId); final mutation = Options$Mutation$EnableService(variables: variables); final response = await client.mutate$EnableService(mutation); - return APIGenericResult( + return GenericResult( data: response.parsedData?.enableService.success ?? false, success: true, code: response.parsedData?.enableService.code ?? 0, @@ -36,7 +36,7 @@ mixin ServicesApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: false, success: false, code: 0, @@ -45,7 +45,7 @@ mixin ServicesApi on ApiMap { } } - Future> disableService( + Future> disableService( final String serviceId, ) async { try { @@ -53,7 +53,7 @@ mixin ServicesApi on ApiMap { final variables = Variables$Mutation$DisableService(serviceId: serviceId); final mutation = Options$Mutation$DisableService(variables: variables); final response = await client.mutate$DisableService(mutation); - return APIGenericResult( + return GenericResult( data: null, success: response.parsedData?.disableService.success ?? false, code: response.parsedData?.disableService.code ?? 0, @@ -61,7 +61,7 @@ mixin ServicesApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: null, success: false, code: 0, @@ -70,7 +70,7 @@ mixin ServicesApi on ApiMap { } } - Future> stopService( + Future> stopService( final String serviceId, ) async { try { @@ -78,7 +78,7 @@ mixin ServicesApi on ApiMap { final variables = Variables$Mutation$StopService(serviceId: serviceId); final mutation = Options$Mutation$StopService(variables: variables); final response = await client.mutate$StopService(mutation); - return APIGenericResult( + return GenericResult( data: response.parsedData?.stopService.success ?? false, success: true, code: response.parsedData?.stopService.code ?? 0, @@ -86,7 +86,7 @@ mixin ServicesApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: false, success: false, code: 0, @@ -95,13 +95,13 @@ mixin ServicesApi on ApiMap { } } - Future startService(final String serviceId) async { + Future startService(final String serviceId) async { try { final GraphQLClient client = await getClient(); final variables = Variables$Mutation$StartService(serviceId: serviceId); final mutation = Options$Mutation$StartService(variables: variables); final response = await client.mutate$StartService(mutation); - return APIGenericResult( + return GenericResult( data: null, success: response.parsedData?.startService.success ?? false, code: response.parsedData?.startService.code ?? 0, @@ -109,7 +109,7 @@ mixin ServicesApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: null, success: false, code: 0, @@ -118,7 +118,7 @@ mixin ServicesApi on ApiMap { } } - Future> restartService( + Future> restartService( final String serviceId, ) async { try { @@ -126,7 +126,7 @@ mixin ServicesApi on ApiMap { final variables = Variables$Mutation$RestartService(serviceId: serviceId); final mutation = Options$Mutation$RestartService(variables: variables); final response = await client.mutate$RestartService(mutation); - return APIGenericResult( + return GenericResult( data: response.parsedData?.restartService.success ?? false, success: true, code: response.parsedData?.restartService.code ?? 0, @@ -134,7 +134,7 @@ mixin ServicesApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: false, success: false, code: 0, @@ -143,7 +143,7 @@ mixin ServicesApi on ApiMap { } } - Future> moveService( + Future> moveService( final String serviceId, final String destination, ) async { @@ -158,7 +158,7 @@ mixin ServicesApi on ApiMap { final mutation = Options$Mutation$MoveService(variables: variables); final response = await client.mutate$MoveService(mutation); final jobJson = response.parsedData?.moveService.job?.toJson(); - return APIGenericResult( + return GenericResult( success: true, code: response.parsedData?.moveService.code ?? 0, message: response.parsedData?.moveService.message, @@ -166,7 +166,7 @@ mixin ServicesApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( success: false, code: 0, message: e.toString(), diff --git a/lib/logic/api_maps/graphql_maps/server_api/users_api.dart b/lib/logic/api_maps/graphql_maps/server_api/users_api.dart index f1851353..11327290 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/users_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/users_api.dart @@ -1,6 +1,6 @@ part of 'server_api.dart'; -mixin UsersApi on ApiMap { +mixin UsersApi on GraphQLApiMap { Future> getAllUsers() async { QueryResult response; List users = []; @@ -45,7 +45,7 @@ mixin UsersApi on ApiMap { return user; } - Future> createUser( + Future> createUser( final String username, final String password, ) async { @@ -56,7 +56,7 @@ mixin UsersApi on ApiMap { ); final mutation = Options$Mutation$CreateUser(variables: variables); final response = await client.mutate$CreateUser(mutation); - return APIGenericResult( + return GenericResult( success: true, code: response.parsedData?.createUser.code ?? 500, message: response.parsedData?.createUser.message, @@ -66,7 +66,7 @@ mixin UsersApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( success: false, code: 0, message: e.toString(), @@ -75,7 +75,7 @@ mixin UsersApi on ApiMap { } } - Future> deleteUser( + Future> deleteUser( final String username, ) async { try { @@ -83,7 +83,7 @@ mixin UsersApi on ApiMap { final variables = Variables$Mutation$DeleteUser(username: username); final mutation = Options$Mutation$DeleteUser(variables: variables); final response = await client.mutate$DeleteUser(mutation); - return APIGenericResult( + return GenericResult( data: response.parsedData?.deleteUser.success ?? false, success: true, code: response.parsedData?.deleteUser.code ?? 500, @@ -91,7 +91,7 @@ mixin UsersApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: false, success: false, code: 500, @@ -100,7 +100,7 @@ mixin UsersApi on ApiMap { } } - Future> updateUser( + Future> updateUser( final String username, final String password, ) async { @@ -111,7 +111,7 @@ mixin UsersApi on ApiMap { ); final mutation = Options$Mutation$UpdateUser(variables: variables); final response = await client.mutate$UpdateUser(mutation); - return APIGenericResult( + return GenericResult( success: true, code: response.parsedData?.updateUser.code ?? 500, message: response.parsedData?.updateUser.message, @@ -121,7 +121,7 @@ mixin UsersApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: null, success: false, code: 0, @@ -130,7 +130,7 @@ mixin UsersApi on ApiMap { } } - Future> addSshKey( + Future> addSshKey( final String username, final String sshKey, ) async { @@ -144,7 +144,7 @@ mixin UsersApi on ApiMap { ); final mutation = Options$Mutation$AddSshKey(variables: variables); final response = await client.mutate$AddSshKey(mutation); - return APIGenericResult( + return GenericResult( success: true, code: response.parsedData?.addSshKey.code ?? 500, message: response.parsedData?.addSshKey.message, @@ -154,7 +154,7 @@ mixin UsersApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: null, success: false, code: 0, @@ -163,7 +163,7 @@ mixin UsersApi on ApiMap { } } - Future> removeSshKey( + Future> removeSshKey( final String username, final String sshKey, ) async { @@ -177,7 +177,7 @@ mixin UsersApi on ApiMap { ); final mutation = Options$Mutation$RemoveSshKey(variables: variables); final response = await client.mutate$RemoveSshKey(mutation); - return APIGenericResult( + return GenericResult( success: response.parsedData?.removeSshKey.success ?? false, code: response.parsedData?.removeSshKey.code ?? 500, message: response.parsedData?.removeSshKey.message, @@ -187,7 +187,7 @@ mixin UsersApi on ApiMap { ); } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: null, success: false, code: 0, diff --git a/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart b/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart index e830cabd..a7d23ba8 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart @@ -1,6 +1,6 @@ part of 'server_api.dart'; -mixin VolumeApi on ApiMap { +mixin VolumeApi on GraphQLApiMap { Future> getServerDiskVolumes() async { QueryResult response; List volumes = []; @@ -57,10 +57,10 @@ mixin VolumeApi on ApiMap { } } - Future> migrateToBinds( + Future> migrateToBinds( final Map serviceToDisk, ) async { - APIGenericResult? mutation; + GenericResult? mutation; try { final GraphQLClient client = await getClient(); @@ -78,7 +78,7 @@ mixin VolumeApi on ApiMap { await client.mutate$MigrateToBinds( migrateMutation, ); - mutation = mutation = APIGenericResult( + mutation = mutation = GenericResult( success: true, code: result.parsedData!.migrateToBinds.code, message: result.parsedData!.migrateToBinds.message, @@ -86,7 +86,7 @@ mixin VolumeApi on ApiMap { ); } catch (e) { print(e); - mutation = APIGenericResult( + mutation = GenericResult( success: false, code: 0, message: e.toString(), diff --git a/lib/logic/api_maps/rest_maps/api_controller.dart b/lib/logic/api_maps/rest_maps/api_controller.dart deleted file mode 100644 index 440d25af..00000000 --- a/lib/logic/api_maps/rest_maps/api_controller.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart'; - -class ApiController { - static VolumeProviderApiFactory? get currentVolumeProviderApiFactory => - _volumeProviderApiFactory; - static DnsProviderApiFactory? get currentDnsProviderApiFactory => - _dnsProviderApiFactory; - static ServerProviderApiFactory? get currentServerProviderApiFactory => - _serverProviderApiFactory; - - static void initVolumeProviderApiFactory( - final ServerProviderApiFactorySettings settings, - ) { - _volumeProviderApiFactory = - VolumeApiFactoryCreator.createVolumeProviderApiFactory(settings); - } - - static void initDnsProviderApiFactory( - final DnsProviderApiFactorySettings settings, - ) { - _dnsProviderApiFactory = - ApiFactoryCreator.createDnsProviderApiFactory(settings); - } - - static void initServerProviderApiFactory( - final ServerProviderApiFactorySettings settings, - ) { - _serverProviderApiFactory = - ApiFactoryCreator.createServerProviderApiFactory(settings); - } - - static void clearProviderApiFactories() { - _volumeProviderApiFactory = null; - _dnsProviderApiFactory = null; - _serverProviderApiFactory = null; - } - - static VolumeProviderApiFactory? _volumeProviderApiFactory; - static DnsProviderApiFactory? _dnsProviderApiFactory; - static ServerProviderApiFactory? _serverProviderApiFactory; -} diff --git a/lib/logic/api_maps/rest_maps/api_factory_creator.dart b/lib/logic/api_maps/rest_maps/api_factory_creator.dart deleted file mode 100644 index c1762429..00000000 --- a/lib/logic/api_maps/rest_maps/api_factory_creator.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart'; -import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; - -class UnknownApiProviderException implements Exception { - UnknownApiProviderException(this.message); - final String message; -} - -class ApiFactoryCreator { - static ServerProviderApiFactory createServerProviderApiFactory( - final ServerProviderApiFactorySettings settings, - ) { - switch (settings.provider) { - case ServerProvider.hetzner: - return HetznerApiFactory(region: settings.location); - case ServerProvider.digitalOcean: - return DigitalOceanApiFactory(region: settings.location); - case ServerProvider.unknown: - throw UnknownApiProviderException('Unknown server provider'); - } - } - - static DnsProviderApiFactory createDnsProviderApiFactory( - final DnsProviderApiFactorySettings settings, - ) { - switch (settings.provider) { - case DnsProvider.desec: - return DesecApiFactory(); - case DnsProvider.cloudflare: - return CloudflareApiFactory(); - case DnsProvider.unknown: - throw UnknownApiProviderException('Unknown DNS provider'); - } - } -} - -class VolumeApiFactoryCreator { - static VolumeProviderApiFactory createVolumeProviderApiFactory( - final ServerProviderApiFactorySettings settings, - ) { - switch (settings.provider) { - case ServerProvider.hetzner: - return HetznerApiFactory(); - case ServerProvider.digitalOcean: - return DigitalOceanApiFactory(); - case ServerProvider.unknown: - throw UnknownApiProviderException('Unknown volume provider'); - } - } -} diff --git a/lib/logic/api_maps/rest_maps/backblaze.dart b/lib/logic/api_maps/rest_maps/backblaze.dart index 59292775..7169f5cb 100644 --- a/lib/logic/api_maps/rest_maps/backblaze.dart +++ b/lib/logic/api_maps/rest_maps/backblaze.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; -export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; +export 'package:selfprivacy/logic/api_maps/generic_result.dart'; class BackblazeApiAuth { BackblazeApiAuth({required this.authorizationToken, required this.apiUrl}); @@ -25,7 +25,7 @@ class BackblazeApplicationKey { final String applicationKey; } -class BackblazeApi extends ApiMap { +class BackblazeApi extends RestApiMap { BackblazeApi({this.hasLogger = false, this.isWithToken = true}); @override @@ -78,7 +78,7 @@ class BackblazeApi extends ApiMap { ); } - Future> isApiTokenValid( + Future> isApiTokenValid( final String encodedApiKey, ) async { final Dio client = await getClient(); @@ -103,7 +103,7 @@ class BackblazeApi extends ApiMap { } } on DioError catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: false, success: false, message: e.toString(), @@ -112,7 +112,7 @@ class BackblazeApi extends ApiMap { close(client); } - return APIGenericResult( + return GenericResult( data: isTokenValid, success: true, ); diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart deleted file mode 100644 index f594029f..00000000 --- a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart +++ /dev/null @@ -1,468 +0,0 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/logic/models/json/dns_records.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; - -class CloudflareApi extends DnsProviderApi { - CloudflareApi({ - this.hasLogger = false, - this.isWithToken = true, - this.customToken, - }); - @override - final bool hasLogger; - @override - final bool isWithToken; - - final String? customToken; - - @override - RegExp getApiTokenValidation() => - RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); - - @override - BaseOptions get options { - final BaseOptions options = BaseOptions( - baseUrl: rootAddress, - contentType: Headers.jsonContentType, - responseType: ResponseType.json, - ); - if (isWithToken) { - final String? token = getIt().dnsProviderKey; - assert(token != null); - options.headers = {'Authorization': 'Bearer $token'}; - } - - if (customToken != null) { - options.headers = {'Authorization': 'Bearer $customToken'}; - } - - if (validateStatus != null) { - options.validateStatus = validateStatus!; - } - return options; - } - - @override - String rootAddress = 'https://api.cloudflare.com/client/v4'; - - @override - Future> isApiTokenValid(final String token) async { - bool isValid = false; - Response? response; - String message = ''; - final Dio client = await getClient(); - try { - response = await client.get( - '/user/tokens/verify', - options: Options( - followRedirects: false, - validateStatus: (final status) => - status != null && (status >= 200 || status == 401), - headers: {'Authorization': 'Bearer $token'}, - ), - ); - } catch (e) { - print(e); - isValid = false; - message = e.toString(); - } finally { - close(client); - } - - if (response == null) { - return APIGenericResult( - data: isValid, - success: false, - message: message, - ); - } - - if (response.statusCode == HttpStatus.ok) { - isValid = true; - } else if (response.statusCode == HttpStatus.unauthorized) { - isValid = false; - } else { - throw Exception('code: ${response.statusCode}'); - } - - return APIGenericResult( - data: isValid, - success: true, - message: response.statusMessage, - ); - } - - @override - Future getZoneId(final String domain) async { - String? zoneId; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - '/zones', - queryParameters: {'name': domain}, - ); - zoneId = response.data['result'][0]['id']; - } catch (e) { - print(e); - } finally { - close(client); - } - - return zoneId; - } - - @override - Future> removeSimilarRecords({ - required final ServerDomain domain, - final String? ip4, - }) async { - final String domainName = domain.domainName; - final String domainZoneId = domain.zoneId; - - final String url = '/zones/$domainZoneId/dns_records'; - - final Dio client = await getClient(); - try { - final Response response = await client.get(url); - - final List records = response.data['result'] ?? []; - final List allDeleteFutures = []; - - for (final record in records) { - if (record['zone_name'] == domainName) { - allDeleteFutures.add( - client.delete('$url/${record["id"]}'), - ); - } - } - await Future.wait(allDeleteFutures); - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: null, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(success: true, data: null); - } - - @override - Future> getDnsRecords({ - required final ServerDomain domain, - }) async { - Response response; - final String domainName = domain.domainName; - final String domainZoneId = domain.zoneId; - final List allRecords = []; - - final String url = '/zones/$domainZoneId/dns_records'; - - final Dio client = await getClient(); - try { - response = await client.get(url); - final List records = response.data['result'] ?? []; - - for (final record in records) { - if (record['zone_name'] == domainName) { - allRecords.add( - DnsRecord( - name: record['name'], - type: record['type'], - content: record['content'], - ttl: record['ttl'], - proxied: record['proxied'], - ), - ); - } - } - } catch (e) { - print(e); - } finally { - close(client); - } - - return allRecords; - } - - @override - Future> createMultipleDnsRecords({ - required final ServerDomain domain, - final String? ip4, - }) async { - final String domainName = domain.domainName; - final String domainZoneId = domain.zoneId; - final List listDnsRecords = projectDnsRecords(domainName, ip4); - final List allCreateFutures = []; - - final Dio client = await getClient(); - try { - for (final DnsRecord record in listDnsRecords) { - allCreateFutures.add( - client.post( - '/zones/$domainZoneId/dns_records', - data: record.toJson(), - ), - ); - } - await Future.wait(allCreateFutures); - } on DioError catch (e) { - print(e.message); - rethrow; - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: null, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(success: true, data: null); - } - - List projectDnsRecords( - final String? domainName, - final String? ip4, - ) { - final DnsRecord domainA = - DnsRecord(type: 'A', name: domainName, content: ip4); - - final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName); - final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); - final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); - final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); - final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); - final DnsRecord passwordA = - DnsRecord(type: 'A', name: 'password', content: ip4); - final DnsRecord socialA = - DnsRecord(type: 'A', name: 'social', content: ip4); - final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); - - final DnsRecord txt1 = DnsRecord( - type: 'TXT', - name: '_dmarc', - content: 'v=DMARC1; p=none', - ttl: 18000, - ); - - final DnsRecord txt2 = DnsRecord( - type: 'TXT', - name: domainName, - content: 'v=spf1 a mx ip4:$ip4 -all', - ttl: 18000, - ); - - return [ - domainA, - apiA, - cloudA, - gitA, - meetA, - passwordA, - socialA, - mx, - txt1, - txt2, - vpn - ]; - } - - @override - Future setDnsRecord( - final DnsRecord record, - final ServerDomain domain, - ) async { - final String domainZoneId = domain.zoneId; - final String url = '$rootAddress/zones/$domainZoneId/dns_records'; - - final Dio client = await getClient(); - try { - await client.post( - url, - data: record.toJson(), - ); - } catch (e) { - print(e); - } finally { - close(client); - } - } - - @override - Future> domainList() async { - final String url = '$rootAddress/zones'; - List domains = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - url, - queryParameters: {'per_page': 50}, - ); - domains = response.data['result'] - .map((final el) => el['name'] as String) - .toList(); - } catch (e) { - print(e); - } finally { - close(client); - } - - return domains; - } - - @override - Future>> validateDnsRecords( - final ServerDomain domain, - final String ip4, - final String dkimPublicKey, - ) async { - final List records = await getDnsRecords(domain: domain); - final List foundRecords = []; - try { - final List desiredRecords = - getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); - for (final DesiredDnsRecord record in desiredRecords) { - if (record.description == 'record.dkim') { - final DnsRecord foundRecord = records.firstWhere( - (final r) => (r.name == record.name) && r.type == record.type, - orElse: () => DnsRecord( - name: record.name, - type: record.type, - content: '', - ttl: 800, - proxied: false, - ), - ); - // remove all spaces and tabulators from - // the foundRecord.content and the record.content - // to compare them - final String? foundContent = - foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); - final String content = record.content.replaceAll(RegExp(r'\s+'), ''); - if (foundContent == content) { - foundRecords.add(record.copyWith(isSatisfied: true)); - } else { - foundRecords.add(record.copyWith(isSatisfied: false)); - } - } else { - if (records.any( - (final r) => - (r.name == record.name) && - r.type == record.type && - r.content == record.content, - )) { - foundRecords.add(record.copyWith(isSatisfied: true)); - } else { - foundRecords.add(record.copyWith(isSatisfied: false)); - } - } - } - } catch (e) { - print(e); - return APIGenericResult( - data: [], - success: false, - message: e.toString(), - ); - } - return APIGenericResult( - data: foundRecords, - success: true, - ); - } - - @override - List getDesiredDnsRecords( - final String? domainName, - final String? ip4, - final String? dkimPublicKey, - ) { - if (domainName == null || ip4 == null) { - return []; - } - return [ - DesiredDnsRecord( - name: domainName, - content: ip4, - description: 'record.root', - ), - DesiredDnsRecord( - name: 'api.$domainName', - content: ip4, - description: 'record.api', - ), - DesiredDnsRecord( - name: 'cloud.$domainName', - content: ip4, - description: 'record.cloud', - ), - DesiredDnsRecord( - name: 'git.$domainName', - content: ip4, - description: 'record.git', - ), - DesiredDnsRecord( - name: 'meet.$domainName', - content: ip4, - description: 'record.meet', - ), - DesiredDnsRecord( - name: 'social.$domainName', - content: ip4, - description: 'record.social', - ), - DesiredDnsRecord( - name: 'password.$domainName', - content: ip4, - description: 'record.password', - ), - DesiredDnsRecord( - name: 'vpn.$domainName', - content: ip4, - description: 'record.vpn', - ), - DesiredDnsRecord( - name: domainName, - content: domainName, - description: 'record.mx', - type: 'MX', - category: DnsRecordsCategory.email, - ), - DesiredDnsRecord( - name: '_dmarc.$domainName', - content: 'v=DMARC1; p=none', - description: 'record.dmarc', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - DesiredDnsRecord( - name: domainName, - content: 'v=spf1 a mx ip4:$ip4 -all', - description: 'record.spf', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - if (dkimPublicKey != null) - DesiredDnsRecord( - name: 'selector._domainkey.$domainName', - content: dkimPublicKey, - description: 'record.dkim', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - ]; - } -} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart new file mode 100644 index 00000000..9fe74841 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart @@ -0,0 +1,252 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; + +class CloudflareApi extends RestApiMap { + CloudflareApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions( + baseUrl: rootAddress, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + ); + if (isWithToken) { + final String? token = getIt().dnsProviderKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; + } + + if (customToken != null) { + options.headers = {'Authorization': 'Bearer $customToken'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + return options; + } + + @override + String rootAddress = 'https://api.cloudflare.com/client/v4'; + + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '/user/tokens/verify', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Bearer $token'}, + ), + ); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return GenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return GenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + Future>> getZones(final String domain) async { + List zones = []; + + late final Response? response; + final Dio client = await getClient(); + try { + response = await client.get( + '/zones', + queryParameters: {'name': domain}, + ); + zones = response.data['result']; + } catch (e) { + print(e); + GenericResult( + success: false, + data: zones, + code: response?.statusCode, + message: response?.statusMessage, + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: zones); + } + + Future> removeSimilarRecords({ + required final ServerDomain domain, + required final List records, + }) async { + final String domainZoneId = domain.zoneId; + final String url = '/zones/$domainZoneId/dns_records'; + + final Dio client = await getClient(); + try { + final List allDeleteFutures = []; + + for (final record in records) { + allDeleteFutures.add( + client.delete('$url/${record["id"]}'), + ); + } + await Future.wait(allDeleteFutures); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> getDnsRecords({ + required final ServerDomain domain, + }) async { + Response response; + final String domainName = domain.domainName; + final String domainZoneId = domain.zoneId; + final List allRecords = []; + + final String url = '/zones/$domainZoneId/dns_records'; + + final Dio client = await getClient(); + try { + response = await client.get(url); + final List records = response.data['result'] ?? []; + + for (final record in records) { + if (record['zone_name'] == domainName) { + allRecords.add(record); + } + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: allRecords, success: true); + } + + Future> createMultipleDnsRecords({ + required final ServerDomain domain, + required final List records, + }) async { + final String domainZoneId = domain.zoneId; + final List allCreateFutures = []; + + final Dio client = await getClient(); + try { + for (final DnsRecord record in records) { + allCreateFutures.add( + client.post( + '/zones/$domainZoneId/dns_records', + data: record.toJson(), + ), + ); + } + await Future.wait(allCreateFutures); + } on DioError catch (e) { + print(e.message); + rethrow; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> getDomains() async { + final String url = '$rootAddress/zones'; + List domains = []; + + late final Response? response; + final Dio client = await getClient(); + try { + response = await client.get( + url, + queryParameters: {'per_page': 50}, + ); + domains = response.data['result']; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: domains, + code: response?.statusCode, + message: response?.statusMessage, + ); + } finally { + close(client); + } + + return GenericResult( + success: true, + data: domains, + code: response.statusCode, + message: response.statusMessage, + ); + } +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart deleted file mode 100644 index ccb58e6a..00000000 --- a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; - -class CloudflareApiFactory extends DnsProviderApiFactory { - @override - DnsProviderApi getDnsProvider({ - final DnsProviderApiSettings settings = const DnsProviderApiSettings(), - }) => - CloudflareApi( - hasLogger: settings.hasLogger, - isWithToken: settings.isWithToken, - customToken: settings.customToken, - ); -} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart new file mode 100644 index 00000000..e5eff146 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart @@ -0,0 +1,204 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; + +class DesecApi extends RestApiMap { + DesecApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions( + baseUrl: rootAddress, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + ); + if (isWithToken) { + final String? token = getIt().dnsProviderKey; + assert(token != null); + options.headers = {'Authorization': 'Token $token'}; + } + + if (customToken != null) { + options.headers = {'Authorization': 'Token $customToken'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + return options; + } + + @override + String rootAddress = 'https://desec.io/api/v1/domains/'; + + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Token $token'}, + ), + ); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return GenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return GenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + Future> updateRecords({ + required final ServerDomain domain, + required final List records, + }) async { + final String domainName = domain.domainName; + final String url = '/$domainName/rrsets/'; + + final Dio client = await getClient(); + try { + await client.put(url, data: records); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future>> getDnsRecords({ + required final ServerDomain domain, + }) async { + Response? response; + final String domainName = domain.domainName; + List allRecords = []; + + final String url = '/$domainName/rrsets/'; + + final Dio client = await getClient(); + try { + response = await client.get(url); + await Future.delayed(const Duration(seconds: 1)); + allRecords = response.data; + } catch (e) { + print(e); + return GenericResult( + data: allRecords, + success: false, + message: e.toString(), + code: response?.statusCode, + ); + } finally { + close(client); + } + + return GenericResult(data: allRecords, success: true); + } + + Future> createRecords({ + required final ServerDomain domain, + required final List records, + }) async { + final String domainName = domain.domainName; + final String url = '/$domainName/rrsets/'; + + final Dio client = await getClient(); + try { + await client.post(url, data: records); + await Future.delayed(const Duration(seconds: 1)); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> getDomains() async { + List domains = []; + + late final Response? response; + final Dio client = await getClient(); + try { + response = await client.get( + '', + ); + await Future.delayed(const Duration(seconds: 1)); + domains = response.data; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: domains, + code: response?.statusCode, + message: response?.statusMessage, + ); + } finally { + close(client); + } + + return GenericResult( + success: true, + data: domains, + code: response.statusCode, + message: response.statusMessage, + ); + } +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart deleted file mode 100644 index 6c10259b..00000000 --- a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; - -class DesecApiFactory extends DnsProviderApiFactory { - @override - DnsProviderApi getDnsProvider({ - final DnsProviderApiSettings settings = const DnsProviderApiSettings(), - }) => - DesecApi( - hasLogger: settings.hasLogger, - isWithToken: settings.isWithToken, - customToken: settings.customToken, - ); -} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart b/lib/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart new file mode 100644 index 00000000..4a64d49e --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart @@ -0,0 +1,44 @@ +enum DnsRecordsCategory { + services, + email, + other, +} + +class DesiredDnsRecord { + const DesiredDnsRecord({ + required this.name, + required this.content, + this.type = 'A', + this.description = '', + this.category = DnsRecordsCategory.services, + this.isSatisfied = false, + this.displayName, + }); + + final String name; + final String type; + final String content; + final String description; + final String? displayName; + final DnsRecordsCategory category; + final bool isSatisfied; + + DesiredDnsRecord copyWith({ + final String? name, + final String? type, + final String? content, + final String? description, + final String? displayName, + final DnsRecordsCategory? category, + final bool? isSatisfied, + }) => + DesiredDnsRecord( + name: name ?? this.name, + type: type ?? this.type, + content: content ?? this.content, + description: description ?? this.description, + category: category ?? this.category, + isSatisfied: isSatisfied ?? this.isSatisfied, + displayName: displayName ?? this.displayName, + ); +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart new file mode 100644 index 00000000..348edc77 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart @@ -0,0 +1,217 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; + +class DigitalOceanDnsApi extends RestApiMap { + DigitalOceanDnsApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions( + baseUrl: rootAddress, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + ); + if (isWithToken) { + final String? token = getIt().dnsProviderKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; + } + + if (customToken != null) { + options.headers = {'Authorization': 'Bearer $customToken'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + return options; + } + + @override + String rootAddress = 'https://api.digitalocean.com/v2'; + + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '/account', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Bearer $token'}, + ), + ); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return GenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return GenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + Future> removeSimilarRecords({ + required final ServerDomain domain, + required final List records, + }) async { + final String domainName = domain.domainName; + + final Dio client = await getClient(); + try { + final List allDeleteFutures = []; + for (final record in records) { + allDeleteFutures.add( + client.delete("/domains/$domainName/records/${record['id']}"), + ); + } + await Future.wait(allDeleteFutures); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> getDnsRecords({ + required final ServerDomain domain, + }) async { + Response response; + final String domainName = domain.domainName; + List allRecords = []; + + /// Default amount is 20, but we will eventually overflow it, + /// so I hardcode it to the maximum available amount in advance just in case + /// + /// https://docs.digitalocean.com/reference/api/api-reference/#operation/domains_list_records + const int amountPerPage = 200; + final String url = '/domains/$domainName/records?per_page=$amountPerPage'; + + final Dio client = await getClient(); + try { + response = await client.get(url); + allRecords = response.data['domain_records'] ?? []; + } catch (e) { + print(e); + GenericResult( + data: allRecords, + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: allRecords, success: true); + } + + Future> createMultipleDnsRecords({ + required final ServerDomain domain, + required final List records, + }) async { + final String domainName = domain.domainName; + final List allCreateFutures = []; + + final Dio client = await getClient(); + try { + for (final DnsRecord record in records) { + allCreateFutures.add( + client.post( + '/domains/$domainName/records', + data: { + 'type': record.type, + 'name': record.name, + 'data': record.content, + 'ttl': record.ttl, + 'priority': record.priority, + }, + ), + ); + } + await Future.wait(allCreateFutures); + } on DioError catch (e) { + print(e.message); + rethrow; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> domainList() async { + List domains = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get('/domains'); + domains = response.data['domains']; + } catch (e) { + print(e); + return GenericResult( + data: domains, + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: domains, success: true); + } +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart deleted file mode 100644 index b85f94d1..00000000 --- a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/logic/models/json/dns_records.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; - -export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; - -class DomainNotFoundException implements Exception { - DomainNotFoundException(this.message); - final String message; -} - -abstract class DnsProviderApi extends ApiMap { - Future> getDnsRecords({ - required final ServerDomain domain, - }); - Future> removeSimilarRecords({ - required final ServerDomain domain, - final String? ip4, - }); - Future> createMultipleDnsRecords({ - required final ServerDomain domain, - final String? ip4, - }); - Future setDnsRecord( - final DnsRecord record, - final ServerDomain domain, - ); - Future>> validateDnsRecords( - final ServerDomain domain, - final String ip4, - final String dkimPublicKey, - ); - List getDesiredDnsRecords( - final String? domainName, - final String? ip4, - final String? dkimPublicKey, - ); - Future getZoneId(final String domain); - Future> domainList(); - Future> isApiTokenValid(final String token); - RegExp getApiTokenValidation(); -} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart deleted file mode 100644 index 6b737df5..00000000 --- a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart'; - -class DnsProviderApiSettings extends ProviderApiSettings { - const DnsProviderApiSettings({ - super.hasLogger = false, - super.isWithToken = true, - this.customToken, - }); - final String? customToken; -} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart deleted file mode 100644 index fb573135..00000000 --- a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; - -abstract class DnsProviderApiFactory { - DnsProviderApi getDnsProvider({ - final DnsProviderApiSettings settings, - }); -} diff --git a/lib/logic/api_maps/rest_maps/provider_api_settings.dart b/lib/logic/api_maps/rest_maps/provider_api_settings.dart deleted file mode 100644 index 9e601d2a..00000000 --- a/lib/logic/api_maps/rest_maps/provider_api_settings.dart +++ /dev/null @@ -1,8 +0,0 @@ -class ProviderApiSettings { - const ProviderApiSettings({ - this.hasLogger = false, - this.isWithToken = true, - }); - final bool hasLogger; - final bool isWithToken; -} diff --git a/lib/logic/api_maps/rest_maps/api_map.dart b/lib/logic/api_maps/rest_maps/rest_api_map.dart similarity index 99% rename from lib/logic/api_maps/rest_maps/api_map.dart rename to lib/logic/api_maps/rest_maps/rest_api_map.dart index 86f53e25..547ce4aa 100644 --- a/lib/logic/api_maps/rest_maps/api_map.dart +++ b/lib/logic/api_maps/rest_maps/rest_api_map.dart @@ -8,7 +8,7 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/models/message.dart'; -abstract class ApiMap { +abstract class RestApiMap { Future getClient({final BaseOptions? customOptions}) async { final Dio dio = Dio(customOptions ?? (await options)); if (hasLogger) { 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 deleted file mode 100644 index 81b7a9fb..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart +++ /dev/null @@ -1,866 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/staging_options.dart'; -import 'package:selfprivacy/logic/models/disk_size.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/logic/models/price.dart'; -import 'package:selfprivacy/logic/models/server_basic_info.dart'; -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 { - DigitalOceanApi({ - required this.region, - this.hasLogger = true, - this.isWithToken = true, - }); - @override - bool hasLogger; - @override - bool isWithToken; - - final String? region; - - @override - BaseOptions get options { - final BaseOptions options = BaseOptions( - baseUrl: rootAddress, - contentType: Headers.jsonContentType, - responseType: ResponseType.json, - ); - if (isWithToken) { - final String? token = getIt().serverProviderKey; - assert(token != null); - options.headers = {'Authorization': 'Bearer $token'}; - } - - if (validateStatus != null) { - options.validateStatus = validateStatus!; - } - - return options; - } - - @override - String get rootAddress => 'https://api.digitalocean.com/v2'; - - @override - String get infectProviderName => 'digitalocean'; - - @override - String get displayProviderName => 'Digital Ocean'; - - @override - Future> isApiTokenValid(final String token) async { - bool isValid = false; - Response? response; - String message = ''; - final Dio client = await getClient(); - try { - response = await client.get( - '/account', - options: Options( - followRedirects: false, - validateStatus: (final status) => - status != null && (status >= 200 || status == 401), - headers: {'Authorization': 'Bearer $token'}, - ), - ); - } catch (e) { - print(e); - isValid = false; - message = e.toString(); - } finally { - close(client); - } - - if (response == null) { - return APIGenericResult( - data: isValid, - success: false, - message: message, - ); - } - - if (response.statusCode == HttpStatus.ok) { - isValid = true; - } else if (response.statusCode == HttpStatus.unauthorized) { - isValid = false; - } else { - throw Exception('code: ${response.statusCode}'); - } - - return APIGenericResult( - data: isValid, - success: true, - message: response.statusMessage, - ); - } - - /// Hardcoded on their documentation and there is no pricing API at all - /// Probably we should scrap the doc page manually - @override - Future getPricePerGb() async => Price( - value: 0.10, - currency: 'USD', - ); - - @override - Future> createVolume() async { - ServerVolume? volume; - - Response? createVolumeResponse; - final Dio client = await getClient(); - try { - final List volumes = await getVolumes(); - await Future.delayed(const Duration(seconds: 6)); - - createVolumeResponse = await client.post( - '/volumes', - data: { - 'size_gigabytes': 10, - 'name': 'volume${StringGenerators.storageName()}', - 'labels': {'labelkey': 'value'}, - 'region': region, - 'filesystem_type': 'ext4', - }, - ); - final volumeId = createVolumeResponse.data['volume']['id']; - final volumeSize = createVolumeResponse.data['volume']['size_gigabytes']; - final volumeName = createVolumeResponse.data['volume']['name']; - volume = ServerVolume( - id: volumes.length, - name: volumeName, - sizeByte: volumeSize, - serverId: null, - linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName', - uuid: volumeId, - ); - } catch (e) { - print(e); - return APIGenericResult( - data: null, - success: false, - message: e.toString(), - ); - } finally { - client.close(); - } - - return APIGenericResult( - data: volume, - success: true, - code: createVolumeResponse.statusCode, - message: createVolumeResponse.statusMessage, - ); - } - - @override - Future> getVolumes({final String? status}) async { - final List volumes = []; - - final Response getVolumesResponse; - final Dio client = await getClient(); - try { - getVolumesResponse = await client.get( - '/volumes', - queryParameters: { - 'status': status, - }, - ); - final List rawVolumes = getVolumesResponse.data['volumes']; - int id = 0; - for (final rawVolume in rawVolumes) { - final volumeId = rawVolume['id']; - final int volumeSize = rawVolume['size_gigabytes'] * 1024 * 1024 * 1024; - final volumeDropletIds = rawVolume['droplet_ids']; - final String volumeName = rawVolume['name']; - final volume = ServerVolume( - id: id++, - name: volumeName, - sizeByte: volumeSize, - serverId: volumeDropletIds.isNotEmpty ? volumeDropletIds[0] : null, - linuxDevice: 'scsi-0DO_Volume_$volumeName', - uuid: volumeId, - ); - volumes.add(volume); - } - } catch (e) { - print(e); - } finally { - client.close(); - } - - return volumes; - } - - Future getVolume(final String volumeUuid) async { - ServerVolume? requestedVolume; - - final List volumes = await getVolumes(); - - for (final volume in volumes) { - if (volume.uuid == volumeUuid) { - requestedVolume = volume; - } - } - - return requestedVolume; - } - - @override - Future deleteVolume(final ServerVolume volume) async { - final Dio client = await getClient(); - try { - await client.delete('/volumes/${volume.uuid}'); - } catch (e) { - print(e); - } finally { - client.close(); - } - } - - @override - Future> attachVolume( - final ServerVolume volume, - final int serverId, - ) async { - bool success = false; - - Response? attachVolumeResponse; - final Dio client = await getClient(); - try { - attachVolumeResponse = await client.post( - '/volumes/actions', - data: { - 'type': 'attach', - 'volume_name': volume.name, - 'region': region, - 'droplet_id': serverId, - }, - ); - success = - attachVolumeResponse.data['action']['status'].toString() != 'error'; - } catch (e) { - print(e); - return APIGenericResult( - data: false, - success: false, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult( - data: success, - success: true, - code: attachVolumeResponse.statusCode, - message: attachVolumeResponse.statusMessage, - ); - } - - @override - Future detachVolume(final ServerVolume volume) async { - bool success = false; - - final Response detachVolumeResponse; - final Dio client = await getClient(); - try { - detachVolumeResponse = await client.post( - '/volumes/actions', - data: { - 'type': 'detach', - 'volume_name': volume.name, - 'droplet_id': volume.serverId, - 'region': region, - }, - ); - success = - detachVolumeResponse.data['action']['status'].toString() != 'error'; - } catch (e) { - print(e); - } finally { - client.close(); - } - - return success; - } - - @override - Future resizeVolume( - final ServerVolume volume, - final DiskSize size, - ) async { - bool success = false; - - final Response resizeVolumeResponse; - final Dio client = await getClient(); - try { - resizeVolumeResponse = await client.post( - '/volumes/actions', - data: { - 'type': 'resize', - 'volume_name': volume.name, - 'size_gigabytes': size.gibibyte, - 'region': region, - }, - ); - success = - resizeVolumeResponse.data['action']['status'].toString() != 'error'; - } catch (e) { - print(e); - } finally { - client.close(); - } - - return success; - } - - @override - Future> createServer({ - required final String dnsApiToken, - required final User rootUser, - required final String domainName, - required final String serverType, - required final DnsProvider dnsProvider, - }) async { - ServerHostingDetails? serverDetails; - - final String dbPassword = StringGenerators.dbPassword(); - final String apiToken = StringGenerators.apiToken(); - - final String base64Password = - base64.encode(utf8.encode(rootUser.password ?? 'PASS')); - - final String formattedHostname = getHostnameFromDomain(domainName); - const String infectBranch = 'providers/digital-ocean'; - final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false'; - final String dnsProviderType = dnsProviderToInfectName(dnsProvider); - - final String userdataString = - "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | DNS_PROVIDER_TYPE=$dnsProviderType PROVIDER=$infectProviderName STAGING_ACME='$stagingAcme' DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$formattedHostname bash 2>&1 | tee /tmp/infect.log"; - print(userdataString); - - Response? serverCreateResponse; - final Dio client = await getClient(); - try { - final Map data = { - 'name': formattedHostname, - 'size': serverType, - 'image': 'ubuntu-20-04-x64', - 'user_data': userdataString, - 'region': region!, - }; - print('Decoded data: $data'); - - serverCreateResponse = await client.post( - '/droplets', - data: data, - ); - - final int serverId = serverCreateResponse.data['droplet']['id']; - final ServerVolume? newVolume = (await createVolume()).data; - final bool attachedVolume = - (await attachVolume(newVolume!, serverId)).data; - - String? ipv4; - int attempts = 0; - while (attempts < 5 && ipv4 == null) { - await Future.delayed(const Duration(seconds: 20)); - final List servers = await getServers(); - for (final server in servers) { - if (server.name == formattedHostname && server.ip != '0.0.0.0') { - ipv4 = server.ip; - break; - } - } - ++attempts; - } - - if (attachedVolume && ipv4 != null) { - serverDetails = ServerHostingDetails( - id: serverId, - ip4: ipv4, - createTime: DateTime.now(), - volume: newVolume, - apiToken: apiToken, - provider: ServerProvider.digitalOcean, - ); - } - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: null, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult( - data: serverDetails, - success: true, - code: serverCreateResponse.statusCode, - message: serverCreateResponse.statusMessage, - ); - } - - @override - Future> deleteServer({ - required final String domainName, - }) 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 == hostname, - ); - } 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); - await Future.delayed(const Duration(seconds: 10)); - - try { - laterFutures.add(deleteVolume(volumeToRemove)); - laterFutures.add(client.delete('/droplets/${serverToRemove.id}')); - 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 - Future restart() async { - final ServerHostingDetails server = getIt().serverDetails!; - - final Dio client = await getClient(); - try { - await client.post( - '/droplets/${server.id}/actions', - data: { - 'type': 'reboot', - }, - ); - } catch (e) { - print(e); - } finally { - close(client); - } - - return server.copyWith(startTime: DateTime.now()); - } - - @override - Future powerOn() async { - final ServerHostingDetails server = getIt().serverDetails!; - - final Dio client = await getClient(); - try { - await client.post( - '/droplets/${server.id}/actions', - data: { - 'type': 'power_on', - }, - ); - } catch (e) { - print(e); - } finally { - close(client); - } - - return server.copyWith(startTime: DateTime.now()); - } - - /// Digital Ocean returns a map of lists of /proc/stat values, - /// so here we are trying to implement average CPU - /// load calculation for each point in time on a given interval. - /// - /// For each point of time: - /// - /// `Average Load = 100 * (1 - (Idle Load / Total Load))` - /// - /// For more info please proceed to read: - /// https://rosettacode.org/wiki/Linux_CPU_utilization - List calculateCpuLoadMetrics(final List rawProcStatMetrics) { - final List cpuLoads = []; - - final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length; - for (int i = 0; i < pointsInTime; ++i) { - double currentMetricLoad = 0.0; - double? currentMetricIdle; - for (final rawProcStat in rawProcStatMetrics) { - final String rawProcValue = rawProcStat['values'][i][1]; - // Converting MBit into bit - final double procValue = double.parse(rawProcValue) * 1000000; - currentMetricLoad += procValue; - if (currentMetricIdle == null && - rawProcStat['metric']['mode'] == 'idle') { - currentMetricIdle = procValue; - } - } - currentMetricIdle ??= 0.0; - currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad)); - cpuLoads.add( - TimeSeriesData( - rawProcStatMetrics[0]['values'][i][0], - currentMetricLoad, - ), - ); - } - - return cpuLoads; - } - - @override - Future getMetrics( - final int serverId, - final DateTime start, - final DateTime end, - ) async { - ServerMetrics? metrics; - - const int step = 15; - final Dio client = await getClient(); - try { - Response response = await client.get( - '/monitoring/metrics/droplet/bandwidth', - queryParameters: { - 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', - 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', - 'host_id': '$serverId', - 'interface': 'public', - 'direction': 'inbound', - }, - ); - - final List inbound = response.data['data']['result'][0]['values']; - - response = await client.get( - '/monitoring/metrics/droplet/bandwidth', - queryParameters: { - 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', - 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', - 'host_id': '$serverId', - 'interface': 'public', - 'direction': 'outbound', - }, - ); - - final List outbound = response.data['data']['result'][0]['values']; - - response = await client.get( - '/monitoring/metrics/droplet/cpu', - queryParameters: { - 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', - 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', - 'host_id': '$serverId', - }, - ); - - metrics = ServerMetrics( - bandwidthIn: inbound - .map( - (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), - ) - .toList(), - bandwidthOut: outbound - .map( - (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), - ) - .toList(), - cpu: calculateCpuLoadMetrics(response.data['data']['result']), - start: start, - end: end, - stepsInSecond: step, - ); - } catch (e) { - print(e); - } finally { - close(client); - } - - return metrics; - } - - @override - Future> getMetadata(final int serverId) async { - List metadata = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get('/droplets/$serverId'); - final droplet = response.data!['droplet']; - metadata = [ - ServerMetadataEntity( - type: MetadataType.id, - name: 'server.server_id'.tr(), - value: droplet['id'].toString(), - ), - ServerMetadataEntity( - type: MetadataType.status, - name: 'server.status'.tr(), - value: droplet['status'].toString().capitalize(), - ), - ServerMetadataEntity( - type: MetadataType.cpu, - name: 'server.cpu'.tr(), - value: 'server.core_count'.plural(droplet['vcpus']), - ), - ServerMetadataEntity( - type: MetadataType.ram, - name: 'server.ram'.tr(), - value: "${droplet['memory'].toString()} MB", - ), - ServerMetadataEntity( - type: MetadataType.cost, - name: 'server.monthly_cost'.tr(), - value: droplet['size']['price_monthly'].toString(), - ), - ServerMetadataEntity( - type: MetadataType.location, - name: 'server.location'.tr(), - value: - '${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}', - ), - ServerMetadataEntity( - type: MetadataType.other, - name: 'server.provider'.tr(), - value: displayProviderName, - ), - ]; - } catch (e) { - print(e); - } finally { - close(client); - } - - return metadata; - } - - @override - Future> getServers() async { - List servers = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get('/droplets'); - servers = response.data!['droplets'].map( - (final server) { - String ipv4 = '0.0.0.0'; - if (server['networks']['v4'].isNotEmpty) { - for (final v4 in server['networks']['v4']) { - if (v4['type'].toString() == 'public') { - ipv4 = v4['ip_address'].toString(); - } - } - } - - return ServerBasicInfo( - id: server['id'], - reverseDns: server['name'], - created: DateTime.now(), - ip: ipv4, - name: server['name'], - ); - }, - ).toList(); - } catch (e) { - print(e); - } finally { - close(client); - } - - print(servers); - return servers; - } - - String? getEmojiFlag(final String query) { - String? emoji; - - switch (query.toLowerCase().substring(0, 3)) { - case 'fra': - emoji = '🇩🇪'; - break; - - case 'ams': - emoji = '🇳🇱'; - break; - - case 'sgp': - emoji = '🇸🇬'; - break; - - case 'lon': - emoji = '🇬🇧'; - break; - - case 'tor': - emoji = '🇨🇦'; - break; - - case 'blr': - emoji = '🇮🇳'; - break; - - case 'nyc': - case 'sfo': - emoji = '🇺🇸'; - break; - } - - return emoji; - } - - @override - Future>> - getAvailableLocations() async { - List locations = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - '/regions', - ); - - locations = response.data!['regions'] - .map( - (final location) => ServerProviderLocation( - title: location['slug'], - description: location['name'], - flag: getEmojiFlag(location['slug']), - identifier: location['slug'], - ), - ) - .toList(); - } catch (e) { - print(e); - return APIGenericResult( - data: [], - success: false, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(data: locations, success: true); - } - - @override - Future>> getServerTypesByLocation({ - required final ServerProviderLocation location, - }) async { - final List types = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - '/sizes', - ); - final rawSizes = response.data!['sizes']; - for (final rawSize in rawSizes) { - for (final rawRegion in rawSize['regions']) { - final ramMb = rawSize['memory'].toDouble(); - if (rawRegion.toString() == location.identifier && ramMb > 1024) { - types.add( - ServerType( - title: rawSize['description'], - identifier: rawSize['slug'], - ram: ramMb / 1024, - cores: rawSize['vcpus'], - disk: DiskSize(byte: rawSize['disk'] * 1024 * 1024 * 1024), - price: Price( - value: rawSize['price_monthly'], - currency: 'USD', - ), - location: location, - ), - ); - } - } - } - } catch (e) { - print(e); - return APIGenericResult( - data: [], - success: false, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(data: types, success: true); - } - - @override - Future> createReverseDns({ - required final ServerHostingDetails serverDetails, - required final ServerDomain domain, - }) async { - /// TODO remove from provider interface - const bool success = true; - return APIGenericResult(success: success, data: null); - } - - @override - ProviderApiTokenValidation getApiTokenValidation() => - ProviderApiTokenValidation( - regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'), - length: 71, - ); -} diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart new file mode 100644 index 00000000..807d03a0 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart @@ -0,0 +1,561 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart'; +import 'package:selfprivacy/logic/api_maps/tls_options.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/logic/models/json/digital_ocean_server_info.dart'; +import 'package:selfprivacy/utils/password_generator.dart'; + +class DigitalOceanApi extends RestApiMap { + DigitalOceanApi({ + required this.region, + this.hasLogger = true, + this.isWithToken = true, + }); + @override + bool hasLogger; + @override + bool isWithToken; + + final String? region; + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions( + baseUrl: rootAddress, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + ); + if (isWithToken) { + final String? token = getIt().serverProviderKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + + return options; + } + + @override + String get rootAddress => 'https://api.digitalocean.com/v2'; + String get infectProviderName => 'digitalocean'; + String get displayProviderName => 'Digital Ocean'; + + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '/account', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Bearer $token'}, + ), + ); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return GenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return GenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + Future> createVolume() async { + DigitalOceanVolume? volume; + Response? createVolumeResponse; + final Dio client = await getClient(); + try { + await Future.delayed(const Duration(seconds: 6)); + + createVolumeResponse = await client.post( + '/volumes', + data: { + 'size_gigabytes': 10, + 'name': 'volume${StringGenerators.storageName()}', + 'labels': {'labelkey': 'value'}, + 'region': region, + 'filesystem_type': 'ext4', + }, + ); + volume = DigitalOceanVolume.fromJson(createVolumeResponse.data['volume']); + } catch (e) { + print(e); + return GenericResult( + data: null, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: volume, + success: true, + code: createVolumeResponse.statusCode, + message: createVolumeResponse.statusMessage, + ); + } + + Future>> getVolumes({ + final String? status, + }) async { + final List volumes = []; + + Response? getVolumesResponse; + final Dio client = await getClient(); + try { + getVolumesResponse = await client.get( + '/volumes', + queryParameters: { + 'status': status, + }, + ); + for (final volume in getVolumesResponse.data['volumes']) { + volumes.add(DigitalOceanVolume.fromJson(volume)); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: volumes, + success: true, + ); + } + + Future> deleteVolume(final String uuid) async { + final Dio client = await getClient(); + try { + await client.delete('/volumes/$uuid'); + } catch (e) { + print(e); + return GenericResult( + data: null, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: null, + success: true, + ); + } + + Future> attachVolume( + final String name, + final int serverId, + ) async { + bool success = false; + + Response? attachVolumeResponse; + final Dio client = await getClient(); + try { + attachVolumeResponse = await client.post( + '/volumes/actions', + data: { + 'type': 'attach', + 'volume_name': name, + 'region': region, + 'droplet_id': serverId, + }, + ); + success = + attachVolumeResponse.data['action']['status'].toString() != 'error'; + } catch (e) { + print(e); + return GenericResult( + data: false, + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult( + data: success, + success: true, + code: attachVolumeResponse.statusCode, + message: attachVolumeResponse.statusMessage, + ); + } + + Future> detachVolume( + final String name, + final int serverId, + ) async { + bool success = false; + + final Response detachVolumeResponse; + final Dio client = await getClient(); + try { + detachVolumeResponse = await client.post( + '/volumes/actions', + data: { + 'type': 'detach', + 'volume_name': name, + 'droplet_id': serverId, + 'region': region, + }, + ); + success = + detachVolumeResponse.data['action']['status'].toString() != 'error'; + } catch (e) { + print(e); + return GenericResult( + data: false, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: success, + success: true, + ); + } + + Future> resizeVolume( + final String name, + final DiskSize size, + ) async { + bool success = false; + + final Response resizeVolumeResponse; + final Dio client = await getClient(); + try { + resizeVolumeResponse = await client.post( + '/volumes/actions', + data: { + 'type': 'resize', + 'volume_name': name, + 'size_gigabytes': size.gibibyte, + 'region': region, + }, + ); + success = + resizeVolumeResponse.data['action']['status'].toString() != 'error'; + } catch (e) { + print(e); + return GenericResult( + data: false, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: success, + success: true, + ); + } + + Future> createServer({ + required final String dnsApiToken, + required final String dnsProviderType, + required final String serverApiToken, + required final User rootUser, + required final String base64Password, + required final String databasePassword, + required final String domainName, + required final String hostName, + required final String serverType, + }) async { + final String stagingAcme = TlsOptions.stagingAcme ? 'true' : 'false'; + + int? dropletId; + Response? serverCreateResponse; + final Dio client = await getClient(); + try { + final Map data = { + 'name': hostName, + 'size': serverType, + 'image': 'ubuntu-20-04-x64', + 'user_data': '#cloud-config\n' + 'runcmd:\n' + '- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/providers/digital-ocean/nixos-infect | ' + "PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType STAGING_ACME='$stagingAcme' DOMAIN='$domainName' " + "LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$databasePassword " + 'API_TOKEN=$serverApiToken HOSTNAME=$hostName bash 2>&1 | tee /tmp/infect.log', + 'region': region!, + }; + print('Decoded data: $data'); + + serverCreateResponse = await client.post( + '/droplets', + data: data, + ); + dropletId = serverCreateResponse.data['droplet']['id']; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult( + data: dropletId, + success: true, + code: serverCreateResponse.statusCode, + message: serverCreateResponse.statusMessage, + ); + } + + Future> deleteServer(final int serverId) async { + final Dio client = await getClient(); + try { + await client.delete('/droplets/$serverId'); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> restart(final int serverId) async { + final Dio client = await getClient(); + try { + await client.post( + '/droplets/$serverId/actions', + data: { + 'type': 'reboot', + }, + ); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> powerOn(final int serverId) async { + final Dio client = await getClient(); + try { + await client.post( + '/droplets/$serverId/actions', + data: { + 'type': 'power_on', + }, + ); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> getMetricsCpu( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + List metrics = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/monitoring/metrics/droplet/cpu', + queryParameters: { + 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', + 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', + 'host_id': '$serverId', + }, + ); + metrics = response.data['data']['result']; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: metrics); + } + + Future> getMetricsBandwidth( + final int serverId, + final DateTime start, + final DateTime end, + final bool isInbound, + ) async { + List metrics = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/monitoring/metrics/droplet/bandwidth', + queryParameters: { + 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', + 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', + 'host_id': '$serverId', + 'interface': 'public', + 'direction': isInbound ? 'inbound' : 'outbound', + }, + ); + metrics = response.data['data']['result'][0]['values']; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: metrics); + } + + Future> getServers() async { + List servers = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get('/droplets'); + servers = response.data['droplets']; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: servers, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: servers); + } + + Future>> + getAvailableLocations() async { + final List locations = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/regions', + ); + + for (final region in response.data!['regions']) { + locations.add(DigitalOceanLocation.fromJson(region)); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: locations, success: true); + } + + Future>> + getAvailableServerTypes() async { + final List types = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/sizes', + ); + for (final size in response.data!['sizes']) { + types.add(DigitalOceanServerType.fromJson(size)); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: types, success: true); + } +} diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart deleted file mode 100644 index 73a1e647..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; - -class DigitalOceanApiFactory extends ServerProviderApiFactory - with VolumeProviderApiFactory { - DigitalOceanApiFactory({this.region}); - - final String? region; - - @override - ServerProviderApi getServerProvider({ - final ServerProviderApiSettings settings = - const ServerProviderApiSettings(), - }) => - DigitalOceanApi( - region: settings.region ?? region, - hasLogger: settings.hasLogger, - isWithToken: settings.isWithToken, - ); - - @override - VolumeProviderApi getVolumeProvider({ - final ServerProviderApiSettings settings = - const ServerProviderApiSettings(), - }) => - DigitalOceanApi( - region: settings.region ?? region, - hasLogger: settings.hasLogger, - isWithToken: settings.isWithToken, - ); -} 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 deleted file mode 100644 index 372722fa..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart +++ /dev/null @@ -1,850 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/staging_options.dart'; -import 'package:selfprivacy/logic/models/disk_size.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; -import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/logic/models/price.dart'; -import 'package:selfprivacy/logic/models/server_basic_info.dart'; -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 { - HetznerApi({ - this.region, - this.hasLogger = true, - this.isWithToken = true, - }); - @override - bool hasLogger; - @override - bool isWithToken; - - final String? region; - - @override - BaseOptions get options { - final BaseOptions options = BaseOptions( - baseUrl: rootAddress, - contentType: Headers.jsonContentType, - responseType: ResponseType.json, - ); - if (isWithToken) { - final String? token = getIt().serverProviderKey; - assert(token != null); - options.headers = {'Authorization': 'Bearer $token'}; - } - - if (validateStatus != null) { - options.validateStatus = validateStatus!; - } - - return options; - } - - @override - String get rootAddress => 'https://api.hetzner.cloud/v1'; - - @override - String get infectProviderName => 'hetzner'; - - @override - String get displayProviderName => 'Hetzner'; - - @override - Future> isApiTokenValid(final String token) async { - bool isValid = false; - Response? response; - String message = ''; - final Dio client = await getClient(); - try { - response = await client.get( - '/servers', - options: Options( - followRedirects: false, - validateStatus: (final status) => - status != null && (status >= 200 || status == 401), - headers: {'Authorization': 'Bearer $token'}, - ), - ); - } catch (e) { - print(e); - isValid = false; - message = e.toString(); - } finally { - close(client); - } - - if (response == null) { - return APIGenericResult( - data: isValid, - success: false, - message: message, - ); - } - - if (response.statusCode == HttpStatus.ok) { - isValid = true; - } else if (response.statusCode == HttpStatus.unauthorized) { - isValid = false; - } else { - throw Exception('code: ${response.statusCode}'); - } - - return APIGenericResult( - data: isValid, - success: true, - message: response.statusMessage, - ); - } - - @override - ProviderApiTokenValidation getApiTokenValidation() => - ProviderApiTokenValidation( - regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'), - length: 64, - ); - - @override - Future getPricePerGb() async { - double? price; - - final Response pricingResponse; - final Dio client = await getClient(); - try { - pricingResponse = await client.get('/pricing'); - - final volume = pricingResponse.data['pricing']['volume']; - final volumePrice = volume['price_per_gb_month']['gross']; - price = double.parse(volumePrice); - } catch (e) { - print(e); - } finally { - client.close(); - } - - return price == null - ? null - : Price( - value: price, - currency: 'EUR', - ); - } - - @override - Future> createVolume() async { - ServerVolume? volume; - - Response? createVolumeResponse; - final Dio client = await getClient(); - try { - createVolumeResponse = await client.post( - '/volumes', - data: { - 'size': 10, - 'name': StringGenerators.storageName(), - 'labels': {'labelkey': 'value'}, - 'location': region, - 'automount': false, - 'format': 'ext4' - }, - ); - final volumeId = createVolumeResponse.data['volume']['id']; - final volumeSize = createVolumeResponse.data['volume']['size']; - final volumeServer = createVolumeResponse.data['volume']['server']; - final volumeName = createVolumeResponse.data['volume']['name']; - final volumeDevice = createVolumeResponse.data['volume']['linux_device']; - volume = ServerVolume( - id: volumeId, - name: volumeName, - sizeByte: volumeSize, - serverId: volumeServer, - linuxDevice: volumeDevice, - ); - } catch (e) { - print(e); - return APIGenericResult( - data: null, - success: false, - message: e.toString(), - ); - } finally { - client.close(); - } - - return APIGenericResult( - data: volume, - success: true, - code: createVolumeResponse.statusCode, - message: createVolumeResponse.statusMessage, - ); - } - - @override - Future> getVolumes({final String? status}) async { - final List volumes = []; - - final Response getVolumesResonse; - final Dio client = await getClient(); - try { - getVolumesResonse = await client.get( - '/volumes', - queryParameters: { - 'status': status, - }, - ); - final List rawVolumes = getVolumesResonse.data['volumes']; - for (final rawVolume in rawVolumes) { - final int volumeId = rawVolume['id']; - final int volumeSize = rawVolume['size'] * 1024 * 1024 * 1024; - final volumeServer = rawVolume['server']; - final String volumeName = rawVolume['name']; - final volumeDevice = rawVolume['linux_device']; - final volume = ServerVolume( - id: volumeId, - name: volumeName, - sizeByte: volumeSize, - serverId: volumeServer, - linuxDevice: volumeDevice, - ); - volumes.add(volume); - } - } catch (e) { - print(e); - } finally { - client.close(); - } - - return volumes; - } - - Future getVolume( - final String volumeId, - ) async { - ServerVolume? volume; - - final Response getVolumeResponse; - final Dio client = await getClient(); - try { - getVolumeResponse = await client.get('/volumes/$volumeId'); - final int responseVolumeId = getVolumeResponse.data['volume']['id']; - final int volumeSize = getVolumeResponse.data['volume']['size']; - final int volumeServer = getVolumeResponse.data['volume']['server']; - final String volumeName = getVolumeResponse.data['volume']['name']; - final volumeDevice = getVolumeResponse.data['volume']['linux_device']; - volume = ServerVolume( - id: responseVolumeId, - name: volumeName, - sizeByte: volumeSize, - serverId: volumeServer, - linuxDevice: volumeDevice, - ); - } catch (e) { - print(e); - } finally { - client.close(); - } - - return volume; - } - - @override - Future deleteVolume(final ServerVolume volume) async { - final Dio client = await getClient(); - try { - await client.delete('/volumes/${volume.id}'); - } catch (e) { - print(e); - } finally { - client.close(); - } - } - - @override - Future> attachVolume( - final ServerVolume volume, - final int serverId, - ) async { - bool success = false; - - Response? attachVolumeResponse; - final Dio client = await getClient(); - try { - attachVolumeResponse = await client.post( - '/volumes/${volume.id}/actions/attach', - data: { - 'automount': true, - 'server': serverId, - }, - ); - success = - attachVolumeResponse.data['action']['status'].toString() != 'error'; - } catch (e) { - print(e); - } finally { - client.close(); - } - - return APIGenericResult( - data: success, - success: true, - code: attachVolumeResponse?.statusCode, - message: attachVolumeResponse?.statusMessage, - ); - } - - @override - Future detachVolume(final ServerVolume volume) async { - bool success = false; - - final Response detachVolumeResponse; - final Dio client = await getClient(); - try { - detachVolumeResponse = await client.post( - '/volumes/${volume.id}/actions/detach', - ); - success = - detachVolumeResponse.data['action']['status'].toString() != 'error'; - } catch (e) { - print(e); - } finally { - client.close(); - } - - return success; - } - - @override - Future resizeVolume( - final ServerVolume volume, - final DiskSize size, - ) async { - bool success = false; - - final Response resizeVolumeResponse; - final Dio client = await getClient(); - try { - resizeVolumeResponse = await client.post( - '/volumes/${volume.id}/actions/resize', - data: { - 'size': size.gibibyte, - }, - ); - success = - resizeVolumeResponse.data['action']['status'].toString() != 'error'; - } catch (e) { - print(e); - } finally { - client.close(); - } - - return success; - } - - @override - Future> createServer({ - required final String dnsApiToken, - required final User rootUser, - required final String domainName, - required final String serverType, - required final DnsProvider dnsProvider, - }) async { - final APIGenericResult newVolumeResponse = - await createVolume(); - - if (!newVolumeResponse.success || newVolumeResponse.data == null) { - return APIGenericResult( - data: null, - success: false, - message: newVolumeResponse.message, - code: newVolumeResponse.code, - ); - } - return createServerWithVolume( - dnsApiToken: dnsApiToken, - rootUser: rootUser, - domainName: domainName, - volume: newVolumeResponse.data!, - serverType: serverType, - dnsProvider: dnsProvider, - ); - } - - Future> createServerWithVolume({ - required final String dnsApiToken, - required final User rootUser, - required final String domainName, - required final ServerVolume volume, - required final String serverType, - required final DnsProvider dnsProvider, - }) async { - final Dio client = await getClient(); - - final String dbPassword = StringGenerators.dbPassword(); - final int volumeId = volume.id; - - final String apiToken = StringGenerators.apiToken(); - final String hostname = getHostnameFromDomain(domainName); - const String infectBranch = 'providers/hetzner'; - final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false'; - final String base64Password = - base64.encode(utf8.encode(rootUser.password ?? 'PASS')); - final String dnsProviderType = dnsProviderToInfectName(dnsProvider); - - final String userdataString = - "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | DNS_PROVIDER_TYPE=$dnsProviderType STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log"; - - Response? serverCreateResponse; - ServerHostingDetails? serverDetails; - DioError? hetznerError; - bool success = false; - - try { - final Map data = { - 'name': hostname, - 'server_type': serverType, - 'start_after_create': false, - 'image': 'ubuntu-20.04', - 'volumes': [volumeId], - 'networks': [], - 'user_data': userdataString, - 'labels': {}, - 'automount': true, - 'location': region!, - }; - print('Decoded data: $data'); - - serverCreateResponse = await client.post( - '/servers', - data: data, - ); - print(serverCreateResponse.data); - serverDetails = ServerHostingDetails( - id: serverCreateResponse.data['server']['id'], - ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], - createTime: DateTime.now(), - volume: volume, - apiToken: apiToken, - provider: ServerProvider.hetzner, - ); - success = true; - } on DioError catch (e) { - print(e); - hetznerError = e; - } catch (e) { - print(e); - } finally { - client.close(); - } - - if (!success) { - await Future.delayed(const Duration(seconds: 10)); - await deleteVolume(volume); - } - - String? apiResultMessage = serverCreateResponse?.statusMessage; - if (hetznerError != null && - hetznerError.response!.data['error']['code'] == 'uniqueness_error') { - apiResultMessage = 'uniqueness_error'; - } - - return APIGenericResult( - data: serverDetails, - success: success && hetznerError == null, - code: serverCreateResponse?.statusCode ?? - hetznerError?.response?.statusCode, - message: apiResultMessage, - ); - } - - @override - Future> deleteServer({ - required final String domainName, - }) async { - final Dio client = await getClient(); - try { - 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 = []; - - for (final volumeId in volumes) { - await client.post('/volumes/$volumeId/actions/detach'); - } - 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); - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: false, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult( - success: true, - data: true, - ); - } - - @override - Future restart() async { - final ServerHostingDetails server = getIt().serverDetails!; - - final Dio client = await getClient(); - try { - await client.post('/servers/${server.id}/actions/reset'); - } catch (e) { - print(e); - } finally { - close(client); - } - - return server.copyWith(startTime: DateTime.now()); - } - - @override - Future powerOn() async { - final ServerHostingDetails server = getIt().serverDetails!; - - final Dio client = await getClient(); - try { - await client.post('/servers/${server.id}/actions/poweron'); - } catch (e) { - print(e); - } finally { - close(client); - } - - return server.copyWith(startTime: DateTime.now()); - } - - Future> requestRawMetrics( - final int serverId, - final DateTime start, - final DateTime end, - final String type, - ) async { - Map metrics = {}; - final Dio client = await getClient(); - try { - final Map queryParameters = { - 'start': start.toUtc().toIso8601String(), - 'end': end.toUtc().toIso8601String(), - 'type': type - }; - final Response res = await client.get( - '/servers/$serverId/metrics', - queryParameters: queryParameters, - ); - metrics = res.data['metrics']; - } catch (e) { - print(e); - } finally { - close(client); - } - - return metrics; - } - - List serializeTimeSeries( - final Map json, - final String type, - ) { - final List list = json['time_series'][type]['values']; - return list - .map((final el) => TimeSeriesData(el[0], double.parse(el[1]))) - .toList(); - } - - @override - Future getMetrics( - final int serverId, - final DateTime start, - final DateTime end, - ) async { - ServerMetrics? metrics; - - final Map rawCpuMetrics = await requestRawMetrics( - serverId, - start, - end, - 'cpu', - ); - final Map rawNetworkMetrics = await requestRawMetrics( - serverId, - start, - end, - 'network', - ); - - if (rawNetworkMetrics.isEmpty || rawCpuMetrics.isEmpty) { - return metrics; - } - - metrics = ServerMetrics( - cpu: serializeTimeSeries( - rawCpuMetrics, - 'cpu', - ), - bandwidthIn: serializeTimeSeries( - rawNetworkMetrics, - 'network.0.bandwidth.in', - ), - bandwidthOut: serializeTimeSeries( - rawNetworkMetrics, - 'network.0.bandwidth.out', - ), - end: end, - start: start, - stepsInSecond: rawCpuMetrics['step'], - ); - - return metrics; - } - - @override - Future> getMetadata(final int serverId) async { - List metadata = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get('/servers/$serverId'); - final hetznerInfo = HetznerServerInfo.fromJson(response.data!['server']); - metadata = [ - ServerMetadataEntity( - type: MetadataType.id, - name: 'server.server_id'.tr(), - value: hetznerInfo.id.toString(), - ), - ServerMetadataEntity( - type: MetadataType.status, - name: 'server.status'.tr(), - value: hetznerInfo.status.toString().split('.')[1].capitalize(), - ), - ServerMetadataEntity( - type: MetadataType.cpu, - name: 'server.cpu'.tr(), - value: 'server.core_count'.plural(hetznerInfo.serverType.cores), - ), - ServerMetadataEntity( - type: MetadataType.ram, - name: 'server.ram'.tr(), - value: '${hetznerInfo.serverType.memory.toString()} GB', - ), - ServerMetadataEntity( - type: MetadataType.cost, - name: 'server.monthly_cost'.tr(), - value: hetznerInfo.serverType.prices[1].monthly.toStringAsFixed(2), - ), - ServerMetadataEntity( - type: MetadataType.location, - name: 'server.location'.tr(), - value: - '${hetznerInfo.location.city}, ${hetznerInfo.location.country}', - ), - ServerMetadataEntity( - type: MetadataType.other, - name: 'server.provider'.tr(), - value: displayProviderName, - ), - ]; - } catch (e) { - print(e); - } finally { - close(client); - } - - return metadata; - } - - @override - Future> getServers() async { - List servers = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get('/servers'); - servers = response.data!['servers'] - .map( - (final e) => HetznerServerInfo.fromJson(e), - ) - .toList() - .where( - (final server) => server.publicNet.ipv4 != null, - ) - .map( - (final server) => ServerBasicInfo( - id: server.id, - name: server.name, - ip: server.publicNet.ipv4.ip, - reverseDns: server.publicNet.ipv4.reverseDns, - created: server.created, - ), - ) - .toList(); - } catch (e) { - print(e); - } finally { - close(client); - } - - print(servers); - return servers; - } - - String? getEmojiFlag(final String query) { - String? emoji; - - switch (query.toLowerCase()) { - case 'de': - emoji = '🇩🇪'; - break; - - case 'fi': - emoji = '🇫🇮'; - break; - - case 'us': - emoji = '🇺🇸'; - break; - } - - return emoji; - } - - @override - Future>> - getAvailableLocations() async { - List locations = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - '/locations', - ); - - locations = response.data!['locations'] - .map( - (final location) => ServerProviderLocation( - title: location['city'], - description: location['description'], - flag: getEmojiFlag(location['country']), - identifier: location['name'], - ), - ) - .toList(); - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: [], - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(success: true, data: locations); - } - - @override - Future>> getServerTypesByLocation({ - required final ServerProviderLocation location, - }) async { - final List types = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - '/server_types', - ); - final rawTypes = response.data!['server_types']; - for (final rawType in rawTypes) { - for (final rawPrice in rawType['prices']) { - if (rawPrice['location'].toString() == location.identifier) { - types.add( - ServerType( - title: rawType['description'], - identifier: rawType['name'], - ram: rawType['memory'], - cores: rawType['cores'], - disk: DiskSize(byte: rawType['disk'] * 1024 * 1024 * 1024), - price: Price( - value: double.parse(rawPrice['price_monthly']['gross']), - currency: 'EUR', - ), - location: location, - ), - ); - } - } - } - } catch (e) { - print(e); - return APIGenericResult( - data: [], - success: false, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(data: types, success: true); - } - - @override - Future> createReverseDns({ - required final ServerHostingDetails serverDetails, - required final ServerDomain domain, - }) async { - final Dio client = await getClient(); - try { - await client.post( - '/servers/${serverDetails.id}/actions/change_dns_ptr', - data: { - 'ip': serverDetails.ip4, - 'dns_ptr': domain.domainName, - }, - ); - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: null, - message: e.toString(), - ); - } finally { - close(client); - } - - return APIGenericResult(success: true, data: null); - } -} diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart new file mode 100644 index 00000000..b320d4f5 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart @@ -0,0 +1,595 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart'; +import 'package:selfprivacy/logic/api_maps/tls_options.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/utils/password_generator.dart'; + +class HetznerApi extends RestApiMap { + HetznerApi({ + this.region, + this.hasLogger = true, + this.isWithToken = true, + }); + @override + bool hasLogger; + @override + bool isWithToken; + + final String? region; + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions( + baseUrl: rootAddress, + contentType: Headers.jsonContentType, + responseType: ResponseType.json, + ); + if (isWithToken) { + final String? token = getIt().serverProviderKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + + return options; + } + + @override + String get rootAddress => 'https://api.hetzner.cloud/v1'; + String get infectProviderName => 'hetzner'; + String get displayProviderName => 'Hetzner'; + + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '/servers', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Bearer $token'}, + ), + ); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return GenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return GenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + Future> getPricePerGb() async { + double? price; + + final Response pricingResponse; + final Dio client = await getClient(); + try { + pricingResponse = await client.get('/pricing'); + + final volume = pricingResponse.data['pricing']['volume']; + final volumePrice = volume['price_per_gb_month']['gross']; + price = double.parse(volumePrice); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: price, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult(success: true, data: price); + } + + Future> createVolume() async { + Response? createVolumeResponse; + HetznerVolume? volume; + final Dio client = await getClient(); + try { + createVolumeResponse = await client.post( + '/volumes', + data: { + 'size': 10, + 'name': StringGenerators.storageName(), + 'labels': {'labelkey': 'value'}, + 'location': region, + 'automount': false, + 'format': 'ext4' + }, + ); + volume = HetznerVolume.fromJson(createVolumeResponse.data['volume']); + } catch (e) { + print(e); + return GenericResult( + data: null, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: volume, + success: true, + code: createVolumeResponse.statusCode, + message: createVolumeResponse.statusMessage, + ); + } + + Future>> getVolumes({ + final String? status, + }) async { + final List volumes = []; + + Response? getVolumesResonse; + final Dio client = await getClient(); + try { + getVolumesResonse = await client.get( + '/volumes', + queryParameters: { + 'status': status, + }, + ); + for (final volume in getVolumesResonse.data['volumes']) { + volumes.add(HetznerVolume.fromJson(volume)); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: volumes, + success: true, + code: getVolumesResonse.statusCode, + message: getVolumesResonse.statusMessage, + ); + } + + Future> getVolume( + final String volumeId, + ) async { + HetznerVolume? volume; + + final Response getVolumeResponse; + final Dio client = await getClient(); + try { + getVolumeResponse = await client.get('/volumes/$volumeId'); + volume = HetznerVolume.fromJson(getVolumeResponse.data['volume']); + } catch (e) { + print(e); + return GenericResult( + data: null, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: volume, + success: true, + ); + } + + Future> deleteVolume(final int volumeId) async { + final Dio client = await getClient(); + try { + await client.delete('/volumes/$volumeId'); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + success: true, + data: true, + ); + } + + Future> attachVolume( + final HetznerVolume volume, + final int serverId, + ) async { + bool success = false; + + Response? attachVolumeResponse; + final Dio client = await getClient(); + try { + attachVolumeResponse = await client.post( + '/volumes/${volume.id}/actions/attach', + data: { + 'automount': true, + 'server': serverId, + }, + ); + success = + attachVolumeResponse.data['action']['status'].toString() != 'error'; + } catch (e) { + print(e); + } finally { + client.close(); + } + + return GenericResult( + data: success, + success: true, + code: attachVolumeResponse?.statusCode, + message: attachVolumeResponse?.statusMessage, + ); + } + + Future> detachVolume(final int volumeId) async { + bool success = false; + + final Response detachVolumeResponse; + final Dio client = await getClient(); + try { + detachVolumeResponse = await client.post( + '/volumes/$volumeId/actions/detach', + ); + success = + detachVolumeResponse.data['action']['status'].toString() != 'error'; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + success: false, + data: success, + ); + } + + Future> resizeVolume( + final HetznerVolume volume, + final DiskSize size, + ) async { + bool success = false; + + final Response resizeVolumeResponse; + final Dio client = await getClient(); + try { + resizeVolumeResponse = await client.post( + '/volumes/${volume.id}/actions/resize', + data: { + 'size': size.gibibyte, + }, + ); + success = + resizeVolumeResponse.data['action']['status'].toString() != 'error'; + } catch (e) { + print(e); + return GenericResult( + data: false, + success: false, + message: e.toString(), + ); + } finally { + client.close(); + } + + return GenericResult( + data: success, + success: true, + ); + } + + Future> createServer({ + required final String dnsApiToken, + required final String dnsProviderType, + required final String serverApiToken, + required final User rootUser, + required final String base64Password, + required final String databasePassword, + required final String domainName, + required final String hostName, + required final int volumeId, + required final String serverType, + }) async { + final String stagingAcme = TlsOptions.stagingAcme ? 'true' : 'false'; + Response? serverCreateResponse; + HetznerServerInfo? serverInfo; + DioError? hetznerError; + bool success = false; + + final Dio client = await getClient(); + try { + final Map data = { + 'name': hostName, + 'server_type': serverType, + 'start_after_create': false, + 'image': 'ubuntu-20.04', + 'volumes': [volumeId], + 'networks': [], + 'user_data': '#cloud-config\n' + 'runcmd:\n' + '- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/providers/hetzner/nixos-infect | ' + "STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType " + "NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' " + 'CF_TOKEN=$dnsApiToken DB_PASSWORD=$databasePassword API_TOKEN=$serverApiToken HOSTNAME=$hostName bash 2>&1 | ' + 'tee /tmp/infect.log', + 'labels': {}, + 'automount': true, + 'location': region!, + }; + print('Decoded data: $data'); + + serverCreateResponse = await client.post('/servers', data: data); + serverInfo = HetznerServerInfo.fromJson( + serverCreateResponse.data['server'], + ); + success = true; + } on DioError catch (e) { + print(e); + hetznerError = e; + } catch (e) { + print(e); + } finally { + close(client); + } + + String? apiResultMessage = serverCreateResponse?.statusMessage; + if (hetznerError != null && + hetznerError.response!.data['error']['code'] == 'uniqueness_error') { + apiResultMessage = 'uniqueness_error'; + } + + return GenericResult( + data: serverInfo, + success: success && hetznerError == null, + code: serverCreateResponse?.statusCode ?? + hetznerError?.response?.statusCode, + message: apiResultMessage, + ); + } + + Future> deleteServer({ + required final int serverId, + }) async { + final Dio client = await getClient(); + try { + await client.delete('/servers/$serverId'); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> restart(final int serverId) async { + final Dio client = await getClient(); + try { + await client.post('/servers/$serverId/actions/reset'); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future> powerOn(final int serverId) async { + final Dio client = await getClient(); + try { + await client.post('/servers/$serverId/actions/poweron'); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } + + Future>> getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + final String type, + ) async { + Map metrics = {}; + final Dio client = await getClient(); + try { + final Map queryParameters = { + 'start': start.toUtc().toIso8601String(), + 'end': end.toUtc().toIso8601String(), + 'type': type + }; + final Response res = await client.get( + '/servers/$serverId/metrics', + queryParameters: queryParameters, + ); + metrics = res.data['metrics']; + } catch (e) { + print(e); + return GenericResult( + success: false, + data: {}, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: metrics, success: true); + } + + Future>> getServers() async { + List servers = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get('/servers'); + servers = response.data!['servers'] + .map( + (final e) => HetznerServerInfo.fromJson(e), + ) + .toList(); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: servers, success: true); + } + + Future>> getAvailableLocations() async { + final List locations = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get('/locations'); + for (final location in response.data!['locations']) { + locations.add(HetznerLocation.fromJson(location)); + } + } catch (e) { + print(e); + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: locations); + } + + Future>> + getAvailableServerTypes() async { + final List types = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/server_types', + ); + for (final type in response.data!['server_types']) { + types.add(HetznerServerTypeInfo.fromJson(type)); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(data: types, success: true); + } + + Future> createReverseDns({ + required final int serverId, + required final String ip4, + required final String dnsPtr, + }) async { + final Dio client = await getClient(); + try { + await client.post( + '/servers/$serverId/actions/change_dns_ptr', + data: { + 'ip': ip4, + 'dns_ptr': dnsPtr, + }, + ); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return GenericResult(success: true, data: null); + } +} diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart deleted file mode 100644 index 5f8fcab5..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; - -class HetznerApiFactory extends ServerProviderApiFactory - with VolumeProviderApiFactory { - HetznerApiFactory({this.region}); - - final String? region; - - @override - ServerProviderApi getServerProvider({ - final ServerProviderApiSettings settings = - const ServerProviderApiSettings(), - }) => - HetznerApi( - region: settings.region ?? region, - hasLogger: settings.hasLogger, - isWithToken: settings.isWithToken, - ); - - @override - VolumeProviderApi getVolumeProvider({ - final ServerProviderApiSettings settings = - const ServerProviderApiSettings(), - }) => - HetznerApi( - region: settings.region ?? region, - hasLogger: settings.hasLogger, - isWithToken: settings.isWithToken, - ); -} 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 deleted file mode 100644 index ae7911d0..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/logic/models/server_basic_info.dart'; -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'; - -export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; - -class ProviderApiTokenValidation { - ProviderApiTokenValidation({ - required this.length, - required this.regexp, - }); - final int length; - final RegExp regexp; -} - -abstract class ServerProviderApi extends ApiMap { - Future> getServers(); - Future>> - getAvailableLocations(); - Future>> getServerTypesByLocation({ - required final ServerProviderLocation location, - }); - - Future restart(); - Future powerOn(); - - Future> deleteServer({ - required final String domainName, - }); - Future> createServer({ - required final String dnsApiToken, - required final User rootUser, - required final String domainName, - required final String serverType, - required final DnsProvider dnsProvider, - }); - Future> createReverseDns({ - required final ServerHostingDetails serverDetails, - required final ServerDomain domain, - }); - - Future> isApiTokenValid(final String token); - ProviderApiTokenValidation getApiTokenValidation(); - Future> getMetadata(final int serverId); - Future getMetrics( - final int serverId, - final DateTime start, - final DateTime end, - ); - - String dnsProviderToInfectName(final DnsProvider dnsProvider) { - String dnsProviderType; - switch (dnsProvider) { - case DnsProvider.desec: - dnsProviderType = 'DESEC'; - break; - case DnsProvider.cloudflare: - default: - dnsProviderType = 'CLOUDFLARE'; - break; - } - return dnsProviderType; - } - - /// Provider name key which lets infect understand what kind of installation - /// it requires, for example 'digitaloceal' for Digital Ocean - String get infectProviderName; - - /// Actual provider name to render on information page for user, - /// for example 'Digital Ocean' for Digital Ocean - String get displayProviderName; -} diff --git a/lib/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart b/lib/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart deleted file mode 100644 index 3931b45b..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart'; - -class ServerProviderApiSettings extends ProviderApiSettings { - const ServerProviderApiSettings({ - this.region, - super.hasLogger = false, - super.isWithToken = true, - }); - - final String? region; -} diff --git a/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart b/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart deleted file mode 100644 index dbbb8035..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; - -abstract class ServerProviderApiFactory { - ServerProviderApi getServerProvider({ - final ServerProviderApiSettings settings, - }); -} - -mixin VolumeProviderApiFactory { - VolumeProviderApi getVolumeProvider({ - final ServerProviderApiSettings settings, - }); -} diff --git a/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart b/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart deleted file mode 100644 index 5e01d268..00000000 --- a/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/disk_size.dart'; -import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/price.dart'; - -export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; - -mixin VolumeProviderApi on ApiMap { - Future> createVolume(); - Future> getVolumes({final String? status}); - Future> attachVolume( - final ServerVolume volume, - final int serverId, - ); - Future detachVolume(final ServerVolume volume); - Future resizeVolume(final ServerVolume volume, final DiskSize size); - Future deleteVolume(final ServerVolume volume); - Future getPricePerGb(); -} diff --git a/lib/logic/api_maps/staging_options.dart b/lib/logic/api_maps/tls_options.dart similarity index 89% rename from lib/logic/api_maps/staging_options.dart rename to lib/logic/api_maps/tls_options.dart index a4e98fe8..b216841c 100644 --- a/lib/logic/api_maps/staging_options.dart +++ b/lib/logic/api_maps/tls_options.dart @@ -1,11 +1,11 @@ /// Controls staging environment for network -class StagingOptions { +class TlsOptions { /// Whether we request for staging temprorary certificates. /// Hardcode to 'true' in the middle of testing to not /// get your domain banned by constant certificate renewal /// /// If set to 'true', the 'verifyCertificate' becomes useless - static bool get stagingAcme => false; + static bool stagingAcme = false; /// Should we consider CERTIFICATE_VERIFY_FAILED code an error /// For now it's just a global variable and DNS API diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index d76e3651..5debf20e 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -35,7 +35,7 @@ class ApiDevicesCubit } Future?> _getApiTokens() async { - final APIGenericResult> response = await api.getApiTokens(); + final GenericResult> response = await api.getApiTokens(); if (response.success) { return response.data; } else { @@ -44,8 +44,7 @@ class ApiDevicesCubit } Future deleteDevice(final ApiToken device) async { - final APIGenericResult response = - await api.deleteApiToken(device.name); + final GenericResult response = await api.deleteApiToken(device.name); if (response.success) { emit( ApiDevicesState( @@ -60,7 +59,7 @@ class ApiDevicesCubit } Future getNewDeviceKey() async { - final APIGenericResult response = await api.createDeviceToken(); + final GenericResult response = await api.createDeviceToken(); if (response.success) { return response.data; } else { diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 472ed954..3fc2d199 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,11 +1,11 @@ import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; import 'package:selfprivacy/utils/network_utils.dart'; part 'dns_records_state.dart'; @@ -25,14 +25,13 @@ class DnsRecordsCubit emit( DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, - dnsRecords: ApiController.currentDnsProviderApiFactory - ?.getDnsProvider() - .getDesiredDnsRecords( + dnsRecords: + ProvidersController.currentDnsProvider?.getDesiredDnsRecords( serverInstallationCubit.state.serverDomain?.domainName, '', '', ) ?? - [], + [], ), ); @@ -45,13 +44,12 @@ class DnsRecordsCubit return; } - final foundRecords = await ApiController.currentDnsProviderApiFactory! - .getDnsProvider() - .validateDnsRecords( - domain!, - ipAddress!, - extractDkimRecord(await api.getDnsRecords())?.content ?? '', - ); + final foundRecords = + await ProvidersController.currentDnsProvider!.validateDnsRecords( + domain!, + ipAddress!, + extractDkimRecord(await api.getDnsRecords())?.content ?? '', + ); if (!foundRecords.success || foundRecords.data.isEmpty) { emit(const DnsRecordsState()); @@ -89,10 +87,10 @@ class DnsRecordsCubit emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; - final DnsProviderApi dnsProviderApi = - ApiController.currentDnsProviderApiFactory!.getDnsProvider(); - await dnsProviderApi.removeSimilarRecords(domain: domain!); - await dnsProviderApi.createMultipleDnsRecords( + await ProvidersController.currentDnsProvider!.removeDomainRecords( + domain: domain!, + ); + await ProvidersController.currentDnsProvider!.createDomainRecords( domain: domain, ip4: ipAddress, ); @@ -100,7 +98,10 @@ class DnsRecordsCubit final List records = await api.getDnsRecords(); final DnsRecord? dkimRecord = extractDkimRecord(records); if (dkimRecord != null) { - await dnsProviderApi.setDnsRecord(dkimRecord, domain); + await ProvidersController.currentDnsProvider!.setDnsRecord( + dkimRecord, + domain, + ); } await load(); diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index 21d17a84..af20a8aa 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -40,7 +40,7 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr asyncValidation() async { - late APIGenericResult backblazeResponse; + late GenericResult backblazeResponse; final BackblazeApi apiClient = BackblazeApi(isWithToken: false); try { @@ -51,7 +51,7 @@ class BackblazeFormCubit extends FormCubit { backblazeResponse = await apiClient.isApiTokenValid(encodedApiKey); } catch (e) { addError(e); - backblazeResponse = APIGenericResult( + backblazeResponse = GenericResult( success: false, data: false, message: e.toString(), diff --git a/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart index edb97de6..084adb83 100644 --- a/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart @@ -41,7 +41,7 @@ class DnsProviderFormCubit extends FormCubit { } if (!isKeyValid) { - apiKey.setError('initializing.cloudflare_bad_key_error'.tr()); + apiKey.setError('initializing.dns_provider_bad_key_error'.tr()); } return isKeyValid; diff --git a/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart b/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart index 62fc1050..8c66deb7 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart @@ -1,7 +1,8 @@ import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; class DomainSetupCubit extends Cubit { DomainSetupCubit(this.serverInstallationCubit) : super(Initial()); @@ -10,36 +11,32 @@ class DomainSetupCubit extends Cubit { Future load() async { emit(Loading(LoadingTypes.loadingDomain)); - final List list = await ApiController.currentDnsProviderApiFactory! - .getDnsProvider() - .domainList(); - if (list.isEmpty) { + final GenericResult> result = + await ProvidersController.currentDnsProvider!.domainList(); + if (!result.success || result.data.isEmpty) { emit(Empty()); - } else if (list.length == 1) { - emit(Loaded(list.first)); + } else if (result.data.length == 1) { + emit(Loaded(result.data.first)); } else { emit(MoreThenOne()); } } - @override - Future close() => super.close(); - Future saveDomain() async { assert(state is Loaded, 'wrong state'); final String domainName = (state as Loaded).domain; emit(Loading(LoadingTypes.saving)); - final String? zoneId = await ApiController.currentDnsProviderApiFactory! - .getDnsProvider() - .getZoneId(domainName); + final dnsProvider = ProvidersController.currentDnsProvider!; + final GenericResult zoneIdResult = + await dnsProvider.getZoneId(domainName); - if (zoneId != null) { + if (zoneIdResult.success || zoneIdResult.data != null) { final ServerDomain domain = ServerDomain( domainName: domainName, - zoneId: zoneId, - provider: DnsProvider.cloudflare, + zoneId: zoneIdResult.data!, + provider: dnsProvider.type, ); serverInstallationCubit.setDomain(domain); diff --git a/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart similarity index 80% rename from lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart index ebabb5e7..5df3e31a 100644 --- a/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart @@ -4,15 +4,12 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -class ProviderFormCubit extends FormCubit { - ProviderFormCubit(this.serverInstallationCubit) { - //final int tokenLength = - // serverInstallationCubit.serverProviderApiTokenValidation().length; +class ServerProviderFormCubit extends FormCubit { + ServerProviderFormCubit(this.serverInstallationCubit) { apiKey = FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), - //LengthStringNotEqualValidation(tokenLength), ], ); diff --git a/lib/logic/cubit/metrics/metrics_repository.dart b/lib/logic/cubit/metrics/metrics_repository.dart index 71c298bf..0c6a82ef 100644 --- a/lib/logic/cubit/metrics/metrics_repository.dart +++ b/lib/logic/cubit/metrics/metrics_repository.dart @@ -1,9 +1,8 @@ import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; class MetricsLoadException implements Exception { MetricsLoadException(this.message); @@ -12,8 +11,7 @@ class MetricsLoadException implements Exception { class MetricsRepository { Future getMetrics(final Period period) async { - final providerApiFactory = ApiController.currentServerProviderApiFactory; - if (providerApiFactory == null) { + if (ProvidersController.currentServerProvider == null) { throw MetricsLoadException('Server Provider data is null'); } @@ -33,20 +31,19 @@ class MetricsRepository { } final serverId = getIt().serverDetails!.id; - final ServerMetrics? metrics = - await providerApiFactory.getServerProvider().getMetrics( - serverId, - start, - end, - ); + final result = await ProvidersController.currentServerProvider!.getMetrics( + serverId, + start, + end, + ); - if (metrics == null) { + if (result.data == null || !result.success) { throw MetricsLoadException('Metrics data is null'); } return MetricsLoaded( period: period, - metrics: metrics, + metrics: result.data!, ); } } diff --git a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart index ff8eb797..46137c59 100644 --- a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart +++ b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart @@ -1,13 +1,15 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/disk_status.dart'; import 'package:selfprivacy/logic/models/price.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; part 'provider_volume_state.dart'; @@ -20,50 +22,50 @@ class ApiProviderVolumeCubit @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - _refetch(); + unawaited(_refetch()); } } Future getPricePerGb() async => - ApiController.currentVolumeProviderApiFactory! - .getVolumeProvider() - .getPricePerGb(); + (await ProvidersController.currentServerProvider!.getPricePerGb()).data; Future refresh() async { emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); - _refetch(); + unawaited(_refetch()); } Future _refetch() async { - if (ApiController.currentVolumeProviderApiFactory == null) { + if (ProvidersController.currentServerProvider == null) { return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); } - final List volumes = await ApiController - .currentVolumeProviderApiFactory! - .getVolumeProvider() - .getVolumes(); + final volumesResult = + await ProvidersController.currentServerProvider!.getVolumes(); - if (volumes.isEmpty) { + if (!volumesResult.success || volumesResult.data.isEmpty) { return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); } - emit(ApiProviderVolumeState(volumes, LoadingStatus.success, false)); + emit( + ApiProviderVolumeState( + volumesResult.data, + LoadingStatus.success, + false, + ), + ); } Future attachVolume(final DiskVolume volume) async { final ServerHostingDetails server = getIt().serverDetails!; - await ApiController.currentVolumeProviderApiFactory! - .getVolumeProvider() + await ProvidersController.currentServerProvider! .attachVolume(volume.providerVolume!, server.id); - refresh(); + unawaited(refresh()); } Future detachVolume(final DiskVolume volume) async { - await ApiController.currentVolumeProviderApiFactory! - .getVolumeProvider() + await ProvidersController.currentServerProvider! .detachVolume(volume.providerVolume!); - refresh(); + unawaited(refresh()); } Future resizeVolume( @@ -75,14 +77,13 @@ class ApiProviderVolumeCubit 'Starting resize', ); emit(state.copyWith(isResizing: true)); - final bool resized = await ApiController.currentVolumeProviderApiFactory! - .getVolumeProvider() - .resizeVolume( - volume.providerVolume!, - newSize, - ); + final resizedResult = + await ProvidersController.currentServerProvider!.resizeVolume( + volume.providerVolume!, + newSize, + ); - if (!resized) { + if (!resizedResult.success || !resizedResult.data) { getIt().showSnackBar( 'storage.extending_volume_error'.tr(), ); @@ -113,11 +114,8 @@ class ApiProviderVolumeCubit } Future createVolume() async { - final ServerVolume? volume = (await ApiController - .currentVolumeProviderApiFactory! - .getVolumeProvider() - .createVolume()) - .data; + final ServerVolume? volume = + (await ProvidersController.currentServerProvider!.createVolume()).data; final diskVolume = DiskVolume(providerVolume: volume); await attachVolume(diskVolume); @@ -125,14 +123,13 @@ class ApiProviderVolumeCubit await Future.delayed(const Duration(seconds: 10)); await ServerApi().mountVolume(volume!.name); - refresh(); + unawaited(refresh()); } Future deleteVolume(final DiskVolume volume) async { - await ApiController.currentVolumeProviderApiFactory! - .getVolumeProvider() + await ProvidersController.currentServerProvider! .deleteVolume(volume.providerVolume!); - refresh(); + unawaited(refresh()); } @override diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 5ca2bfa9..c5f68d57 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; @@ -32,7 +34,7 @@ class RecoveryKeyCubit } Future _getRecoveryKeyStatus() async { - final APIGenericResult response = + final GenericResult response = await api.getRecoveryTokenStatus(); if (response.success) { return response.data; @@ -57,10 +59,10 @@ class RecoveryKeyCubit final DateTime? expirationDate, final int? numberOfUses, }) async { - final APIGenericResult response = + final GenericResult response = await api.generateRecoveryToken(expirationDate, numberOfUses); if (response.success) { - refresh(); + unawaited(refresh()); return response.data; } else { throw GenerationError(response.message ?? 'Unknown error'); diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index ca6848bc..3bad75eb 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -1,23 +1,22 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; class ServerDetailsRepository { ServerApi server = ServerApi(); Future load() async { - final serverProviderApi = ApiController.currentServerProviderApiFactory; + final serverProviderApi = ProvidersController.currentServerProvider; final settings = await server.getSystemSettings(); final serverId = getIt().serverDetails!.id; - final metadata = - await serverProviderApi!.getServerProvider().getMetadata(serverId); + final metadata = await serverProviderApi?.getMetadata(serverId); return ServerDetailsRepositoryDto( autoUpgradeSettings: settings.autoUpgradeSettings, - metadata: metadata, + metadata: metadata!.data, serverTimezone: TimeZoneSettings.fromString( settings.timezone, ), diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 47952c9a..7730c5a3 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -5,12 +5,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart'; -import 'package:selfprivacy/logic/api_maps/staging_options.dart'; +import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; +import 'package:selfprivacy/logic/models/launch_installation_data.dart'; +import 'package:selfprivacy/logic/providers/provider_settings.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; +import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; @@ -20,6 +19,7 @@ import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; +import 'package:selfprivacy/ui/helpers/modals.dart'; export 'package:provider/provider.dart'; @@ -58,45 +58,29 @@ class ServerInstallationCubit extends Cubit { } } - void setServerProviderType(final ServerProvider providerType) async { + void setServerProviderType(final ServerProviderType providerType) async { await repository.saveServerProviderType(providerType); - ApiController.initServerProviderApiFactory( - ServerProviderApiFactorySettings( - provider: providerType, - ), + ProvidersController.initServerProvider( + ServerProviderSettings(provider: providerType), ); } - void setDnsProviderType(final DnsProvider providerType) async { + void setDnsProviderType(final DnsProviderType providerType) async { await repository.saveDnsProviderType(providerType); - ApiController.initDnsProviderApiFactory( - DnsProviderApiFactorySettings( + ProvidersController.initDnsProvider( + DnsProviderSettings( provider: providerType, ), ); } - ProviderApiTokenValidation serverProviderApiTokenValidation() => - ApiController.currentServerProviderApiFactory! - .getServerProvider() - .getApiTokenValidation(); - - RegExp getDnsProviderApiTokenValidation() => - ApiController.currentDnsProviderApiFactory! - .getDnsProvider() - .getApiTokenValidation(); - Future isServerProviderApiTokenValid( final String providerToken, ) async { - final APIGenericResult apiResponse = - await ApiController.currentServerProviderApiFactory! - .getServerProvider( - settings: const ServerProviderApiSettings( - isWithToken: false, - ), - ) - .isApiTokenValid(providerToken); + final GenericResult apiResponse = + await ProvidersController.currentServerProvider!.tryInitApiByToken( + providerToken, + ); if (!apiResponse.success) { getIt().showSnackBar( @@ -111,12 +95,10 @@ class ServerInstallationCubit extends Cubit { Future isDnsProviderApiTokenValid( final String providerToken, ) async { - final APIGenericResult apiResponse = - await ApiController.currentDnsProviderApiFactory! - .getDnsProvider( - settings: const DnsProviderApiSettings(isWithToken: false), - ) - .isApiTokenValid(providerToken); + final GenericResult apiResponse = + await ProvidersController.currentDnsProvider!.tryInitApiByToken( + providerToken, + ); if (!apiResponse.success) { getIt().showSnackBar( @@ -129,35 +111,33 @@ class ServerInstallationCubit extends Cubit { } Future> fetchAvailableLocations() async { - if (ApiController.currentServerProviderApiFactory == null) { + if (ProvidersController.currentServerProvider == null) { return []; } - final APIGenericResult apiResult = await ApiController - .currentServerProviderApiFactory! - .getServerProvider() + final GenericResult apiResponse = await ProvidersController + .currentServerProvider! .getAvailableLocations(); - if (!apiResult.success) { + if (!apiResponse.success) { getIt().showSnackBar( 'initializing.could_not_connect'.tr(), ); } - return apiResult.data; + return apiResponse.data; } Future> fetchAvailableTypesByLocation( final ServerProviderLocation location, ) async { - if (ApiController.currentServerProviderApiFactory == null) { + if (ProvidersController.currentServerProvider == null) { return []; } - final APIGenericResult apiResult = await ApiController - .currentServerProviderApiFactory! - .getServerProvider() - .getServerTypesByLocation(location: location); + final GenericResult apiResult = await ProvidersController + .currentServerProvider! + .getServerTypes(location: location); if (!apiResult.success) { getIt().showSnackBar( @@ -191,21 +171,8 @@ class ServerInstallationCubit extends Cubit { void setServerType(final ServerType serverType) async { await repository.saveServerType(serverType); - ApiController.initServerProviderApiFactory( - ServerProviderApiFactorySettings( - provider: getIt().serverProvider!, - location: serverType.location.identifier, - ), - ); - - // All server providers support volumes for now, - // so it's safe to initialize. - ApiController.initVolumeProviderApiFactory( - ServerProviderApiFactorySettings( - provider: getIt().serverProvider!, - location: serverType.location.identifier, - ), - ); + await ProvidersController.currentServerProvider! + .trySetServerLocation(serverType.location.identifier); emit( (state as ServerInstallationNotFinished).copyWith( @@ -216,10 +183,10 @@ class ServerInstallationCubit extends Cubit { void setDnsApiToken(final String dnsApiToken) async { if (state is ServerInstallationRecovery) { - setAndValidateDnsApiToken(dnsApiToken); + await setAndValidateDnsApiToken(dnsApiToken); return; } - await repository.saveDnsProviderKey(dnsApiToken); + await repository.setDnsApiToken(dnsApiToken); emit( (state as ServerInstallationNotFinished) @@ -256,41 +223,53 @@ class ServerInstallationCubit extends Cubit { emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser)); } + Future onCreateServerSuccess( + final ServerHostingDetails serverDetails, + ) async { + await repository.saveServerDetails(serverDetails); + await ProvidersController.currentDnsProvider!.removeDomainRecords( + ip4: serverDetails.ip4, + domain: state.serverDomain!, + ); + await ProvidersController.currentDnsProvider!.createDomainRecords( + ip4: serverDetails.ip4, + domain: state.serverDomain!, + ); + + emit( + (state as ServerInstallationNotFinished).copyWith( + isLoading: false, + serverDetails: serverDetails, + installationDialoguePopUp: null, + ), + ); + runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null); + } + void createServerAndSetDnsRecords() async { - final ServerInstallationNotFinished stateCopy = - state as ServerInstallationNotFinished; - void onCancel() => emit( - (state as ServerInstallationNotFinished).copyWith(isLoading: false), - ); + emit((state as ServerInstallationNotFinished).copyWith(isLoading: true)); - Future onSuccess(final ServerHostingDetails serverDetails) async { - await repository.createDnsRecords( - serverDetails, - state.serverDomain!, - onCancel: onCancel, - ); + final installationData = LaunchInstallationData( + rootUser: state.rootUser!, + dnsApiToken: state.dnsApiToken!, + dnsProviderType: state.serverDomain!.provider, + serverDomain: state.serverDomain!, + serverTypeId: state.serverTypeIdentificator!, + errorCallback: clearAppConfig, + successCallback: onCreateServerSuccess, + ); + final result = + await ProvidersController.currentServerProvider!.launchInstallation( + installationData, + ); + + if (!result.success && result.data != null) { emit( (state as ServerInstallationNotFinished).copyWith( - isLoading: false, - serverDetails: serverDetails, + installationDialoguePopUp: result.data, ), ); - runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null); - } - - try { - emit((state as ServerInstallationNotFinished).copyWith(isLoading: true)); - await repository.createServer( - state.rootUser!, - state.serverDomain!.domainName, - state.dnsApiToken!, - state.backblazeCredential!, - onCancel: onCancel, - onSuccess: onSuccess, - ); - } catch (e) { - emit(stateCopy); } } @@ -437,7 +416,7 @@ class ServerInstallationCubit extends Cubit { emit(TimerState(dataState: dataState, isLoading: true)); final bool isServerWorking = await repository.isHttpServerWorking(); - StagingOptions.verifyCertificate = true; + TlsOptions.verifyCertificate = true; if (isServerWorking) { bool dkimCreated = true; @@ -487,7 +466,7 @@ class ServerInstallationCubit extends Cubit { void submitDomainForAccessRecovery(final String domain) async { final ServerDomain serverDomain = ServerDomain( domainName: domain, - provider: DnsProvider.unknown, + provider: DnsProviderType.unknown, zoneId: '', ); final ServerRecoveryCapabilities recoveryCapabilities = @@ -539,7 +518,7 @@ class ServerInstallationCubit extends Cubit { token, dataState.recoveryCapabilities, ); - final ServerProvider provider = await ServerApi( + final ServerProviderType serverProvider = await ServerApi( customToken: serverDetails.apiToken, isWithToken: true, ).getServerProviderType(); @@ -547,15 +526,15 @@ class ServerInstallationCubit extends Cubit { customToken: serverDetails.apiToken, isWithToken: true, ).getDnsProviderType(); - if (provider == ServerProvider.unknown || - dnsProvider == DnsProvider.unknown) { + if (serverProvider == ServerProviderType.unknown || + dnsProvider == DnsProviderType.unknown) { getIt() .showSnackBar('recovering.generic_error'.tr()); return; } await repository.saveServerDetails(serverDetails); await repository.saveDnsProviderType(dnsProvider); - setServerProviderType(provider); + setServerProviderType(serverProvider); setDnsProviderType(dnsProvider); emit( dataState.copyWith( @@ -683,7 +662,7 @@ class ServerInstallationCubit extends Cubit { linuxDevice: '', ), apiToken: dataState.serverDetails!.apiToken, - provider: ServerProvider.hetzner, + provider: ServerProviderType.hetzner, ); await repository.saveDomain(serverDomain); await repository.saveServerDetails(serverDetails); @@ -720,13 +699,13 @@ class ServerInstallationCubit extends Cubit { provider: dnsProviderType, ), ); - await repository.saveDnsProviderKey(token); + await repository.setDnsApiToken(token); emit( dataState.copyWith( serverDomain: ServerDomain( domainName: serverDomain.domainName, zoneId: zoneId, - provider: DnsProvider.cloudflare, + provider: dnsProviderType, ), dnsApiToken: token, currentStep: RecoveryStep.backblazeToken, @@ -754,13 +733,44 @@ class ServerInstallationCubit extends Cubit { @override void onChange(final Change change) { + if (change.nextState.installationDialoguePopUp != null && + change.currentState.installationDialoguePopUp != + change.nextState.installationDialoguePopUp) { + final branching = change.nextState.installationDialoguePopUp; + showPopUpAlert( + alertTitle: branching!.title, + description: branching.description, + actionButtonTitle: branching.choices[1].title, + actionButtonOnPressed: () async { + final branchingResult = await branching.choices[1].callback!(); + if (!branchingResult.success) { + emit( + (state as ServerInstallationNotFinished).copyWith( + installationDialoguePopUp: branchingResult.data, + ), + ); + } + }, + cancelButtonTitle: branching.choices[0].title, + cancelButtonOnPressed: () async { + final branchingResult = await branching.choices[0].callback!(); + if (!branchingResult.success) { + emit( + (state as ServerInstallationNotFinished).copyWith( + installationDialoguePopUp: branchingResult.data, + ), + ); + } + }, + ); + } super.onChange(change); } void clearAppConfig() { closeTimer(); - ApiController.clearProviderApiFactories(); - StagingOptions.verifyCertificate = false; + ProvidersController.clearProviders(); + TlsOptions.verifyCertificate = false; repository.clearAppConfig(); emit(const ServerInstallationEmpty()); } diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index c138a340..7c45bc20 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -1,30 +1,26 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/logic/providers/provider_settings.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; -import 'package:selfprivacy/logic/api_maps/staging_options.dart'; +import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; -import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; -import 'package:selfprivacy/ui/helpers/modals.dart'; +import 'package:selfprivacy/logic/providers/providers_controller.dart'; import 'package:selfprivacy/utils/network_utils.dart'; class IpNotFoundException implements Exception { @@ -47,49 +43,39 @@ class ServerInstallationRepository { final String? dnsApiToken = getIt().dnsProviderKey; final String? serverTypeIdentificator = getIt().serverType; final ServerDomain? serverDomain = getIt().serverDomain; - final ServerProvider? serverProvider = + final DnsProviderType? dnsProvider = getIt().dnsProvider; + final ServerProviderType? serverProvider = getIt().serverProvider; final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; final ServerHostingDetails? serverDetails = getIt().serverDetails; - final DnsProvider? dnsProvider = getIt().dnsProvider; if (serverProvider != null || (serverDetails != null && - serverDetails.provider != ServerProvider.unknown)) { - ApiController.initServerProviderApiFactory( - ServerProviderApiFactorySettings( - provider: serverProvider ?? serverDetails!.provider, - location: location, - ), - ); - - // All current providers support volumes - // so it's safe to hardcode for now - ApiController.initVolumeProviderApiFactory( - ServerProviderApiFactorySettings( + serverDetails.provider != ServerProviderType.unknown)) { + ProvidersController.initServerProvider( + ServerProviderSettings( provider: serverProvider ?? serverDetails!.provider, location: location, ), ); } - if (ApiController.currentDnsProviderApiFactory == null) { - if (dnsProvider != null || - (serverDomain != null && - serverDomain.provider != DnsProvider.unknown)) { - ApiController.initDnsProviderApiFactory( - DnsProviderApiFactorySettings( - provider: dnsProvider ?? serverDomain!.provider, - ), - ); - } + if (dnsProvider != null || + (serverDomain != null && + serverDomain.provider != DnsProviderType.unknown)) { + ProvidersController.initDnsProvider( + DnsProviderSettings( + provider: dnsProvider ?? serverDomain!.provider, + ), + ); } if (box.get(BNames.hasFinalChecked, defaultValue: false)) { - StagingOptions.verifyCertificate = true; + TlsOptions.verifyCertificate = true; return ServerInstallationFinished( + installationDialoguePopUp: null, providerApiToken: providerApiToken!, serverTypeIdentificator: serverTypeIdentificator ?? '', dnsApiToken: dnsApiToken!, @@ -149,8 +135,8 @@ class ServerInstallationRepository { ) { if (serverDetails != null) { if (serverProviderToken != null) { - if (serverDetails.provider != ServerProvider.unknown) { - if (serverDomain.provider != DnsProvider.unknown) { + if (serverDetails.provider != ServerProviderType.unknown) { + if (serverDomain.provider != DnsProviderType.unknown) { return RecoveryStep.backblazeToken; } return RecoveryStep.dnsProviderToken; @@ -170,35 +156,26 @@ class ServerInstallationRepository { Future startServer( final ServerHostingDetails server, ) async { - ServerHostingDetails serverDetails; + final result = await ProvidersController.currentServerProvider!.powerOn( + server.id, + ); - serverDetails = await ApiController.currentServerProviderApiFactory! - .getServerProvider() - .powerOn(); + if (result.success && result.data != null) { + server.copyWith(startTime: result.data); + } - return serverDetails; + return server; } Future getDomainId(final String token, final String domain) async { - final DnsProviderApi dnsProviderApi = - ApiController.currentDnsProviderApiFactory!.getDnsProvider( - settings: DnsProviderApiSettings( - isWithToken: false, - customToken: token, - ), - ); - - /// TODO: nvm it's because only Cloudflare uses Zone - /// for other providers we need to implement a different kind of - /// functionality here... but it's on refactoring, let it be here for now. - final APIGenericResult apiResponse = - await dnsProviderApi.isApiTokenValid(token); - - String? domainId; - if (apiResponse.success && apiResponse.data) { - domainId = await dnsProviderApi.getZoneId(domain); - } - return domainId; + final result = + await ProvidersController.currentDnsProvider!.tryInitApiByToken(token); + return result.success + ? (await ProvidersController.currentDnsProvider!.getZoneId( + domain, + )) + .data + : null; } Future> isDnsAddressesMatch( @@ -224,181 +201,7 @@ class ServerInstallationRepository { return matches; } - Future createServer( - final User rootUser, - final String domainName, - final String dnsApiToken, - final BackblazeCredential backblazeCredential, { - required final void Function() onCancel, - required final Future Function(ServerHostingDetails serverDetails) - onSuccess, - }) async { - final ServerProviderApi api = - ApiController.currentServerProviderApiFactory!.getServerProvider(); - - void showInstallationErrorPopUp() { - showPopUpAlert( - alertTitle: 'modals.unexpected_error'.tr(), - description: 'modals.try_again'.tr(), - actionButtonTitle: 'modals.yes'.tr(), - actionButtonOnPressed: () async { - ServerHostingDetails? serverDetails; - try { - final APIGenericResult createResult = await api.createServer( - dnsProvider: getIt().dnsProvider!, - dnsApiToken: dnsApiToken, - rootUser: rootUser, - domainName: domainName, - serverType: getIt().serverType!, - ); - serverDetails = createResult.data; - } catch (e) { - print(e); - } - - if (serverDetails == null) { - print('Server is not initialized!'); - return; - } - await saveServerDetails(serverDetails); - onSuccess(serverDetails); - }, - cancelButtonOnPressed: onCancel, - ); - } - - try { - final APIGenericResult createServerResult = - await api.createServer( - dnsProvider: getIt().dnsProvider!, - dnsApiToken: dnsApiToken, - rootUser: rootUser, - domainName: domainName, - serverType: getIt().serverType!, - ); - - if (createServerResult.data == null) { - const String e = 'Server is not initialized!'; - print(e); - } - - if (createServerResult.message == 'uniqueness_error') { - showPopUpAlert( - alertTitle: 'modals.already_exists'.tr(), - description: 'modals.destroy_server'.tr(), - actionButtonTitle: 'modals.yes'.tr(), - actionButtonOnPressed: () async { - await api.deleteServer( - domainName: domainName, - ); - - ServerHostingDetails? serverDetails; - try { - final APIGenericResult createResult = await api.createServer( - dnsProvider: getIt().dnsProvider!, - dnsApiToken: dnsApiToken, - rootUser: rootUser, - domainName: domainName, - serverType: getIt().serverType!, - ); - serverDetails = createResult.data; - } catch (e) { - print(e); - } - - if (serverDetails == null) { - print('Server is not initialized!'); - return; - } - await saveServerDetails(serverDetails); - onSuccess(serverDetails); - }, - cancelButtonOnPressed: onCancel, - ); - return; - } - - saveServerDetails(createServerResult.data!); - onSuccess(createServerResult.data!); - } catch (e) { - print(e); - showInstallationErrorPopUp(); - } - } - - Future createDnsRecords( - final ServerHostingDetails serverDetails, - final ServerDomain domain, { - required final void Function() onCancel, - }) async { - final DnsProviderApi dnsProviderApi = - ApiController.currentDnsProviderApiFactory!.getDnsProvider(); - final ServerProviderApi serverApi = - ApiController.currentServerProviderApiFactory!.getServerProvider(); - - void showDomainErrorPopUp(final String error) { - showPopUpAlert( - alertTitle: error, - description: 'modals.delete_server_volume'.tr(), - cancelButtonOnPressed: onCancel, - actionButtonTitle: 'basis.delete'.tr(), - actionButtonOnPressed: () async { - await serverApi.deleteServer( - domainName: domain.domainName, - ); - onCancel(); - }, - ); - } - - final APIGenericResult removingResult = - await dnsProviderApi.removeSimilarRecords( - ip4: serverDetails.ip4, - domain: domain, - ); - - if (!removingResult.success) { - showDomainErrorPopUp('domain.error'.tr()); - return false; - } - - bool createdSuccessfully = false; - String errorMessage = 'domain.error'.tr(); - try { - final APIGenericResult createResult = - await dnsProviderApi.createMultipleDnsRecords( - ip4: serverDetails.ip4, - domain: domain, - ); - createdSuccessfully = createResult.success; - } on DioError catch (e) { - if (e.response!.data['errors'][0]['code'] == 1038) { - errorMessage = 'modals.you_cant_use_this_api'.tr(); - } - } - - if (!createdSuccessfully) { - showDomainErrorPopUp(errorMessage); - return false; - } - - final APIGenericResult createReverseResult = - await serverApi.createReverseDns( - serverDetails: serverDetails, - domain: domain, - ); - - if (!createReverseResult.success) { - showDomainErrorPopUp(errorMessage); - return false; - } - - return true; - } - Future createDkimRecord(final ServerDomain cloudFlareDomain) async { - final DnsProviderApi dnsProviderApi = - ApiController.currentDnsProviderApiFactory!.getDnsProvider(); final ServerApi api = ServerApi(); late DnsRecord record; @@ -409,7 +212,10 @@ class ServerInstallationRepository { rethrow; } - await dnsProviderApi.setDnsRecord(record, cloudFlareDomain); + await ProvidersController.currentDnsProvider!.setDnsRecord( + record, + cloudFlareDomain, + ); } Future isHttpServerWorking() async { @@ -417,15 +223,24 @@ class ServerInstallationRepository { return api.isHttpServerWorking(); } - Future restart() async => - ApiController.currentServerProviderApiFactory! - .getServerProvider() - .restart(); + Future restart() async { + final server = getIt().serverDetails!; - Future powerOn() async => - ApiController.currentServerProviderApiFactory! - .getServerProvider() - .powerOn(); + final result = await ProvidersController.currentServerProvider!.restart( + server.id, + ); + + if (result.success && result.data != null) { + server.copyWith(startTime: result.data); + } + + return server; + } + + Future powerOn() async { + final server = getIt().serverDetails!; + return startServer(server); + } Future getRecoveryCapabilities( final ServerDomain serverDomain, @@ -508,7 +323,7 @@ class ServerInstallationRepository { overrideDomain: serverDomain.domainName, ); final String serverIp = await getServerIpFromDomain(serverDomain); - final APIGenericResult result = await serverApi.authorizeDevice( + final GenericResult result = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: newDeviceKey), ); @@ -522,7 +337,7 @@ class ServerInstallationRepository { serverId: 0, linuxDevice: '', ), - provider: ServerProvider.unknown, + provider: ServerProviderType.unknown, id: 0, ip4: serverIp, startTime: null, @@ -545,7 +360,7 @@ class ServerInstallationRepository { overrideDomain: serverDomain.domainName, ); final String serverIp = await getServerIpFromDomain(serverDomain); - final APIGenericResult result = await serverApi.useRecoveryToken( + final GenericResult result = await serverApi.useRecoveryToken( DeviceToken(device: await getDeviceName(), token: recoveryKey), ); @@ -559,7 +374,7 @@ class ServerInstallationRepository { serverId: 0, linuxDevice: '', ), - provider: ServerProvider.unknown, + provider: ServerProviderType.unknown, id: 0, ip4: serverIp, startTime: null, @@ -594,7 +409,7 @@ class ServerInstallationRepository { sizeByte: 0, linuxDevice: '', ), - provider: ServerProvider.unknown, + provider: ServerProviderType.unknown, id: 0, ip4: serverIp, startTime: null, @@ -606,9 +421,9 @@ class ServerInstallationRepository { ); } } - final APIGenericResult deviceAuthKey = + final GenericResult deviceAuthKey = await serverApi.createDeviceToken(); - final APIGenericResult result = await serverApi.authorizeDevice( + final GenericResult result = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), ); @@ -622,7 +437,7 @@ class ServerInstallationRepository { serverId: 0, linuxDevice: '', ), - provider: ServerProvider.unknown, + provider: ServerProviderType.unknown, id: 0, ip4: serverIp, startTime: null, @@ -664,9 +479,7 @@ class ServerInstallationRepository { } Future> getServersOnProviderAccount() async => - ApiController.currentServerProviderApiFactory! - .getServerProvider() - .getServers(); + (await ProvidersController.currentServerProvider!.getServers()).data; Future saveServerDetails( final ServerHostingDetails serverDetails, @@ -679,10 +492,14 @@ class ServerInstallationRepository { getIt().init(); } - Future saveServerProviderType(final ServerProvider type) async { + Future saveServerProviderType(final ServerProviderType type) async { await getIt().storeServerProviderType(type); } + Future saveDnsProviderType(final DnsProviderType type) async { + await getIt().storeDnsProviderType(type); + } + Future saveServerProviderKey(final String key) async { await getIt().storeServerProviderKey(key); } @@ -701,10 +518,6 @@ class ServerInstallationRepository { getIt().init(); } - Future saveDnsProviderType(final DnsProvider type) async { - await getIt().storeDnsProviderType(type); - } - Future saveBackblazeKey( final BackblazeCredential backblazeCredential, ) async { @@ -716,7 +529,7 @@ class ServerInstallationRepository { getIt().init(); } - Future saveDnsProviderKey(final String key) async { + Future setDnsApiToken(final String key) async { await getIt().storeDnsProviderKey(key); } @@ -759,12 +572,10 @@ class ServerInstallationRepository { } Future deleteServer(final ServerDomain serverDomain) async { - final APIGenericResult deletionResult = await ApiController - .currentServerProviderApiFactory! - .getServerProvider() - .deleteServer( - domainName: serverDomain.domainName, - ); + final deletionResult = + await ProvidersController.currentServerProvider!.deleteServer( + serverDomain.domainName, + ); if (!deletionResult.success) { getIt() @@ -772,12 +583,6 @@ class ServerInstallationRepository { 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); @@ -785,10 +590,9 @@ class ServerInstallationRepository { await box.put(BNames.isLoading, false); await box.put(BNames.serverDetails, null); - final APIGenericResult removalResult = await ApiController - .currentDnsProviderApiFactory! - .getDnsProvider() - .removeSimilarRecords(domain: serverDomain); + final GenericResult removalResult = await ProvidersController + .currentDnsProvider! + .removeDomainRecords(domain: serverDomain); if (!removalResult.success) { getIt().showSnackBar('modals.dns_removal_error'.tr()); diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 5ceaafdd..c6356c36 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -12,6 +12,7 @@ abstract class ServerInstallationState extends Equatable { required this.isServerStarted, required this.isServerResetedFirstTime, required this.isServerResetedSecondTime, + required this.installationDialoguePopUp, }); @override @@ -25,6 +26,7 @@ abstract class ServerInstallationState extends Equatable { serverDetails, isServerStarted, isServerResetedFirstTime, + installationDialoguePopUp ]; final String? providerApiToken; @@ -37,6 +39,7 @@ abstract class ServerInstallationState extends Equatable { final bool isServerStarted; final bool isServerResetedFirstTime; final bool isServerResetedSecondTime; + final CallbackDialogueBranching? installationDialoguePopUp; bool get isServerProviderApiKeyFilled => providerApiToken != null; bool get isServerTypeFilled => serverTypeIdentificator != null; @@ -96,6 +99,7 @@ class TimerState extends ServerInstallationNotFinished { isServerResetedFirstTime: dataState.isServerResetedFirstTime, isServerResetedSecondTime: dataState.isServerResetedSecondTime, dnsMatches: dataState.dnsMatches, + installationDialoguePopUp: dataState.installationDialoguePopUp, ); final ServerInstallationNotFinished dataState; @@ -138,6 +142,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { super.serverDomain, super.rootUser, super.serverDetails, + super.installationDialoguePopUp, }); final bool isLoading; final Map? dnsMatches; @@ -155,6 +160,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { isServerResetedFirstTime, isLoading, dnsMatches, + installationDialoguePopUp, ]; ServerInstallationNotFinished copyWith({ @@ -170,6 +176,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { final bool? isServerResetedSecondTime, final bool? isLoading, final Map? dnsMatches, + final CallbackDialogueBranching? installationDialoguePopUp, }) => ServerInstallationNotFinished( providerApiToken: providerApiToken ?? this.providerApiToken, @@ -187,6 +194,8 @@ class ServerInstallationNotFinished extends ServerInstallationState { isServerResetedSecondTime ?? this.isServerResetedSecondTime, isLoading: isLoading ?? this.isLoading, dnsMatches: dnsMatches ?? this.dnsMatches, + installationDialoguePopUp: + installationDialoguePopUp ?? this.installationDialoguePopUp, ); ServerInstallationFinished finish() => ServerInstallationFinished( @@ -200,6 +209,7 @@ class ServerInstallationNotFinished extends ServerInstallationState { isServerStarted: isServerStarted, isServerResetedFirstTime: isServerResetedFirstTime, isServerResetedSecondTime: isServerResetedSecondTime, + installationDialoguePopUp: installationDialoguePopUp, ); } @@ -218,6 +228,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished { isServerResetedSecondTime: false, isLoading: false, dnsMatches: null, + installationDialoguePopUp: null, ); } @@ -233,6 +244,7 @@ class ServerInstallationFinished extends ServerInstallationState { required super.isServerStarted, required super.isServerResetedFirstTime, required super.isServerResetedSecondTime, + required super.installationDialoguePopUp, }); @override @@ -246,6 +258,7 @@ class ServerInstallationFinished extends ServerInstallationState { serverDetails, isServerStarted, isServerResetedFirstTime, + installationDialoguePopUp, ]; } @@ -287,6 +300,7 @@ class ServerInstallationRecovery extends ServerInstallationState { isServerStarted: true, isServerResetedFirstTime: true, isServerResetedSecondTime: true, + installationDialoguePopUp: null, ); final RecoveryStep currentStep; final ServerRecoveryCapabilities recoveryCapabilities; @@ -302,7 +316,8 @@ class ServerInstallationRecovery extends ServerInstallationState { serverDetails, isServerStarted, isServerResetedFirstTime, - currentStep + currentStep, + installationDialoguePopUp ]; ServerInstallationRecovery copyWith({ @@ -340,5 +355,6 @@ class ServerInstallationRecovery extends ServerInstallationState { isServerStarted: true, isServerResetedFirstTime: true, isServerResetedSecondTime: true, + installationDialoguePopUp: null, ); } diff --git a/lib/logic/cubit/server_volumes/server_volume_cubit.dart b/lib/logic/cubit/server_volumes/server_volume_cubit.dart index c10bc377..bf30c8c0 100644 --- a/lib/logic/cubit/server_volumes/server_volume_cubit.dart +++ b/lib/logic/cubit/server_volumes/server_volume_cubit.dart @@ -24,7 +24,7 @@ class ApiServerVolumeCubit @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - reload(); + unawaited(reload()); } } diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 54e22b3d..60476c2d 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -53,7 +53,7 @@ class ServicesCubit extends ServerInstallationDependendCubit { } await Future.delayed(const Duration(seconds: 2)); - reload(); + unawaited(reload()); await Future.delayed(const Duration(seconds: 10)); emit( state.copyWith( @@ -62,7 +62,7 @@ class ServicesCubit extends ServerInstallationDependendCubit { .toList(), ), ); - reload(); + unawaited(reload()); } Future moveService( diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 001ce8d0..cb717441 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/get_it_config.dart'; @@ -39,7 +41,7 @@ class UsersCubit extends ServerInstallationDependendCubit { ); } - refresh(); + unawaited(refresh()); } Future refresh() async { @@ -78,7 +80,7 @@ class UsersCubit extends ServerInstallationDependendCubit { return; } // If API returned error, do nothing - final APIGenericResult result = + final GenericResult result = await api.createUser(user.login, password); if (result.data == null) { getIt() @@ -101,7 +103,7 @@ class UsersCubit extends ServerInstallationDependendCubit { return; } final List loadedUsers = List.from(state.users); - final APIGenericResult result = await api.deleteUser(user.login); + final GenericResult result = await api.deleteUser(user.login); if (result.success && result.data) { loadedUsers.removeWhere((final User u) => u.login == user.login); await box.clear(); @@ -128,7 +130,7 @@ class UsersCubit extends ServerInstallationDependendCubit { .showSnackBar('users.could_not_change_password'.tr()); return; } - final APIGenericResult result = + final GenericResult result = await api.updateUser(user.login, newPassword); if (result.data == null) { getIt().showSnackBar( @@ -138,7 +140,7 @@ class UsersCubit extends ServerInstallationDependendCubit { } Future addSshKey(final User user, final String publicKey) async { - final APIGenericResult result = + final GenericResult result = await api.addSshKey(user.login, publicKey); if (result.data != null) { final User updatedUser = result.data!; @@ -157,7 +159,7 @@ class UsersCubit extends ServerInstallationDependendCubit { } Future deleteSshKey(final User user, final String publicKey) async { - final APIGenericResult result = + final GenericResult result = await api.removeSshKey(user.login, publicKey); if (result.data != null) { final User updatedUser = result.data!; diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index ba49b9b3..b93105d3 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -13,9 +13,8 @@ class ApiConfigModel { String? get serverLocation => _serverLocation; String? get serverType => _serverType; String? get dnsProviderKey => _dnsProviderKey; - ServerProvider? get serverProvider => _serverProvider; - DnsProvider? get dnsProvider => _dnsProvider; - + ServerProviderType? get serverProvider => _serverProvider; + DnsProviderType? get dnsProvider => _dnsProvider; BackblazeCredential? get backblazeCredential => _backblazeCredential; ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; @@ -24,19 +23,19 @@ class ApiConfigModel { String? _serverLocation; String? _dnsProviderKey; String? _serverType; - ServerProvider? _serverProvider; - DnsProvider? _dnsProvider; + ServerProviderType? _serverProvider; + DnsProviderType? _dnsProvider; ServerHostingDetails? _serverDetails; BackblazeCredential? _backblazeCredential; ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; - Future storeServerProviderType(final ServerProvider value) async { + Future storeServerProviderType(final ServerProviderType value) async { await _box.put(BNames.serverProvider, value); _serverProvider = value; } - Future storeDnsProviderType(final DnsProvider value) async { + Future storeDnsProviderType(final DnsProviderType value) async { await _box.put(BNames.dnsProvider, value); _dnsProvider = value; } diff --git a/lib/logic/models/callback_dialogue_branching.dart b/lib/logic/models/callback_dialogue_branching.dart new file mode 100644 index 00000000..614a7c22 --- /dev/null +++ b/lib/logic/models/callback_dialogue_branching.dart @@ -0,0 +1,21 @@ +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; + +class CallbackDialogueBranching { + CallbackDialogueBranching({ + required this.title, + required this.description, + required this.choices, + }); + final String title; + final String description; + final List choices; +} + +class CallbackDialogueChoice { + CallbackDialogueChoice({ + required this.title, + required this.callback, + }); + final String title; + final Future> Function()? callback; +} diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart index 54ec257f..e746dd75 100644 --- a/lib/logic/models/hive/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -33,8 +33,8 @@ class ServerHostingDetails { @HiveField(5) final String apiToken; - @HiveField(6, defaultValue: ServerProvider.hetzner) - final ServerProvider provider; + @HiveField(6, defaultValue: ServerProviderType.hetzner) + final ServerProviderType provider; ServerHostingDetails copyWith({final DateTime? startTime}) => ServerHostingDetails( @@ -77,7 +77,7 @@ class ServerVolume { } @HiveType(typeId: 101) -enum ServerProvider { +enum ServerProviderType { @HiveField(0) unknown, @HiveField(1) @@ -85,7 +85,7 @@ enum ServerProvider { @HiveField(2) digitalOcean; - factory ServerProvider.fromGraphQL(final Enum$ServerProvider provider) { + factory ServerProviderType.fromGraphQL(final Enum$ServerProvider provider) { switch (provider) { case Enum$ServerProvider.HETZNER: return hetzner; @@ -98,9 +98,9 @@ enum ServerProvider { String get displayName { switch (this) { - case ServerProvider.hetzner: + case ServerProviderType.hetzner: return 'Hetzner Cloud'; - case ServerProvider.digitalOcean: + case ServerProviderType.digitalOcean: return 'Digital Ocean'; default: return 'Unknown'; diff --git a/lib/logic/models/hive/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart index 6fad2e92..3bb443d7 100644 --- a/lib/logic/models/hive/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -23,8 +23,8 @@ class ServerHostingDetailsAdapter extends TypeAdapter { volume: fields[4] as ServerVolume, apiToken: fields[5] as String, provider: fields[6] == null - ? ServerProvider.hetzner - : fields[6] as ServerProvider, + ? ServerProviderType.hetzner + : fields[6] as ServerProviderType, startTime: fields[2] as DateTime?, ); } @@ -109,34 +109,34 @@ class ServerVolumeAdapter extends TypeAdapter { typeId == other.typeId; } -class ServerProviderAdapter extends TypeAdapter { +class ServerProviderTypeAdapter extends TypeAdapter { @override final int typeId = 101; @override - ServerProvider read(BinaryReader reader) { + ServerProviderType read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return ServerProvider.unknown; + return ServerProviderType.unknown; case 1: - return ServerProvider.hetzner; + return ServerProviderType.hetzner; case 2: - return ServerProvider.digitalOcean; + return ServerProviderType.digitalOcean; default: - return ServerProvider.unknown; + return ServerProviderType.unknown; } } @override - void write(BinaryWriter writer, ServerProvider obj) { + void write(BinaryWriter writer, ServerProviderType obj) { switch (obj) { - case ServerProvider.unknown: + case ServerProviderType.unknown: writer.writeByte(0); break; - case ServerProvider.hetzner: + case ServerProviderType.hetzner: writer.writeByte(1); break; - case ServerProvider.digitalOcean: + case ServerProviderType.digitalOcean: writer.writeByte(2); break; } @@ -148,7 +148,7 @@ class ServerProviderAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is ServerProviderAdapter && + other is ServerProviderTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart index de3ef92a..1649be2a 100644 --- a/lib/logic/models/hive/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -17,28 +17,32 @@ class ServerDomain { @HiveField(1) final String zoneId; - @HiveField(2, defaultValue: DnsProvider.cloudflare) - final DnsProvider provider; + @HiveField(2, defaultValue: DnsProviderType.cloudflare) + final DnsProviderType provider; @override String toString() => '$domainName: $zoneId'; } @HiveType(typeId: 100) -enum DnsProvider { +enum DnsProviderType { @HiveField(0) unknown, @HiveField(1) cloudflare, @HiveField(2) - desec; + desec, + @HiveField(3) + digitalOcean; - factory DnsProvider.fromGraphQL(final Enum$DnsProvider provider) { + factory DnsProviderType.fromGraphQL(final Enum$DnsProvider provider) { switch (provider) { case Enum$DnsProvider.CLOUDFLARE: return cloudflare; case Enum$DnsProvider.DESEC: return desec; + case Enum$DnsProvider.DIGITALOCEAN: + return digitalOcean; default: return unknown; } diff --git a/lib/logic/models/hive/server_domain.g.dart b/lib/logic/models/hive/server_domain.g.dart index 6770d9bc..303407bc 100644 --- a/lib/logic/models/hive/server_domain.g.dart +++ b/lib/logic/models/hive/server_domain.g.dart @@ -19,8 +19,9 @@ class ServerDomainAdapter extends TypeAdapter { return ServerDomain( domainName: fields[0] as String, zoneId: fields[1] as String, - provider: - fields[2] == null ? DnsProvider.cloudflare : fields[2] as DnsProvider, + provider: fields[2] == null + ? DnsProviderType.cloudflare + : fields[2] as DnsProviderType, ); } @@ -47,36 +48,41 @@ class ServerDomainAdapter extends TypeAdapter { typeId == other.typeId; } -class DnsProviderAdapter extends TypeAdapter { +class DnsProviderTypeAdapter extends TypeAdapter { @override final int typeId = 100; @override - DnsProvider read(BinaryReader reader) { + DnsProviderType read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return DnsProvider.unknown; + return DnsProviderType.unknown; case 1: - return DnsProvider.cloudflare; + return DnsProviderType.cloudflare; case 2: - return DnsProvider.desec; + return DnsProviderType.desec; + case 3: + return DnsProviderType.digitalOcean; default: - return DnsProvider.unknown; + return DnsProviderType.unknown; } } @override - void write(BinaryWriter writer, DnsProvider obj) { + void write(BinaryWriter writer, DnsProviderType obj) { switch (obj) { - case DnsProvider.unknown: + case DnsProviderType.unknown: writer.writeByte(0); break; - case DnsProvider.cloudflare: + case DnsProviderType.cloudflare: writer.writeByte(1); break; - case DnsProvider.desec: + case DnsProviderType.desec: writer.writeByte(2); break; + case DnsProviderType.digitalOcean: + writer.writeByte(3); + break; } } @@ -86,7 +92,7 @@ class DnsProviderAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is DnsProviderAdapter && + other is DnsProviderTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/logic/models/json/digital_ocean_server_info.dart b/lib/logic/models/json/digital_ocean_server_info.dart new file mode 100644 index 00000000..9351761d --- /dev/null +++ b/lib/logic/models/json/digital_ocean_server_info.dart @@ -0,0 +1,65 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'digital_ocean_server_info.g.dart'; + +@JsonSerializable() +class DigitalOceanVolume { + DigitalOceanVolume( + this.id, + this.name, + this.sizeGigabytes, + this.dropletIds, + ); + + final String id; + final String name; + + @JsonKey(name: 'droplet_ids') + final List? dropletIds; + + @JsonKey(name: 'size_gigabytes') + final int sizeGigabytes; + + static DigitalOceanVolume fromJson(final Map json) => + _$DigitalOceanVolumeFromJson(json); +} + +@JsonSerializable() +class DigitalOceanLocation { + DigitalOceanLocation( + this.slug, + this.name, + ); + + final String slug; + final String name; + + static DigitalOceanLocation fromJson(final Map json) => + _$DigitalOceanLocationFromJson(json); +} + +@JsonSerializable() +class DigitalOceanServerType { + DigitalOceanServerType( + this.regions, + this.memory, + this.description, + this.disk, + this.priceMonthly, + this.slug, + this.vcpus, + ); + + final List regions; + final double memory; + final String slug; + final String description; + final int vcpus; + final int disk; + + @JsonKey(name: 'price_monthly') + final double priceMonthly; + + static DigitalOceanServerType fromJson(final Map json) => + _$DigitalOceanServerTypeFromJson(json); +} diff --git a/lib/logic/models/json/digital_ocean_server_info.g.dart b/lib/logic/models/json/digital_ocean_server_info.g.dart new file mode 100644 index 00000000..9610dbce --- /dev/null +++ b/lib/logic/models/json/digital_ocean_server_info.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'digital_ocean_server_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DigitalOceanVolume _$DigitalOceanVolumeFromJson(Map json) => + DigitalOceanVolume( + json['id'] as String, + json['name'] as String, + json['size_gigabytes'] as int, + (json['droplet_ids'] as List?)?.map((e) => e as int).toList(), + ); + +Map _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) => + { + 'id': instance.id, + 'name': instance.name, + 'droplet_ids': instance.dropletIds, + 'size_gigabytes': instance.sizeGigabytes, + }; + +DigitalOceanLocation _$DigitalOceanLocationFromJson( + Map json) => + DigitalOceanLocation( + json['slug'] as String, + json['name'] as String, + ); + +Map _$DigitalOceanLocationToJson( + DigitalOceanLocation instance) => + { + 'slug': instance.slug, + 'name': instance.name, + }; + +DigitalOceanServerType _$DigitalOceanServerTypeFromJson( + Map json) => + DigitalOceanServerType( + (json['regions'] as List).map((e) => e as String).toList(), + (json['memory'] as num).toDouble(), + json['description'] as String, + json['disk'] as int, + (json['price_monthly'] as num).toDouble(), + json['slug'] as String, + json['vcpus'] as int, + ); + +Map _$DigitalOceanServerTypeToJson( + DigitalOceanServerType instance) => + { + 'regions': instance.regions, + 'memory': instance.memory, + 'slug': instance.slug, + 'description': instance.description, + 'vcpus': instance.vcpus, + 'disk': instance.disk, + 'price_monthly': instance.priceMonthly, + }; diff --git a/lib/logic/models/json/dns_records.dart b/lib/logic/models/json/dns_records.dart index c4799876..1680e943 100644 --- a/lib/logic/models/json/dns_records.dart +++ b/lib/logic/models/json/dns_records.dart @@ -9,6 +9,7 @@ class DnsRecord { required this.type, required this.name, required this.content, + this.id, this.ttl = 3600, this.priority = 10, this.proxied = false, @@ -31,5 +32,8 @@ class DnsRecord { final int priority; final bool proxied; + /// TODO: Refactoring refactoring refactoring refactoring >:c + final int? id; + Map toJson() => _$DnsRecordToJson(this); } diff --git a/lib/logic/models/json/dns_records.g.dart b/lib/logic/models/json/dns_records.g.dart index c8c12c34..b58db5de 100644 --- a/lib/logic/models/json/dns_records.g.dart +++ b/lib/logic/models/json/dns_records.g.dart @@ -13,4 +13,5 @@ Map _$DnsRecordToJson(DnsRecord instance) => { 'ttl': instance.ttl, 'priority': instance.priority, 'proxied': instance.proxied, + 'id': instance.id, }; diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 6e28f1cf..b0706599 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -72,11 +72,21 @@ enum ServerStatus { @JsonSerializable() class HetznerServerTypeInfo { - HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); + HetznerServerTypeInfo( + this.cores, + this.memory, + this.disk, + this.prices, + this.name, + this.description, + ); final int cores; final num memory; final int disk; + final String name; + final String description; + final List prices; static HetznerServerTypeInfo fromJson(final Map json) => @@ -85,7 +95,11 @@ class HetznerServerTypeInfo { @JsonSerializable() class HetznerPriceInfo { - HetznerPriceInfo(this.hourly, this.monthly); + HetznerPriceInfo( + this.hourly, + this.monthly, + this.location, + ); @JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice) final double hourly; @@ -93,6 +107,8 @@ class HetznerPriceInfo { @JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice) final double monthly; + final String location; + static HetznerPriceInfo fromJson(final Map json) => _$HetznerPriceInfoFromJson(json); @@ -102,7 +118,14 @@ class HetznerPriceInfo { @JsonSerializable() class HetznerLocation { - HetznerLocation(this.country, this.city, this.description, this.zone); + HetznerLocation( + this.country, + this.city, + this.description, + this.zone, + this.name, + ); + final String name; final String country; final String city; final String description; @@ -113,3 +136,24 @@ class HetznerLocation { static HetznerLocation fromJson(final Map json) => _$HetznerLocationFromJson(json); } + +@JsonSerializable() +class HetznerVolume { + HetznerVolume( + this.id, + this.size, + this.serverId, + this.name, + this.linuxDevice, + ); + final int id; + final int size; + final int? serverId; + final String name; + + @JsonKey(name: 'linux_device') + final String? linuxDevice; + + static HetznerVolume fromJson(final Map json) => + _$HetznerVolumeFromJson(json); +} diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index 5201a1c5..b73a0a9d 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -81,6 +81,8 @@ HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( (json['prices'] as List) .map((e) => HetznerPriceInfo.fromJson(e as Map)) .toList(), + json['name'] as String, + json['description'] as String, ); Map _$HetznerServerTypeInfoToJson( @@ -89,6 +91,8 @@ Map _$HetznerServerTypeInfoToJson( 'cores': instance.cores, 'memory': instance.memory, 'disk': instance.disk, + 'name': instance.name, + 'description': instance.description, 'prices': instance.prices, }; @@ -96,12 +100,14 @@ HetznerPriceInfo _$HetznerPriceInfoFromJson(Map json) => HetznerPriceInfo( HetznerPriceInfo.getPrice(json['price_hourly'] as Map), HetznerPriceInfo.getPrice(json['price_monthly'] as Map), + json['location'] as String, ); Map _$HetznerPriceInfoToJson(HetznerPriceInfo instance) => { 'price_hourly': instance.hourly, 'price_monthly': instance.monthly, + 'location': instance.location, }; HetznerLocation _$HetznerLocationFromJson(Map json) => @@ -110,12 +116,32 @@ HetznerLocation _$HetznerLocationFromJson(Map json) => json['city'] as String, json['description'] as String, json['network_zone'] as String, + json['name'] as String, ); Map _$HetznerLocationToJson(HetznerLocation instance) => { + 'name': instance.name, 'country': instance.country, 'city': instance.city, 'description': instance.description, 'network_zone': instance.zone, }; + +HetznerVolume _$HetznerVolumeFromJson(Map json) => + HetznerVolume( + json['id'] as int, + json['size'] as int, + json['serverId'] as int?, + json['name'] as String, + json['linux_device'] as String?, + ); + +Map _$HetznerVolumeToJson(HetznerVolume instance) => + { + 'id': instance.id, + 'size': instance.size, + 'serverId': instance.serverId, + 'name': instance.name, + 'linux_device': instance.linuxDevice, + }; diff --git a/lib/logic/models/launch_installation_data.dart b/lib/logic/models/launch_installation_data.dart new file mode 100644 index 00000000..c1f32ee6 --- /dev/null +++ b/lib/logic/models/launch_installation_data.dart @@ -0,0 +1,23 @@ +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; + +class LaunchInstallationData { + LaunchInstallationData({ + required this.rootUser, + required this.dnsApiToken, + required this.dnsProviderType, + required this.serverDomain, + required this.serverTypeId, + required this.errorCallback, + required this.successCallback, + }); + + final User rootUser; + final String dnsApiToken; + final ServerDomain serverDomain; + final DnsProviderType dnsProviderType; + final String serverTypeId; + final Function() errorCallback; + final Function(ServerHostingDetails details) successCallback; +} diff --git a/lib/logic/models/server_metadata.dart b/lib/logic/models/server_metadata.dart index 0275a2ef..553fcbb5 100644 --- a/lib/logic/models/server_metadata.dart +++ b/lib/logic/models/server_metadata.dart @@ -19,11 +19,11 @@ enum MetadataType { class ServerMetadataEntity { ServerMetadataEntity({ - required this.name, + required this.trId, required this.value, this.type = MetadataType.other, }); final MetadataType type; - final String name; + final String trId; final String value; } diff --git a/lib/logic/providers/dns_providers/cloudflare.dart b/lib/logic/providers/dns_providers/cloudflare.dart new file mode 100644 index 00000000..abd4f1ba --- /dev/null +++ b/lib/logic/providers/dns_providers/cloudflare.dart @@ -0,0 +1,356 @@ +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; + +class ApiAdapter { + ApiAdapter({final bool isWithToken = true}) + : _api = CloudflareApi( + isWithToken: isWithToken, + ); + + CloudflareApi api({final bool getInitialized = true}) => getInitialized + ? _api + : CloudflareApi( + isWithToken: false, + ); + + final CloudflareApi _api; +} + +class CloudflareDnsProvider extends DnsProvider { + CloudflareDnsProvider() : _adapter = ApiAdapter(); + CloudflareDnsProvider.load( + final bool isAuthotized, + ) : _adapter = ApiAdapter( + isWithToken: isAuthotized, + ); + + ApiAdapter _adapter; + + @override + DnsProviderType get type => DnsProviderType.cloudflare; + + @override + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } + + _adapter = ApiAdapter(isWithToken: true); + return result; + } + + @override + Future> getZoneId(final String domain) async { + String? id; + final result = await _adapter.api().getZones(domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: id, + code: result.code, + message: result.message, + ); + } + + id = result.data[0]['id']; + + return GenericResult(success: true, data: id); + } + + @override + Future> removeDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: null, + code: result.code, + message: result.message, + ); + } + + return _adapter.api().removeSimilarRecords( + domain: domain, + records: result.data, + ); + } + + @override + Future>> getDnsRecords({ + required final ServerDomain domain, + }) async { + final List records = []; + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: records, + code: result.code, + message: result.message, + ); + } + + for (final rawRecord in result.data) { + records.add( + DnsRecord( + name: rawRecord['name'], + type: rawRecord['type'], + content: rawRecord['content'], + ttl: rawRecord['ttl'], + proxied: rawRecord['proxied'], + ), + ); + } + + return GenericResult( + success: result.success, + data: records, + ); + } + + @override + Future> createDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) { + final records = getProjectDnsRecords(domain.domainName, ip4); + return _adapter.api().createMultipleDnsRecords( + domain: domain, + records: records, + ); + } + + @override + Future> setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ) async => + _adapter.api().createMultipleDnsRecords( + domain: domain, + records: [record], + ); + + @override + Future>> domainList() async { + List domains = []; + final result = await _adapter.api().getDomains(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: domains, + code: result.code, + message: result.message, + ); + } + + domains = result.data + .map( + (final el) => el['name'] as String, + ) + .toList(); + + return GenericResult( + success: true, + data: domains, + ); + } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final GenericResult> records = + await getDnsRecords(domain: domain); + final List foundRecords = []; + try { + final List desiredRecords = + getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); + for (final DesiredDnsRecord record in desiredRecords) { + if (record.description == 'record.dkim') { + final DnsRecord foundRecord = records.data.firstWhere( + (final r) => (r.name == record.name) && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final String? foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.data.any( + (final r) => + (r.name == record.name) && + r.type == record.type && + r.content == record.content, + )) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return GenericResult( + data: foundRecords, + success: true, + ); + } + + @override + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ) { + if (domainName == null || ip4 == null) { + return []; + } + return [ + DesiredDnsRecord( + name: domainName, + content: ip4, + description: 'record.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ip4, + description: 'record.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ip4, + description: 'record.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ip4, + description: 'record.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ip4, + description: 'record.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ip4, + description: 'record.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ip4, + description: 'record.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ip4, + description: 'record.vpn', + ), + DesiredDnsRecord( + name: domainName, + content: domainName, + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: 'v=DMARC1; p=none', + description: 'record.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: domainName, + content: 'v=spf1 a mx ip4:$ip4 -all', + description: 'record.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + if (dkimPublicKey != null) + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: dkimPublicKey, + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } + + List getProjectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = + DnsRecord(type: 'A', name: domainName, content: ip4); + + final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName); + final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + + final DnsRecord txt1 = DnsRecord( + type: 'TXT', + name: '_dmarc', + content: 'v=DMARC1; p=none', + ttl: 18000, + ); + + final DnsRecord txt2 = DnsRecord( + type: 'TXT', + name: domainName, + content: 'v=spf1 a mx ip4:$ip4 -all', + ttl: 18000, + ); + + return [ + domainA, + apiA, + cloudA, + gitA, + meetA, + passwordA, + socialA, + mx, + txt1, + txt2, + vpn + ]; + } +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart b/lib/logic/providers/dns_providers/desec.dart similarity index 55% rename from lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart rename to lib/logic/providers/dns_providers/desec.dart index e8192cb9..7111c0ba 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart +++ b/lib/logic/providers/dns_providers/desec.dart @@ -1,173 +1,113 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; -class DesecApi extends DnsProviderApi { - DesecApi({ - this.hasLogger = false, - this.isWithToken = true, - this.customToken, - }); - @override - final bool hasLogger; - @override - final bool isWithToken; +class ApiAdapter { + ApiAdapter({final bool isWithToken = true}) + : _api = DesecApi( + isWithToken: isWithToken, + ); - final String? customToken; + DesecApi api({final bool getInitialized = true}) => getInitialized + ? _api + : DesecApi( + isWithToken: false, + ); + + final DesecApi _api; +} + +class DesecDnsProvider extends DnsProvider { + DesecDnsProvider() : _adapter = ApiAdapter(); + DesecDnsProvider.load( + final bool isAuthotized, + ) : _adapter = ApiAdapter( + isWithToken: isAuthotized, + ); + + ApiAdapter _adapter; @override - RegExp getApiTokenValidation() => - RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); + DnsProviderType get type => DnsProviderType.desec; @override - BaseOptions get options { - final BaseOptions options = BaseOptions( - baseUrl: rootAddress, - contentType: Headers.jsonContentType, - responseType: ResponseType.json, - ); - if (isWithToken) { - final String? token = getIt().dnsProviderKey; - assert(token != null); - options.headers = {'Authorization': 'Token $token'}; + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; } - if (customToken != null) { - options.headers = {'Authorization': 'Token $customToken'}; - } - - if (validateStatus != null) { - options.validateStatus = validateStatus!; - } - return options; + _adapter = ApiAdapter(isWithToken: true); + return result; } @override - String rootAddress = 'https://desec.io/api/v1/domains/'; - - @override - Future> isApiTokenValid(final String token) async { - bool isValid = false; - Response? response; - String message = ''; - final Dio client = await getClient(); - try { - response = await client.get( - '', - options: Options( - followRedirects: false, - validateStatus: (final status) => - status != null && (status >= 200 || status == 401), - headers: {'Authorization': 'Token $token'}, - ), + Future> getZoneId(final String domain) async => + GenericResult( + data: domain, + success: true, ); - await Future.delayed(const Duration(seconds: 1)); - } catch (e) { - print(e); - isValid = false; - message = e.toString(); - } finally { - close(client); - } - - if (response == null) { - return APIGenericResult( - data: isValid, - success: false, - message: message, - ); - } - - if (response.statusCode == HttpStatus.ok) { - isValid = true; - } else if (response.statusCode == HttpStatus.unauthorized) { - isValid = false; - } else { - throw Exception('code: ${response.statusCode}'); - } - - return APIGenericResult( - data: isValid, - success: true, - message: response.statusMessage, - ); - } @override - Future getZoneId(final String domain) async => domain; - - @override - Future> removeSimilarRecords({ + Future> removeDomainRecords({ required final ServerDomain domain, final String? ip4, }) async { - final String domainName = domain.domainName; - final String url = '/$domainName/rrsets/'; - final List listDnsRecords = projectDnsRecords(domainName, ip4); + final List listDnsRecords = projectDnsRecords( + domain.domainName, + ip4, + ); - final Dio client = await getClient(); - try { - final List bulkRecords = []; - for (final DnsRecord record in listDnsRecords) { - bulkRecords.add( - { - 'subname': record.name, - 'type': record.type, - 'ttl': record.ttl, - 'records': [], - }, - ); - } + final List bulkRecords = []; + for (final DnsRecord record in listDnsRecords) { bulkRecords.add( { - 'subname': 'selector._domainkey', - 'type': 'TXT', - 'ttl': 18000, + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, 'records': [], }, ); - await client.put(url, data: bulkRecords); - await Future.delayed(const Duration(seconds: 1)); - } catch (e) { - print(e); - return APIGenericResult( - success: false, - data: null, - message: e.toString(), - ); - } finally { - close(client); } + bulkRecords.add( + { + 'subname': 'selector._domainkey', + 'type': 'TXT', + 'ttl': 18000, + 'records': [], + }, + ); - return APIGenericResult(success: true, data: null); + return _adapter.api().updateRecords( + domain: domain, + records: bulkRecords, + ); } @override - Future> getDnsRecords({ + Future>> getDnsRecords({ required final ServerDomain domain, }) async { - Response response; - final String domainName = domain.domainName; - final List allRecords = []; + final List records = []; + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: records, + code: result.code, + message: result.message, + ); + } - final String url = '/$domainName/rrsets/'; - - final Dio client = await getClient(); try { - response = await client.get(url); - await Future.delayed(const Duration(seconds: 1)); - final List records = response.data; - - for (final record in records) { + for (final record in result.data) { final String? content = (record['records'] is List) ? record['records'][0] : record['records']; - allRecords.add( + records.add( DnsRecord( name: record['subname'], type: record['type'], @@ -178,54 +118,14 @@ class DesecApi extends DnsProviderApi { } } catch (e) { print(e); - } finally { - close(client); - } - - return allRecords; - } - - @override - Future> createMultipleDnsRecords({ - required final ServerDomain domain, - final String? ip4, - }) async { - final String domainName = domain.domainName; - final List listDnsRecords = projectDnsRecords(domainName, ip4); - - final Dio client = await getClient(); - try { - final List bulkRecords = []; - for (final DnsRecord record in listDnsRecords) { - bulkRecords.add( - { - 'subname': record.name, - 'type': record.type, - 'ttl': record.ttl, - 'records': [extractContent(record)], - }, - ); - } - await client.post( - '/$domainName/rrsets/', - data: bulkRecords, - ); - await Future.delayed(const Duration(seconds: 1)); - } on DioError catch (e) { - print(e.message); - rethrow; - } catch (e) { - print(e); - return APIGenericResult( + return GenericResult( success: false, - data: null, + data: records, message: e.toString(), ); - } finally { - close(client); } - return APIGenericResult(success: true, data: null); + return GenericResult(success: true, data: records); } List projectDnsRecords( @@ -275,6 +175,57 @@ class DesecApi extends DnsProviderApi { ]; } + @override + Future> createDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final List listDnsRecords = projectDnsRecords( + domain.domainName, + ip4, + ); + + final List bulkRecords = []; + for (final DnsRecord record in listDnsRecords) { + bulkRecords.add( + { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [extractContent(record)], + }, + ); + } + + return _adapter.api().createRecords( + domain: domain, + records: bulkRecords, + ); + } + + @override + Future> setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ) async { + final result = await _adapter.api().createRecords( + domain: domain, + records: [ + { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [extractContent(record)], + }, + ], + ); + + return GenericResult( + success: result.success, + data: null, + ); + } + String? extractContent(final DnsRecord record) { String? content = record.content; if (record.type == 'TXT' && content != null && !content.startsWith('"')) { @@ -285,60 +236,47 @@ class DesecApi extends DnsProviderApi { } @override - Future setDnsRecord( - final DnsRecord record, - final ServerDomain domain, - ) async { - final String url = '/${domain.domainName}/rrsets/'; - - final Dio client = await getClient(); - try { - await client.post( - url, - data: { - 'subname': record.name, - 'type': record.type, - 'ttl': record.ttl, - 'records': [extractContent(record)], - }, - ); - await Future.delayed(const Duration(seconds: 1)); - } catch (e) { - print(e); - } finally { - close(client); - } - } - - @override - Future> domainList() async { + Future>> domainList() async { List domains = []; - - final Dio client = await getClient(); - try { - final Response response = await client.get( - '', + final result = await _adapter.api().getDomains(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: domains, + code: result.code, + message: result.message, ); - await Future.delayed(const Duration(seconds: 1)); - domains = response.data - .map((final el) => el['name'] as String) - .toList(); - } catch (e) { - print(e); - } finally { - close(client); } - return domains; + domains = result.data + .map( + (final el) => el['name'] as String, + ) + .toList(); + + return GenericResult( + success: true, + data: domains, + ); } @override - Future>> validateDnsRecords( + Future>> validateDnsRecords( final ServerDomain domain, final String ip4, final String dkimPublicKey, ) async { - final List records = await getDnsRecords(domain: domain); + final result = await getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: [], + code: result.code, + message: result.message, + ); + } + + final records = result.data; final List foundRecords = []; try { final List desiredRecords = @@ -384,13 +322,13 @@ class DesecApi extends DnsProviderApi { } } catch (e) { print(e); - return APIGenericResult( + return GenericResult( data: [], success: false, message: e.toString(), ); } - return APIGenericResult( + return GenericResult( data: foundRecords, success: true, ); diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart new file mode 100644 index 00000000..7f852a44 --- /dev/null +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -0,0 +1,359 @@ +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; + +class ApiAdapter { + ApiAdapter({final bool isWithToken = true}) + : _api = DigitalOceanDnsApi( + isWithToken: isWithToken, + ); + + DigitalOceanDnsApi api({final bool getInitialized = true}) => getInitialized + ? _api + : DigitalOceanDnsApi( + isWithToken: false, + ); + + final DigitalOceanDnsApi _api; +} + +class DigitalOceanDnsProvider extends DnsProvider { + DigitalOceanDnsProvider() : _adapter = ApiAdapter(); + DigitalOceanDnsProvider.load( + final bool isAuthotized, + ) : _adapter = ApiAdapter( + isWithToken: isAuthotized, + ); + + ApiAdapter _adapter; + + @override + DnsProviderType get type => DnsProviderType.digitalOcean; + + @override + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } + + _adapter = ApiAdapter(isWithToken: true); + return result; + } + + @override + Future> getZoneId(final String domain) async => + GenericResult( + data: domain, + success: true, + ); + + @override + Future> removeDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: null, + code: result.code, + message: result.message, + ); + } + + const ignoreType = 'SOA'; + final filteredRecords = []; + for (final record in result.data) { + if (record['type'] != ignoreType) { + filteredRecords.add(record); + } + } + + return _adapter.api().removeSimilarRecords( + domain: domain, + records: filteredRecords, + ); + } + + @override + Future>> getDnsRecords({ + required final ServerDomain domain, + }) async { + final List records = []; + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: records, + code: result.code, + message: result.message, + ); + } + + for (final rawRecord in result.data) { + records.add( + DnsRecord( + id: rawRecord['id'], + name: rawRecord['name'], + type: rawRecord['type'], + content: rawRecord['data'], + ttl: rawRecord['ttl'], + proxied: false, + ), + ); + } + + return GenericResult(data: records, success: true); + } + + @override + Future> createDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) async => + _adapter.api().createMultipleDnsRecords( + domain: domain, + records: getProjectDnsRecords( + domain.domainName, + ip4, + ), + ); + + @override + Future> setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ) async => + _adapter.api().createMultipleDnsRecords( + domain: domain, + records: [record], + ); + + @override + Future>> domainList() async { + List domains = []; + final result = await _adapter.api().domainList(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: domains, + code: result.code, + message: result.message, + ); + } + + domains = result.data + .map( + (final el) => el['name'] as String, + ) + .toList(); + + return GenericResult( + success: true, + data: domains, + ); + } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final GenericResult> records = + await getDnsRecords(domain: domain); + final List foundRecords = []; + try { + final List desiredRecords = + getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); + for (final DesiredDnsRecord record in desiredRecords) { + if (record.description == 'record.dkim') { + final DnsRecord foundRecord = records.data.firstWhere( + (final r) => (r.name == record.name) && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final String? foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.data.any( + (final r) => + (r.name == record.name) && + r.type == record.type && + r.content == record.content, + )) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return GenericResult( + data: foundRecords, + success: true, + ); + } + + List getProjectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = DnsRecord(type: 'A', name: '@', content: ip4); + + final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: '@'); + final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + + final DnsRecord txt1 = DnsRecord( + type: 'TXT', + name: '_dmarc', + content: 'v=DMARC1; p=none', + ttl: 18000, + ); + + final DnsRecord txt2 = DnsRecord( + type: 'TXT', + name: '@', + content: 'v=spf1 a mx ip4:$ip4 -all', + ttl: 18000, + ); + + return [ + domainA, + apiA, + cloudA, + gitA, + meetA, + passwordA, + socialA, + mx, + txt1, + txt2, + vpn + ]; + } + + @override + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ) { + if (domainName == null || ip4 == null) { + return []; + } + return [ + DesiredDnsRecord( + name: '@', + content: ip4, + description: 'record.root', + displayName: domainName, + ), + DesiredDnsRecord( + name: 'api', + content: ip4, + description: 'record.api', + displayName: 'api.$domainName', + ), + DesiredDnsRecord( + name: 'cloud', + content: ip4, + description: 'record.cloud', + displayName: 'cloud.$domainName', + ), + DesiredDnsRecord( + name: 'git', + content: ip4, + description: 'record.git', + displayName: 'git.$domainName', + ), + DesiredDnsRecord( + name: 'meet', + content: ip4, + description: 'record.meet', + displayName: 'meet.$domainName', + ), + DesiredDnsRecord( + name: 'social', + content: ip4, + description: 'record.social', + displayName: 'social.$domainName', + ), + DesiredDnsRecord( + name: 'password', + content: ip4, + description: 'record.password', + displayName: 'password.$domainName', + ), + DesiredDnsRecord( + name: 'vpn', + content: ip4, + description: 'record.vpn', + displayName: 'vpn.$domainName', + ), + const DesiredDnsRecord( + name: '@', + content: '@', + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + const DesiredDnsRecord( + name: '_dmarc', + content: 'v=DMARC1; p=none', + description: 'record.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '@', + content: 'v=spf1 a mx ip4:$ip4 -all', + description: 'record.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + if (dkimPublicKey != null) + DesiredDnsRecord( + name: 'selector._domainkey', + content: dkimPublicKey, + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } +} diff --git a/lib/logic/providers/dns_providers/dns_provider.dart b/lib/logic/providers/dns_providers/dns_provider.dart new file mode 100644 index 00000000..60976f68 --- /dev/null +++ b/lib/logic/providers/dns_providers/dns_provider.dart @@ -0,0 +1,37 @@ +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; +export 'package:selfprivacy/logic/api_maps/generic_result.dart'; + +abstract class DnsProvider { + DnsProviderType get type; + Future> tryInitApiByToken(final String token); + Future> getZoneId(final String domain); + Future> removeDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }); + Future>> getDnsRecords({ + required final ServerDomain domain, + }); + Future> createDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }); + Future> setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ); + Future>> domainList(); + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ); + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ); +} diff --git a/lib/logic/providers/dns_providers/dns_provider_factory.dart b/lib/logic/providers/dns_providers/dns_provider_factory.dart new file mode 100644 index 00000000..a2b1694e --- /dev/null +++ b/lib/logic/providers/dns_providers/dns_provider_factory.dart @@ -0,0 +1,28 @@ +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/cloudflare.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/desec.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/digital_ocean_dns.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/providers/provider_settings.dart'; + +class UnknownProviderException implements Exception { + UnknownProviderException(this.message); + final String message; +} + +class DnsProviderFactory { + static DnsProvider createDnsProviderInterface( + final DnsProviderSettings settings, + ) { + switch (settings.provider) { + case DnsProviderType.cloudflare: + return CloudflareDnsProvider(); + case DnsProviderType.digitalOcean: + return DigitalOceanDnsProvider(); + case DnsProviderType.desec: + return DesecDnsProvider(); + case DnsProviderType.unknown: + throw UnknownProviderException('Unknown server provider'); + } + } +} diff --git a/lib/logic/api_maps/rest_maps/api_factory_settings.dart b/lib/logic/providers/provider_settings.dart similarity index 54% rename from lib/logic/api_maps/rest_maps/api_factory_settings.dart rename to lib/logic/providers/provider_settings.dart index 438b92d5..8ffe79e6 100644 --- a/lib/logic/api_maps/rest_maps/api_factory_settings.dart +++ b/lib/logic/providers/provider_settings.dart @@ -1,20 +1,20 @@ import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; -class ServerProviderApiFactorySettings { - ServerProviderApiFactorySettings({ +class ServerProviderSettings { + ServerProviderSettings({ required this.provider, this.location, }); - final ServerProvider provider; + final ServerProviderType provider; final String? location; } -class DnsProviderApiFactorySettings { - DnsProviderApiFactorySettings({ +class DnsProviderSettings { + DnsProviderSettings({ required this.provider, }); - final DnsProvider provider; + final DnsProviderType provider; } diff --git a/lib/logic/providers/providers_controller.dart b/lib/logic/providers/providers_controller.dart new file mode 100644 index 00000000..dab74221 --- /dev/null +++ b/lib/logic/providers/providers_controller.dart @@ -0,0 +1,34 @@ +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/dns_provider_factory.dart'; +import 'package:selfprivacy/logic/providers/provider_settings.dart'; +import 'package:selfprivacy/logic/providers/server_providers/server_provider.dart'; +import 'package:selfprivacy/logic/providers/server_providers/server_provider_factory.dart'; + +class ProvidersController { + static ServerProvider? get currentServerProvider => _serverProvider; + static DnsProvider? get currentDnsProvider => _dnsProvider; + + static void initServerProvider( + final ServerProviderSettings settings, + ) { + _serverProvider = ServerProviderFactory.createServerProviderInterface( + settings, + ); + } + + static void initDnsProvider( + final DnsProviderSettings settings, + ) { + _dnsProvider = DnsProviderFactory.createDnsProviderInterface( + settings, + ); + } + + static void clearProviders() { + _serverProvider = null; + _dnsProvider = null; + } + + static ServerProvider? _serverProvider; + static DnsProvider? _dnsProvider; +} diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart new file mode 100644 index 00000000..65a7b124 --- /dev/null +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -0,0 +1,812 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart'; +import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/digital_ocean_server_info.dart'; +import 'package:selfprivacy/logic/models/metrics.dart'; +import 'package:selfprivacy/logic/models/price.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; +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/logic/providers/server_providers/server_provider.dart'; +import 'package:selfprivacy/utils/extensions/string_extensions.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; +import 'package:selfprivacy/utils/password_generator.dart'; + +class ApiAdapter { + ApiAdapter({final String? region, final bool isWithToken = true}) + : _api = DigitalOceanApi( + region: region, + isWithToken: isWithToken, + ); + + DigitalOceanApi api({final bool getInitialized = true}) => getInitialized + ? _api + : DigitalOceanApi( + region: _api.region, + isWithToken: false, + ); + + final DigitalOceanApi _api; +} + +class DigitalOceanServerProvider extends ServerProvider { + DigitalOceanServerProvider() : _adapter = ApiAdapter(); + DigitalOceanServerProvider.load( + final ServerType serverType, + final bool isAuthotized, + ) : _adapter = ApiAdapter( + isWithToken: isAuthotized, + region: serverType.location.identifier, + ); + + ApiAdapter _adapter; + final String currency = 'USD'; + + @override + ServerProviderType get type => ServerProviderType.digitalOcean; + + @override + Future> trySetServerLocation( + final String location, + ) async { + final bool apiInitialized = _adapter.api().isWithToken; + if (!apiInitialized) { + return GenericResult( + success: true, + data: false, + message: 'Not authorized!', + ); + } + + _adapter = ApiAdapter( + isWithToken: true, + region: location, + ); + return success; + } + + @override + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } + + _adapter = ApiAdapter(region: api.region, isWithToken: true); + return result; + } + + String? getEmojiFlag(final String query) { + String? emoji; + + switch (query.toLowerCase().substring(0, 3)) { + case 'fra': + emoji = '🇩🇪'; + break; + + case 'ams': + emoji = '🇳🇱'; + break; + + case 'sgp': + emoji = '🇸🇬'; + break; + + case 'lon': + emoji = '🇬🇧'; + break; + + case 'tor': + emoji = '🇨🇦'; + break; + + case 'blr': + emoji = '🇮🇳'; + break; + + case 'nyc': + case 'sfo': + emoji = '🇺🇸'; + break; + } + + return emoji; + } + + String dnsProviderToInfectName(final DnsProviderType dnsProvider) { + String dnsProviderType; + switch (dnsProvider) { + case DnsProviderType.digitalOcean: + dnsProviderType = 'DIGITALOCEAN'; + break; + case DnsProviderType.cloudflare: + default: + dnsProviderType = 'CLOUDFLARE'; + break; + } + return dnsProviderType; + } + + @override + Future> launchInstallation( + final LaunchInstallationData installationData, + ) async { + ServerHostingDetails? serverDetails; + final serverApiToken = StringGenerators.apiToken(); + final hostname = getHostnameFromDomain( + installationData.serverDomain.domainName, + ); + final serverResult = await _adapter.api().createServer( + dnsApiToken: installationData.dnsApiToken, + rootUser: installationData.rootUser, + domainName: installationData.serverDomain.domainName, + serverType: installationData.serverTypeId, + dnsProviderType: + dnsProviderToInfectName(installationData.dnsProviderType), + hostName: hostname, + base64Password: base64.encode( + utf8.encode(installationData.rootUser.password ?? 'PASS'), + ), + databasePassword: StringGenerators.dbPassword(), + serverApiToken: serverApiToken, + ); + + if (!serverResult.success || serverResult.data == null) { + GenericResult( + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: () async => await installationData.errorCallback(), + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async => launchInstallation(installationData), + ), + ], + description: serverResult.message ?? 'recovering.generic_error'.tr(), + title: 'modals.unexpected_error'.tr(), + ), + success: false, + message: serverResult.message, + code: serverResult.code, + ); + } + + try { + final int dropletId = serverResult.data!; + final newVolume = (await createVolume()).data; + final bool attachedVolume = (await _adapter.api().attachVolume( + newVolume!.name, + dropletId, + )) + .data; + + String? ipv4; + int attempts = 0; + while (attempts < 5 && ipv4 == null) { + await Future.delayed(const Duration(seconds: 20)); + final servers = await getServers(); + for (final server in servers.data) { + if (server.name == hostname && server.ip != '0.0.0.0') { + ipv4 = server.ip; + break; + } + } + ++attempts; + } + + if (attachedVolume && ipv4 != null) { + serverDetails = ServerHostingDetails( + id: dropletId, + ip4: ipv4, + createTime: DateTime.now(), + volume: newVolume, + apiToken: serverApiToken, + provider: ServerProviderType.digitalOcean, + ); + } + } catch (e) { + return GenericResult( + success: false, + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: null, + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async { + await Future.delayed(const Duration(seconds: 5)); + final deletion = await deleteServer(hostname); + return deletion.success + ? await launchInstallation(installationData) + : deletion; + }, + ), + ], + description: 'modals.try_again'.tr(), + title: 'modals.server_deletion_error'.tr(), + ), + message: e.toString(), + ); + } + + await installationData.successCallback(serverDetails!); + return GenericResult(success: true, data: null); + } + + @override + Future>> + getAvailableLocations() async { + final List locations = []; + final result = await _adapter.api().getAvailableLocations(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: locations, + code: result.code, + message: result.message, + ); + } + + final List rawLocations = result.data; + for (final rawLocation in rawLocations) { + ServerProviderLocation? location; + try { + location = ServerProviderLocation( + title: rawLocation.slug, + description: rawLocation.name, + flag: getEmojiFlag(rawLocation.slug), + identifier: rawLocation.slug, + ); + } catch (e) { + continue; + } + locations.add(location); + } + + return GenericResult(success: true, data: locations); + } + + @override + Future>> getServerTypes({ + required final ServerProviderLocation location, + }) async { + final List types = []; + final result = await _adapter.api().getAvailableServerTypes(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: types, + code: result.code, + message: result.message, + ); + } + + final List rawSizes = result.data; + for (final rawSize in rawSizes) { + for (final rawRegion in rawSize.regions) { + if (rawRegion == location.identifier && rawSize.memory > 1024) { + types.add( + 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: types); + } + + @override + Future>> getServers() async { + List servers = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: servers, + code: result.code, + message: result.message, + ); + } + + final List rawServers = result.data; + servers = rawServers.map( + (final server) { + String ipv4 = '0.0.0.0'; + if (server['networks']['v4'].isNotEmpty) { + for (final v4 in server['networks']['v4']) { + if (v4['type'].toString() == 'public') { + ipv4 = v4['ip_address'].toString(); + } + } + } + + return ServerBasicInfo( + id: server['id'], + reverseDns: server['name'], + created: DateTime.now(), + ip: ipv4, + name: server['name'], + ); + }, + ).toList(); + + return GenericResult(success: true, data: servers); + } + + @override + Future>> getMetadata( + final int serverId, + ) async { + List metadata = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: false, + data: metadata, + code: result.code, + message: result.message, + ); + } + + final List servers = result.data; + try { + final droplet = servers.firstWhere( + (final server) => server['id'] == serverId, + ); + + metadata = [ + ServerMetadataEntity( + type: MetadataType.id, + trId: 'server.server_id', + value: droplet['id'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.status, + trId: 'server.status', + value: droplet['status'].toString().capitalize(), + ), + ServerMetadataEntity( + type: MetadataType.cpu, + trId: 'server.cpu', + value: droplet['vcpus'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.ram, + trId: 'server.ram', + value: "${droplet['memory'].toString()} MB", + ), + ServerMetadataEntity( + type: MetadataType.cost, + trId: 'server.monthly_cost', + value: '${droplet['size']['price_monthly']} $currency', + ), + ServerMetadataEntity( + type: MetadataType.location, + trId: 'server.location', + value: + '${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}', + ), + ServerMetadataEntity( + type: MetadataType.other, + trId: 'server.provider', + value: _adapter.api().displayProviderName, + ), + ]; + } catch (e) { + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } + + return GenericResult(success: true, data: metadata); + } + + /// Digital Ocean returns a map of lists of /proc/stat values, + /// so here we are trying to implement average CPU + /// load calculation for each point in time on a given interval. + /// + /// For each point of time: + /// + /// `Average Load = 100 * (1 - (Idle Load / Total Load))` + /// + /// For more info please proceed to read: + /// https://rosettacode.org/wiki/Linux_CPU_utilization + List calculateCpuLoadMetrics(final List rawProcStatMetrics) { + final List cpuLoads = []; + + final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length; + for (int i = 0; i < pointsInTime; ++i) { + double currentMetricLoad = 0.0; + double? currentMetricIdle; + for (final rawProcStat in rawProcStatMetrics) { + final String rawProcValue = rawProcStat['values'][i][1]; + // Converting MBit into bit + final double procValue = double.parse(rawProcValue) * 1000000; + currentMetricLoad += procValue; + if (currentMetricIdle == null && + rawProcStat['metric']['mode'] == 'idle') { + currentMetricIdle = procValue; + } + } + currentMetricIdle ??= 0.0; + currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad)); + cpuLoads.add( + TimeSeriesData( + rawProcStatMetrics[0]['values'][i][0], + currentMetricLoad, + ), + ); + } + + return cpuLoads; + } + + @override + Future> getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + ServerMetrics? metrics; + + const int step = 15; + final inboundResult = await _adapter.api().getMetricsBandwidth( + serverId, + start, + end, + true, + ); + + if (inboundResult.data.isEmpty || !inboundResult.success) { + return GenericResult( + success: false, + data: null, + code: inboundResult.code, + message: inboundResult.message, + ); + } + + final outboundResult = await _adapter.api().getMetricsBandwidth( + serverId, + start, + end, + false, + ); + + if (outboundResult.data.isEmpty || !outboundResult.success) { + return GenericResult( + success: false, + data: null, + code: outboundResult.code, + message: outboundResult.message, + ); + } + + final cpuResult = await _adapter.api().getMetricsCpu(serverId, start, end); + + if (cpuResult.data.isEmpty || !cpuResult.success) { + return GenericResult( + success: false, + data: null, + code: cpuResult.code, + message: cpuResult.message, + ); + } + + metrics = ServerMetrics( + bandwidthIn: inboundResult.data + .map( + (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), + ) + .toList(), + bandwidthOut: outboundResult.data + .map( + (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), + ) + .toList(), + cpu: calculateCpuLoadMetrics(cpuResult.data), + start: start, + end: end, + stepsInSecond: step, + ); + + return GenericResult(success: true, data: metrics); + } + + @override + Future> restart(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().restart(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } + + @override + Future> deleteServer( + final String hostname, + ) async { + final String deletionName = getHostnameFromDomain(hostname); + final serversResult = await getServers(); + try { + final servers = serversResult.data; + ServerBasicInfo? foundServer; + for (final server in servers) { + if (server.name == deletionName) { + foundServer = server; + break; + } + } + + final volumes = await getVolumes(); + final ServerVolume volumeToRemove; + volumeToRemove = volumes.data.firstWhere( + (final el) => el.serverId == foundServer!.id, + ); + + await _adapter.api().detachVolume( + volumeToRemove.name, + volumeToRemove.serverId!, + ); + + await Future.delayed(const Duration(seconds: 10)); + final List laterFutures = []; + laterFutures.add(_adapter.api().deleteVolume(volumeToRemove.uuid!)); + laterFutures.add(_adapter.api().deleteServer(foundServer!.id)); + + await Future.wait(laterFutures); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: null, + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async { + await Future.delayed(const Duration(seconds: 5)); + return deleteServer(hostname); + }, + ), + ], + description: 'modals.try_again'.tr(), + title: 'modals.server_deletion_error'.tr(), + ), + message: e.toString(), + ); + } + + return GenericResult( + success: true, + data: null, + ); + } + + @override + Future>> getVolumes({ + final String? status, + }) async { + final List volumes = []; + + final result = await _adapter.api().getVolumes(); + + if (!result.success || result.data.isEmpty) { + return GenericResult( + data: [], + success: false, + code: result.code, + message: result.message, + ); + } + + try { + int id = 0; + for (final rawVolume in result.data) { + final String volumeName = rawVolume.name; + final volume = ServerVolume( + id: id++, + name: volumeName, + sizeByte: rawVolume.sizeGigabytes * 1024 * 1024 * 1024, + serverId: + (rawVolume.dropletIds != null && rawVolume.dropletIds!.isNotEmpty) + ? rawVolume.dropletIds![0] + : null, + linuxDevice: 'scsi-0DO_Volume_$volumeName', + uuid: rawVolume.id, + ); + volumes.add(volume); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + + return GenericResult( + data: volumes, + success: true, + ); + } + + @override + Future> createVolume() async { + ServerVolume? volume; + + final result = await _adapter.api().createVolume(); + + if (!result.success || result.data == null) { + return GenericResult( + data: null, + success: false, + code: result.code, + message: result.message, + ); + } + + final getVolumesResult = await _adapter.api().getVolumes(); + + if (!getVolumesResult.success || getVolumesResult.data.isEmpty) { + return GenericResult( + data: null, + success: false, + code: result.code, + message: result.message, + ); + } + + final String volumeName = result.data!.name; + volume = ServerVolume( + id: getVolumesResult.data.length, + name: volumeName, + sizeByte: result.data!.sizeGigabytes, + serverId: null, + linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName', + uuid: result.data!.id, + ); + + return GenericResult( + data: volume, + success: true, + ); + } + + Future> getVolume( + final String volumeUuid, + ) async { + ServerVolume? requestedVolume; + + final result = await getVolumes(); + + if (!result.success || result.data.isEmpty) { + return GenericResult( + data: null, + success: false, + code: result.code, + message: result.message, + ); + } + + for (final volume in result.data) { + if (volume.uuid == volumeUuid) { + requestedVolume = volume; + } + } + + return GenericResult( + data: requestedVolume, + success: true, + ); + } + + @override + Future> deleteVolume( + final ServerVolume volume, + ) async => + _adapter.api().deleteVolume( + volume.uuid!, + ); + + @override + Future> attachVolume( + final ServerVolume volume, + final int serverId, + ) async => + _adapter.api().attachVolume( + volume.name, + serverId, + ); + + @override + Future> detachVolume( + final ServerVolume volume, + ) async => + _adapter.api().detachVolume( + volume.name, + volume.serverId!, + ); + + @override + Future> resizeVolume( + final ServerVolume volume, + final DiskSize size, + ) async => + _adapter.api().resizeVolume( + volume.name, + size, + ); + + /// Hardcoded on their documentation and there is no pricing API at all + /// Probably we should scrap the doc page manually + @override + Future> getPricePerGb() async => GenericResult( + success: true, + data: Price( + value: 0.10, + currency: currency, + ), + ); + + @override + Future> powerOn(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().powerOn(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } +} diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart new file mode 100644 index 00000000..874f4f59 --- /dev/null +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -0,0 +1,807 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart'; +import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/metrics.dart'; +import 'package:selfprivacy/logic/models/price.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; +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/logic/providers/server_providers/server_provider.dart'; +import 'package:selfprivacy/utils/extensions/string_extensions.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; +import 'package:selfprivacy/utils/password_generator.dart'; + +class ApiAdapter { + ApiAdapter({final String? region, final bool isWithToken = true}) + : _api = HetznerApi( + region: region, + isWithToken: isWithToken, + ); + + HetznerApi api({final bool getInitialized = true}) => getInitialized + ? _api + : HetznerApi( + region: _api.region, + isWithToken: false, + ); + + final HetznerApi _api; +} + +class HetznerServerProvider extends ServerProvider { + HetznerServerProvider() : _adapter = ApiAdapter(); + HetznerServerProvider.load( + final ServerType serverType, + final bool isAuthotized, + ) : _adapter = ApiAdapter( + isWithToken: isAuthotized, + region: serverType.location.identifier, + ); + + ApiAdapter _adapter; + final String currency = 'EUR'; + + @override + ServerProviderType get type => ServerProviderType.hetzner; + + @override + Future> trySetServerLocation( + final String location, + ) async { + final bool apiInitialized = _adapter.api().isWithToken; + if (!apiInitialized) { + return GenericResult( + success: true, + data: false, + message: 'Not authorized!', + ); + } + + _adapter = ApiAdapter( + isWithToken: true, + region: location, + ); + return success; + } + + @override + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } + + _adapter = ApiAdapter(region: api.region, isWithToken: true); + return result; + } + + String? getEmojiFlag(final String query) { + String? emoji; + + switch (query.toLowerCase()) { + case 'de': + emoji = '🇩🇪'; + break; + + case 'fi': + emoji = '🇫🇮'; + break; + + case 'us': + emoji = '🇺🇸'; + break; + } + + return emoji; + } + + @override + Future>> + getAvailableLocations() async { + final List locations = []; + final result = await _adapter.api().getAvailableLocations(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: locations, + code: result.code, + message: result.message, + ); + } + + final List rawLocations = result.data; + for (final rawLocation in rawLocations) { + ServerProviderLocation? location; + try { + location = ServerProviderLocation( + title: rawLocation.city, + description: rawLocation.description, + flag: getEmojiFlag(rawLocation.country), + identifier: rawLocation.name, + ); + } catch (e) { + continue; + } + locations.add(location); + } + + return GenericResult(success: true, data: locations); + } + + @override + Future>> getServerTypes({ + required final ServerProviderLocation location, + }) async { + final List types = []; + final result = await _adapter.api().getAvailableServerTypes(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: types, + code: result.code, + message: result.message, + ); + } + + final rawTypes = result.data; + for (final rawType in rawTypes) { + for (final rawPrice in rawType.prices) { + if (rawPrice.location == location.identifier) { + types.add( + ServerType( + title: rawType.description, + identifier: rawType.name, + ram: rawType.memory.toDouble(), + cores: rawType.cores, + disk: DiskSize(byte: rawType.disk * 1024 * 1024 * 1024), + price: Price( + value: rawPrice.monthly, + currency: currency, + ), + location: location, + ), + ); + } + } + } + + return GenericResult(success: true, data: types); + } + + @override + Future>> getServers() async { + final List servers = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: servers, + code: result.code, + message: result.message, + ); + } + + final List hetznerServers = result.data; + for (final hetznerServer in hetznerServers) { + if (hetznerServer.publicNet.ipv4 == null) { + continue; + } + + ServerBasicInfo? server; + try { + server = ServerBasicInfo( + id: hetznerServer.id, + name: hetznerServer.name, + ip: hetznerServer.publicNet.ipv4!.ip, + reverseDns: hetznerServer.publicNet.ipv4!.reverseDns, + created: hetznerServer.created, + ); + } catch (e) { + continue; + } + + servers.add(server); + } + + return GenericResult(success: true, data: servers); + } + + @override + Future>> getMetadata( + final int serverId, + ) async { + List metadata = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: false, + data: metadata, + code: result.code, + message: result.message, + ); + } + + final List servers = result.data; + try { + final HetznerServerInfo server = servers.firstWhere( + (final server) => server.id == serverId, + ); + + metadata = [ + ServerMetadataEntity( + type: MetadataType.id, + trId: 'server.server_id', + value: server.id.toString(), + ), + ServerMetadataEntity( + type: MetadataType.status, + trId: 'server.status', + value: server.status.toString().split('.')[1].capitalize(), + ), + ServerMetadataEntity( + type: MetadataType.cpu, + trId: 'server.cpu', + value: server.serverType.cores.toString(), + ), + ServerMetadataEntity( + type: MetadataType.ram, + trId: 'server.ram', + value: '${server.serverType.memory.toString()} GB', + ), + ServerMetadataEntity( + type: MetadataType.cost, + trId: 'server.monthly_cost', + value: + '${server.serverType.prices[1].monthly.toStringAsFixed(2)} $currency', + ), + ServerMetadataEntity( + type: MetadataType.location, + trId: 'server.location', + value: '${server.location.city}, ${server.location.country}', + ), + ServerMetadataEntity( + type: MetadataType.other, + trId: 'server.provider', + value: _adapter.api().displayProviderName, + ), + ]; + } catch (e) { + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } + + return GenericResult(success: true, data: metadata); + } + + @override + Future> getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + ServerMetrics? metrics; + + List serializeTimeSeries( + final Map json, + final String type, + ) { + final List list = json['time_series'][type]['values']; + return list + .map((final el) => TimeSeriesData(el[0], double.parse(el[1]))) + .toList(); + } + + final cpuResult = await _adapter.api().getMetrics( + serverId, + start, + end, + 'cpu', + ); + + if (cpuResult.data.isEmpty || !cpuResult.success) { + return GenericResult( + success: false, + data: metrics, + code: cpuResult.code, + message: cpuResult.message, + ); + } + + final netResult = await _adapter.api().getMetrics( + serverId, + start, + end, + 'network', + ); + + if (cpuResult.data.isEmpty || !netResult.success) { + return GenericResult( + success: false, + data: metrics, + code: netResult.code, + message: netResult.message, + ); + } + + metrics = ServerMetrics( + cpu: serializeTimeSeries( + cpuResult.data, + 'cpu', + ), + bandwidthIn: serializeTimeSeries( + netResult.data, + 'network.0.bandwidth.in', + ), + bandwidthOut: serializeTimeSeries( + netResult.data, + 'network.0.bandwidth.out', + ), + end: end, + start: start, + stepsInSecond: cpuResult.data['step'], + ); + + return GenericResult(data: metrics, success: true); + } + + @override + Future> restart(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().restart(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } + + @override + Future> powerOn(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().powerOn(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } + + String dnsProviderToInfectName(final DnsProviderType dnsProvider) { + String dnsProviderType; + switch (dnsProvider) { + case DnsProviderType.digitalOcean: + dnsProviderType = 'DIGITALOCEAN'; + break; + case DnsProviderType.desec: + dnsProviderType = 'DESEC'; + break; + case DnsProviderType.cloudflare: + default: + dnsProviderType = 'CLOUDFLARE'; + break; + } + return dnsProviderType; + } + + @override + Future> launchInstallation( + final LaunchInstallationData installationData, + ) async { + final volumeResult = await _adapter.api().createVolume(); + + if (!volumeResult.success || volumeResult.data == null) { + return GenericResult( + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: () async => await installationData.errorCallback(), + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async => launchInstallation(installationData), + ), + ], + description: + volumeResult.message ?? 'modals.volume_creation_error'.tr(), + title: 'modals.unexpected_error'.tr(), + ), + success: false, + message: volumeResult.message, + code: volumeResult.code, + ); + } + + final volume = volumeResult.data!; + final serverApiToken = StringGenerators.apiToken(); + final hostname = getHostnameFromDomain( + installationData.serverDomain.domainName, + ); + + final serverResult = await _adapter.api().createServer( + dnsApiToken: installationData.dnsApiToken, + rootUser: installationData.rootUser, + domainName: installationData.serverDomain.domainName, + serverType: installationData.serverTypeId, + dnsProviderType: + dnsProviderToInfectName(installationData.dnsProviderType), + hostName: hostname, + volumeId: volume.id, + base64Password: base64.encode( + utf8.encode(installationData.rootUser.password ?? 'PASS'), + ), + databasePassword: StringGenerators.dbPassword(), + serverApiToken: serverApiToken, + ); + + if (!serverResult.success || serverResult.data == null) { + await _adapter.api().deleteVolume(volume.id); + await Future.delayed(const Duration(seconds: 5)); + if (serverResult.message != null && + serverResult.message == 'uniqueness_error') { + return GenericResult( + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: () async => installationData.errorCallback(), + ), + CallbackDialogueChoice( + title: 'modals.yes'.tr(), + callback: () async { + final deleting = await deleteServer(hostname); + if (deleting.success) { + return launchInstallation(installationData); + } + + return deleting; + }, + ), + ], + description: 'modals.destroy_server'.tr(), + title: 'modals.already_exists'.tr(), + ), + success: false, + message: volumeResult.message, + code: volumeResult.code, + ); + } else { + return GenericResult( + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: () async { + final deletion = await deleteServer(hostname); + if (deletion.success) { + installationData.errorCallback(); + } + + return deletion; + }, + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async => launchInstallation(installationData), + ), + ], + description: + volumeResult.message ?? 'recovering.generic_error'.tr(), + title: 'modals.unexpected_error'.tr(), + ), + success: false, + message: volumeResult.message, + code: volumeResult.code, + ); + } + } + + final serverDetails = ServerHostingDetails( + id: serverResult.data!.id, + ip4: serverResult.data!.publicNet.ipv4!.ip, + createTime: DateTime.now(), + volume: ServerVolume( + id: volume.id, + name: volume.name, + sizeByte: volume.size * 1024 * 1024 * 1024, + serverId: volume.serverId, + linuxDevice: volume.linuxDevice, + ), + apiToken: serverApiToken, + provider: ServerProviderType.hetzner, + ); + + final createDnsResult = await _adapter.api().createReverseDns( + serverId: serverDetails.id, + ip4: serverDetails.ip4, + dnsPtr: installationData.serverDomain.domainName, + ); + + if (!createDnsResult.success) { + return GenericResult( + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: () async { + final deletion = await deleteServer(hostname); + if (deletion.success) { + installationData.errorCallback(); + } + + return deletion; + }, + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async { + await _adapter.api().deleteVolume(volume.id); + await Future.delayed(const Duration(seconds: 5)); + final deletion = await deleteServer(hostname); + if (deletion.success) { + await Future.delayed(const Duration(seconds: 5)); + return launchInstallation(installationData); + } + + return deletion; + }, + ), + ], + description: volumeResult.message ?? 'recovering.generic_error'.tr(), + title: 'modals.unexpected_error'.tr(), + ), + success: false, + message: volumeResult.message, + code: volumeResult.code, + ); + } + + await installationData.successCallback(serverDetails); + return GenericResult(success: true, data: null); + } + + @override + Future> deleteServer( + final String hostname, + ) async { + final serversResult = await _adapter.api().getServers(); + try { + final servers = serversResult.data; + HetznerServerInfo? foundServer; + for (final server in servers) { + if (server.name == hostname) { + foundServer = server; + break; + } + } + + for (final volumeId in foundServer!.volumes) { + await _adapter.api().detachVolume(volumeId); + } + + await Future.delayed(const Duration(seconds: 10)); + final List laterFutures = []; + + for (final volumeId in foundServer.volumes) { + laterFutures.add(_adapter.api().deleteVolume(volumeId)); + } + laterFutures.add(_adapter.api().deleteServer(serverId: foundServer.id)); + + await Future.wait(laterFutures); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: null, + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async { + await Future.delayed(const Duration(seconds: 5)); + return deleteServer(hostname); + }, + ), + ], + description: 'modals.try_again'.tr(), + title: 'modals.server_deletion_error'.tr(), + ), + message: e.toString(), + ); + } + + return GenericResult( + success: true, + data: null, + ); + } + + @override + Future> createVolume() async { + ServerVolume? volume; + + final result = await _adapter.api().createVolume(); + + if (!result.success || result.data == null) { + return GenericResult( + data: null, + success: false, + message: result.message, + code: result.code, + ); + } + + try { + volume = ServerVolume( + id: result.data!.id, + name: result.data!.name, + sizeByte: result.data!.size * 1024 * 1024 * 1024, + serverId: result.data!.serverId, + linuxDevice: result.data!.linuxDevice, + ); + } catch (e) { + print(e); + return GenericResult( + data: null, + success: false, + message: e.toString(), + ); + } + + return GenericResult( + data: volume, + success: true, + code: result.code, + message: result.message, + ); + } + + @override + Future>> getVolumes({ + final String? status, + }) async { + final List volumes = []; + + final result = await _adapter.api().getVolumes(); + + if (!result.success) { + return GenericResult( + data: [], + success: false, + message: result.message, + code: result.code, + ); + } + + try { + for (final rawVolume in result.data) { + final int volumeId = rawVolume.id; + final int volumeSize = rawVolume.size * 1024 * 1024 * 1024; + final volumeServer = rawVolume.serverId; + final String volumeName = rawVolume.name; + final volumeDevice = rawVolume.linuxDevice; + final volume = ServerVolume( + id: volumeId, + name: volumeName, + sizeByte: volumeSize, + serverId: volumeServer, + linuxDevice: volumeDevice, + ); + volumes.add(volume); + } + } catch (e) { + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + + return GenericResult( + data: volumes, + success: true, + code: result.code, + message: result.message, + ); + } + + @override + Future> deleteVolume(final ServerVolume volume) async => + _adapter.api().deleteVolume(volume.id); + + @override + Future> attachVolume( + final ServerVolume volume, + final int serverId, + ) async => + _adapter.api().attachVolume( + HetznerVolume( + volume.id, + volume.sizeByte, + volume.serverId, + volume.name, + volume.linuxDevice, + ), + serverId, + ); + + @override + Future> detachVolume( + final ServerVolume volume, + ) async => + _adapter.api().detachVolume( + volume.id, + ); + + @override + Future> resizeVolume( + final ServerVolume volume, + final DiskSize size, + ) async => + _adapter.api().resizeVolume( + HetznerVolume( + volume.id, + volume.sizeByte, + volume.serverId, + volume.name, + volume.linuxDevice, + ), + size, + ); + + @override + Future> getPricePerGb() async { + final result = await _adapter.api().getPricePerGb(); + + if (!result.success || result.data == null) { + return GenericResult( + data: null, + success: false, + message: result.message, + code: result.code, + ); + } + + return GenericResult( + success: true, + data: Price( + value: result.data!, + currency: currency, + ), + ); + } +} diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart new file mode 100644 index 00000000..b48662bf --- /dev/null +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -0,0 +1,57 @@ +import 'package:selfprivacy/logic/api_maps/generic_result.dart'; +import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/launch_installation_data.dart'; +import 'package:selfprivacy/logic/models/metrics.dart'; +import 'package:selfprivacy/logic/models/price.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; +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'; + +export 'package:selfprivacy/logic/api_maps/generic_result.dart'; +export 'package:selfprivacy/logic/models/launch_installation_data.dart'; + +abstract class ServerProvider { + ServerProviderType get type; + Future>> getServers(); + Future> trySetServerLocation(final String location); + Future> tryInitApiByToken(final String token); + Future>> getAvailableLocations(); + Future>> getServerTypes({ + required final ServerProviderLocation location, + }); + + Future> deleteServer( + final String hostname, + ); + Future> launchInstallation( + final LaunchInstallationData installationData, + ); + Future> powerOn(final int serverId); + Future> restart(final int serverId); + Future> getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ); + + Future> getPricePerGb(); + Future>> getVolumes({final String? status}); + Future> createVolume(); + Future> deleteVolume(final ServerVolume volume); + Future> resizeVolume( + final ServerVolume volume, + final DiskSize size, + ); + Future> attachVolume( + final ServerVolume volume, + final int serverId, + ); + Future> detachVolume(final ServerVolume volume); + Future>> getMetadata( + final int serverId, + ); + GenericResult get success => GenericResult(success: true, data: true); +} diff --git a/lib/logic/providers/server_providers/server_provider_factory.dart b/lib/logic/providers/server_providers/server_provider_factory.dart new file mode 100644 index 00000000..786b1c02 --- /dev/null +++ b/lib/logic/providers/server_providers/server_provider_factory.dart @@ -0,0 +1,25 @@ +import 'package:selfprivacy/logic/providers/provider_settings.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/providers/server_providers/server_provider.dart'; +import 'package:selfprivacy/logic/providers/server_providers/digital_ocean.dart'; +import 'package:selfprivacy/logic/providers/server_providers/hetzner.dart'; + +class UnknownProviderException implements Exception { + UnknownProviderException(this.message); + final String message; +} + +class ServerProviderFactory { + static ServerProvider createServerProviderInterface( + final ServerProviderSettings settings, + ) { + switch (settings.provider) { + case ServerProviderType.hetzner: + return HetznerServerProvider(); + case ServerProviderType.digitalOcean: + return DigitalOceanServerProvider(); + case ServerProviderType.unknown: + throw UnknownProviderException('Unknown server provider'); + } + } +} diff --git a/lib/ui/components/storage_list_items/service_migration_list_item.dart b/lib/ui/components/storage_list_items/service_migration_list_item.dart index 8eee284c..5229658d 100644 --- a/lib/ui/components/storage_list_items/service_migration_list_item.dart +++ b/lib/ui/components/storage_list_items/service_migration_list_item.dart @@ -72,7 +72,10 @@ class ServiceConsumptionTitle extends StatelessWidget { service.svgIcon, width: 24.0, height: 24.0, - color: Theme.of(context).colorScheme.onBackground, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn, + ), ), ), const SizedBox(width: 16), diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index 5efc4164..98e567a9 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -2,12 +2,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/ui/components/cards/filled_card.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import 'package:selfprivacy/utils/network_utils.dart'; @RoutePage() class DnsDetailsPage extends StatefulWidget { @@ -111,9 +111,12 @@ class _DnsDetailsPageState extends State { heroIcon: BrandIcons.globe, heroTitle: 'domain.screen_title'.tr(), children: [ - _getStateCard(dnsCubit.dnsState, () { - context.read().fix(); - }), + _getStateCard( + dnsCubit.dnsState, + () { + context.read().fix(); + }, + ), const SizedBox(height: 16.0), ListTile( title: Text( @@ -152,7 +155,7 @@ class _DnsDetailsPageState extends State { dnsRecord.description.tr(), ), subtitle: Text( - dnsRecord.name, + dnsRecord.displayName ?? dnsRecord.name, ), ), ], diff --git a/lib/ui/pages/more/app_settings/developer_settings.dart b/lib/ui/pages/more/app_settings/developer_settings.dart index 220cb791..c2a34916 100644 --- a/lib/ui/pages/more/app_settings/developer_settings.dart +++ b/lib/ui/pages/more/app_settings/developer_settings.dart @@ -1,6 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/api_maps/staging_options.dart'; +import 'package:selfprivacy/logic/api_maps/tls_options.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; @@ -37,8 +37,19 @@ class _DeveloperSettingsPageState extends State { title: Text('developer_settings.use_staging_acme'.tr()), subtitle: Text('developer_settings.use_staging_acme_description'.tr()), - value: StagingOptions.stagingAcme, - onChanged: null, + value: TlsOptions.stagingAcme, + onChanged: (final bool value) => setState( + () => TlsOptions.stagingAcme = value, + ), + ), + SwitchListTile( + title: Text('developer_settings.ignore_tls'.tr()), + subtitle: + Text('developer_settings.ignore_tls_description'.tr()), + value: TlsOptions.verifyCertificate, + onChanged: (final bool value) => setState( + () => TlsOptions.verifyCertificate = value, + ), ), Padding( padding: const EdgeInsets.all(16), diff --git a/lib/ui/pages/more/console.dart b/lib/ui/pages/more/console.dart index 95670475..f338b580 100644 --- a/lib/ui/pages/more/console.dart +++ b/lib/ui/pages/more/console.dart @@ -49,7 +49,8 @@ class _ConsolePageState extends State { actions: [ IconButton( icon: Icon( - paused ? Icons.play_arrow_outlined : Icons.pause_outlined), + paused ? Icons.play_arrow_outlined : Icons.pause_outlined, + ), onPressed: () => setState(() => paused = !paused), ), ], diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index 03126ba5..2fef5440 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -26,7 +26,7 @@ class _TextDetails extends StatelessWidget { ...details.metadata.map( (final metadata) => ListTileOnSurfaceVariant( leadingIcon: metadata.type.icon, - title: metadata.name, + title: metadata.trId.tr(), subtitle: metadata.value, ), ), diff --git a/lib/ui/pages/server_storage/server_storage.dart b/lib/ui/pages/server_storage/server_storage.dart index e3391070..eb7a586b 100644 --- a/lib/ui/pages/server_storage/server_storage.dart +++ b/lib/ui/pages/server_storage/server_storage.dart @@ -133,7 +133,10 @@ class ServerConsumptionListTile extends StatelessWidget { service.svgIcon, width: 24.0, height: 24.0, - color: Theme.of(context).colorScheme.onBackground, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn, + ), ), rightSideText: service.storageUsage.used.toString(), percentage: service.storageUsage.used.byte / volume.sizeTotal.byte, diff --git a/lib/ui/pages/setup/initializing/dns_provider_picker.dart b/lib/ui/pages/setup/initializing/dns_provider_picker.dart index 7e494af4..a05b1233 100644 --- a/lib/ui/pages/setup/initializing/dns_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/dns_provider_picker.dart @@ -2,14 +2,15 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; -import 'package:selfprivacy/utils/launch_url.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class DnsProviderPicker extends StatefulWidget { const DnsProviderPicker({ @@ -26,9 +27,9 @@ class DnsProviderPicker extends StatefulWidget { } class _DnsProviderPickerState extends State { - DnsProvider selectedProvider = DnsProvider.unknown; + DnsProviderType selectedProvider = DnsProviderType.unknown; - void setProvider(final DnsProvider provider) { + void setProvider(final DnsProviderType provider) { setState(() { selectedProvider = provider; }); @@ -37,35 +38,36 @@ class _DnsProviderPickerState extends State { @override Widget build(final BuildContext context) { switch (selectedProvider) { - case DnsProvider.unknown: + case DnsProviderType.unknown: return ProviderSelectionPage( serverInstallationCubit: widget.serverInstallationCubit, callback: setProvider, ); - case DnsProvider.cloudflare: + case DnsProviderType.cloudflare: return ProviderInputDataPage( providerCubit: widget.formCubit, - providerInfo: ProviderPageInfo( - providerType: DnsProvider.cloudflare, + providerInfo: const ProviderPageInfo( + providerType: DnsProviderType.cloudflare, pathToHow: 'how_cloudflare', - image: Image.asset( - 'assets/images/logos/cloudflare.svg', - width: 150, - ), ), ); - case DnsProvider.desec: + case DnsProviderType.digitalOcean: return ProviderInputDataPage( providerCubit: widget.formCubit, - providerInfo: ProviderPageInfo( - providerType: DnsProvider.desec, + providerInfo: const ProviderPageInfo( + providerType: DnsProviderType.digitalOcean, + pathToHow: 'how_digital_ocean_dns', + ), + ); + + case DnsProviderType.desec: + return ProviderInputDataPage( + providerCubit: widget.formCubit, + providerInfo: const ProviderPageInfo( + providerType: DnsProviderType.desec, pathToHow: 'how_desec', - image: Image.asset( - 'assets/images/logos/desec.svg', - width: 150, - ), ), ); } @@ -76,12 +78,10 @@ class ProviderPageInfo { const ProviderPageInfo({ required this.providerType, required this.pathToHow, - required this.image, }); final String pathToHow; - final Image image; - final DnsProvider providerType; + final DnsProviderType providerType; } class ProviderInputDataPage extends StatelessWidget { @@ -99,7 +99,7 @@ class ProviderInputDataPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'initializing.cloudflare_api_token'.tr(), + 'initializing.connect_to_dns'.tr(), style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 16), @@ -124,12 +124,22 @@ class ProviderInputDataPage extends StatelessWidget { const SizedBox(height: 10), BrandOutlinedButton( child: Text('initializing.how'.tr()), - onPressed: () { - context.read().showArticle( - article: providerInfo.pathToHow, - context: context, - ); - }, + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + BrandMarkdown( + fileName: providerInfo.pathToHow, + ), + ], + ), + ), + ), ), ], ); @@ -150,6 +160,8 @@ class ProviderSelectionPage extends StatelessWidget { width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, + + /// TODO: Remove obvious repetition children: [ Text( 'initializing.select_dns'.tr(), @@ -202,13 +214,13 @@ class ProviderSelectionPage extends StatelessWidget { text: 'basis.select'.tr(), onPressed: () { serverInstallationCubit - .setDnsProviderType(DnsProvider.desec); - callback(DnsProvider.desec); + .setDnsProviderType(DnsProviderType.desec); + callback(DnsProviderType.desec); }, ), // Outlined button that will open website BrandOutlinedButton( - onPressed: () => launchURL('https://desec.io/'), + onPressed: () => launchUrlString('https://desec.io/'), title: 'initializing.select_provider_site_button'.tr(), ), ], @@ -257,14 +269,70 @@ class ProviderSelectionPage extends StatelessWidget { text: 'basis.select'.tr(), onPressed: () { serverInstallationCubit - .setDnsProviderType(DnsProvider.cloudflare); - callback(DnsProvider.cloudflare); + .setDnsProviderType(DnsProviderType.cloudflare); + callback(DnsProviderType.cloudflare); }, ), // Outlined button that will open website BrandOutlinedButton( onPressed: () => - launchURL('https://dash.cloudflare.com/'), + launchUrlString('https://dash.cloudflare.com/'), + title: 'initializing.select_provider_site_button'.tr(), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + OutlinedCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: const Color.fromARGB(255, 1, 126, 251), + ), + child: SvgPicture.asset( + 'assets/images/logos/digital_ocean.svg', + ), + ), + const SizedBox(width: 16), + Text( + 'Digital Ocean', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_price_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_price_free'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + BrandButton.rised( + text: 'basis.select'.tr(), + onPressed: () { + serverInstallationCubit + .setDnsProviderType(DnsProviderType.digitalOcean); + callback(DnsProviderType.digitalOcean); + }, + ), + // Outlined button that will open website + BrandOutlinedButton( + onPressed: () => + launchUrlString('https://cloud.digitalocean.com/'), title: 'initializing.select_provider_site_button'.tr(), ), ], diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index a9f9f337..c74fa4a4 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; @@ -211,10 +211,11 @@ class InitializingPage extends StatelessWidget { final ServerInstallationCubit serverInstallationCubit, ) => BlocProvider( - create: (final context) => ProviderFormCubit(serverInstallationCubit), + create: (final context) => + ServerProviderFormCubit(serverInstallationCubit), child: Builder( builder: (final context) { - final providerCubit = context.watch(); + final providerCubit = context.watch(); return ServerProviderPicker( formCubit: providerCubit, serverInstallationCubit: serverInstallationCubit, @@ -227,7 +228,8 @@ class InitializingPage extends StatelessWidget { final ServerInstallationCubit serverInstallationCubit, ) => BlocProvider( - create: (final context) => ProviderFormCubit(serverInstallationCubit), + create: (final context) => + ServerProviderFormCubit(serverInstallationCubit), child: Builder( builder: (final context) => ServerTypePicker( serverInstallationCubit: serverInstallationCubit, diff --git a/lib/ui/pages/setup/initializing/server_provider_picker.dart b/lib/ui/pages/setup/initializing/server_provider_picker.dart index 1257cc75..bde1435c 100644 --- a/lib/ui/pages/setup/initializing/server_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/server_provider_picker.dart @@ -3,7 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; @@ -20,7 +20,7 @@ class ServerProviderPicker extends StatefulWidget { super.key, }); - final ProviderFormCubit formCubit; + final ServerProviderFormCubit formCubit; final ServerInstallationCubit serverInstallationCubit; @override @@ -28,9 +28,9 @@ class ServerProviderPicker extends StatefulWidget { } class _ServerProviderPickerState extends State { - ServerProvider selectedProvider = ServerProvider.unknown; + ServerProviderType selectedProvider = ServerProviderType.unknown; - void setProvider(final ServerProvider provider) { + void setProvider(final ServerProviderType provider) { setState(() { selectedProvider = provider; }); @@ -39,17 +39,17 @@ class _ServerProviderPickerState extends State { @override Widget build(final BuildContext context) { switch (selectedProvider) { - case ServerProvider.unknown: + case ServerProviderType.unknown: return ProviderSelectionPage( serverInstallationCubit: widget.serverInstallationCubit, callback: setProvider, ); - case ServerProvider.hetzner: + case ServerProviderType.hetzner: return ProviderInputDataPage( providerCubit: widget.formCubit, providerInfo: ProviderPageInfo( - providerType: ServerProvider.hetzner, + providerType: ServerProviderType.hetzner, pathToHow: 'how_hetzner', image: Image.asset( 'assets/images/logos/hetzner.png', @@ -58,11 +58,11 @@ class _ServerProviderPickerState extends State { ), ); - case ServerProvider.digitalOcean: + case ServerProviderType.digitalOcean: return ProviderInputDataPage( providerCubit: widget.formCubit, providerInfo: ProviderPageInfo( - providerType: ServerProvider.digitalOcean, + providerType: ServerProviderType.digitalOcean, pathToHow: 'how_digital_ocean', image: Image.asset( 'assets/images/logos/digital_ocean.png', @@ -83,7 +83,7 @@ class ProviderPageInfo { final String pathToHow; final Image image; - final ServerProvider providerType; + final ServerProviderType providerType; } class ProviderInputDataPage extends StatelessWidget { @@ -94,7 +94,7 @@ class ProviderInputDataPage extends StatelessWidget { }); final ProviderPageInfo providerInfo; - final ProviderFormCubit providerCubit; + final ServerProviderFormCubit providerCubit; @override Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox( @@ -238,9 +238,10 @@ class ProviderSelectionPage extends StatelessWidget { BrandButton.filled( child: Text('basis.select'.tr()), onPressed: () { - serverInstallationCubit - .setServerProviderType(ServerProvider.hetzner); - callback(ServerProvider.hetzner); + serverInstallationCubit.setServerProviderType( + ServerProviderType.hetzner, + ); + callback(ServerProviderType.hetzner); }, ), // Outlined button that will open website @@ -313,9 +314,9 @@ class ProviderSelectionPage extends StatelessWidget { child: Text('basis.select'.tr()), onPressed: () { serverInstallationCubit.setServerProviderType( - ServerProvider.digitalOcean, + ServerProviderType.digitalOcean, ); - callback(ServerProvider.digitalOcean); + callback(ServerProviderType.digitalOcean); }, ), // Outlined button that will open website diff --git a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart index 93d0eb87..82c29ba2 100644 --- a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart @@ -1,6 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; @@ -16,52 +16,48 @@ class RecoveryServerProviderConnected extends StatelessWidget { context.watch(); return BlocProvider( - create: (final BuildContext context) => ProviderFormCubit(appConfig), + create: (final BuildContext context) => + ServerProviderFormCubit(appConfig), child: Builder( - builder: (final BuildContext context) { - final FormCubitState formCubitState = - context.watch().state; - - return BrandHeroScreen( - heroTitle: 'recovering.server_provider_connected'.tr(), - heroSubtitle: 'recovering.server_provider_connected_description'.tr( - args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], + builder: (final BuildContext context) => BrandHeroScreen( + heroTitle: 'recovering.server_provider_connected'.tr(), + heroSubtitle: 'recovering.server_provider_connected_description'.tr( + args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], + ), + hasBackButton: true, + hasFlashButton: false, + ignoreBreakpoints: true, + hasSupportDrawer: true, + onBackButtonPressed: () { + Navigator.of(context).popUntil((final route) => route.isFirst); + }, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: + 'recovering.server_provider_connected_placeholder'.tr(), + ), ), - hasBackButton: true, - hasFlashButton: false, - ignoreBreakpoints: true, - hasSupportDrawer: true, - onBackButtonPressed: () { - Navigator.of(context).popUntil((final route) => route.isFirst); - }, - children: [ - CubitFormTextField( - formFieldCubit: context.read().apiKey, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: - 'recovering.server_provider_connected_placeholder'.tr(), - ), + const SizedBox(height: 16), + BrandButton.filled( + onPressed: () => + context.read().trySubmit(), + child: Text('basis.continue'.tr()), + ), + const SizedBox(height: 16), + Builder( + builder: (final context) => BrandButton.text( + title: 'initializing.how'.tr(), + onPressed: () => context.read().showArticle( + article: 'how_hetzner', + context: context, + ), ), - const SizedBox(height: 16), - BrandButton.filled( - onPressed: () => context.read().trySubmit(), - child: Text('basis.continue'.tr()), - ), - const SizedBox(height: 16), - Builder( - builder: (final context) => BrandButton.text( - title: 'initializing.how'.tr(), - onPressed: () => - context.read().showArticle( - article: 'how_hetzner', - context: context, - ), - ), - ), - ], - ); - }, + ), + ], + ), ), ); } diff --git a/lib/ui/router/router.gr.dart b/lib/ui/router/router.gr.dart index a4e919b6..6fbfd79b 100644 --- a/lib/ui/router/router.gr.dart +++ b/lib/ui/router/router.gr.dart @@ -15,52 +15,16 @@ abstract class _$RootRouter extends RootStackRouter { @override final Map pagesMap = { - AppSettingsRoute.name: (routeData) { + BackupDetailsRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: const AppSettingsPage(), + child: const BackupDetailsPage(), ); }, - DeveloperSettingsRoute.name: (routeData) { + RootRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: const DeveloperSettingsPage(), - ); - }, - ConsoleRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ConsolePage(), - ); - }, - MoreRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const MorePage(), - ); - }, - AboutApplicationRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AboutApplicationPage(), - ); - }, - OnboardingRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const OnboardingPage(), - ); - }, - ProvidersRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ProvidersPage(), - ); - }, - ServerDetailsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ServerDetailsScreen(), + child: WrappedRoute(child: const RootPage()), ); }, ServiceRoute.name: (routeData) { @@ -79,6 +43,12 @@ abstract class _$RootRouter extends RootStackRouter { child: const ServicesPage(), ); }, + ServerDetailsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ServerDetailsScreen(), + ); + }, UsersRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -101,10 +71,46 @@ abstract class _$RootRouter extends RootStackRouter { ), ); }, - BackupDetailsRoute.name: (routeData) { + AppSettingsRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: const BackupDetailsPage(), + child: const AppSettingsPage(), + ); + }, + DeveloperSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const DeveloperSettingsPage(), + ); + }, + MoreRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const MorePage(), + ); + }, + AboutApplicationRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const AboutApplicationPage(), + ); + }, + ConsoleRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ConsolePage(), + ); + }, + ProvidersRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ProvidersPage(), + ); + }, + RecoveryKeyRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const RecoveryKeyPage(), ); }, DnsDetailsRoute.name: (routeData) { @@ -125,26 +131,12 @@ abstract class _$RootRouter extends RootStackRouter { child: const InitializingPage(), ); }, - RecoveryKeyRoute.name: (routeData) { + ServerStorageRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const RecoveryKeyPage(), - ); - }, - DevicesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const DevicesScreen(), - ); - }, - ServicesMigrationRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: ServicesMigrationPage( - services: args.services, + child: ServerStoragePage( diskStatus: args.diskStatus, - isMigration: args.isMigration, key: args.key, ), ); @@ -160,133 +152,57 @@ abstract class _$RootRouter extends RootStackRouter { ), ); }, - ServerStorageRoute.name: (routeData) { - final args = routeData.argsAs(); + ServicesMigrationRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ServerStoragePage( + child: ServicesMigrationPage( + services: args.services, diskStatus: args.diskStatus, + isMigration: args.isMigration, key: args.key, ), ); }, - RootRoute.name: (routeData) { + DevicesRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: WrappedRoute(child: const RootPage()), + child: const DevicesScreen(), + ); + }, + OnboardingRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const OnboardingPage(), ); }, }; } /// generated route for -/// [AppSettingsPage] -class AppSettingsRoute extends PageRouteInfo { - const AppSettingsRoute({List? children}) +/// [BackupDetailsPage] +class BackupDetailsRoute extends PageRouteInfo { + const BackupDetailsRoute({List? children}) : super( - AppSettingsRoute.name, + BackupDetailsRoute.name, initialChildren: children, ); - static const String name = 'AppSettingsRoute'; + static const String name = 'BackupDetailsRoute'; static const PageInfo page = PageInfo(name); } /// generated route for -/// [DeveloperSettingsPage] -class DeveloperSettingsRoute extends PageRouteInfo { - const DeveloperSettingsRoute({List? children}) +/// [RootPage] +class RootRoute extends PageRouteInfo { + const RootRoute({List? children}) : super( - DeveloperSettingsRoute.name, + RootRoute.name, initialChildren: children, ); - static const String name = 'DeveloperSettingsRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [ConsolePage] -class ConsoleRoute extends PageRouteInfo { - const ConsoleRoute({List? children}) - : super( - ConsoleRoute.name, - initialChildren: children, - ); - - static const String name = 'ConsoleRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [MorePage] -class MoreRoute extends PageRouteInfo { - const MoreRoute({List? children}) - : super( - MoreRoute.name, - initialChildren: children, - ); - - static const String name = 'MoreRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [AboutApplicationPage] -class AboutApplicationRoute extends PageRouteInfo { - const AboutApplicationRoute({List? children}) - : super( - AboutApplicationRoute.name, - initialChildren: children, - ); - - static const String name = 'AboutApplicationRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [OnboardingPage] -class OnboardingRoute extends PageRouteInfo { - const OnboardingRoute({List? children}) - : super( - OnboardingRoute.name, - initialChildren: children, - ); - - static const String name = 'OnboardingRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [ProvidersPage] -class ProvidersRoute extends PageRouteInfo { - const ProvidersRoute({List? children}) - : super( - ProvidersRoute.name, - initialChildren: children, - ); - - static const String name = 'ProvidersRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [ServerDetailsScreen] -class ServerDetailsRoute extends PageRouteInfo { - const ServerDetailsRoute({List? children}) - : super( - ServerDetailsRoute.name, - initialChildren: children, - ); - - static const String name = 'ServerDetailsRoute'; + static const String name = 'RootRoute'; static const PageInfo page = PageInfo(name); } @@ -343,6 +259,20 @@ class ServicesRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [ServerDetailsScreen] +class ServerDetailsRoute extends PageRouteInfo { + const ServerDetailsRoute({List? children}) + : super( + ServerDetailsRoute.name, + initialChildren: children, + ); + + static const String name = 'ServerDetailsRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [UsersPage] class UsersRoute extends PageRouteInfo { @@ -410,15 +340,99 @@ class UserDetailsRouteArgs { } /// generated route for -/// [BackupDetailsPage] -class BackupDetailsRoute extends PageRouteInfo { - const BackupDetailsRoute({List? children}) +/// [AppSettingsPage] +class AppSettingsRoute extends PageRouteInfo { + const AppSettingsRoute({List? children}) : super( - BackupDetailsRoute.name, + AppSettingsRoute.name, initialChildren: children, ); - static const String name = 'BackupDetailsRoute'; + static const String name = 'AppSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [DeveloperSettingsPage] +class DeveloperSettingsRoute extends PageRouteInfo { + const DeveloperSettingsRoute({List? children}) + : super( + DeveloperSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'DeveloperSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [MorePage] +class MoreRoute extends PageRouteInfo { + const MoreRoute({List? children}) + : super( + MoreRoute.name, + initialChildren: children, + ); + + static const String name = 'MoreRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [AboutApplicationPage] +class AboutApplicationRoute extends PageRouteInfo { + const AboutApplicationRoute({List? children}) + : super( + AboutApplicationRoute.name, + initialChildren: children, + ); + + static const String name = 'AboutApplicationRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ConsolePage] +class ConsoleRoute extends PageRouteInfo { + const ConsoleRoute({List? children}) + : super( + ConsoleRoute.name, + initialChildren: children, + ); + + static const String name = 'ConsoleRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ProvidersPage] +class ProvidersRoute extends PageRouteInfo { + const ProvidersRoute({List? children}) + : super( + ProvidersRoute.name, + initialChildren: children, + ); + + static const String name = 'ProvidersRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [RecoveryKeyPage] +class RecoveryKeyRoute extends PageRouteInfo { + const RecoveryKeyRoute({List? children}) + : super( + RecoveryKeyRoute.name, + initialChildren: children, + ); + + static const String name = 'RecoveryKeyRoute'; static const PageInfo page = PageInfo(name); } @@ -466,31 +480,84 @@ class InitializingRoute extends PageRouteInfo { } /// generated route for -/// [RecoveryKeyPage] -class RecoveryKeyRoute extends PageRouteInfo { - const RecoveryKeyRoute({List? children}) - : super( - RecoveryKeyRoute.name, +/// [ServerStoragePage] +class ServerStorageRoute extends PageRouteInfo { + ServerStorageRoute({ + required DiskStatus diskStatus, + Key? key, + List? children, + }) : super( + ServerStorageRoute.name, + args: ServerStorageRouteArgs( + diskStatus: diskStatus, + key: key, + ), initialChildren: children, ); - static const String name = 'RecoveryKeyRoute'; + static const String name = 'ServerStorageRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); +} + +class ServerStorageRouteArgs { + const ServerStorageRouteArgs({ + required this.diskStatus, + this.key, + }); + + final DiskStatus diskStatus; + + final Key? key; + + @override + String toString() { + return 'ServerStorageRouteArgs{diskStatus: $diskStatus, key: $key}'; + } } /// generated route for -/// [DevicesScreen] -class DevicesRoute extends PageRouteInfo { - const DevicesRoute({List? children}) - : super( - DevicesRoute.name, +/// [ExtendingVolumePage] +class ExtendingVolumeRoute extends PageRouteInfo { + ExtendingVolumeRoute({ + required DiskVolume diskVolumeToResize, + required DiskStatus diskStatus, + Key? key, + List? children, + }) : super( + ExtendingVolumeRoute.name, + args: ExtendingVolumeRouteArgs( + diskVolumeToResize: diskVolumeToResize, + diskStatus: diskStatus, + key: key, + ), initialChildren: children, ); - static const String name = 'DevicesRoute'; + static const String name = 'ExtendingVolumeRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); +} + +class ExtendingVolumeRouteArgs { + const ExtendingVolumeRouteArgs({ + required this.diskVolumeToResize, + required this.diskStatus, + this.key, + }); + + final DiskVolume diskVolumeToResize; + + final DiskStatus diskStatus; + + final Key? key; + + @override + String toString() { + return 'ExtendingVolumeRouteArgs{diskVolumeToResize: $diskVolumeToResize, diskStatus: $diskStatus, key: $key}'; + } } /// generated route for @@ -542,96 +609,29 @@ class ServicesMigrationRouteArgs { } /// generated route for -/// [ExtendingVolumePage] -class ExtendingVolumeRoute extends PageRouteInfo { - ExtendingVolumeRoute({ - required DiskVolume diskVolumeToResize, - required DiskStatus diskStatus, - Key? key, - List? children, - }) : super( - ExtendingVolumeRoute.name, - args: ExtendingVolumeRouteArgs( - diskVolumeToResize: diskVolumeToResize, - diskStatus: diskStatus, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'ExtendingVolumeRoute'; - - static const PageInfo page = - PageInfo(name); -} - -class ExtendingVolumeRouteArgs { - const ExtendingVolumeRouteArgs({ - required this.diskVolumeToResize, - required this.diskStatus, - this.key, - }); - - final DiskVolume diskVolumeToResize; - - final DiskStatus diskStatus; - - final Key? key; - - @override - String toString() { - return 'ExtendingVolumeRouteArgs{diskVolumeToResize: $diskVolumeToResize, diskStatus: $diskStatus, key: $key}'; - } -} - -/// generated route for -/// [ServerStoragePage] -class ServerStorageRoute extends PageRouteInfo { - ServerStorageRoute({ - required DiskStatus diskStatus, - Key? key, - List? children, - }) : super( - ServerStorageRoute.name, - args: ServerStorageRouteArgs( - diskStatus: diskStatus, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'ServerStorageRoute'; - - static const PageInfo page = - PageInfo(name); -} - -class ServerStorageRouteArgs { - const ServerStorageRouteArgs({ - required this.diskStatus, - this.key, - }); - - final DiskStatus diskStatus; - - final Key? key; - - @override - String toString() { - return 'ServerStorageRouteArgs{diskStatus: $diskStatus, key: $key}'; - } -} - -/// generated route for -/// [RootPage] -class RootRoute extends PageRouteInfo { - const RootRoute({List? children}) +/// [DevicesScreen] +class DevicesRoute extends PageRouteInfo { + const DevicesRoute({List? children}) : super( - RootRoute.name, + DevicesRoute.name, initialChildren: children, ); - static const String name = 'RootRoute'; + static const String name = 'DevicesRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [OnboardingPage] +class OnboardingRoute extends PageRouteInfo { + const OnboardingRoute({List? children}) + : super( + OnboardingRoute.name, + initialChildren: children, + ); + + static const String name = 'OnboardingRoute'; static const PageInfo page = PageInfo(name); } diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index 3b7f9219..a94ecb35 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -1,45 +1,5 @@ import 'package:selfprivacy/logic/models/json/dns_records.dart'; - -enum DnsRecordsCategory { - services, - email, - other, -} - -class DesiredDnsRecord { - const DesiredDnsRecord({ - required this.name, - required this.content, - this.type = 'A', - this.description = '', - this.category = DnsRecordsCategory.services, - this.isSatisfied = false, - }); - - final String name; - final String type; - final String content; - final String description; - final DnsRecordsCategory category; - final bool isSatisfied; - - DesiredDnsRecord copyWith({ - final String? name, - final String? type, - final String? content, - final String? description, - final DnsRecordsCategory? category, - final bool? isSatisfied, - }) => - DesiredDnsRecord( - name: name ?? this.name, - type: type ?? this.type, - content: content ?? this.content, - description: description ?? this.description, - category: category ?? this.category, - isSatisfied: isSatisfied ?? this.isSatisfied, - ); -} +import 'package:url_launcher/url_launcher.dart'; DnsRecord? extractDkimRecord(final List records) { DnsRecord? dkimRecord; @@ -69,3 +29,15 @@ String getHostnameFromDomain(final String domain) { return hostname; } + +void launchURL(final url) async { + try { + final Uri uri = Uri.parse(url); + await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + } catch (e) { + print(e); + } +}