chore: Merge master into backups-rewrite

pull/228/head
NaiJi ✨ 2023-06-19 17:56:00 -03:00
commit f4700965c6
100 changed files with 5934 additions and 4040 deletions

View File

@ -1 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 209.51 94.74"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z"/><path class="cls-1" d="M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z"/></svg> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 209.51 94.74"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z"/><path class="cls-1" d="M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z"/></svg>

Before

Width:  |  Height:  |  Size: 922 B

After

Width:  |  Height:  |  Size: 923 B

View File

@ -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." "no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon."
}, },
"initializing": { "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.", "connect_to_server": "Let's start with a server.",
"select_provider": "Pick any provider from the following list, they all support SelfPrivacy", "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.", "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_storage": "{} GB of system storage",
"choose_server_type_payment_per_month": "{} per month", "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.", "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", "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", "select_dns": "Now let's select a DNS provider",
"manage_domain_dns": "To manage your domain's DNS", "manage_domain_dns": "To manage your domain's DNS",
"use_this_domain": "Use this domain?", "use_this_domain": "Use this domain?",
@ -444,6 +448,7 @@
"modals": { "modals": {
"dns_removal_error": "Couldn't remove DNS records.", "dns_removal_error": "Couldn't remove DNS records.",
"server_deletion_error": "Couldn't delete active server.", "server_deletion_error": "Couldn't delete active server.",
"volume_creation_error": "Couldn't create volume.",
"server_validators_error": "Couldn't fetch available servers.", "server_validators_error": "Couldn't fetch available servers.",
"already_exists": "Such server already exists.", "already_exists": "Such server already exists.",
"unexpected_error": "Unexpected error during placement from the provider side.", "unexpected_error": "Unexpected error during placement from the provider side.",
@ -488,7 +493,7 @@
"required": "Required", "required": "Required",
"already_exist": "Already exists", "already_exist": "Already exists",
"invalid_format": "Invalid format", "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", "invalid_format_ssh": "Must follow the SSH key format",
"root_name": "Cannot be 'root'", "root_name": "Cannot be 'root'",
"length_not_equal": "Length is [], should be {}", "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.", "subtitle": "These settings are for debugging only. Don't change them unless you know what you're doing.",
"server_setup": "Server setup", "server_setup": "Server setup",
"use_staging_acme": "Use staging ACME server", "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", "routing": "App routing",
"reset_onboarding": "Reset onboarding switch", "reset_onboarding": "Reset onboarding switch",
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses" "cubit_statuses": "Cubit loading statuses"
} }
} }

View File

@ -273,6 +273,7 @@
"no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунты. Единая авторизация для всех сервисов ещё не реализована." "no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунты. Единая авторизация для всех сервисов ещё не реализована."
}, },
"initializing": { "initializing": {
"dns_provider_description": "Это позволит связать ваш домен с IP адресом:",
"connect_to_server": "Начнём с сервера.", "connect_to_server": "Начнём с сервера.",
"select_provider": "Ниже подборка провайдеров, которых поддерживает SelfPrivacy", "select_provider": "Ниже подборка провайдеров, которых поддерживает SelfPrivacy",
"select_provider_notice": "Под 'Небольшим сервером' имеется ввиду сервер с двумя потоками процессора и двумя гигабайтами оперативной памяти.", "select_provider_notice": "Под 'Небольшим сервером' имеется ввиду сервер с двумя потоками процессора и двумя гигабайтами оперативной памяти.",
@ -307,8 +308,10 @@
"choose_server_type_storage": "{} GB системного хранилища", "choose_server_type_storage": "{} GB системного хранилища",
"choose_server_type_payment_per_month": "{} в месяц", "choose_server_type_payment_per_month": "{} в месяц",
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...", "no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
"cloudflare_bad_key_error": "API ключ неверен", "dns_provider_bad_key_error": "API ключ неверен",
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
"connect_to_dns": "Подключите DNS провайдера",
"connect_to_dns_provider_text": "С помощью API токена приложение SelfPrivacy настроит DNS записи",
"manage_domain_dns": "Для управления DNS вашего домена", "manage_domain_dns": "Для управления DNS вашего домена",
"use_this_domain": "Используем этот домен?", "use_this_domain": "Используем этот домен?",
"use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом", "use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом",
@ -469,10 +472,10 @@
"required": "Обязательное поле", "required": "Обязательное поле",
"already_exist": "Уже существует", "already_exist": "Уже существует",
"invalid_format": "Неверный формат", "invalid_format": "Неверный формат",
"invalid_format_password": "Должен не содержать пустые символы", "invalid_format_password": "Пароль не должен содержать пробелы",
"invalid_format_ssh": "Должен следовать формату SSH ключей", "invalid_format_ssh": "Должен следовать формату SSH ключей",
"root_name": "Имя пользователя не может быть 'root'", "root_name": "Имя пользователя не может быть 'root'",
"length_not_equal": "Длина строки [], должна быть равна {}", "length_not_equal": "Длина строки [], должна быть равна {}",
"length_longer": "Длина строки [], должна быть меньше либо равна {}" "length_longer": "Длина строки [], должна быть меньше либо равна {}"
} }
} }

View File

@ -15,13 +15,12 @@ class HiveConfig {
Hive.registerAdapter(UserAdapter()); Hive.registerAdapter(UserAdapter());
Hive.registerAdapter(ServerHostingDetailsAdapter()); Hive.registerAdapter(ServerHostingDetailsAdapter());
Hive.registerAdapter(ServerDomainAdapter()); Hive.registerAdapter(ServerDomainAdapter());
Hive.registerAdapter(BackblazeCredentialAdapter()); Hive.registerAdapter(BackupsCredentialAdapter());
Hive.registerAdapter(BackblazeBucketAdapter()); Hive.registerAdapter(BackblazeBucketAdapter());
Hive.registerAdapter(ServerVolumeAdapter()); Hive.registerAdapter(ServerVolumeAdapter());
Hive.registerAdapter(DnsProviderAdapter());
Hive.registerAdapter(ServerProviderAdapter());
Hive.registerAdapter(UserTypeAdapter()); Hive.registerAdapter(UserTypeAdapter());
Hive.registerAdapter(DnsProviderTypeAdapter());
Hive.registerAdapter(ServerProviderTypeAdapter());
await Hive.openBox(BNames.appSettingsBox); await Hive.openBox(BNames.appSettingsBox);
@ -35,8 +34,8 @@ class HiveConfig {
final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated); final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated);
if (deprecatedUsers.isNotEmpty) { if (deprecatedUsers.isNotEmpty) {
final Box<User> users = Hive.box<User>(BNames.usersBox); final Box<User> users = Hive.box<User>(BNames.usersBox);
users.addAll(deprecatedUsers.values.toList()); await users.addAll(deprecatedUsers.values.toList());
deprecatedUsers.clear(); await deprecatedUsers.clear();
} }
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);

View File

@ -1,5 +1,5 @@
class APIGenericResult<T> { class GenericResult<T> {
APIGenericResult({ GenericResult({
required this.success, required this.success,
required this.data, required this.data,
this.message, this.message,

View File

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:http/io_client.dart'; import 'package:http/io_client.dart';
import 'package:selfprivacy/config/get_it_config.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'; import 'package:selfprivacy/logic/models/message.dart';
void _logToAppConsole<T>(final T objectToLog) { void _logToAppConsole<T>(final T objectToLog) {
@ -53,10 +53,10 @@ class ResponseLoggingParser extends ResponseParser {
} }
} }
abstract class ApiMap { abstract class GraphQLApiMap {
Future<GraphQLClient> getClient() async { Future<GraphQLClient> getClient() async {
IOClient? ioClient; IOClient? ioClient;
if (StagingOptions.stagingAcme || !StagingOptions.verifyCertificate) { if (TlsOptions.stagingAcme || !TlsOptions.verifyCertificate) {
final HttpClient httpClient = HttpClient(); final HttpClient httpClient = HttpClient();
httpClient.badCertificateCallback = ( httpClient.badCertificateCallback = (
final cert, final cert,

View File

@ -3,6 +3,7 @@ import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart'; import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields { class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({ Fragment$basicMutationReturnFields({

View File

@ -96,9 +96,9 @@ type DeviceApiTokenMutationReturn implements MutationReturnInterface {
} }
enum DnsProvider { enum DnsProvider {
CLOUDFLARE CLOUDFLARE,
DESEC,
DIGITALOCEAN DIGITALOCEAN
DESEC
} }
type DnsRecord { type DnsRecord {

View File

@ -1312,16 +1312,16 @@ Enum$BackupProvider fromJson$Enum$BackupProvider(String value) {
} }
} }
enum Enum$DnsProvider { CLOUDFLARE, DIGITALOCEAN, DESEC, $unknown } enum Enum$DnsProvider { CLOUDFLARE, DESEC, DIGITALOCEAN, $unknown }
String toJson$Enum$DnsProvider(Enum$DnsProvider e) { String toJson$Enum$DnsProvider(Enum$DnsProvider e) {
switch (e) { switch (e) {
case Enum$DnsProvider.CLOUDFLARE: case Enum$DnsProvider.CLOUDFLARE:
return r'CLOUDFLARE'; return r'CLOUDFLARE';
case Enum$DnsProvider.DIGITALOCEAN:
return r'DIGITALOCEAN';
case Enum$DnsProvider.DESEC: case Enum$DnsProvider.DESEC:
return r'DESEC'; return r'DESEC';
case Enum$DnsProvider.DIGITALOCEAN:
return r'DIGITALOCEAN';
case Enum$DnsProvider.$unknown: case Enum$DnsProvider.$unknown:
return r'$unknown'; return r'$unknown';
} }
@ -1331,10 +1331,10 @@ Enum$DnsProvider fromJson$Enum$DnsProvider(String value) {
switch (value) { switch (value) {
case r'CLOUDFLARE': case r'CLOUDFLARE':
return Enum$DnsProvider.CLOUDFLARE; return Enum$DnsProvider.CLOUDFLARE;
case r'DIGITALOCEAN':
return Enum$DnsProvider.DIGITALOCEAN;
case r'DESEC': case r'DESEC':
return Enum$DnsProvider.DESEC; return Enum$DnsProvider.DESEC;
case r'DIGITALOCEAN':
return Enum$DnsProvider.DIGITALOCEAN;
default: default:
return Enum$DnsProvider.$unknown; return Enum$DnsProvider.$unknown;
} }

View File

@ -80,7 +80,6 @@ query SystemDnsProvider {
} }
} }
query GetApiTokens { query GetApiTokens {
api { api {
devices { devices {

View File

@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart'; import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart'; import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields { class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({ Fragment$basicMutationReturnFields({

View File

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart'; import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields { class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({ Fragment$basicMutationReturnFields({

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart'; import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart'; import 'package:selfprivacy/utils/scalars.dart';

View File

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart'; import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields { class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({ Fragment$basicMutationReturnFields({

View File

@ -1,6 +1,6 @@
part of 'server_api.dart'; part of 'server_api.dart';
mixin JobsApi on ApiMap { mixin JobsApi on GraphQLApiMap {
Future<List<ServerJob>> getServerJobs() async { Future<List<ServerJob>> getServerJobs() async {
QueryResult<Query$GetApiJobs> response; QueryResult<Query$GetApiJobs> response;
List<ServerJob> jobsList = []; List<ServerJob> jobsList = [];
@ -22,13 +22,13 @@ mixin JobsApi on ApiMap {
return jobsList; return jobsList;
} }
Future<APIGenericResult<bool>> removeApiJob(final String uid) async { Future<GenericResult<bool>> removeApiJob(final String uid) async {
try { try {
final GraphQLClient client = await getClient(); final GraphQLClient client = await getClient();
final variables = Variables$Mutation$RemoveJob(jobId: uid); final variables = Variables$Mutation$RemoveJob(jobId: uid);
final mutation = Options$Mutation$RemoveJob(variables: variables); final mutation = Options$Mutation$RemoveJob(variables: variables);
final response = await client.mutate$RemoveJob(mutation); final response = await client.mutate$RemoveJob(mutation);
return APIGenericResult( return GenericResult(
data: response.parsedData?.removeJob.success ?? false, data: response.parsedData?.removeJob.success ?? false,
success: true, success: true,
code: response.parsedData?.removeJob.code ?? 0, code: response.parsedData?.removeJob.code ?? 0,
@ -36,7 +36,7 @@ mixin JobsApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: false, data: false,
success: false, success: false,
code: 0, code: 0,

View File

@ -1,6 +1,6 @@
part of 'server_api.dart'; part of 'server_api.dart';
mixin ServerActionsApi on ApiMap { mixin ServerActionsApi on GraphQLApiMap {
Future<bool> _commonBoolRequest(final Function graphQLMethod) async { Future<bool> _commonBoolRequest(final Function graphQLMethod) async {
QueryResult response; QueryResult response;
bool result = false; bool result = false;

View File

@ -1,7 +1,7 @@
import 'package:graphql/client.dart'; import 'package:graphql/client.dart';
import 'package:selfprivacy/config/get_it_config.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/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/api_map.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/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/schema.graphql.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_api.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/ssh_settings.dart';
import 'package:selfprivacy/logic/models/system_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 'jobs_api.dart';
part 'server_actions_api.dart'; part 'server_actions_api.dart';
@ -32,7 +32,7 @@ part 'services_api.dart';
part 'users_api.dart'; part 'users_api.dart';
part 'volume_api.dart'; part 'volume_api.dart';
class ServerApi extends ApiMap class ServerApi extends GraphQLApiMap
with VolumeApi, JobsApi, ServerActionsApi, ServicesApi, UsersApi { with VolumeApi, JobsApi, ServerActionsApi, ServicesApi, UsersApi {
ServerApi({ ServerApi({
this.hasLogger = false, this.hasLogger = false,
@ -69,9 +69,9 @@ class ServerApi extends ApiMap
return apiVersion; return apiVersion;
} }
Future<ServerProvider> getServerProviderType() async { Future<ServerProviderType> getServerProviderType() async {
QueryResult<Query$SystemServerProvider> response; QueryResult<Query$SystemServerProvider> response;
ServerProvider providerType = ServerProvider.unknown; ServerProviderType providerType = ServerProviderType.unknown;
try { try {
final GraphQLClient client = await getClient(); final GraphQLClient client = await getClient();
@ -79,7 +79,7 @@ class ServerApi extends ApiMap
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
} }
providerType = ServerProvider.fromGraphQL( providerType = ServerProviderType.fromGraphQL(
response.parsedData!.system.provider.provider, response.parsedData!.system.provider.provider,
); );
} catch (e) { } catch (e) {
@ -88,9 +88,9 @@ class ServerApi extends ApiMap
return providerType; return providerType;
} }
Future<DnsProvider> getDnsProviderType() async { Future<DnsProviderType> getDnsProviderType() async {
QueryResult<Query$SystemDnsProvider> response; QueryResult<Query$SystemDnsProvider> response;
DnsProvider providerType = DnsProvider.unknown; DnsProviderType providerType = DnsProviderType.unknown;
try { try {
final GraphQLClient client = await getClient(); final GraphQLClient client = await getClient();
@ -98,7 +98,7 @@ class ServerApi extends ApiMap
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
} }
providerType = DnsProvider.fromGraphQL( providerType = DnsProviderType.fromGraphQL(
response.parsedData!.system.domainInfo.provider, response.parsedData!.system.domainInfo.provider,
); );
} catch (e) { } catch (e) {
@ -205,7 +205,7 @@ class ServerApi extends ApiMap
return settings; return settings;
} }
Future<APIGenericResult<RecoveryKeyStatus?>> getRecoveryTokenStatus() async { Future<GenericResult<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
RecoveryKeyStatus? key; RecoveryKeyStatus? key;
QueryResult<Query$RecoveryKey> response; QueryResult<Query$RecoveryKey> response;
String? error; String? error;
@ -222,18 +222,18 @@ class ServerApi extends ApiMap
print(e); print(e);
} }
return APIGenericResult<RecoveryKeyStatus?>( return GenericResult<RecoveryKeyStatus?>(
success: error == null, success: error == null,
data: key, data: key,
message: error, message: error,
); );
} }
Future<APIGenericResult<String>> generateRecoveryToken( Future<GenericResult<String>> generateRecoveryToken(
final DateTime? expirationDate, final DateTime? expirationDate,
final int? numberOfUses, final int? numberOfUses,
) async { ) async {
APIGenericResult<String> key; GenericResult<String> key;
QueryResult<Mutation$GetNewRecoveryApiKey> response; QueryResult<Mutation$GetNewRecoveryApiKey> response;
try { try {
@ -254,19 +254,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
key = APIGenericResult<String>( key = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
key = APIGenericResult<String>( key = GenericResult<String>(
success: true, success: true,
data: response.parsedData!.getNewRecoveryApiKey.key!, data: response.parsedData!.getNewRecoveryApiKey.key!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
key = APIGenericResult<String>( key = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),
@ -299,8 +299,8 @@ class ServerApi extends ApiMap
return records; return records;
} }
Future<APIGenericResult<List<ApiToken>>> getApiTokens() async { Future<GenericResult<List<ApiToken>>> getApiTokens() async {
APIGenericResult<List<ApiToken>> tokens; GenericResult<List<ApiToken>> tokens;
QueryResult<Query$GetApiTokens> response; QueryResult<Query$GetApiTokens> response;
try { try {
@ -309,7 +309,7 @@ class ServerApi extends ApiMap
if (response.hasException) { if (response.hasException) {
final message = response.exception.toString(); final message = response.exception.toString();
print(message); print(message);
tokens = APIGenericResult<List<ApiToken>>( tokens = GenericResult<List<ApiToken>>(
success: false, success: false,
data: [], data: [],
message: message, message: message,
@ -323,13 +323,13 @@ class ServerApi extends ApiMap
ApiToken.fromGraphQL(device), ApiToken.fromGraphQL(device),
) )
.toList(); .toList();
tokens = APIGenericResult<List<ApiToken>>( tokens = GenericResult<List<ApiToken>>(
success: true, success: true,
data: parsed, data: parsed,
); );
} catch (e) { } catch (e) {
print(e); print(e);
tokens = APIGenericResult<List<ApiToken>>( tokens = GenericResult<List<ApiToken>>(
success: false, success: false,
data: [], data: [],
message: e.toString(), message: e.toString(),
@ -339,8 +339,8 @@ class ServerApi extends ApiMap
return tokens; return tokens;
} }
Future<APIGenericResult<void>> deleteApiToken(final String name) async { Future<GenericResult<void>> deleteApiToken(final String name) async {
APIGenericResult<void> returnable; GenericResult<void> returnable;
QueryResult<Mutation$DeleteDeviceApiToken> response; QueryResult<Mutation$DeleteDeviceApiToken> response;
try { try {
@ -357,19 +357,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
returnable = APIGenericResult<void>( returnable = GenericResult<void>(
success: false, success: false,
data: null, data: null,
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
returnable = APIGenericResult<void>( returnable = GenericResult<void>(
success: true, success: true,
data: null, data: null,
); );
} catch (e) { } catch (e) {
print(e); print(e);
returnable = APIGenericResult<void>( returnable = GenericResult<void>(
success: false, success: false,
data: null, data: null,
message: e.toString(), message: e.toString(),
@ -379,8 +379,8 @@ class ServerApi extends ApiMap
return returnable; return returnable;
} }
Future<APIGenericResult<String>> createDeviceToken() async { Future<GenericResult<String>> createDeviceToken() async {
APIGenericResult<String> token; GenericResult<String> token;
QueryResult<Mutation$GetNewDeviceApiKey> response; QueryResult<Mutation$GetNewDeviceApiKey> response;
try { try {
@ -392,19 +392,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
token = APIGenericResult<String>( token = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
token = APIGenericResult<String>( token = GenericResult<String>(
success: true, success: true,
data: response.parsedData!.getNewDeviceApiKey.key!, data: response.parsedData!.getNewDeviceApiKey.key!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
token = APIGenericResult<String>( token = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),
@ -416,10 +416,10 @@ class ServerApi extends ApiMap
Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null; Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null;
Future<APIGenericResult<String>> authorizeDevice( Future<GenericResult<String>> authorizeDevice(
final DeviceToken deviceToken, final DeviceToken deviceToken,
) async { ) async {
APIGenericResult<String> token; GenericResult<String> token;
QueryResult<Mutation$AuthorizeWithNewDeviceApiKey> response; QueryResult<Mutation$AuthorizeWithNewDeviceApiKey> response;
try { try {
@ -441,19 +441,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
token = APIGenericResult<String>( token = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
token = APIGenericResult<String>( token = GenericResult<String>(
success: true, success: true,
data: response.parsedData!.authorizeWithNewDeviceApiKey.token!, data: response.parsedData!.authorizeWithNewDeviceApiKey.token!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
token = APIGenericResult<String>( token = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),
@ -463,10 +463,10 @@ class ServerApi extends ApiMap
return token; return token;
} }
Future<APIGenericResult<String>> useRecoveryToken( Future<GenericResult<String>> useRecoveryToken(
final DeviceToken deviceToken, final DeviceToken deviceToken,
) async { ) async {
APIGenericResult<String> token; GenericResult<String> token;
QueryResult<Mutation$UseRecoveryApiKey> response; QueryResult<Mutation$UseRecoveryApiKey> response;
try { try {
@ -488,19 +488,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
token = APIGenericResult<String>( token = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
token = APIGenericResult<String>( token = GenericResult<String>(
success: true, success: true,
data: response.parsedData!.useRecoveryApiKey.token!, data: response.parsedData!.useRecoveryApiKey.token!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
token = APIGenericResult<String>( token = GenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),

View File

@ -1,6 +1,6 @@
part of 'server_api.dart'; part of 'server_api.dart';
mixin ServicesApi on ApiMap { mixin ServicesApi on GraphQLApiMap {
Future<List<Service>> getAllServices() async { Future<List<Service>> getAllServices() async {
QueryResult<Query$AllServices> response; QueryResult<Query$AllServices> response;
List<Service> services = []; List<Service> services = [];
@ -20,7 +20,7 @@ mixin ServicesApi on ApiMap {
return services; return services;
} }
Future<APIGenericResult<bool>> enableService( Future<GenericResult<bool>> enableService(
final String serviceId, final String serviceId,
) async { ) async {
try { try {
@ -28,7 +28,7 @@ mixin ServicesApi on ApiMap {
final variables = Variables$Mutation$EnableService(serviceId: serviceId); final variables = Variables$Mutation$EnableService(serviceId: serviceId);
final mutation = Options$Mutation$EnableService(variables: variables); final mutation = Options$Mutation$EnableService(variables: variables);
final response = await client.mutate$EnableService(mutation); final response = await client.mutate$EnableService(mutation);
return APIGenericResult( return GenericResult(
data: response.parsedData?.enableService.success ?? false, data: response.parsedData?.enableService.success ?? false,
success: true, success: true,
code: response.parsedData?.enableService.code ?? 0, code: response.parsedData?.enableService.code ?? 0,
@ -36,7 +36,7 @@ mixin ServicesApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: false, data: false,
success: false, success: false,
code: 0, code: 0,
@ -45,7 +45,7 @@ mixin ServicesApi on ApiMap {
} }
} }
Future<APIGenericResult<void>> disableService( Future<GenericResult<void>> disableService(
final String serviceId, final String serviceId,
) async { ) async {
try { try {
@ -53,7 +53,7 @@ mixin ServicesApi on ApiMap {
final variables = Variables$Mutation$DisableService(serviceId: serviceId); final variables = Variables$Mutation$DisableService(serviceId: serviceId);
final mutation = Options$Mutation$DisableService(variables: variables); final mutation = Options$Mutation$DisableService(variables: variables);
final response = await client.mutate$DisableService(mutation); final response = await client.mutate$DisableService(mutation);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: response.parsedData?.disableService.success ?? false, success: response.parsedData?.disableService.success ?? false,
code: response.parsedData?.disableService.code ?? 0, code: response.parsedData?.disableService.code ?? 0,
@ -61,7 +61,7 @@ mixin ServicesApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: false, success: false,
code: 0, code: 0,
@ -70,7 +70,7 @@ mixin ServicesApi on ApiMap {
} }
} }
Future<APIGenericResult<bool>> stopService( Future<GenericResult<bool>> stopService(
final String serviceId, final String serviceId,
) async { ) async {
try { try {
@ -78,7 +78,7 @@ mixin ServicesApi on ApiMap {
final variables = Variables$Mutation$StopService(serviceId: serviceId); final variables = Variables$Mutation$StopService(serviceId: serviceId);
final mutation = Options$Mutation$StopService(variables: variables); final mutation = Options$Mutation$StopService(variables: variables);
final response = await client.mutate$StopService(mutation); final response = await client.mutate$StopService(mutation);
return APIGenericResult( return GenericResult(
data: response.parsedData?.stopService.success ?? false, data: response.parsedData?.stopService.success ?? false,
success: true, success: true,
code: response.parsedData?.stopService.code ?? 0, code: response.parsedData?.stopService.code ?? 0,
@ -86,7 +86,7 @@ mixin ServicesApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: false, data: false,
success: false, success: false,
code: 0, code: 0,
@ -95,13 +95,13 @@ mixin ServicesApi on ApiMap {
} }
} }
Future<APIGenericResult> startService(final String serviceId) async { Future<GenericResult> startService(final String serviceId) async {
try { try {
final GraphQLClient client = await getClient(); final GraphQLClient client = await getClient();
final variables = Variables$Mutation$StartService(serviceId: serviceId); final variables = Variables$Mutation$StartService(serviceId: serviceId);
final mutation = Options$Mutation$StartService(variables: variables); final mutation = Options$Mutation$StartService(variables: variables);
final response = await client.mutate$StartService(mutation); final response = await client.mutate$StartService(mutation);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: response.parsedData?.startService.success ?? false, success: response.parsedData?.startService.success ?? false,
code: response.parsedData?.startService.code ?? 0, code: response.parsedData?.startService.code ?? 0,
@ -109,7 +109,7 @@ mixin ServicesApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: false, success: false,
code: 0, code: 0,
@ -118,7 +118,7 @@ mixin ServicesApi on ApiMap {
} }
} }
Future<APIGenericResult<bool>> restartService( Future<GenericResult<bool>> restartService(
final String serviceId, final String serviceId,
) async { ) async {
try { try {
@ -126,7 +126,7 @@ mixin ServicesApi on ApiMap {
final variables = Variables$Mutation$RestartService(serviceId: serviceId); final variables = Variables$Mutation$RestartService(serviceId: serviceId);
final mutation = Options$Mutation$RestartService(variables: variables); final mutation = Options$Mutation$RestartService(variables: variables);
final response = await client.mutate$RestartService(mutation); final response = await client.mutate$RestartService(mutation);
return APIGenericResult( return GenericResult(
data: response.parsedData?.restartService.success ?? false, data: response.parsedData?.restartService.success ?? false,
success: true, success: true,
code: response.parsedData?.restartService.code ?? 0, code: response.parsedData?.restartService.code ?? 0,
@ -134,7 +134,7 @@ mixin ServicesApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: false, data: false,
success: false, success: false,
code: 0, code: 0,
@ -143,7 +143,7 @@ mixin ServicesApi on ApiMap {
} }
} }
Future<APIGenericResult<ServerJob?>> moveService( Future<GenericResult<ServerJob?>> moveService(
final String serviceId, final String serviceId,
final String destination, final String destination,
) async { ) async {
@ -158,7 +158,7 @@ mixin ServicesApi on ApiMap {
final mutation = Options$Mutation$MoveService(variables: variables); final mutation = Options$Mutation$MoveService(variables: variables);
final response = await client.mutate$MoveService(mutation); final response = await client.mutate$MoveService(mutation);
final jobJson = response.parsedData?.moveService.job?.toJson(); final jobJson = response.parsedData?.moveService.job?.toJson();
return APIGenericResult( return GenericResult(
success: true, success: true,
code: response.parsedData?.moveService.code ?? 0, code: response.parsedData?.moveService.code ?? 0,
message: response.parsedData?.moveService.message, message: response.parsedData?.moveService.message,
@ -166,7 +166,7 @@ mixin ServicesApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
success: false, success: false,
code: 0, code: 0,
message: e.toString(), message: e.toString(),

View File

@ -1,6 +1,6 @@
part of 'server_api.dart'; part of 'server_api.dart';
mixin UsersApi on ApiMap { mixin UsersApi on GraphQLApiMap {
Future<List<User>> getAllUsers() async { Future<List<User>> getAllUsers() async {
QueryResult<Query$AllUsers> response; QueryResult<Query$AllUsers> response;
List<User> users = []; List<User> users = [];
@ -45,7 +45,7 @@ mixin UsersApi on ApiMap {
return user; return user;
} }
Future<APIGenericResult<User?>> createUser( Future<GenericResult<User?>> createUser(
final String username, final String username,
final String password, final String password,
) async { ) async {
@ -56,7 +56,7 @@ mixin UsersApi on ApiMap {
); );
final mutation = Options$Mutation$CreateUser(variables: variables); final mutation = Options$Mutation$CreateUser(variables: variables);
final response = await client.mutate$CreateUser(mutation); final response = await client.mutate$CreateUser(mutation);
return APIGenericResult( return GenericResult(
success: true, success: true,
code: response.parsedData?.createUser.code ?? 500, code: response.parsedData?.createUser.code ?? 500,
message: response.parsedData?.createUser.message, message: response.parsedData?.createUser.message,
@ -66,7 +66,7 @@ mixin UsersApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
success: false, success: false,
code: 0, code: 0,
message: e.toString(), message: e.toString(),
@ -75,7 +75,7 @@ mixin UsersApi on ApiMap {
} }
} }
Future<APIGenericResult<bool>> deleteUser( Future<GenericResult<bool>> deleteUser(
final String username, final String username,
) async { ) async {
try { try {
@ -83,7 +83,7 @@ mixin UsersApi on ApiMap {
final variables = Variables$Mutation$DeleteUser(username: username); final variables = Variables$Mutation$DeleteUser(username: username);
final mutation = Options$Mutation$DeleteUser(variables: variables); final mutation = Options$Mutation$DeleteUser(variables: variables);
final response = await client.mutate$DeleteUser(mutation); final response = await client.mutate$DeleteUser(mutation);
return APIGenericResult( return GenericResult(
data: response.parsedData?.deleteUser.success ?? false, data: response.parsedData?.deleteUser.success ?? false,
success: true, success: true,
code: response.parsedData?.deleteUser.code ?? 500, code: response.parsedData?.deleteUser.code ?? 500,
@ -91,7 +91,7 @@ mixin UsersApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: false, data: false,
success: false, success: false,
code: 500, code: 500,
@ -100,7 +100,7 @@ mixin UsersApi on ApiMap {
} }
} }
Future<APIGenericResult<User?>> updateUser( Future<GenericResult<User?>> updateUser(
final String username, final String username,
final String password, final String password,
) async { ) async {
@ -111,7 +111,7 @@ mixin UsersApi on ApiMap {
); );
final mutation = Options$Mutation$UpdateUser(variables: variables); final mutation = Options$Mutation$UpdateUser(variables: variables);
final response = await client.mutate$UpdateUser(mutation); final response = await client.mutate$UpdateUser(mutation);
return APIGenericResult( return GenericResult(
success: true, success: true,
code: response.parsedData?.updateUser.code ?? 500, code: response.parsedData?.updateUser.code ?? 500,
message: response.parsedData?.updateUser.message, message: response.parsedData?.updateUser.message,
@ -121,7 +121,7 @@ mixin UsersApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: false, success: false,
code: 0, code: 0,
@ -130,7 +130,7 @@ mixin UsersApi on ApiMap {
} }
} }
Future<APIGenericResult<User?>> addSshKey( Future<GenericResult<User?>> addSshKey(
final String username, final String username,
final String sshKey, final String sshKey,
) async { ) async {
@ -144,7 +144,7 @@ mixin UsersApi on ApiMap {
); );
final mutation = Options$Mutation$AddSshKey(variables: variables); final mutation = Options$Mutation$AddSshKey(variables: variables);
final response = await client.mutate$AddSshKey(mutation); final response = await client.mutate$AddSshKey(mutation);
return APIGenericResult( return GenericResult(
success: true, success: true,
code: response.parsedData?.addSshKey.code ?? 500, code: response.parsedData?.addSshKey.code ?? 500,
message: response.parsedData?.addSshKey.message, message: response.parsedData?.addSshKey.message,
@ -154,7 +154,7 @@ mixin UsersApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: false, success: false,
code: 0, code: 0,
@ -163,7 +163,7 @@ mixin UsersApi on ApiMap {
} }
} }
Future<APIGenericResult<User?>> removeSshKey( Future<GenericResult<User?>> removeSshKey(
final String username, final String username,
final String sshKey, final String sshKey,
) async { ) async {
@ -177,7 +177,7 @@ mixin UsersApi on ApiMap {
); );
final mutation = Options$Mutation$RemoveSshKey(variables: variables); final mutation = Options$Mutation$RemoveSshKey(variables: variables);
final response = await client.mutate$RemoveSshKey(mutation); final response = await client.mutate$RemoveSshKey(mutation);
return APIGenericResult( return GenericResult(
success: response.parsedData?.removeSshKey.success ?? false, success: response.parsedData?.removeSshKey.success ?? false,
code: response.parsedData?.removeSshKey.code ?? 500, code: response.parsedData?.removeSshKey.code ?? 500,
message: response.parsedData?.removeSshKey.message, message: response.parsedData?.removeSshKey.message,
@ -187,7 +187,7 @@ mixin UsersApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: null, data: null,
success: false, success: false,
code: 0, code: 0,

View File

@ -1,6 +1,6 @@
part of 'server_api.dart'; part of 'server_api.dart';
mixin VolumeApi on ApiMap { mixin VolumeApi on GraphQLApiMap {
Future<List<ServerDiskVolume>> getServerDiskVolumes() async { Future<List<ServerDiskVolume>> getServerDiskVolumes() async {
QueryResult response; QueryResult response;
List<ServerDiskVolume> volumes = []; List<ServerDiskVolume> volumes = [];
@ -57,10 +57,10 @@ mixin VolumeApi on ApiMap {
} }
} }
Future<APIGenericResult<String?>> migrateToBinds( Future<GenericResult<String?>> migrateToBinds(
final Map<String, String> serviceToDisk, final Map<String, String> serviceToDisk,
) async { ) async {
APIGenericResult<String?>? mutation; GenericResult<String?>? mutation;
try { try {
final GraphQLClient client = await getClient(); final GraphQLClient client = await getClient();
@ -78,7 +78,7 @@ mixin VolumeApi on ApiMap {
await client.mutate$MigrateToBinds( await client.mutate$MigrateToBinds(
migrateMutation, migrateMutation,
); );
mutation = mutation = APIGenericResult( mutation = mutation = GenericResult(
success: true, success: true,
code: result.parsedData!.migrateToBinds.code, code: result.parsedData!.migrateToBinds.code,
message: result.parsedData!.migrateToBinds.message, message: result.parsedData!.migrateToBinds.message,
@ -86,7 +86,7 @@ mixin VolumeApi on ApiMap {
); );
} catch (e) { } catch (e) {
print(e); print(e);
mutation = APIGenericResult( mutation = GenericResult(
success: false, success: false,
code: 0, code: 0,
message: e.toString(), message: e.toString(),

View File

@ -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;
}

View File

@ -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');
}
}
}

View File

@ -2,11 +2,11 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.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/models/hive/backups_credential.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; export 'package:selfprivacy/logic/api_maps/generic_result.dart';
class BackblazeApiAuth { class BackblazeApiAuth {
BackblazeApiAuth({required this.authorizationToken, required this.apiUrl}); BackblazeApiAuth({required this.authorizationToken, required this.apiUrl});
@ -25,7 +25,7 @@ class BackblazeApplicationKey {
final String applicationKey; final String applicationKey;
} }
class BackblazeApi extends ApiMap { class BackblazeApi extends RestApiMap {
BackblazeApi({this.hasLogger = false, this.isWithToken = true}); BackblazeApi({this.hasLogger = false, this.isWithToken = true});
@override @override
@ -78,7 +78,7 @@ class BackblazeApi extends ApiMap {
); );
} }
Future<APIGenericResult<bool>> isApiTokenValid( Future<GenericResult<bool>> isApiTokenValid(
final String encodedApiKey, final String encodedApiKey,
) async { ) async {
final Dio client = await getClient(); final Dio client = await getClient();
@ -103,7 +103,7 @@ class BackblazeApi extends ApiMap {
} }
} on DioError catch (e) { } on DioError catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: false, data: false,
success: false, success: false,
message: e.toString(), message: e.toString(),
@ -112,7 +112,7 @@ class BackblazeApi extends ApiMap {
close(client); close(client);
} }
return APIGenericResult( return GenericResult(
data: isTokenValid, data: isTokenValid,
success: true, success: true,
); );

View File

@ -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<ApiConfigModel>().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<APIGenericResult<bool>> 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<String?> 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<APIGenericResult<void>> 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<Future> allDeleteFutures = <Future>[];
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<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response response;
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List<DnsRecord> allRecords = <DnsRecord>[];
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<APIGenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final List<Future> allCreateFutures = <Future>[];
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<DnsRecord> 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 <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
@override
Future<void> 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<List<String>> domainList() async {
final String url = '$rootAddress/zones';
List<String> domains = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
url,
queryParameters: {'per_page': 50},
);
domains = response.data['result']
.map<String>((final el) => el['name'] as String)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
}
return domains;
}
@override
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final List<DnsRecord> records = await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DesiredDnsRecord> 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,
),
];
}
}

View File

@ -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<ApiConfigModel>().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<GenericResult<bool>> 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<GenericResult<List<dynamic>>> 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<GenericResult<void>> 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<Future> allDeleteFutures = <Future>[];
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<GenericResult<List>> 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<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
required final List<DnsRecord> records,
}) async {
final String domainZoneId = domain.zoneId;
final List<Future> allCreateFutures = <Future>[];
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<GenericResult<List>> 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,
);
}
}

View File

@ -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,
);
}

View File

@ -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<ApiConfigModel>().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<GenericResult<bool>> 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<GenericResult<void>> updateRecords({
required final ServerDomain domain,
required final List<dynamic> 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<GenericResult<List<dynamic>>> 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<GenericResult<void>> createRecords({
required final ServerDomain domain,
required final List<dynamic> 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<GenericResult<List>> 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,
);
}
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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<ApiConfigModel>().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<GenericResult<bool>> 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<GenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
required final List records,
}) async {
final String domainName = domain.domainName;
final Dio client = await getClient();
try {
final List<Future> 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<GenericResult<List>> 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<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
required final List<DnsRecord> records,
}) async {
final String domainName = domain.domainName;
final List<Future> allCreateFutures = <Future>[];
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<GenericResult<List>> 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);
}
}

View File

@ -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<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
});
Future<APIGenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<APIGenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<void> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
);
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
);
Future<String?> getZoneId(final String domain);
Future<List<String>> domainList();
Future<APIGenericResult<bool>> isApiTokenValid(final String token);
RegExp getApiTokenValidation();
}

View File

@ -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;
}

View File

@ -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,
});
}

View File

@ -1,8 +0,0 @@
class ProviderApiSettings {
const ProviderApiSettings({
this.hasLogger = false,
this.isWithToken = true,
});
final bool hasLogger;
final bool isWithToken;
}

View File

@ -8,7 +8,7 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/message.dart';
abstract class ApiMap { abstract class RestApiMap {
Future<Dio> getClient({final BaseOptions? customOptions}) async { Future<Dio> getClient({final BaseOptions? customOptions}) async {
final Dio dio = Dio(customOptions ?? (await options)); final Dio dio = Dio(customOptions ?? (await options));
if (hasLogger) { if (hasLogger) {

View File

@ -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<ApiConfigModel>().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<APIGenericResult<bool>> 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<Price?> getPricePerGb() async => Price(
value: 0.10,
currency: 'USD',
);
@override
Future<APIGenericResult<ServerVolume?>> createVolume() async {
ServerVolume? volume;
Response? createVolumeResponse;
final Dio client = await getClient();
try {
final List<ServerVolume> 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<List<ServerVolume>> getVolumes({final String? status}) async {
final List<ServerVolume> volumes = [];
final Response getVolumesResponse;
final Dio client = await getClient();
try {
getVolumesResponse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
final List<dynamic> 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<ServerVolume?> getVolume(final String volumeUuid) async {
ServerVolume? requestedVolume;
final List<ServerVolume> volumes = await getVolumes();
for (final volume in volumes) {
if (volume.uuid == volumeUuid) {
requestedVolume = volume;
}
}
return requestedVolume;
}
@override
Future<void> 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<APIGenericResult<bool>> 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<bool> 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<bool> 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<APIGenericResult<ServerHostingDetails?>> 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<String, Object> 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<ServerBasicInfo> 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<APIGenericResult<bool>> 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<Future> laterFutures = <Future>[];
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<ServerHostingDetails> restart() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().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<ServerHostingDetails> powerOn() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().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<TimeSeriesData> calculateCpuLoadMetrics(final List rawProcStatMetrics) {
final List<TimeSeriesData> 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<ServerMetrics?> 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<List<ServerMetadataEntity>> getMetadata(final int serverId) async {
List<ServerMetadataEntity> 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<List<ServerBasicInfo>> getServers() async {
List<ServerBasicInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/droplets');
servers = response.data!['droplets'].map<ServerBasicInfo>(
(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<APIGenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
List<ServerProviderLocation> locations = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/regions',
);
locations = response.data!['regions']
.map<ServerProviderLocation>(
(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<APIGenericResult<List<ServerType>>> getServerTypesByLocation({
required final ServerProviderLocation location,
}) async {
final List<ServerType> 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<APIGenericResult<void>> 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,
);
}

View File

@ -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<ApiConfigModel>().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<GenericResult<bool>> 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<GenericResult<DigitalOceanVolume?>> 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<GenericResult<List<DigitalOceanVolume>>> getVolumes({
final String? status,
}) async {
final List<DigitalOceanVolume> 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<GenericResult<void>> 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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<int?>> 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<String, Object> 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<GenericResult<void>> 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<GenericResult<void>> 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<GenericResult<void>> 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<GenericResult<List>> 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<GenericResult<List>> 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<GenericResult<List>> 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<GenericResult<List<DigitalOceanLocation>>>
getAvailableLocations() async {
final List<DigitalOceanLocation> 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<GenericResult<List<DigitalOceanServerType>>>
getAvailableServerTypes() async {
final List<DigitalOceanServerType> 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);
}
}

View File

@ -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,
);
}

View File

@ -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<ApiConfigModel>().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<APIGenericResult<bool>> 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<Price?> 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<APIGenericResult<ServerVolume?>> 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<List<ServerVolume>> getVolumes({final String? status}) async {
final List<ServerVolume> volumes = [];
final Response getVolumesResonse;
final Dio client = await getClient();
try {
getVolumesResonse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
final List<dynamic> 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<ServerVolume?> 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<void> 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<APIGenericResult<bool>> 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<bool> 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<bool> 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<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final String serverType,
required final DnsProvider dnsProvider,
}) async {
final APIGenericResult<ServerVolume?> 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<APIGenericResult<ServerHostingDetails?>> 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<String, Object> 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<APIGenericResult<bool>> 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<Future> laterFutures = <Future>[];
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<ServerHostingDetails> restart() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().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<ServerHostingDetails> powerOn() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().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<Map<String, dynamic>> requestRawMetrics(
final int serverId,
final DateTime start,
final DateTime end,
final String type,
) async {
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
final Map<String, dynamic> 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<TimeSeriesData> serializeTimeSeries(
final Map<String, dynamic> 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<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
final Map<String, dynamic> rawCpuMetrics = await requestRawMetrics(
serverId,
start,
end,
'cpu',
);
final Map<String, dynamic> 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<List<ServerMetadataEntity>> getMetadata(final int serverId) async {
List<ServerMetadataEntity> 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<List<ServerBasicInfo>> getServers() async {
List<ServerBasicInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/servers');
servers = response.data!['servers']
.map<HetznerServerInfo>(
(final e) => HetznerServerInfo.fromJson(e),
)
.toList()
.where(
(final server) => server.publicNet.ipv4 != null,
)
.map<ServerBasicInfo>(
(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<APIGenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
List<ServerProviderLocation> locations = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/locations',
);
locations = response.data!['locations']
.map<ServerProviderLocation>(
(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<APIGenericResult<List<ServerType>>> getServerTypesByLocation({
required final ServerProviderLocation location,
}) async {
final List<ServerType> 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<APIGenericResult<void>> 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);
}
}

View File

@ -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<ApiConfigModel>().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<GenericResult<bool>> 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<GenericResult<double?>> 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<GenericResult<HetznerVolume?>> 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<GenericResult<List<HetznerVolume>>> getVolumes({
final String? status,
}) async {
final List<HetznerVolume> 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<GenericResult<HetznerVolume?>> 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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<HetznerServerInfo?>> 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<String, Object> 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<GenericResult<void>> 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<GenericResult<void>> 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<GenericResult<void>> 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<GenericResult<Map<String, dynamic>>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
final String type,
) async {
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
final Map<String, dynamic> 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<GenericResult<List<HetznerServerInfo>>> getServers() async {
List<HetznerServerInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/servers');
servers = response.data!['servers']
.map<HetznerServerInfo>(
(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<GenericResult<List<HetznerLocation>>> getAvailableLocations() async {
final List<HetznerLocation> 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<GenericResult<List<HetznerServerTypeInfo>>>
getAvailableServerTypes() async {
final List<HetznerServerTypeInfo> 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<GenericResult<void>> 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);
}
}

View File

@ -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,
);
}

View File

@ -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<List<ServerBasicInfo>> getServers();
Future<APIGenericResult<List<ServerProviderLocation>>>
getAvailableLocations();
Future<APIGenericResult<List<ServerType>>> getServerTypesByLocation({
required final ServerProviderLocation location,
});
Future<ServerHostingDetails> restart();
Future<ServerHostingDetails> powerOn();
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
});
Future<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final String serverType,
required final DnsProvider dnsProvider,
});
Future<APIGenericResult<void>> createReverseDns({
required final ServerHostingDetails serverDetails,
required final ServerDomain domain,
});
Future<APIGenericResult<bool>> isApiTokenValid(final String token);
ProviderApiTokenValidation getApiTokenValidation();
Future<List<ServerMetadataEntity>> getMetadata(final int serverId);
Future<ServerMetrics?> 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;
}

View File

@ -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;
}

View File

@ -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,
});
}

View File

@ -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<APIGenericResult<ServerVolume?>> createVolume();
Future<List<ServerVolume>> getVolumes({final String? status});
Future<APIGenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
);
Future<bool> detachVolume(final ServerVolume volume);
Future<bool> resizeVolume(final ServerVolume volume, final DiskSize size);
Future<void> deleteVolume(final ServerVolume volume);
Future<Price?> getPricePerGb();
}

View File

@ -1,11 +1,11 @@
/// Controls staging environment for network /// Controls staging environment for network
class StagingOptions { class TlsOptions {
/// Whether we request for staging temprorary certificates. /// Whether we request for staging temprorary certificates.
/// Hardcode to 'true' in the middle of testing to not /// Hardcode to 'true' in the middle of testing to not
/// get your domain banned by constant certificate renewal /// get your domain banned by constant certificate renewal
/// ///
/// If set to 'true', the 'verifyCertificate' becomes useless /// 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 /// Should we consider CERTIFICATE_VERIFY_FAILED code an error
/// For now it's just a global variable and DNS API /// For now it's just a global variable and DNS API

View File

@ -35,7 +35,7 @@ class ApiDevicesCubit
} }
Future<List<ApiToken>?> _getApiTokens() async { Future<List<ApiToken>?> _getApiTokens() async {
final APIGenericResult<List<ApiToken>> response = await api.getApiTokens(); final GenericResult<List<ApiToken>> response = await api.getApiTokens();
if (response.success) { if (response.success) {
return response.data; return response.data;
} else { } else {
@ -44,8 +44,7 @@ class ApiDevicesCubit
} }
Future<void> deleteDevice(final ApiToken device) async { Future<void> deleteDevice(final ApiToken device) async {
final APIGenericResult<void> response = final GenericResult<void> response = await api.deleteApiToken(device.name);
await api.deleteApiToken(device.name);
if (response.success) { if (response.success) {
emit( emit(
ApiDevicesState( ApiDevicesState(
@ -60,7 +59,7 @@ class ApiDevicesCubit
} }
Future<String?> getNewDeviceKey() async { Future<String?> getNewDeviceKey() async {
final APIGenericResult<String> response = await api.createDeviceToken(); final GenericResult<String> response = await api.createDeviceToken();
if (response.success) { if (response.success) {
return response.data; return response.data;
} else { } else {

View File

@ -1,11 +1,11 @@
import 'package:cubit_form/cubit_form.dart'; 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/desired_dns_record.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.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/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.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/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/network_utils.dart';
part 'dns_records_state.dart'; part 'dns_records_state.dart';
@ -25,14 +25,13 @@ class DnsRecordsCubit
emit( emit(
DnsRecordsState( DnsRecordsState(
dnsState: DnsRecordsStatus.refreshing, dnsState: DnsRecordsStatus.refreshing,
dnsRecords: ApiController.currentDnsProviderApiFactory dnsRecords:
?.getDnsProvider() ProvidersController.currentDnsProvider?.getDesiredDnsRecords(
.getDesiredDnsRecords(
serverInstallationCubit.state.serverDomain?.domainName, serverInstallationCubit.state.serverDomain?.domainName,
'', '',
'', '',
) ?? ) ??
[], [],
), ),
); );
@ -45,13 +44,12 @@ class DnsRecordsCubit
return; return;
} }
final foundRecords = await ApiController.currentDnsProviderApiFactory! final foundRecords =
.getDnsProvider() await ProvidersController.currentDnsProvider!.validateDnsRecords(
.validateDnsRecords( domain!,
domain!, ipAddress!,
ipAddress!, extractDkimRecord(await api.getDnsRecords())?.content ?? '',
extractDkimRecord(await api.getDnsRecords())?.content ?? '', );
);
if (!foundRecords.success || foundRecords.data.isEmpty) { if (!foundRecords.success || foundRecords.data.isEmpty) {
emit(const DnsRecordsState()); emit(const DnsRecordsState());
@ -89,10 +87,10 @@ class DnsRecordsCubit
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
final DnsProviderApi dnsProviderApi = await ProvidersController.currentDnsProvider!.removeDomainRecords(
ApiController.currentDnsProviderApiFactory!.getDnsProvider(); domain: domain!,
await dnsProviderApi.removeSimilarRecords(domain: domain!); );
await dnsProviderApi.createMultipleDnsRecords( await ProvidersController.currentDnsProvider!.createDomainRecords(
domain: domain, domain: domain,
ip4: ipAddress, ip4: ipAddress,
); );
@ -100,7 +98,10 @@ class DnsRecordsCubit
final List<DnsRecord> records = await api.getDnsRecords(); final List<DnsRecord> records = await api.getDnsRecords();
final DnsRecord? dkimRecord = extractDkimRecord(records); final DnsRecord? dkimRecord = extractDkimRecord(records);
if (dkimRecord != null) { if (dkimRecord != null) {
await dnsProviderApi.setDnsRecord(dkimRecord, domain); await ProvidersController.currentDnsProvider!.setDnsRecord(
dkimRecord,
domain,
);
} }
await load(); await load();

View File

@ -40,7 +40,7 @@ class BackblazeFormCubit extends FormCubit {
@override @override
FutureOr<bool> asyncValidation() async { FutureOr<bool> asyncValidation() async {
late APIGenericResult<bool> backblazeResponse; late GenericResult<bool> backblazeResponse;
final BackblazeApi apiClient = BackblazeApi(isWithToken: false); final BackblazeApi apiClient = BackblazeApi(isWithToken: false);
try { try {
@ -51,7 +51,7 @@ class BackblazeFormCubit extends FormCubit {
backblazeResponse = await apiClient.isApiTokenValid(encodedApiKey); backblazeResponse = await apiClient.isApiTokenValid(encodedApiKey);
} catch (e) { } catch (e) {
addError(e); addError(e);
backblazeResponse = APIGenericResult( backblazeResponse = GenericResult(
success: false, success: false,
data: false, data: false,
message: e.toString(), message: e.toString(),

View File

@ -41,7 +41,7 @@ class DnsProviderFormCubit extends FormCubit {
} }
if (!isKeyValid) { if (!isKeyValid) {
apiKey.setError('initializing.cloudflare_bad_key_error'.tr()); apiKey.setError('initializing.dns_provider_bad_key_error'.tr());
} }
return isKeyValid; return isKeyValid;

View File

@ -1,7 +1,8 @@
import 'package:cubit_form/cubit_form.dart'; 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/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
class DomainSetupCubit extends Cubit<DomainSetupState> { class DomainSetupCubit extends Cubit<DomainSetupState> {
DomainSetupCubit(this.serverInstallationCubit) : super(Initial()); DomainSetupCubit(this.serverInstallationCubit) : super(Initial());
@ -10,36 +11,32 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
Future<void> load() async { Future<void> load() async {
emit(Loading(LoadingTypes.loadingDomain)); emit(Loading(LoadingTypes.loadingDomain));
final List<String> list = await ApiController.currentDnsProviderApiFactory! final GenericResult<List<String>> result =
.getDnsProvider() await ProvidersController.currentDnsProvider!.domainList();
.domainList(); if (!result.success || result.data.isEmpty) {
if (list.isEmpty) {
emit(Empty()); emit(Empty());
} else if (list.length == 1) { } else if (result.data.length == 1) {
emit(Loaded(list.first)); emit(Loaded(result.data.first));
} else { } else {
emit(MoreThenOne()); emit(MoreThenOne());
} }
} }
@override
Future<void> close() => super.close();
Future<void> saveDomain() async { Future<void> saveDomain() async {
assert(state is Loaded, 'wrong state'); assert(state is Loaded, 'wrong state');
final String domainName = (state as Loaded).domain; final String domainName = (state as Loaded).domain;
emit(Loading(LoadingTypes.saving)); emit(Loading(LoadingTypes.saving));
final String? zoneId = await ApiController.currentDnsProviderApiFactory! final dnsProvider = ProvidersController.currentDnsProvider!;
.getDnsProvider() final GenericResult<String?> zoneIdResult =
.getZoneId(domainName); await dnsProvider.getZoneId(domainName);
if (zoneId != null) { if (zoneIdResult.success || zoneIdResult.data != null) {
final ServerDomain domain = ServerDomain( final ServerDomain domain = ServerDomain(
domainName: domainName, domainName: domainName,
zoneId: zoneId, zoneId: zoneIdResult.data!,
provider: DnsProvider.cloudflare, provider: dnsProvider.type,
); );
serverInstallationCubit.setDomain(domain); serverInstallationCubit.setDomain(domain);

View File

@ -4,15 +4,12 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
class ProviderFormCubit extends FormCubit { class ServerProviderFormCubit extends FormCubit {
ProviderFormCubit(this.serverInstallationCubit) { ServerProviderFormCubit(this.serverInstallationCubit) {
//final int tokenLength =
// serverInstallationCubit.serverProviderApiTokenValidation().length;
apiKey = FieldCubit( apiKey = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('validations.required'.tr()), RequiredStringValidation('validations.required'.tr()),
//LengthStringNotEqualValidation(tokenLength),
], ],
); );

View File

@ -1,9 +1,8 @@
import 'package:selfprivacy/config/get_it_config.dart'; 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/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.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 { class MetricsLoadException implements Exception {
MetricsLoadException(this.message); MetricsLoadException(this.message);
@ -12,8 +11,7 @@ class MetricsLoadException implements Exception {
class MetricsRepository { class MetricsRepository {
Future<MetricsLoaded> getMetrics(final Period period) async { Future<MetricsLoaded> getMetrics(final Period period) async {
final providerApiFactory = ApiController.currentServerProviderApiFactory; if (ProvidersController.currentServerProvider == null) {
if (providerApiFactory == null) {
throw MetricsLoadException('Server Provider data is null'); throw MetricsLoadException('Server Provider data is null');
} }
@ -33,20 +31,19 @@ class MetricsRepository {
} }
final serverId = getIt<ApiConfigModel>().serverDetails!.id; final serverId = getIt<ApiConfigModel>().serverDetails!.id;
final ServerMetrics? metrics = final result = await ProvidersController.currentServerProvider!.getMetrics(
await providerApiFactory.getServerProvider().getMetrics( serverId,
serverId, start,
start, end,
end, );
);
if (metrics == null) { if (result.data == null || !result.success) {
throw MetricsLoadException('Metrics data is null'); throw MetricsLoadException('Metrics data is null');
} }
return MetricsLoaded( return MetricsLoaded(
period: period, period: period,
metrics: metrics, metrics: result.data!,
); );
} }
} }

View File

@ -1,13 +1,15 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.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/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/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.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/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/disk_status.dart'; import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/price.dart'; import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
part 'provider_volume_state.dart'; part 'provider_volume_state.dart';
@ -20,50 +22,50 @@ class ApiProviderVolumeCubit
@override @override
Future<void> load() async { Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) { if (serverInstallationCubit.state is ServerInstallationFinished) {
_refetch(); unawaited(_refetch());
} }
} }
Future<Price?> getPricePerGb() async => Future<Price?> getPricePerGb() async =>
ApiController.currentVolumeProviderApiFactory! (await ProvidersController.currentServerProvider!.getPricePerGb()).data;
.getVolumeProvider()
.getPricePerGb();
Future<void> refresh() async { Future<void> refresh() async {
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
_refetch(); unawaited(_refetch());
} }
Future<void> _refetch() async { Future<void> _refetch() async {
if (ApiController.currentVolumeProviderApiFactory == null) { if (ProvidersController.currentServerProvider == null) {
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
} }
final List<ServerVolume> volumes = await ApiController final volumesResult =
.currentVolumeProviderApiFactory! await ProvidersController.currentServerProvider!.getVolumes();
.getVolumeProvider()
.getVolumes();
if (volumes.isEmpty) { if (!volumesResult.success || volumesResult.data.isEmpty) {
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
} }
emit(ApiProviderVolumeState(volumes, LoadingStatus.success, false)); emit(
ApiProviderVolumeState(
volumesResult.data,
LoadingStatus.success,
false,
),
);
} }
Future<void> attachVolume(final DiskVolume volume) async { Future<void> attachVolume(final DiskVolume volume) async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!; final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
await ApiController.currentVolumeProviderApiFactory! await ProvidersController.currentServerProvider!
.getVolumeProvider()
.attachVolume(volume.providerVolume!, server.id); .attachVolume(volume.providerVolume!, server.id);
refresh(); unawaited(refresh());
} }
Future<void> detachVolume(final DiskVolume volume) async { Future<void> detachVolume(final DiskVolume volume) async {
await ApiController.currentVolumeProviderApiFactory! await ProvidersController.currentServerProvider!
.getVolumeProvider()
.detachVolume(volume.providerVolume!); .detachVolume(volume.providerVolume!);
refresh(); unawaited(refresh());
} }
Future<bool> resizeVolume( Future<bool> resizeVolume(
@ -75,14 +77,13 @@ class ApiProviderVolumeCubit
'Starting resize', 'Starting resize',
); );
emit(state.copyWith(isResizing: true)); emit(state.copyWith(isResizing: true));
final bool resized = await ApiController.currentVolumeProviderApiFactory! final resizedResult =
.getVolumeProvider() await ProvidersController.currentServerProvider!.resizeVolume(
.resizeVolume( volume.providerVolume!,
volume.providerVolume!, newSize,
newSize, );
);
if (!resized) { if (!resizedResult.success || !resizedResult.data) {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
'storage.extending_volume_error'.tr(), 'storage.extending_volume_error'.tr(),
); );
@ -113,11 +114,8 @@ class ApiProviderVolumeCubit
} }
Future<void> createVolume() async { Future<void> createVolume() async {
final ServerVolume? volume = (await ApiController final ServerVolume? volume =
.currentVolumeProviderApiFactory! (await ProvidersController.currentServerProvider!.createVolume()).data;
.getVolumeProvider()
.createVolume())
.data;
final diskVolume = DiskVolume(providerVolume: volume); final diskVolume = DiskVolume(providerVolume: volume);
await attachVolume(diskVolume); await attachVolume(diskVolume);
@ -125,14 +123,13 @@ class ApiProviderVolumeCubit
await Future.delayed(const Duration(seconds: 10)); await Future.delayed(const Duration(seconds: 10));
await ServerApi().mountVolume(volume!.name); await ServerApi().mountVolume(volume!.name);
refresh(); unawaited(refresh());
} }
Future<void> deleteVolume(final DiskVolume volume) async { Future<void> deleteVolume(final DiskVolume volume) async {
await ApiController.currentVolumeProviderApiFactory! await ProvidersController.currentServerProvider!
.getVolumeProvider()
.deleteVolume(volume.providerVolume!); .deleteVolume(volume.providerVolume!);
refresh(); unawaited(refresh());
} }
@override @override

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; 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/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
@ -32,7 +34,7 @@ class RecoveryKeyCubit
} }
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async { Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
final APIGenericResult<RecoveryKeyStatus?> response = final GenericResult<RecoveryKeyStatus?> response =
await api.getRecoveryTokenStatus(); await api.getRecoveryTokenStatus();
if (response.success) { if (response.success) {
return response.data; return response.data;
@ -57,10 +59,10 @@ class RecoveryKeyCubit
final DateTime? expirationDate, final DateTime? expirationDate,
final int? numberOfUses, final int? numberOfUses,
}) async { }) async {
final APIGenericResult<String> response = final GenericResult<String> response =
await api.generateRecoveryToken(expirationDate, numberOfUses); await api.generateRecoveryToken(expirationDate, numberOfUses);
if (response.success) { if (response.success) {
refresh(); unawaited(refresh());
return response.data; return response.data;
} else { } else {
throw GenerationError(response.message ?? 'Unknown error'); throw GenerationError(response.message ?? 'Unknown error');

View File

@ -1,23 +1,22 @@
import 'package:selfprivacy/config/get_it_config.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/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/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
class ServerDetailsRepository { class ServerDetailsRepository {
ServerApi server = ServerApi(); ServerApi server = ServerApi();
Future<ServerDetailsRepositoryDto> load() async { Future<ServerDetailsRepositoryDto> load() async {
final serverProviderApi = ApiController.currentServerProviderApiFactory; final serverProviderApi = ProvidersController.currentServerProvider;
final settings = await server.getSystemSettings(); final settings = await server.getSystemSettings();
final serverId = getIt<ApiConfigModel>().serverDetails!.id; final serverId = getIt<ApiConfigModel>().serverDetails!.id;
final metadata = final metadata = await serverProviderApi?.getMetadata(serverId);
await serverProviderApi!.getServerProvider().getMetadata(serverId);
return ServerDetailsRepositoryDto( return ServerDetailsRepositoryDto(
autoUpgradeSettings: settings.autoUpgradeSettings, autoUpgradeSettings: settings.autoUpgradeSettings,
metadata: metadata, metadata: metadata!.data,
serverTimezone: TimeZoneSettings.fromString( serverTimezone: TimeZoneSettings.fromString(
settings.timezone, settings.timezone,
), ),

View File

@ -5,21 +5,20 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:selfprivacy/config/get_it_config.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/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; import 'package:selfprivacy/logic/api_maps/tls_options.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/hive/backups_credential.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/hive/server_details.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/hive/user.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/launch_installation_data.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
export 'package:provider/provider.dart'; export 'package:provider/provider.dart';
@ -58,45 +57,29 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
} }
} }
void setServerProviderType(final ServerProvider providerType) async { void setServerProviderType(final ServerProviderType providerType) async {
await repository.saveServerProviderType(providerType); await repository.saveServerProviderType(providerType);
ApiController.initServerProviderApiFactory( ProvidersController.initServerProvider(
ServerProviderApiFactorySettings( ServerProviderSettings(provider: providerType),
provider: providerType,
),
); );
} }
void setDnsProviderType(final DnsProvider providerType) async { void setDnsProviderType(final DnsProviderType providerType) async {
await repository.saveDnsProviderType(providerType); await repository.saveDnsProviderType(providerType);
ApiController.initDnsProviderApiFactory( ProvidersController.initDnsProvider(
DnsProviderApiFactorySettings( DnsProviderSettings(
provider: providerType, provider: providerType,
), ),
); );
} }
ProviderApiTokenValidation serverProviderApiTokenValidation() =>
ApiController.currentServerProviderApiFactory!
.getServerProvider()
.getApiTokenValidation();
RegExp getDnsProviderApiTokenValidation() =>
ApiController.currentDnsProviderApiFactory!
.getDnsProvider()
.getApiTokenValidation();
Future<bool?> isServerProviderApiTokenValid( Future<bool?> isServerProviderApiTokenValid(
final String providerToken, final String providerToken,
) async { ) async {
final APIGenericResult<bool> apiResponse = final GenericResult<bool> apiResponse =
await ApiController.currentServerProviderApiFactory! await ProvidersController.currentServerProvider!.tryInitApiByToken(
.getServerProvider( providerToken,
settings: const ServerProviderApiSettings( );
isWithToken: false,
),
)
.isApiTokenValid(providerToken);
if (!apiResponse.success) { if (!apiResponse.success) {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
@ -111,12 +94,10 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
Future<bool?> isDnsProviderApiTokenValid( Future<bool?> isDnsProviderApiTokenValid(
final String providerToken, final String providerToken,
) async { ) async {
final APIGenericResult<bool> apiResponse = final GenericResult<bool> apiResponse =
await ApiController.currentDnsProviderApiFactory! await ProvidersController.currentDnsProvider!.tryInitApiByToken(
.getDnsProvider( providerToken,
settings: const DnsProviderApiSettings(isWithToken: false), );
)
.isApiTokenValid(providerToken);
if (!apiResponse.success) { if (!apiResponse.success) {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
@ -129,35 +110,33 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
} }
Future<List<ServerProviderLocation>> fetchAvailableLocations() async { Future<List<ServerProviderLocation>> fetchAvailableLocations() async {
if (ApiController.currentServerProviderApiFactory == null) { if (ProvidersController.currentServerProvider == null) {
return []; return [];
} }
final APIGenericResult apiResult = await ApiController final GenericResult apiResponse = await ProvidersController
.currentServerProviderApiFactory! .currentServerProvider!
.getServerProvider()
.getAvailableLocations(); .getAvailableLocations();
if (!apiResult.success) { if (!apiResponse.success) {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
'initializing.could_not_connect'.tr(), 'initializing.could_not_connect'.tr(),
); );
} }
return apiResult.data; return apiResponse.data;
} }
Future<List<ServerType>> fetchAvailableTypesByLocation( Future<List<ServerType>> fetchAvailableTypesByLocation(
final ServerProviderLocation location, final ServerProviderLocation location,
) async { ) async {
if (ApiController.currentServerProviderApiFactory == null) { if (ProvidersController.currentServerProvider == null) {
return []; return [];
} }
final APIGenericResult apiResult = await ApiController final GenericResult apiResult = await ProvidersController
.currentServerProviderApiFactory! .currentServerProvider!
.getServerProvider() .getServerTypes(location: location);
.getServerTypesByLocation(location: location);
if (!apiResult.success) { if (!apiResult.success) {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
@ -191,21 +170,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
void setServerType(final ServerType serverType) async { void setServerType(final ServerType serverType) async {
await repository.saveServerType(serverType); await repository.saveServerType(serverType);
ApiController.initServerProviderApiFactory( await ProvidersController.currentServerProvider!
ServerProviderApiFactorySettings( .trySetServerLocation(serverType.location.identifier);
provider: getIt<ApiConfigModel>().serverProvider!,
location: serverType.location.identifier,
),
);
// All server providers support volumes for now,
// so it's safe to initialize.
ApiController.initVolumeProviderApiFactory(
ServerProviderApiFactorySettings(
provider: getIt<ApiConfigModel>().serverProvider!,
location: serverType.location.identifier,
),
);
emit( emit(
(state as ServerInstallationNotFinished).copyWith( (state as ServerInstallationNotFinished).copyWith(
@ -216,10 +182,10 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
void setDnsApiToken(final String dnsApiToken) async { void setDnsApiToken(final String dnsApiToken) async {
if (state is ServerInstallationRecovery) { if (state is ServerInstallationRecovery) {
setAndValidateDnsApiToken(dnsApiToken); await setAndValidateDnsApiToken(dnsApiToken);
return; return;
} }
await repository.saveDnsProviderKey(dnsApiToken); await repository.setDnsApiToken(dnsApiToken);
emit( emit(
(state as ServerInstallationNotFinished) (state as ServerInstallationNotFinished)
@ -257,41 +223,53 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser)); emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser));
} }
Future<void> 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 { void createServerAndSetDnsRecords() async {
final ServerInstallationNotFinished stateCopy = emit((state as ServerInstallationNotFinished).copyWith(isLoading: true));
state as ServerInstallationNotFinished;
void onCancel() => emit(
(state as ServerInstallationNotFinished).copyWith(isLoading: false),
);
Future<void> onSuccess(final ServerHostingDetails serverDetails) async { final installationData = LaunchInstallationData(
await repository.createDnsRecords( rootUser: state.rootUser!,
serverDetails, dnsApiToken: state.dnsApiToken!,
state.serverDomain!, dnsProviderType: state.serverDomain!.provider,
onCancel: onCancel, serverDomain: state.serverDomain!,
); serverTypeId: state.serverTypeIdentificator!,
errorCallback: clearAppConfig,
successCallback: onCreateServerSuccess,
);
final result =
await ProvidersController.currentServerProvider!.launchInstallation(
installationData,
);
if (!result.success && result.data != null) {
emit( emit(
(state as ServerInstallationNotFinished).copyWith( (state as ServerInstallationNotFinished).copyWith(
isLoading: false, installationDialoguePopUp: result.data,
serverDetails: serverDetails,
), ),
); );
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);
} }
} }
@ -438,7 +416,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
emit(TimerState(dataState: dataState, isLoading: true)); emit(TimerState(dataState: dataState, isLoading: true));
final bool isServerWorking = await repository.isHttpServerWorking(); final bool isServerWorking = await repository.isHttpServerWorking();
StagingOptions.verifyCertificate = true; TlsOptions.verifyCertificate = true;
if (isServerWorking) { if (isServerWorking) {
bool dkimCreated = true; bool dkimCreated = true;
@ -488,7 +466,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
void submitDomainForAccessRecovery(final String domain) async { void submitDomainForAccessRecovery(final String domain) async {
final ServerDomain serverDomain = ServerDomain( final ServerDomain serverDomain = ServerDomain(
domainName: domain, domainName: domain,
provider: DnsProvider.unknown, provider: DnsProviderType.unknown,
zoneId: '', zoneId: '',
); );
final ServerRecoveryCapabilities recoveryCapabilities = final ServerRecoveryCapabilities recoveryCapabilities =
@ -540,7 +518,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
token, token,
dataState.recoveryCapabilities, dataState.recoveryCapabilities,
); );
final ServerProvider provider = await ServerApi( final ServerProviderType serverProvider = await ServerApi(
customToken: serverDetails.apiToken, customToken: serverDetails.apiToken,
isWithToken: true, isWithToken: true,
).getServerProviderType(); ).getServerProviderType();
@ -548,15 +526,15 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
customToken: serverDetails.apiToken, customToken: serverDetails.apiToken,
isWithToken: true, isWithToken: true,
).getDnsProviderType(); ).getDnsProviderType();
if (provider == ServerProvider.unknown || if (serverProvider == ServerProviderType.unknown ||
dnsProvider == DnsProvider.unknown) { dnsProvider == DnsProviderType.unknown) {
getIt<NavigationService>() getIt<NavigationService>()
.showSnackBar('recovering.generic_error'.tr()); .showSnackBar('recovering.generic_error'.tr());
return; return;
} }
await repository.saveServerDetails(serverDetails); await repository.saveServerDetails(serverDetails);
await repository.saveDnsProviderType(dnsProvider); await repository.saveDnsProviderType(dnsProvider);
setServerProviderType(provider); setServerProviderType(serverProvider);
setDnsProviderType(dnsProvider); setDnsProviderType(dnsProvider);
emit( emit(
dataState.copyWith( dataState.copyWith(
@ -684,7 +662,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
linuxDevice: '', linuxDevice: '',
), ),
apiToken: dataState.serverDetails!.apiToken, apiToken: dataState.serverDetails!.apiToken,
provider: ServerProvider.hetzner, provider: ServerProviderType.hetzner,
); );
await repository.saveDomain(serverDomain); await repository.saveDomain(serverDomain);
await repository.saveServerDetails(serverDetails); await repository.saveServerDetails(serverDetails);
@ -721,13 +699,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
provider: dnsProviderType, provider: dnsProviderType,
), ),
); );
await repository.saveDnsProviderKey(token); await repository.setDnsApiToken(token);
emit( emit(
dataState.copyWith( dataState.copyWith(
serverDomain: ServerDomain( serverDomain: ServerDomain(
domainName: serverDomain.domainName, domainName: serverDomain.domainName,
zoneId: zoneId, zoneId: zoneId,
provider: DnsProvider.cloudflare, provider: dnsProviderType,
), ),
dnsApiToken: token, dnsApiToken: token,
currentStep: RecoveryStep.backblazeToken, currentStep: RecoveryStep.backblazeToken,
@ -755,13 +733,44 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
@override @override
void onChange(final Change<ServerInstallationState> change) { void onChange(final Change<ServerInstallationState> 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); super.onChange(change);
} }
void clearAppConfig() { void clearAppConfig() {
closeTimer(); closeTimer();
ApiController.clearProviderApiFactories(); ProvidersController.clearProviders();
StagingOptions.verifyCertificate = false; TlsOptions.verifyCertificate = false;
repository.clearAppConfig(); repository.clearAppConfig();
emit(const ServerInstallationEmpty()); emit(const ServerInstallationEmpty());
} }

View File

@ -1,30 +1,26 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pub_semver/pub_semver.dart'; import 'package:pub_semver/pub_semver.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart'; import 'package:selfprivacy/logic/providers/provider_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/api_maps/graphql_maps/server_api/server_api.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/tls_options.dart';
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/hive/server_details.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/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/device_token.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_basic_info.dart';
import 'package:selfprivacy/logic/models/server_type.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'; import 'package:selfprivacy/utils/network_utils.dart';
class IpNotFoundException implements Exception { class IpNotFoundException implements Exception {
@ -47,49 +43,39 @@ class ServerInstallationRepository {
final String? dnsApiToken = getIt<ApiConfigModel>().dnsProviderKey; final String? dnsApiToken = getIt<ApiConfigModel>().dnsProviderKey;
final String? serverTypeIdentificator = getIt<ApiConfigModel>().serverType; final String? serverTypeIdentificator = getIt<ApiConfigModel>().serverType;
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain; final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
final ServerProvider? serverProvider = final DnsProviderType? dnsProvider = getIt<ApiConfigModel>().dnsProvider;
final ServerProviderType? serverProvider =
getIt<ApiConfigModel>().serverProvider; getIt<ApiConfigModel>().serverProvider;
final BackupsCredential? backblazeCredential = final BackupsCredential? backblazeCredential =
getIt<ApiConfigModel>().backblazeCredential; getIt<ApiConfigModel>().backblazeCredential;
final ServerHostingDetails? serverDetails = final ServerHostingDetails? serverDetails =
getIt<ApiConfigModel>().serverDetails; getIt<ApiConfigModel>().serverDetails;
final DnsProvider? dnsProvider = getIt<ApiConfigModel>().dnsProvider;
if (serverProvider != null || if (serverProvider != null ||
(serverDetails != null && (serverDetails != null &&
serverDetails.provider != ServerProvider.unknown)) { serverDetails.provider != ServerProviderType.unknown)) {
ApiController.initServerProviderApiFactory( ProvidersController.initServerProvider(
ServerProviderApiFactorySettings( ServerProviderSettings(
provider: serverProvider ?? serverDetails!.provider,
location: location,
),
);
// All current providers support volumes
// so it's safe to hardcode for now
ApiController.initVolumeProviderApiFactory(
ServerProviderApiFactorySettings(
provider: serverProvider ?? serverDetails!.provider, provider: serverProvider ?? serverDetails!.provider,
location: location, location: location,
), ),
); );
} }
if (ApiController.currentDnsProviderApiFactory == null) { if (dnsProvider != null ||
if (dnsProvider != null || (serverDomain != null &&
(serverDomain != null && serverDomain.provider != DnsProviderType.unknown)) {
serverDomain.provider != DnsProvider.unknown)) { ProvidersController.initDnsProvider(
ApiController.initDnsProviderApiFactory( DnsProviderSettings(
DnsProviderApiFactorySettings( provider: dnsProvider ?? serverDomain!.provider,
provider: dnsProvider ?? serverDomain!.provider, ),
), );
);
}
} }
if (box.get(BNames.hasFinalChecked, defaultValue: false)) { if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
StagingOptions.verifyCertificate = true; TlsOptions.verifyCertificate = true;
return ServerInstallationFinished( return ServerInstallationFinished(
installationDialoguePopUp: null,
providerApiToken: providerApiToken!, providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '', serverTypeIdentificator: serverTypeIdentificator ?? '',
dnsApiToken: dnsApiToken!, dnsApiToken: dnsApiToken!,
@ -149,8 +135,8 @@ class ServerInstallationRepository {
) { ) {
if (serverDetails != null) { if (serverDetails != null) {
if (serverProviderToken != null) { if (serverProviderToken != null) {
if (serverDetails.provider != ServerProvider.unknown) { if (serverDetails.provider != ServerProviderType.unknown) {
if (serverDomain.provider != DnsProvider.unknown) { if (serverDomain.provider != DnsProviderType.unknown) {
return RecoveryStep.backblazeToken; return RecoveryStep.backblazeToken;
} }
return RecoveryStep.dnsProviderToken; return RecoveryStep.dnsProviderToken;
@ -170,35 +156,26 @@ class ServerInstallationRepository {
Future<ServerHostingDetails> startServer( Future<ServerHostingDetails> startServer(
final ServerHostingDetails server, final ServerHostingDetails server,
) async { ) async {
ServerHostingDetails serverDetails; final result = await ProvidersController.currentServerProvider!.powerOn(
server.id,
);
serverDetails = await ApiController.currentServerProviderApiFactory! if (result.success && result.data != null) {
.getServerProvider() server.copyWith(startTime: result.data);
.powerOn(); }
return serverDetails; return server;
} }
Future<String?> getDomainId(final String token, final String domain) async { Future<String?> getDomainId(final String token, final String domain) async {
final DnsProviderApi dnsProviderApi = final result =
ApiController.currentDnsProviderApiFactory!.getDnsProvider( await ProvidersController.currentDnsProvider!.tryInitApiByToken(token);
settings: DnsProviderApiSettings( return result.success
isWithToken: false, ? (await ProvidersController.currentDnsProvider!.getZoneId(
customToken: token, domain,
), ))
); .data
: null;
/// 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<bool> apiResponse =
await dnsProviderApi.isApiTokenValid(token);
String? domainId;
if (apiResponse.success && apiResponse.data) {
domainId = await dnsProviderApi.getZoneId(domain);
}
return domainId;
} }
Future<Map<String, bool>> isDnsAddressesMatch( Future<Map<String, bool>> isDnsAddressesMatch(
@ -224,181 +201,7 @@ class ServerInstallationRepository {
return matches; return matches;
} }
Future<void> createServer(
final User rootUser,
final String domainName,
final String dnsApiToken,
final BackupsCredential backblazeCredential, {
required final void Function() onCancel,
required final Future<void> 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<ApiConfigModel>().dnsProvider!,
dnsApiToken: dnsApiToken,
rootUser: rootUser,
domainName: domainName,
serverType: getIt<ApiConfigModel>().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<ServerHostingDetails?> createServerResult =
await api.createServer(
dnsProvider: getIt<ApiConfigModel>().dnsProvider!,
dnsApiToken: dnsApiToken,
rootUser: rootUser,
domainName: domainName,
serverType: getIt<ApiConfigModel>().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<ApiConfigModel>().dnsProvider!,
dnsApiToken: dnsApiToken,
rootUser: rootUser,
domainName: domainName,
serverType: getIt<ApiConfigModel>().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<bool> 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<void> createDkimRecord(final ServerDomain cloudFlareDomain) async { Future<void> createDkimRecord(final ServerDomain cloudFlareDomain) async {
final DnsProviderApi dnsProviderApi =
ApiController.currentDnsProviderApiFactory!.getDnsProvider();
final ServerApi api = ServerApi(); final ServerApi api = ServerApi();
late DnsRecord record; late DnsRecord record;
@ -409,7 +212,10 @@ class ServerInstallationRepository {
rethrow; rethrow;
} }
await dnsProviderApi.setDnsRecord(record, cloudFlareDomain); await ProvidersController.currentDnsProvider!.setDnsRecord(
record,
cloudFlareDomain,
);
} }
Future<bool> isHttpServerWorking() async { Future<bool> isHttpServerWorking() async {
@ -417,15 +223,24 @@ class ServerInstallationRepository {
return api.isHttpServerWorking(); return api.isHttpServerWorking();
} }
Future<ServerHostingDetails> restart() async => Future<ServerHostingDetails> restart() async {
ApiController.currentServerProviderApiFactory! final server = getIt<ApiConfigModel>().serverDetails!;
.getServerProvider()
.restart();
Future<ServerHostingDetails> powerOn() async => final result = await ProvidersController.currentServerProvider!.restart(
ApiController.currentServerProviderApiFactory! server.id,
.getServerProvider() );
.powerOn();
if (result.success && result.data != null) {
server.copyWith(startTime: result.data);
}
return server;
}
Future<ServerHostingDetails> powerOn() async {
final server = getIt<ApiConfigModel>().serverDetails!;
return startServer(server);
}
Future<ServerRecoveryCapabilities> getRecoveryCapabilities( Future<ServerRecoveryCapabilities> getRecoveryCapabilities(
final ServerDomain serverDomain, final ServerDomain serverDomain,
@ -508,7 +323,7 @@ class ServerInstallationRepository {
overrideDomain: serverDomain.domainName, overrideDomain: serverDomain.domainName,
); );
final String serverIp = await getServerIpFromDomain(serverDomain); final String serverIp = await getServerIpFromDomain(serverDomain);
final APIGenericResult<String> result = await serverApi.authorizeDevice( final GenericResult<String> result = await serverApi.authorizeDevice(
DeviceToken(device: await getDeviceName(), token: newDeviceKey), DeviceToken(device: await getDeviceName(), token: newDeviceKey),
); );
@ -522,7 +337,7 @@ class ServerInstallationRepository {
serverId: 0, serverId: 0,
linuxDevice: '', linuxDevice: '',
), ),
provider: ServerProvider.unknown, provider: ServerProviderType.unknown,
id: 0, id: 0,
ip4: serverIp, ip4: serverIp,
startTime: null, startTime: null,
@ -545,7 +360,7 @@ class ServerInstallationRepository {
overrideDomain: serverDomain.domainName, overrideDomain: serverDomain.domainName,
); );
final String serverIp = await getServerIpFromDomain(serverDomain); final String serverIp = await getServerIpFromDomain(serverDomain);
final APIGenericResult<String> result = await serverApi.useRecoveryToken( final GenericResult<String> result = await serverApi.useRecoveryToken(
DeviceToken(device: await getDeviceName(), token: recoveryKey), DeviceToken(device: await getDeviceName(), token: recoveryKey),
); );
@ -559,7 +374,7 @@ class ServerInstallationRepository {
serverId: 0, serverId: 0,
linuxDevice: '', linuxDevice: '',
), ),
provider: ServerProvider.unknown, provider: ServerProviderType.unknown,
id: 0, id: 0,
ip4: serverIp, ip4: serverIp,
startTime: null, startTime: null,
@ -594,7 +409,7 @@ class ServerInstallationRepository {
sizeByte: 0, sizeByte: 0,
linuxDevice: '', linuxDevice: '',
), ),
provider: ServerProvider.unknown, provider: ServerProviderType.unknown,
id: 0, id: 0,
ip4: serverIp, ip4: serverIp,
startTime: null, startTime: null,
@ -606,9 +421,9 @@ class ServerInstallationRepository {
); );
} }
} }
final APIGenericResult<String> deviceAuthKey = final GenericResult<String> deviceAuthKey =
await serverApi.createDeviceToken(); await serverApi.createDeviceToken();
final APIGenericResult<String> result = await serverApi.authorizeDevice( final GenericResult<String> result = await serverApi.authorizeDevice(
DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data),
); );
@ -622,7 +437,7 @@ class ServerInstallationRepository {
serverId: 0, serverId: 0,
linuxDevice: '', linuxDevice: '',
), ),
provider: ServerProvider.unknown, provider: ServerProviderType.unknown,
id: 0, id: 0,
ip4: serverIp, ip4: serverIp,
startTime: null, startTime: null,
@ -664,9 +479,7 @@ class ServerInstallationRepository {
} }
Future<List<ServerBasicInfo>> getServersOnProviderAccount() async => Future<List<ServerBasicInfo>> getServersOnProviderAccount() async =>
ApiController.currentServerProviderApiFactory! (await ProvidersController.currentServerProvider!.getServers()).data;
.getServerProvider()
.getServers();
Future<void> saveServerDetails( Future<void> saveServerDetails(
final ServerHostingDetails serverDetails, final ServerHostingDetails serverDetails,
@ -679,10 +492,14 @@ class ServerInstallationRepository {
getIt<ApiConfigModel>().init(); getIt<ApiConfigModel>().init();
} }
Future<void> saveServerProviderType(final ServerProvider type) async { Future<void> saveServerProviderType(final ServerProviderType type) async {
await getIt<ApiConfigModel>().storeServerProviderType(type); await getIt<ApiConfigModel>().storeServerProviderType(type);
} }
Future<void> saveDnsProviderType(final DnsProviderType type) async {
await getIt<ApiConfigModel>().storeDnsProviderType(type);
}
Future<void> saveServerProviderKey(final String key) async { Future<void> saveServerProviderKey(final String key) async {
await getIt<ApiConfigModel>().storeServerProviderKey(key); await getIt<ApiConfigModel>().storeServerProviderKey(key);
} }
@ -701,10 +518,6 @@ class ServerInstallationRepository {
getIt<ApiConfigModel>().init(); getIt<ApiConfigModel>().init();
} }
Future<void> saveDnsProviderType(final DnsProvider type) async {
await getIt<ApiConfigModel>().storeDnsProviderType(type);
}
Future<void> saveBackblazeKey( Future<void> saveBackblazeKey(
final BackupsCredential backblazeCredential, final BackupsCredential backblazeCredential,
) async { ) async {
@ -716,7 +529,7 @@ class ServerInstallationRepository {
getIt<ApiConfigModel>().init(); getIt<ApiConfigModel>().init();
} }
Future<void> saveDnsProviderKey(final String key) async { Future<void> setDnsApiToken(final String key) async {
await getIt<ApiConfigModel>().storeDnsProviderKey(key); await getIt<ApiConfigModel>().storeDnsProviderKey(key);
} }
@ -759,12 +572,10 @@ class ServerInstallationRepository {
} }
Future<bool> deleteServer(final ServerDomain serverDomain) async { Future<bool> deleteServer(final ServerDomain serverDomain) async {
final APIGenericResult<bool> deletionResult = await ApiController final deletionResult =
.currentServerProviderApiFactory! await ProvidersController.currentServerProvider!.deleteServer(
.getServerProvider() serverDomain.domainName,
.deleteServer( );
domainName: serverDomain.domainName,
);
if (!deletionResult.success) { if (!deletionResult.success) {
getIt<NavigationService>() getIt<NavigationService>()
@ -772,12 +583,6 @@ class ServerInstallationRepository {
return false; return false;
} }
if (!deletionResult.data) {
getIt<NavigationService>()
.showSnackBar('modals.server_deletion_error'.tr());
return false;
}
await box.put(BNames.hasFinalChecked, false); await box.put(BNames.hasFinalChecked, false);
await box.put(BNames.isServerStarted, false); await box.put(BNames.isServerStarted, false);
await box.put(BNames.isServerResetedFirstTime, false); await box.put(BNames.isServerResetedFirstTime, false);
@ -785,10 +590,9 @@ class ServerInstallationRepository {
await box.put(BNames.isLoading, false); await box.put(BNames.isLoading, false);
await box.put(BNames.serverDetails, null); await box.put(BNames.serverDetails, null);
final APIGenericResult<void> removalResult = await ApiController final GenericResult<void> removalResult = await ProvidersController
.currentDnsProviderApiFactory! .currentDnsProvider!
.getDnsProvider() .removeDomainRecords(domain: serverDomain);
.removeSimilarRecords(domain: serverDomain);
if (!removalResult.success) { if (!removalResult.success) {
getIt<NavigationService>().showSnackBar('modals.dns_removal_error'.tr()); getIt<NavigationService>().showSnackBar('modals.dns_removal_error'.tr());

View File

@ -12,6 +12,7 @@ abstract class ServerInstallationState extends Equatable {
required this.isServerStarted, required this.isServerStarted,
required this.isServerResetedFirstTime, required this.isServerResetedFirstTime,
required this.isServerResetedSecondTime, required this.isServerResetedSecondTime,
required this.installationDialoguePopUp,
}); });
@override @override
@ -25,6 +26,7 @@ abstract class ServerInstallationState extends Equatable {
serverDetails, serverDetails,
isServerStarted, isServerStarted,
isServerResetedFirstTime, isServerResetedFirstTime,
installationDialoguePopUp
]; ];
final String? providerApiToken; final String? providerApiToken;
@ -37,6 +39,7 @@ abstract class ServerInstallationState extends Equatable {
final bool isServerStarted; final bool isServerStarted;
final bool isServerResetedFirstTime; final bool isServerResetedFirstTime;
final bool isServerResetedSecondTime; final bool isServerResetedSecondTime;
final CallbackDialogueBranching? installationDialoguePopUp;
bool get isServerProviderApiKeyFilled => providerApiToken != null; bool get isServerProviderApiKeyFilled => providerApiToken != null;
bool get isServerTypeFilled => serverTypeIdentificator != null; bool get isServerTypeFilled => serverTypeIdentificator != null;
@ -96,6 +99,7 @@ class TimerState extends ServerInstallationNotFinished {
isServerResetedFirstTime: dataState.isServerResetedFirstTime, isServerResetedFirstTime: dataState.isServerResetedFirstTime,
isServerResetedSecondTime: dataState.isServerResetedSecondTime, isServerResetedSecondTime: dataState.isServerResetedSecondTime,
dnsMatches: dataState.dnsMatches, dnsMatches: dataState.dnsMatches,
installationDialoguePopUp: dataState.installationDialoguePopUp,
); );
final ServerInstallationNotFinished dataState; final ServerInstallationNotFinished dataState;
@ -138,6 +142,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
super.serverDomain, super.serverDomain,
super.rootUser, super.rootUser,
super.serverDetails, super.serverDetails,
super.installationDialoguePopUp,
}); });
final bool isLoading; final bool isLoading;
final Map<String, bool>? dnsMatches; final Map<String, bool>? dnsMatches;
@ -155,6 +160,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
isServerResetedFirstTime, isServerResetedFirstTime,
isLoading, isLoading,
dnsMatches, dnsMatches,
installationDialoguePopUp,
]; ];
ServerInstallationNotFinished copyWith({ ServerInstallationNotFinished copyWith({
@ -170,6 +176,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
final bool? isServerResetedSecondTime, final bool? isServerResetedSecondTime,
final bool? isLoading, final bool? isLoading,
final Map<String, bool>? dnsMatches, final Map<String, bool>? dnsMatches,
final CallbackDialogueBranching? installationDialoguePopUp,
}) => }) =>
ServerInstallationNotFinished( ServerInstallationNotFinished(
providerApiToken: providerApiToken ?? this.providerApiToken, providerApiToken: providerApiToken ?? this.providerApiToken,
@ -187,6 +194,8 @@ class ServerInstallationNotFinished extends ServerInstallationState {
isServerResetedSecondTime ?? this.isServerResetedSecondTime, isServerResetedSecondTime ?? this.isServerResetedSecondTime,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
dnsMatches: dnsMatches ?? this.dnsMatches, dnsMatches: dnsMatches ?? this.dnsMatches,
installationDialoguePopUp:
installationDialoguePopUp ?? this.installationDialoguePopUp,
); );
ServerInstallationFinished finish() => ServerInstallationFinished( ServerInstallationFinished finish() => ServerInstallationFinished(
@ -200,6 +209,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
isServerStarted: isServerStarted, isServerStarted: isServerStarted,
isServerResetedFirstTime: isServerResetedFirstTime, isServerResetedFirstTime: isServerResetedFirstTime,
isServerResetedSecondTime: isServerResetedSecondTime, isServerResetedSecondTime: isServerResetedSecondTime,
installationDialoguePopUp: installationDialoguePopUp,
); );
} }
@ -218,6 +228,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
isServerResetedSecondTime: false, isServerResetedSecondTime: false,
isLoading: false, isLoading: false,
dnsMatches: null, dnsMatches: null,
installationDialoguePopUp: null,
); );
} }
@ -233,6 +244,7 @@ class ServerInstallationFinished extends ServerInstallationState {
required super.isServerStarted, required super.isServerStarted,
required super.isServerResetedFirstTime, required super.isServerResetedFirstTime,
required super.isServerResetedSecondTime, required super.isServerResetedSecondTime,
required super.installationDialoguePopUp,
}); });
@override @override
@ -246,6 +258,7 @@ class ServerInstallationFinished extends ServerInstallationState {
serverDetails, serverDetails,
isServerStarted, isServerStarted,
isServerResetedFirstTime, isServerResetedFirstTime,
installationDialoguePopUp,
]; ];
} }
@ -287,6 +300,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
isServerStarted: true, isServerStarted: true,
isServerResetedFirstTime: true, isServerResetedFirstTime: true,
isServerResetedSecondTime: true, isServerResetedSecondTime: true,
installationDialoguePopUp: null,
); );
final RecoveryStep currentStep; final RecoveryStep currentStep;
final ServerRecoveryCapabilities recoveryCapabilities; final ServerRecoveryCapabilities recoveryCapabilities;
@ -302,7 +316,8 @@ class ServerInstallationRecovery extends ServerInstallationState {
serverDetails, serverDetails,
isServerStarted, isServerStarted,
isServerResetedFirstTime, isServerResetedFirstTime,
currentStep currentStep,
installationDialoguePopUp
]; ];
ServerInstallationRecovery copyWith({ ServerInstallationRecovery copyWith({
@ -340,5 +355,6 @@ class ServerInstallationRecovery extends ServerInstallationState {
isServerStarted: true, isServerStarted: true,
isServerResetedFirstTime: true, isServerResetedFirstTime: true,
isServerResetedSecondTime: true, isServerResetedSecondTime: true,
installationDialoguePopUp: null,
); );
} }

View File

@ -24,7 +24,7 @@ class ApiServerVolumeCubit
@override @override
Future<void> load() async { Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) { if (serverInstallationCubit.state is ServerInstallationFinished) {
reload(); unawaited(reload());
} }
} }

View File

@ -53,7 +53,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
} }
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
reload(); unawaited(reload());
await Future.delayed(const Duration(seconds: 10)); await Future.delayed(const Duration(seconds: 10));
emit( emit(
state.copyWith( state.copyWith(
@ -62,7 +62,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
.toList(), .toList(),
), ),
); );
reload(); unawaited(reload());
} }
Future<void> moveService( Future<void> moveService(

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
@ -39,7 +41,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
); );
} }
refresh(); unawaited(refresh());
} }
Future<void> refresh() async { Future<void> refresh() async {
@ -78,7 +80,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
return; return;
} }
// If API returned error, do nothing // If API returned error, do nothing
final APIGenericResult<User?> result = final GenericResult<User?> result =
await api.createUser(user.login, password); await api.createUser(user.login, password);
if (result.data == null) { if (result.data == null) {
getIt<NavigationService>() getIt<NavigationService>()
@ -101,7 +103,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
return; return;
} }
final List<User> loadedUsers = List<User>.from(state.users); final List<User> loadedUsers = List<User>.from(state.users);
final APIGenericResult result = await api.deleteUser(user.login); final GenericResult result = await api.deleteUser(user.login);
if (result.success && result.data) { if (result.success && result.data) {
loadedUsers.removeWhere((final User u) => u.login == user.login); loadedUsers.removeWhere((final User u) => u.login == user.login);
await box.clear(); await box.clear();
@ -128,7 +130,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
.showSnackBar('users.could_not_change_password'.tr()); .showSnackBar('users.could_not_change_password'.tr());
return; return;
} }
final APIGenericResult<User?> result = final GenericResult<User?> result =
await api.updateUser(user.login, newPassword); await api.updateUser(user.login, newPassword);
if (result.data == null) { if (result.data == null) {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
@ -138,7 +140,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
} }
Future<void> addSshKey(final User user, final String publicKey) async { Future<void> addSshKey(final User user, final String publicKey) async {
final APIGenericResult<User?> result = final GenericResult<User?> result =
await api.addSshKey(user.login, publicKey); await api.addSshKey(user.login, publicKey);
if (result.data != null) { if (result.data != null) {
final User updatedUser = result.data!; final User updatedUser = result.data!;
@ -157,7 +159,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
} }
Future<void> deleteSshKey(final User user, final String publicKey) async { Future<void> deleteSshKey(final User user, final String publicKey) async {
final APIGenericResult<User?> result = final GenericResult<User?> result =
await api.removeSshKey(user.login, publicKey); await api.removeSshKey(user.login, publicKey);
if (result.data != null) { if (result.data != null) {
final User updatedUser = result.data!; final User updatedUser = result.data!;

View File

@ -13,8 +13,8 @@ class ApiConfigModel {
String? get serverLocation => _serverLocation; String? get serverLocation => _serverLocation;
String? get serverType => _serverType; String? get serverType => _serverType;
String? get dnsProviderKey => _dnsProviderKey; String? get dnsProviderKey => _dnsProviderKey;
ServerProvider? get serverProvider => _serverProvider; ServerProviderType? get serverProvider => _serverProvider;
DnsProvider? get dnsProvider => _dnsProvider; DnsProviderType? get dnsProvider => _dnsProvider;
BackupsCredential? get backblazeCredential => _backblazeCredential; BackupsCredential? get backblazeCredential => _backblazeCredential;
ServerDomain? get serverDomain => _serverDomain; ServerDomain? get serverDomain => _serverDomain;
@ -24,19 +24,19 @@ class ApiConfigModel {
String? _serverLocation; String? _serverLocation;
String? _dnsProviderKey; String? _dnsProviderKey;
String? _serverType; String? _serverType;
ServerProvider? _serverProvider; ServerProviderType? _serverProvider;
DnsProvider? _dnsProvider; DnsProviderType? _dnsProvider;
ServerHostingDetails? _serverDetails; ServerHostingDetails? _serverDetails;
BackupsCredential? _backblazeCredential; BackupsCredential? _backblazeCredential;
ServerDomain? _serverDomain; ServerDomain? _serverDomain;
BackblazeBucket? _backblazeBucket; BackblazeBucket? _backblazeBucket;
Future<void> storeServerProviderType(final ServerProvider value) async { Future<void> storeServerProviderType(final ServerProviderType value) async {
await _box.put(BNames.serverProvider, value); await _box.put(BNames.serverProvider, value);
_serverProvider = value; _serverProvider = value;
} }
Future<void> storeDnsProviderType(final DnsProvider value) async { Future<void> storeDnsProviderType(final DnsProviderType value) async {
await _box.put(BNames.dnsProvider, value); await _box.put(BNames.dnsProvider, value);
_dnsProvider = value; _dnsProvider = value;
} }

View File

@ -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<CallbackDialogueChoice> choices;
}
class CallbackDialogueChoice {
CallbackDialogueChoice({
required this.title,
required this.callback,
});
final String title;
final Future<GenericResult<CallbackDialogueBranching?>> Function()? callback;
}

View File

@ -6,30 +6,35 @@ part of 'backups_credential.dart';
// TypeAdapterGenerator // TypeAdapterGenerator
// ************************************************************************** // **************************************************************************
class BackblazeCredentialAdapter extends TypeAdapter<BackblazeCredential> { class BackupsCredentialAdapter extends TypeAdapter<BackupsCredential> {
@override @override
final int typeId = 4; final int typeId = 4;
@override @override
BackblazeCredential read(BinaryReader reader) { BackupsCredential read(BinaryReader reader) {
final numOfFields = reader.readByte(); final numOfFields = reader.readByte();
final fields = <int, dynamic>{ final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
}; };
return BackblazeCredential( return BackupsCredential(
keyId: fields[0] as String, keyId: fields[0] as String,
applicationKey: fields[1] as String, applicationKey: fields[1] as String,
provider: fields[2] == null
? BackupsProvider.backblaze
: fields[2] as BackupsProvider,
); );
} }
@override @override
void write(BinaryWriter writer, BackblazeCredential obj) { void write(BinaryWriter writer, BackupsCredential obj) {
writer writer
..writeByte(2) ..writeByte(3)
..writeByte(0) ..writeByte(0)
..write(obj.keyId) ..write(obj.keyId)
..writeByte(1) ..writeByte(1)
..write(obj.applicationKey); ..write(obj.applicationKey)
..writeByte(2)
..write(obj.provider);
} }
@override @override
@ -38,7 +43,7 @@ class BackblazeCredentialAdapter extends TypeAdapter<BackblazeCredential> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is BackblazeCredentialAdapter && other is BackupsCredentialAdapter &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; typeId == other.typeId;
} }

View File

@ -33,8 +33,8 @@ class ServerHostingDetails {
@HiveField(5) @HiveField(5)
final String apiToken; final String apiToken;
@HiveField(6, defaultValue: ServerProvider.hetzner) @HiveField(6, defaultValue: ServerProviderType.hetzner)
final ServerProvider provider; final ServerProviderType provider;
ServerHostingDetails copyWith({final DateTime? startTime}) => ServerHostingDetails copyWith({final DateTime? startTime}) =>
ServerHostingDetails( ServerHostingDetails(
@ -77,7 +77,7 @@ class ServerVolume {
} }
@HiveType(typeId: 101) @HiveType(typeId: 101)
enum ServerProvider { enum ServerProviderType {
@HiveField(0) @HiveField(0)
unknown, unknown,
@HiveField(1) @HiveField(1)
@ -85,7 +85,7 @@ enum ServerProvider {
@HiveField(2) @HiveField(2)
digitalOcean; digitalOcean;
factory ServerProvider.fromGraphQL(final Enum$ServerProvider provider) { factory ServerProviderType.fromGraphQL(final Enum$ServerProvider provider) {
switch (provider) { switch (provider) {
case Enum$ServerProvider.HETZNER: case Enum$ServerProvider.HETZNER:
return hetzner; return hetzner;
@ -98,9 +98,9 @@ enum ServerProvider {
String get displayName { String get displayName {
switch (this) { switch (this) {
case ServerProvider.hetzner: case ServerProviderType.hetzner:
return 'Hetzner Cloud'; return 'Hetzner Cloud';
case ServerProvider.digitalOcean: case ServerProviderType.digitalOcean:
return 'Digital Ocean'; return 'Digital Ocean';
default: default:
return 'Unknown'; return 'Unknown';

View File

@ -23,8 +23,8 @@ class ServerHostingDetailsAdapter extends TypeAdapter<ServerHostingDetails> {
volume: fields[4] as ServerVolume, volume: fields[4] as ServerVolume,
apiToken: fields[5] as String, apiToken: fields[5] as String,
provider: fields[6] == null provider: fields[6] == null
? ServerProvider.hetzner ? ServerProviderType.hetzner
: fields[6] as ServerProvider, : fields[6] as ServerProviderType,
startTime: fields[2] as DateTime?, startTime: fields[2] as DateTime?,
); );
} }
@ -109,34 +109,34 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
typeId == other.typeId; typeId == other.typeId;
} }
class ServerProviderAdapter extends TypeAdapter<ServerProvider> { class ServerProviderTypeAdapter extends TypeAdapter<ServerProviderType> {
@override @override
final int typeId = 101; final int typeId = 101;
@override @override
ServerProvider read(BinaryReader reader) { ServerProviderType read(BinaryReader reader) {
switch (reader.readByte()) { switch (reader.readByte()) {
case 0: case 0:
return ServerProvider.unknown; return ServerProviderType.unknown;
case 1: case 1:
return ServerProvider.hetzner; return ServerProviderType.hetzner;
case 2: case 2:
return ServerProvider.digitalOcean; return ServerProviderType.digitalOcean;
default: default:
return ServerProvider.unknown; return ServerProviderType.unknown;
} }
} }
@override @override
void write(BinaryWriter writer, ServerProvider obj) { void write(BinaryWriter writer, ServerProviderType obj) {
switch (obj) { switch (obj) {
case ServerProvider.unknown: case ServerProviderType.unknown:
writer.writeByte(0); writer.writeByte(0);
break; break;
case ServerProvider.hetzner: case ServerProviderType.hetzner:
writer.writeByte(1); writer.writeByte(1);
break; break;
case ServerProvider.digitalOcean: case ServerProviderType.digitalOcean:
writer.writeByte(2); writer.writeByte(2);
break; break;
} }
@ -148,7 +148,7 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ServerProviderAdapter && other is ServerProviderTypeAdapter &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; typeId == other.typeId;
} }

View File

@ -17,28 +17,32 @@ class ServerDomain {
@HiveField(1) @HiveField(1)
final String zoneId; final String zoneId;
@HiveField(2, defaultValue: DnsProvider.cloudflare) @HiveField(2, defaultValue: DnsProviderType.cloudflare)
final DnsProvider provider; final DnsProviderType provider;
@override @override
String toString() => '$domainName: $zoneId'; String toString() => '$domainName: $zoneId';
} }
@HiveType(typeId: 100) @HiveType(typeId: 100)
enum DnsProvider { enum DnsProviderType {
@HiveField(0) @HiveField(0)
unknown, unknown,
@HiveField(1) @HiveField(1)
cloudflare, cloudflare,
@HiveField(2) @HiveField(2)
desec; desec,
@HiveField(3)
digitalOcean;
factory DnsProvider.fromGraphQL(final Enum$DnsProvider provider) { factory DnsProviderType.fromGraphQL(final Enum$DnsProvider provider) {
switch (provider) { switch (provider) {
case Enum$DnsProvider.CLOUDFLARE: case Enum$DnsProvider.CLOUDFLARE:
return cloudflare; return cloudflare;
case Enum$DnsProvider.DESEC: case Enum$DnsProvider.DESEC:
return desec; return desec;
case Enum$DnsProvider.DIGITALOCEAN:
return digitalOcean;
default: default:
return unknown; return unknown;
} }

View File

@ -19,8 +19,9 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
return ServerDomain( return ServerDomain(
domainName: fields[0] as String, domainName: fields[0] as String,
zoneId: fields[1] as String, zoneId: fields[1] as String,
provider: provider: fields[2] == null
fields[2] == null ? DnsProvider.cloudflare : fields[2] as DnsProvider, ? DnsProviderType.cloudflare
: fields[2] as DnsProviderType,
); );
} }
@ -47,36 +48,41 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
typeId == other.typeId; typeId == other.typeId;
} }
class DnsProviderAdapter extends TypeAdapter<DnsProvider> { class DnsProviderTypeAdapter extends TypeAdapter<DnsProviderType> {
@override @override
final int typeId = 100; final int typeId = 100;
@override @override
DnsProvider read(BinaryReader reader) { DnsProviderType read(BinaryReader reader) {
switch (reader.readByte()) { switch (reader.readByte()) {
case 0: case 0:
return DnsProvider.unknown; return DnsProviderType.unknown;
case 1: case 1:
return DnsProvider.cloudflare; return DnsProviderType.cloudflare;
case 2: case 2:
return DnsProvider.desec; return DnsProviderType.desec;
case 3:
return DnsProviderType.digitalOcean;
default: default:
return DnsProvider.unknown; return DnsProviderType.unknown;
} }
} }
@override @override
void write(BinaryWriter writer, DnsProvider obj) { void write(BinaryWriter writer, DnsProviderType obj) {
switch (obj) { switch (obj) {
case DnsProvider.unknown: case DnsProviderType.unknown:
writer.writeByte(0); writer.writeByte(0);
break; break;
case DnsProvider.cloudflare: case DnsProviderType.cloudflare:
writer.writeByte(1); writer.writeByte(1);
break; break;
case DnsProvider.desec: case DnsProviderType.desec:
writer.writeByte(2); writer.writeByte(2);
break; break;
case DnsProviderType.digitalOcean:
writer.writeByte(3);
break;
} }
} }
@ -86,7 +92,7 @@ class DnsProviderAdapter extends TypeAdapter<DnsProvider> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is DnsProviderAdapter && other is DnsProviderTypeAdapter &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; typeId == other.typeId;
} }

View File

@ -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<int>? dropletIds;
@JsonKey(name: 'size_gigabytes')
final int sizeGigabytes;
static DigitalOceanVolume fromJson(final Map<String, dynamic> json) =>
_$DigitalOceanVolumeFromJson(json);
}
@JsonSerializable()
class DigitalOceanLocation {
DigitalOceanLocation(
this.slug,
this.name,
);
final String slug;
final String name;
static DigitalOceanLocation fromJson(final Map<String, dynamic> json) =>
_$DigitalOceanLocationFromJson(json);
}
@JsonSerializable()
class DigitalOceanServerType {
DigitalOceanServerType(
this.regions,
this.memory,
this.description,
this.disk,
this.priceMonthly,
this.slug,
this.vcpus,
);
final List<String> 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<String, dynamic> json) =>
_$DigitalOceanServerTypeFromJson(json);
}

View File

@ -0,0 +1,61 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'digital_ocean_server_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DigitalOceanVolume _$DigitalOceanVolumeFromJson(Map<String, dynamic> json) =>
DigitalOceanVolume(
json['id'] as String,
json['name'] as String,
json['size_gigabytes'] as int,
(json['droplet_ids'] as List<dynamic>?)?.map((e) => e as int).toList(),
);
Map<String, dynamic> _$DigitalOceanVolumeToJson(DigitalOceanVolume instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'droplet_ids': instance.dropletIds,
'size_gigabytes': instance.sizeGigabytes,
};
DigitalOceanLocation _$DigitalOceanLocationFromJson(
Map<String, dynamic> json) =>
DigitalOceanLocation(
json['slug'] as String,
json['name'] as String,
);
Map<String, dynamic> _$DigitalOceanLocationToJson(
DigitalOceanLocation instance) =>
<String, dynamic>{
'slug': instance.slug,
'name': instance.name,
};
DigitalOceanServerType _$DigitalOceanServerTypeFromJson(
Map<String, dynamic> json) =>
DigitalOceanServerType(
(json['regions'] as List<dynamic>).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<String, dynamic> _$DigitalOceanServerTypeToJson(
DigitalOceanServerType instance) =>
<String, dynamic>{
'regions': instance.regions,
'memory': instance.memory,
'slug': instance.slug,
'description': instance.description,
'vcpus': instance.vcpus,
'disk': instance.disk,
'price_monthly': instance.priceMonthly,
};

View File

@ -9,6 +9,7 @@ class DnsRecord {
required this.type, required this.type,
required this.name, required this.name,
required this.content, required this.content,
this.id,
this.ttl = 3600, this.ttl = 3600,
this.priority = 10, this.priority = 10,
this.proxied = false, this.proxied = false,
@ -31,5 +32,8 @@ class DnsRecord {
final int priority; final int priority;
final bool proxied; final bool proxied;
/// TODO: Refactoring refactoring refactoring refactoring >:c
final int? id;
Map<String, dynamic> toJson() => _$DnsRecordToJson(this); Map<String, dynamic> toJson() => _$DnsRecordToJson(this);
} }

View File

@ -13,4 +13,5 @@ Map<String, dynamic> _$DnsRecordToJson(DnsRecord instance) => <String, dynamic>{
'ttl': instance.ttl, 'ttl': instance.ttl,
'priority': instance.priority, 'priority': instance.priority,
'proxied': instance.proxied, 'proxied': instance.proxied,
'id': instance.id,
}; };

View File

@ -72,11 +72,21 @@ enum ServerStatus {
@JsonSerializable() @JsonSerializable()
class HetznerServerTypeInfo { 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 int cores;
final num memory; final num memory;
final int disk; final int disk;
final String name;
final String description;
final List<HetznerPriceInfo> prices; final List<HetznerPriceInfo> prices;
static HetznerServerTypeInfo fromJson(final Map<String, dynamic> json) => static HetznerServerTypeInfo fromJson(final Map<String, dynamic> json) =>
@ -85,7 +95,11 @@ class HetznerServerTypeInfo {
@JsonSerializable() @JsonSerializable()
class HetznerPriceInfo { class HetznerPriceInfo {
HetznerPriceInfo(this.hourly, this.monthly); HetznerPriceInfo(
this.hourly,
this.monthly,
this.location,
);
@JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice) @JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice)
final double hourly; final double hourly;
@ -93,6 +107,8 @@ class HetznerPriceInfo {
@JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice) @JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice)
final double monthly; final double monthly;
final String location;
static HetznerPriceInfo fromJson(final Map<String, dynamic> json) => static HetznerPriceInfo fromJson(final Map<String, dynamic> json) =>
_$HetznerPriceInfoFromJson(json); _$HetznerPriceInfoFromJson(json);
@ -102,7 +118,14 @@ class HetznerPriceInfo {
@JsonSerializable() @JsonSerializable()
class HetznerLocation { 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 country;
final String city; final String city;
final String description; final String description;
@ -113,3 +136,24 @@ class HetznerLocation {
static HetznerLocation fromJson(final Map<String, dynamic> json) => static HetznerLocation fromJson(final Map<String, dynamic> json) =>
_$HetznerLocationFromJson(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<String, dynamic> json) =>
_$HetznerVolumeFromJson(json);
}

View File

@ -81,6 +81,8 @@ HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
(json['prices'] as List<dynamic>) (json['prices'] as List<dynamic>)
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>)) .map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
json['name'] as String,
json['description'] as String,
); );
Map<String, dynamic> _$HetznerServerTypeInfoToJson( Map<String, dynamic> _$HetznerServerTypeInfoToJson(
@ -89,6 +91,8 @@ Map<String, dynamic> _$HetznerServerTypeInfoToJson(
'cores': instance.cores, 'cores': instance.cores,
'memory': instance.memory, 'memory': instance.memory,
'disk': instance.disk, 'disk': instance.disk,
'name': instance.name,
'description': instance.description,
'prices': instance.prices, 'prices': instance.prices,
}; };
@ -96,12 +100,14 @@ HetznerPriceInfo _$HetznerPriceInfoFromJson(Map<String, dynamic> json) =>
HetznerPriceInfo( HetznerPriceInfo(
HetznerPriceInfo.getPrice(json['price_hourly'] as Map), HetznerPriceInfo.getPrice(json['price_hourly'] as Map),
HetznerPriceInfo.getPrice(json['price_monthly'] as Map), HetznerPriceInfo.getPrice(json['price_monthly'] as Map),
json['location'] as String,
); );
Map<String, dynamic> _$HetznerPriceInfoToJson(HetznerPriceInfo instance) => Map<String, dynamic> _$HetznerPriceInfoToJson(HetznerPriceInfo instance) =>
<String, dynamic>{ <String, dynamic>{
'price_hourly': instance.hourly, 'price_hourly': instance.hourly,
'price_monthly': instance.monthly, 'price_monthly': instance.monthly,
'location': instance.location,
}; };
HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) => HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) =>
@ -110,12 +116,32 @@ HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) =>
json['city'] as String, json['city'] as String,
json['description'] as String, json['description'] as String,
json['network_zone'] as String, json['network_zone'] as String,
json['name'] as String,
); );
Map<String, dynamic> _$HetznerLocationToJson(HetznerLocation instance) => Map<String, dynamic> _$HetznerLocationToJson(HetznerLocation instance) =>
<String, dynamic>{ <String, dynamic>{
'name': instance.name,
'country': instance.country, 'country': instance.country,
'city': instance.city, 'city': instance.city,
'description': instance.description, 'description': instance.description,
'network_zone': instance.zone, 'network_zone': instance.zone,
}; };
HetznerVolume _$HetznerVolumeFromJson(Map<String, dynamic> json) =>
HetznerVolume(
json['id'] as int,
json['size'] as int,
json['serverId'] as int?,
json['name'] as String,
json['linux_device'] as String?,
);
Map<String, dynamic> _$HetznerVolumeToJson(HetznerVolume instance) =>
<String, dynamic>{
'id': instance.id,
'size': instance.size,
'serverId': instance.serverId,
'name': instance.name,
'linux_device': instance.linuxDevice,
};

View File

@ -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;
}

View File

@ -19,11 +19,11 @@ enum MetadataType {
class ServerMetadataEntity { class ServerMetadataEntity {
ServerMetadataEntity({ ServerMetadataEntity({
required this.name, required this.trId,
required this.value, required this.value,
this.type = MetadataType.other, this.type = MetadataType.other,
}); });
final MetadataType type; final MetadataType type;
final String name; final String trId;
final String value; final String value;
} }

View File

@ -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<GenericResult<bool>> 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<GenericResult<String?>> 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<GenericResult<void>> 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<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
}) async {
final List<DnsRecord> 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<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) {
final records = getProjectDnsRecords(domain.domainName, ip4);
return _adapter.api().createMultipleDnsRecords(
domain: domain,
records: records,
);
}
@override
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: [record],
);
@override
Future<GenericResult<List<String>>> domainList() async {
List<String> 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<String>(
(final el) => el['name'] as String,
)
.toList();
return GenericResult(
success: true,
data: domains,
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final GenericResult<List<DnsRecord>> records =
await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DesiredDnsRecord> 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<DnsRecord> 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 <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
}

View File

@ -1,173 +1,113 @@
import 'dart:io'; 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: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/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.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 { class ApiAdapter {
DesecApi({ ApiAdapter({final bool isWithToken = true})
this.hasLogger = false, : _api = DesecApi(
this.isWithToken = true, isWithToken: isWithToken,
this.customToken, );
});
@override
final bool hasLogger;
@override
final bool 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 @override
RegExp getApiTokenValidation() => DnsProviderType get type => DnsProviderType.desec;
RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
@override @override
BaseOptions get options { Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final BaseOptions options = BaseOptions( final api = _adapter.api(getInitialized: false);
baseUrl: rootAddress, final result = await api.isApiTokenValid(token);
contentType: Headers.jsonContentType, if (!result.data || !result.success) {
responseType: ResponseType.json, return result;
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Token $token'};
} }
if (customToken != null) { _adapter = ApiAdapter(isWithToken: true);
options.headers = {'Authorization': 'Token $customToken'}; return result;
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
} }
@override @override
String rootAddress = 'https://desec.io/api/v1/domains/'; Future<GenericResult<String?>> getZoneId(final String domain) async =>
GenericResult(
@override data: domain,
Future<APIGenericResult<bool>> isApiTokenValid(final String token) async { success: true,
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 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 @override
Future<String?> getZoneId(final String domain) async => domain; Future<GenericResult<void>> removeDomainRecords({
@override
Future<APIGenericResult<void>> removeSimilarRecords({
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4, final String? ip4,
}) async { }) async {
final String domainName = domain.domainName; final List<DnsRecord> listDnsRecords = projectDnsRecords(
final String url = '/$domainName/rrsets/'; domain.domainName,
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4); ip4,
);
final Dio client = await getClient(); final List<dynamic> bulkRecords = [];
try { for (final DnsRecord record in listDnsRecords) {
final List<dynamic> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) {
bulkRecords.add(
{
'subname': record.name,
'type': record.type,
'ttl': record.ttl,
'records': [],
},
);
}
bulkRecords.add( bulkRecords.add(
{ {
'subname': 'selector._domainkey', 'subname': record.name,
'type': 'TXT', 'type': record.type,
'ttl': 18000, 'ttl': record.ttl,
'records': [], '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 @override
Future<List<DnsRecord>> getDnsRecords({ Future<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain, required final ServerDomain domain,
}) async { }) async {
Response response; final List<DnsRecord> records = [];
final String domainName = domain.domainName; final result = await _adapter.api().getDnsRecords(domain: domain);
final List<DnsRecord> allRecords = <DnsRecord>[]; 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 { try {
response = await client.get(url); for (final record in result.data) {
await Future.delayed(const Duration(seconds: 1));
final List records = response.data;
for (final record in records) {
final String? content = (record['records'] is List<dynamic>) final String? content = (record['records'] is List<dynamic>)
? record['records'][0] ? record['records'][0]
: record['records']; : record['records'];
allRecords.add( records.add(
DnsRecord( DnsRecord(
name: record['subname'], name: record['subname'],
type: record['type'], type: record['type'],
@ -178,54 +118,14 @@ class DesecApi extends DnsProviderApi {
} }
} catch (e) { } catch (e) {
print(e); print(e);
} finally { return GenericResult(
close(client);
}
return allRecords;
}
@override
Future<APIGenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final Dio client = await getClient();
try {
final List<dynamic> 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(
success: false, success: false,
data: null, data: records,
message: e.toString(), message: e.toString(),
); );
} finally {
close(client);
} }
return APIGenericResult(success: true, data: null); return GenericResult(success: true, data: records);
} }
List<DnsRecord> projectDnsRecords( List<DnsRecord> projectDnsRecords(
@ -275,6 +175,57 @@ class DesecApi extends DnsProviderApi {
]; ];
} }
@override
Future<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final List<DnsRecord> listDnsRecords = projectDnsRecords(
domain.domainName,
ip4,
);
final List<dynamic> 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<GenericResult<void>> 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? extractContent(final DnsRecord record) {
String? content = record.content; String? content = record.content;
if (record.type == 'TXT' && content != null && !content.startsWith('"')) { if (record.type == 'TXT' && content != null && !content.startsWith('"')) {
@ -285,60 +236,47 @@ class DesecApi extends DnsProviderApi {
} }
@override @override
Future<void> setDnsRecord( Future<GenericResult<List<String>>> domainList() async {
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<List<String>> domainList() async {
List<String> domains = []; List<String> domains = [];
final result = await _adapter.api().getDomains();
final Dio client = await getClient(); if (result.data.isEmpty || !result.success) {
try { return GenericResult(
final Response response = await client.get( success: result.success,
'', data: domains,
code: result.code,
message: result.message,
); );
await Future.delayed(const Duration(seconds: 1));
domains = response.data
.map<String>((final el) => el['name'] as String)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
} }
return domains; domains = result.data
.map<String>(
(final el) => el['name'] as String,
)
.toList();
return GenericResult(
success: true,
data: domains,
);
} }
@override @override
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords( Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain, final ServerDomain domain,
final String ip4, final String ip4,
final String dkimPublicKey, final String dkimPublicKey,
) async { ) async {
final List<DnsRecord> 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<DesiredDnsRecord> foundRecords = []; final List<DesiredDnsRecord> foundRecords = [];
try { try {
final List<DesiredDnsRecord> desiredRecords = final List<DesiredDnsRecord> desiredRecords =
@ -384,13 +322,13 @@ class DesecApi extends DnsProviderApi {
} }
} catch (e) { } catch (e) {
print(e); print(e);
return APIGenericResult( return GenericResult(
data: [], data: [],
success: false, success: false,
message: e.toString(), message: e.toString(),
); );
} }
return APIGenericResult( return GenericResult(
data: foundRecords, data: foundRecords,
success: true, success: true,
); );

View File

@ -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<GenericResult<bool>> 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<GenericResult<String?>> getZoneId(final String domain) async =>
GenericResult(
data: domain,
success: true,
);
@override
Future<GenericResult<void>> 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<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
}) async {
final List<DnsRecord> 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<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: getProjectDnsRecords(
domain.domainName,
ip4,
),
);
@override
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: [record],
);
@override
Future<GenericResult<List<String>>> domainList() async {
List<String> 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<String>(
(final el) => el['name'] as String,
)
.toList();
return GenericResult(
success: true,
data: domains,
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final GenericResult<List<DnsRecord>> records =
await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DnsRecord> 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 <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
@override
List<DesiredDnsRecord> 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,
),
];
}
}

View File

@ -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<GenericResult<bool>> tryInitApiByToken(final String token);
Future<GenericResult<String?>> getZoneId(final String domain);
Future<GenericResult<void>> removeDomainRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
});
Future<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
);
Future<GenericResult<List<String>>> domainList();
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
);
}

View File

@ -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');
}
}
}

View File

@ -1,20 +1,20 @@
import 'package:selfprivacy/logic/models/hive/server_details.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/server_domain.dart';
class ServerProviderApiFactorySettings { class ServerProviderSettings {
ServerProviderApiFactorySettings({ ServerProviderSettings({
required this.provider, required this.provider,
this.location, this.location,
}); });
final ServerProvider provider; final ServerProviderType provider;
final String? location; final String? location;
} }
class DnsProviderApiFactorySettings { class DnsProviderSettings {
DnsProviderApiFactorySettings({ DnsProviderSettings({
required this.provider, required this.provider,
}); });
final DnsProvider provider; final DnsProviderType provider;
} }

View File

@ -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;
}

View File

@ -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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<CallbackDialogueBranching?>> 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<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
final List<ServerProviderLocation> 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<DigitalOceanLocation> 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<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
}) async {
final List<ServerType> 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<DigitalOceanServerType> 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<GenericResult<List<ServerBasicInfo>>> getServers() async {
List<ServerBasicInfo> 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<ServerBasicInfo>(
(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<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
) async {
List<ServerMetadataEntity> 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<TimeSeriesData> calculateCpuLoadMetrics(final List rawProcStatMetrics) {
final List<TimeSeriesData> 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<GenericResult<ServerMetrics?>> 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<GenericResult<DateTime?>> 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<GenericResult<CallbackDialogueBranching?>> 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<Future> laterFutures = <Future>[];
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<GenericResult<List<ServerVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> 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<GenericResult<ServerVolume?>> 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<GenericResult<ServerVolume?>> 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<GenericResult<void>> deleteVolume(
final ServerVolume volume,
) async =>
_adapter.api().deleteVolume(
volume.uuid!,
);
@override
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
volume.name,
serverId,
);
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
) async =>
_adapter.api().detachVolume(
volume.name,
volume.serverId!,
);
@override
Future<GenericResult<bool>> 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<GenericResult<Price?>> getPricePerGb() async => GenericResult(
success: true,
data: Price(
value: 0.10,
currency: currency,
),
);
@override
Future<GenericResult<DateTime?>> 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,
);
}
}

View File

@ -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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
final List<ServerProviderLocation> 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<HetznerLocation> 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<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
}) async {
final List<ServerType> 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<GenericResult<List<ServerBasicInfo>>> getServers() async {
final List<ServerBasicInfo> 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<HetznerServerInfo> 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<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
) async {
List<ServerMetadataEntity> 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<HetznerServerInfo> 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<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
List<TimeSeriesData> serializeTimeSeries(
final Map<String, dynamic> 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<GenericResult<DateTime?>> 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<GenericResult<DateTime?>> 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<GenericResult<CallbackDialogueBranching?>> 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<GenericResult<CallbackDialogueBranching?>> 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<Future> laterFutures = <Future>[];
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<GenericResult<ServerVolume?>> 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<GenericResult<List<ServerVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> 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<GenericResult<void>> deleteVolume(final ServerVolume volume) async =>
_adapter.api().deleteVolume(volume.id);
@override
Future<GenericResult<bool>> 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<GenericResult<bool>> detachVolume(
final ServerVolume volume,
) async =>
_adapter.api().detachVolume(
volume.id,
);
@override
Future<GenericResult<bool>> 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<GenericResult<Price?>> 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,
),
);
}
}

View File

@ -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<GenericResult<List<ServerBasicInfo>>> getServers();
Future<GenericResult<bool>> trySetServerLocation(final String location);
Future<GenericResult<bool>> tryInitApiByToken(final String token);
Future<GenericResult<List<ServerProviderLocation>>> getAvailableLocations();
Future<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
});
Future<GenericResult<CallbackDialogueBranching?>> deleteServer(
final String hostname,
);
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,
);
Future<GenericResult<DateTime?>> powerOn(final int serverId);
Future<GenericResult<DateTime?>> restart(final int serverId);
Future<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
);
Future<GenericResult<Price?>> getPricePerGb();
Future<GenericResult<List<ServerVolume>>> getVolumes({final String? status});
Future<GenericResult<ServerVolume?>> createVolume();
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
);
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
);
Future<GenericResult<bool>> detachVolume(final ServerVolume volume);
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
);
GenericResult<bool> get success => GenericResult(success: true, data: true);
}

View File

@ -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');
}
}
}

View File

@ -72,7 +72,10 @@ class ServiceConsumptionTitle extends StatelessWidget {
service.svgIcon, service.svgIcon,
width: 24.0, width: 24.0,
height: 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), const SizedBox(width: 16),

View File

@ -2,12 +2,12 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.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/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_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/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/utils/network_utils.dart';
@RoutePage() @RoutePage()
class DnsDetailsPage extends StatefulWidget { class DnsDetailsPage extends StatefulWidget {
@ -111,9 +111,12 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
heroIcon: BrandIcons.globe, heroIcon: BrandIcons.globe,
heroTitle: 'domain.screen_title'.tr(), heroTitle: 'domain.screen_title'.tr(),
children: <Widget>[ children: <Widget>[
_getStateCard(dnsCubit.dnsState, () { _getStateCard(
context.read<DnsRecordsCubit>().fix(); dnsCubit.dnsState,
}), () {
context.read<DnsRecordsCubit>().fix();
},
),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
ListTile( ListTile(
title: Text( title: Text(
@ -152,7 +155,7 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
dnsRecord.description.tr(), dnsRecord.description.tr(),
), ),
subtitle: Text( subtitle: Text(
dnsRecord.name, dnsRecord.displayName ?? dnsRecord.name,
), ),
), ),
], ],

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.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/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
@ -37,8 +37,19 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
title: Text('developer_settings.use_staging_acme'.tr()), title: Text('developer_settings.use_staging_acme'.tr()),
subtitle: subtitle:
Text('developer_settings.use_staging_acme_description'.tr()), Text('developer_settings.use_staging_acme_description'.tr()),
value: StagingOptions.stagingAcme, value: TlsOptions.stagingAcme,
onChanged: null, 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(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),

View File

@ -49,7 +49,8 @@ class _ConsolePageState extends State<ConsolePage> {
actions: [ actions: [
IconButton( IconButton(
icon: Icon( icon: Icon(
paused ? Icons.play_arrow_outlined : Icons.pause_outlined), paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
),
onPressed: () => setState(() => paused = !paused), onPressed: () => setState(() => paused = !paused),
), ),
], ],

View File

@ -26,7 +26,7 @@ class _TextDetails extends StatelessWidget {
...details.metadata.map( ...details.metadata.map(
(final metadata) => ListTileOnSurfaceVariant( (final metadata) => ListTileOnSurfaceVariant(
leadingIcon: metadata.type.icon, leadingIcon: metadata.type.icon,
title: metadata.name, title: metadata.trId.tr(),
subtitle: metadata.value, subtitle: metadata.value,
), ),
), ),

View File

@ -133,7 +133,10 @@ class ServerConsumptionListTile extends StatelessWidget {
service.svgIcon, service.svgIcon,
width: 24.0, width: 24.0,
height: 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(), rightSideText: service.storageUsage.used.toString(),
percentage: service.storageUsage.used.byte / volume.sizeTotal.byte, percentage: service.storageUsage.used.byte / volume.sizeTotal.byte,

View File

@ -2,14 +2,15 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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/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/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/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/brand_button.dart';
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
import 'package:selfprivacy/ui/components/cards/outlined_card.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 { class DnsProviderPicker extends StatefulWidget {
const DnsProviderPicker({ const DnsProviderPicker({
@ -26,9 +27,9 @@ class DnsProviderPicker extends StatefulWidget {
} }
class _DnsProviderPickerState extends State<DnsProviderPicker> { class _DnsProviderPickerState extends State<DnsProviderPicker> {
DnsProvider selectedProvider = DnsProvider.unknown; DnsProviderType selectedProvider = DnsProviderType.unknown;
void setProvider(final DnsProvider provider) { void setProvider(final DnsProviderType provider) {
setState(() { setState(() {
selectedProvider = provider; selectedProvider = provider;
}); });
@ -37,35 +38,36 @@ class _DnsProviderPickerState extends State<DnsProviderPicker> {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
switch (selectedProvider) { switch (selectedProvider) {
case DnsProvider.unknown: case DnsProviderType.unknown:
return ProviderSelectionPage( return ProviderSelectionPage(
serverInstallationCubit: widget.serverInstallationCubit, serverInstallationCubit: widget.serverInstallationCubit,
callback: setProvider, callback: setProvider,
); );
case DnsProvider.cloudflare: case DnsProviderType.cloudflare:
return ProviderInputDataPage( return ProviderInputDataPage(
providerCubit: widget.formCubit, providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo( providerInfo: const ProviderPageInfo(
providerType: DnsProvider.cloudflare, providerType: DnsProviderType.cloudflare,
pathToHow: 'how_cloudflare', pathToHow: 'how_cloudflare',
image: Image.asset(
'assets/images/logos/cloudflare.svg',
width: 150,
),
), ),
); );
case DnsProvider.desec: case DnsProviderType.digitalOcean:
return ProviderInputDataPage( return ProviderInputDataPage(
providerCubit: widget.formCubit, providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo( providerInfo: const ProviderPageInfo(
providerType: DnsProvider.desec, 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', pathToHow: 'how_desec',
image: Image.asset(
'assets/images/logos/desec.svg',
width: 150,
),
), ),
); );
} }
@ -76,12 +78,10 @@ class ProviderPageInfo {
const ProviderPageInfo({ const ProviderPageInfo({
required this.providerType, required this.providerType,
required this.pathToHow, required this.pathToHow,
required this.image,
}); });
final String pathToHow; final String pathToHow;
final Image image; final DnsProviderType providerType;
final DnsProvider providerType;
} }
class ProviderInputDataPage extends StatelessWidget { class ProviderInputDataPage extends StatelessWidget {
@ -99,7 +99,7 @@ class ProviderInputDataPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'initializing.cloudflare_api_token'.tr(), 'initializing.connect_to_dns'.tr(),
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -124,12 +124,22 @@ class ProviderInputDataPage extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
BrandOutlinedButton( BrandOutlinedButton(
child: Text('initializing.how'.tr()), child: Text('initializing.how'.tr()),
onPressed: () { onPressed: () => showModalBottomSheet<void>(
context.read<SupportSystemCubit>().showArticle( context: context,
article: providerInfo.pathToHow, isScrollControlled: true,
context: context, 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, width: double.infinity,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
/// TODO: Remove obvious repetition
children: [ children: [
Text( Text(
'initializing.select_dns'.tr(), 'initializing.select_dns'.tr(),
@ -202,13 +214,13 @@ class ProviderSelectionPage extends StatelessWidget {
text: 'basis.select'.tr(), text: 'basis.select'.tr(),
onPressed: () { onPressed: () {
serverInstallationCubit serverInstallationCubit
.setDnsProviderType(DnsProvider.desec); .setDnsProviderType(DnsProviderType.desec);
callback(DnsProvider.desec); callback(DnsProviderType.desec);
}, },
), ),
// Outlined button that will open website // Outlined button that will open website
BrandOutlinedButton( BrandOutlinedButton(
onPressed: () => launchURL('https://desec.io/'), onPressed: () => launchUrlString('https://desec.io/'),
title: 'initializing.select_provider_site_button'.tr(), title: 'initializing.select_provider_site_button'.tr(),
), ),
], ],
@ -257,14 +269,70 @@ class ProviderSelectionPage extends StatelessWidget {
text: 'basis.select'.tr(), text: 'basis.select'.tr(),
onPressed: () { onPressed: () {
serverInstallationCubit serverInstallationCubit
.setDnsProviderType(DnsProvider.cloudflare); .setDnsProviderType(DnsProviderType.cloudflare);
callback(DnsProvider.cloudflare); callback(DnsProviderType.cloudflare);
}, },
), ),
// Outlined button that will open website // Outlined button that will open website
BrandOutlinedButton( BrandOutlinedButton(
onPressed: () => 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(), title: 'initializing.select_provider_site_button'.tr(),
), ),
], ],

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
@ -211,10 +211,11 @@ class InitializingPage extends StatelessWidget {
final ServerInstallationCubit serverInstallationCubit, final ServerInstallationCubit serverInstallationCubit,
) => ) =>
BlocProvider( BlocProvider(
create: (final context) => ProviderFormCubit(serverInstallationCubit), create: (final context) =>
ServerProviderFormCubit(serverInstallationCubit),
child: Builder( child: Builder(
builder: (final context) { builder: (final context) {
final providerCubit = context.watch<ProviderFormCubit>(); final providerCubit = context.watch<ServerProviderFormCubit>();
return ServerProviderPicker( return ServerProviderPicker(
formCubit: providerCubit, formCubit: providerCubit,
serverInstallationCubit: serverInstallationCubit, serverInstallationCubit: serverInstallationCubit,
@ -227,7 +228,8 @@ class InitializingPage extends StatelessWidget {
final ServerInstallationCubit serverInstallationCubit, final ServerInstallationCubit serverInstallationCubit,
) => ) =>
BlocProvider( BlocProvider(
create: (final context) => ProviderFormCubit(serverInstallationCubit), create: (final context) =>
ServerProviderFormCubit(serverInstallationCubit),
child: Builder( child: Builder(
builder: (final context) => ServerTypePicker( builder: (final context) => ServerTypePicker(
serverInstallationCubit: serverInstallationCubit, serverInstallationCubit: serverInstallationCubit,

View File

@ -3,7 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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/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/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
@ -20,7 +20,7 @@ class ServerProviderPicker extends StatefulWidget {
super.key, super.key,
}); });
final ProviderFormCubit formCubit; final ServerProviderFormCubit formCubit;
final ServerInstallationCubit serverInstallationCubit; final ServerInstallationCubit serverInstallationCubit;
@override @override
@ -28,9 +28,9 @@ class ServerProviderPicker extends StatefulWidget {
} }
class _ServerProviderPickerState extends State<ServerProviderPicker> { class _ServerProviderPickerState extends State<ServerProviderPicker> {
ServerProvider selectedProvider = ServerProvider.unknown; ServerProviderType selectedProvider = ServerProviderType.unknown;
void setProvider(final ServerProvider provider) { void setProvider(final ServerProviderType provider) {
setState(() { setState(() {
selectedProvider = provider; selectedProvider = provider;
}); });
@ -39,17 +39,17 @@ class _ServerProviderPickerState extends State<ServerProviderPicker> {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
switch (selectedProvider) { switch (selectedProvider) {
case ServerProvider.unknown: case ServerProviderType.unknown:
return ProviderSelectionPage( return ProviderSelectionPage(
serverInstallationCubit: widget.serverInstallationCubit, serverInstallationCubit: widget.serverInstallationCubit,
callback: setProvider, callback: setProvider,
); );
case ServerProvider.hetzner: case ServerProviderType.hetzner:
return ProviderInputDataPage( return ProviderInputDataPage(
providerCubit: widget.formCubit, providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo( providerInfo: ProviderPageInfo(
providerType: ServerProvider.hetzner, providerType: ServerProviderType.hetzner,
pathToHow: 'how_hetzner', pathToHow: 'how_hetzner',
image: Image.asset( image: Image.asset(
'assets/images/logos/hetzner.png', 'assets/images/logos/hetzner.png',
@ -58,11 +58,11 @@ class _ServerProviderPickerState extends State<ServerProviderPicker> {
), ),
); );
case ServerProvider.digitalOcean: case ServerProviderType.digitalOcean:
return ProviderInputDataPage( return ProviderInputDataPage(
providerCubit: widget.formCubit, providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo( providerInfo: ProviderPageInfo(
providerType: ServerProvider.digitalOcean, providerType: ServerProviderType.digitalOcean,
pathToHow: 'how_digital_ocean', pathToHow: 'how_digital_ocean',
image: Image.asset( image: Image.asset(
'assets/images/logos/digital_ocean.png', 'assets/images/logos/digital_ocean.png',
@ -83,7 +83,7 @@ class ProviderPageInfo {
final String pathToHow; final String pathToHow;
final Image image; final Image image;
final ServerProvider providerType; final ServerProviderType providerType;
} }
class ProviderInputDataPage extends StatelessWidget { class ProviderInputDataPage extends StatelessWidget {
@ -94,7 +94,7 @@ class ProviderInputDataPage extends StatelessWidget {
}); });
final ProviderPageInfo providerInfo; final ProviderPageInfo providerInfo;
final ProviderFormCubit providerCubit; final ServerProviderFormCubit providerCubit;
@override @override
Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox( Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox(
@ -238,9 +238,10 @@ class ProviderSelectionPage extends StatelessWidget {
BrandButton.filled( BrandButton.filled(
child: Text('basis.select'.tr()), child: Text('basis.select'.tr()),
onPressed: () { onPressed: () {
serverInstallationCubit serverInstallationCubit.setServerProviderType(
.setServerProviderType(ServerProvider.hetzner); ServerProviderType.hetzner,
callback(ServerProvider.hetzner); );
callback(ServerProviderType.hetzner);
}, },
), ),
// Outlined button that will open website // Outlined button that will open website
@ -313,9 +314,9 @@ class ProviderSelectionPage extends StatelessWidget {
child: Text('basis.select'.tr()), child: Text('basis.select'.tr()),
onPressed: () { onPressed: () {
serverInstallationCubit.setServerProviderType( serverInstallationCubit.setServerProviderType(
ServerProvider.digitalOcean, ServerProviderType.digitalOcean,
); );
callback(ServerProvider.digitalOcean); callback(ServerProviderType.digitalOcean);
}, },
), ),
// Outlined button that will open website // Outlined button that will open website

View File

@ -1,6 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
@ -16,52 +16,48 @@ class RecoveryServerProviderConnected extends StatelessWidget {
context.watch<ServerInstallationCubit>(); context.watch<ServerInstallationCubit>();
return BlocProvider( return BlocProvider(
create: (final BuildContext context) => ProviderFormCubit(appConfig), create: (final BuildContext context) =>
ServerProviderFormCubit(appConfig),
child: Builder( child: Builder(
builder: (final BuildContext context) { builder: (final BuildContext context) => BrandHeroScreen(
final FormCubitState formCubitState = heroTitle: 'recovering.server_provider_connected'.tr(),
context.watch<ProviderFormCubit>().state; heroSubtitle: 'recovering.server_provider_connected_description'.tr(
args: [appConfig.state.serverDomain?.domainName ?? 'your domain'],
return BrandHeroScreen( ),
heroTitle: 'recovering.server_provider_connected'.tr(), hasBackButton: true,
heroSubtitle: 'recovering.server_provider_connected_description'.tr( hasFlashButton: false,
args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], ignoreBreakpoints: true,
hasSupportDrawer: true,
onBackButtonPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
children: [
CubitFormTextField(
formFieldCubit: context.read<ServerProviderFormCubit>().apiKey,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText:
'recovering.server_provider_connected_placeholder'.tr(),
),
), ),
hasBackButton: true, const SizedBox(height: 16),
hasFlashButton: false, BrandButton.filled(
ignoreBreakpoints: true, onPressed: () =>
hasSupportDrawer: true, context.read<ServerProviderFormCubit>().trySubmit(),
onBackButtonPressed: () { child: Text('basis.continue'.tr()),
Navigator.of(context).popUntil((final route) => route.isFirst); ),
}, const SizedBox(height: 16),
children: [ Builder(
CubitFormTextField( builder: (final context) => BrandButton.text(
formFieldCubit: context.read<ProviderFormCubit>().apiKey, title: 'initializing.how'.tr(),
decoration: InputDecoration( onPressed: () => context.read<SupportSystemCubit>().showArticle(
border: const OutlineInputBorder(), article: 'how_hetzner',
labelText: context: context,
'recovering.server_provider_connected_placeholder'.tr(), ),
),
), ),
const SizedBox(height: 16), ),
BrandButton.filled( ],
onPressed: () => context.read<ProviderFormCubit>().trySubmit(), ),
child: Text('basis.continue'.tr()),
),
const SizedBox(height: 16),
Builder(
builder: (final context) => BrandButton.text(
title: 'initializing.how'.tr(),
onPressed: () =>
context.read<SupportSystemCubit>().showArticle(
article: 'how_hetzner',
context: context,
),
),
),
],
);
},
), ),
); );
} }

View File

@ -21,103 +21,10 @@ abstract class _$RootRouter extends RootStackRouter {
child: const BackupDetailsPage(), child: const BackupDetailsPage(),
); );
}, },
DevicesRoute.name: (routeData) { RootRoute.name: (routeData) {
return AutoRoutePage<dynamic>( return AutoRoutePage<dynamic>(
routeData: routeData, routeData: routeData,
child: const DevicesScreen(), child: WrappedRoute(child: const RootPage()),
);
},
DnsDetailsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const DnsDetailsPage(),
);
},
AppSettingsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const AppSettingsPage(),
);
},
DeveloperSettingsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const DeveloperSettingsPage(),
);
},
AboutApplicationRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const AboutApplicationPage(),
);
},
MoreRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const MorePage(),
);
},
ConsoleRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const ConsolePage(),
);
},
OnboardingRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const OnboardingPage(),
);
},
ProvidersRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const ProvidersPage(),
);
},
RecoveryKeyRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const RecoveryKeyPage(),
);
},
ServerDetailsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const ServerDetailsScreen(),
);
},
ServicesMigrationRoute.name: (routeData) {
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: ServicesMigrationPage(
services: args.services,
diskStatus: args.diskStatus,
isMigration: args.isMigration,
key: args.key,
),
);
},
ExtendingVolumeRoute.name: (routeData) {
final args = routeData.argsAs<ExtendingVolumeRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: ExtendingVolumePage(
diskVolumeToResize: args.diskVolumeToResize,
diskStatus: args.diskStatus,
key: args.key,
),
);
},
ServerStorageRoute.name: (routeData) {
final args = routeData.argsAs<ServerStorageRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: ServerStoragePage(
diskStatus: args.diskStatus,
key: args.key,
),
); );
}, },
ServiceRoute.name: (routeData) { ServiceRoute.name: (routeData) {
@ -136,16 +43,10 @@ abstract class _$RootRouter extends RootStackRouter {
child: const ServicesPage(), child: const ServicesPage(),
); );
}, },
InitializingRoute.name: (routeData) { ServerDetailsRoute.name: (routeData) {
return AutoRoutePage<dynamic>( return AutoRoutePage<dynamic>(
routeData: routeData, routeData: routeData,
child: const InitializingPage(), child: const ServerDetailsScreen(),
);
},
RecoveryRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const RecoveryRouting(),
); );
}, },
UsersRoute.name: (routeData) { UsersRoute.name: (routeData) {
@ -170,10 +71,109 @@ abstract class _$RootRouter extends RootStackRouter {
), ),
); );
}, },
RootRoute.name: (routeData) { AppSettingsRoute.name: (routeData) {
return AutoRoutePage<dynamic>( return AutoRoutePage<dynamic>(
routeData: routeData, routeData: routeData,
child: WrappedRoute(child: const RootPage()), child: const AppSettingsPage(),
);
},
DeveloperSettingsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const DeveloperSettingsPage(),
);
},
MoreRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const MorePage(),
);
},
AboutApplicationRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const AboutApplicationPage(),
);
},
ConsoleRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const ConsolePage(),
);
},
ProvidersRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const ProvidersPage(),
);
},
RecoveryKeyRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const RecoveryKeyPage(),
);
},
DnsDetailsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const DnsDetailsPage(),
);
},
RecoveryRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const RecoveryRouting(),
);
},
InitializingRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const InitializingPage(),
);
},
ServerStorageRoute.name: (routeData) {
final args = routeData.argsAs<ServerStorageRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: ServerStoragePage(
diskStatus: args.diskStatus,
key: args.key,
),
);
},
ExtendingVolumeRoute.name: (routeData) {
final args = routeData.argsAs<ExtendingVolumeRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: ExtendingVolumePage(
diskVolumeToResize: args.diskVolumeToResize,
diskStatus: args.diskStatus,
key: args.key,
),
);
},
ServicesMigrationRoute.name: (routeData) {
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: ServicesMigrationPage(
services: args.services,
diskStatus: args.diskStatus,
isMigration: args.isMigration,
key: args.key,
),
);
},
DevicesRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const DevicesScreen(),
);
},
OnboardingRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const OnboardingPage(),
); );
}, },
}; };
@ -194,288 +194,19 @@ class BackupDetailsRoute extends PageRouteInfo<void> {
} }
/// generated route for /// generated route for
/// [DevicesScreen] /// [RootPage]
class DevicesRoute extends PageRouteInfo<void> { class RootRoute extends PageRouteInfo<void> {
const DevicesRoute({List<PageRouteInfo>? children}) const RootRoute({List<PageRouteInfo>? children})
: super( : super(
DevicesRoute.name, RootRoute.name,
initialChildren: children, initialChildren: children,
); );
static const String name = 'DevicesRoute'; static const String name = 'RootRoute';
static const PageInfo<void> page = PageInfo<void>(name); static const PageInfo<void> page = PageInfo<void>(name);
} }
/// generated route for
/// [DnsDetailsPage]
class DnsDetailsRoute extends PageRouteInfo<void> {
const DnsDetailsRoute({List<PageRouteInfo>? children})
: super(
DnsDetailsRoute.name,
initialChildren: children,
);
static const String name = 'DnsDetailsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [AppSettingsPage]
class AppSettingsRoute extends PageRouteInfo<void> {
const AppSettingsRoute({List<PageRouteInfo>? children})
: super(
AppSettingsRoute.name,
initialChildren: children,
);
static const String name = 'AppSettingsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [DeveloperSettingsPage]
class DeveloperSettingsRoute extends PageRouteInfo<void> {
const DeveloperSettingsRoute({List<PageRouteInfo>? children})
: super(
DeveloperSettingsRoute.name,
initialChildren: children,
);
static const String name = 'DeveloperSettingsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [AboutApplicationPage]
class AboutApplicationRoute extends PageRouteInfo<void> {
const AboutApplicationRoute({List<PageRouteInfo>? children})
: super(
AboutApplicationRoute.name,
initialChildren: children,
);
static const String name = 'AboutApplicationRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [MorePage]
class MoreRoute extends PageRouteInfo<void> {
const MoreRoute({List<PageRouteInfo>? children})
: super(
MoreRoute.name,
initialChildren: children,
);
static const String name = 'MoreRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ConsolePage]
class ConsoleRoute extends PageRouteInfo<void> {
const ConsoleRoute({List<PageRouteInfo>? children})
: super(
ConsoleRoute.name,
initialChildren: children,
);
static const String name = 'ConsoleRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [OnboardingPage]
class OnboardingRoute extends PageRouteInfo<void> {
const OnboardingRoute({List<PageRouteInfo>? children})
: super(
OnboardingRoute.name,
initialChildren: children,
);
static const String name = 'OnboardingRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ProvidersPage]
class ProvidersRoute extends PageRouteInfo<void> {
const ProvidersRoute({List<PageRouteInfo>? children})
: super(
ProvidersRoute.name,
initialChildren: children,
);
static const String name = 'ProvidersRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [RecoveryKeyPage]
class RecoveryKeyRoute extends PageRouteInfo<void> {
const RecoveryKeyRoute({List<PageRouteInfo>? children})
: super(
RecoveryKeyRoute.name,
initialChildren: children,
);
static const String name = 'RecoveryKeyRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ServerDetailsScreen]
class ServerDetailsRoute extends PageRouteInfo<void> {
const ServerDetailsRoute({List<PageRouteInfo>? children})
: super(
ServerDetailsRoute.name,
initialChildren: children,
);
static const String name = 'ServerDetailsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ServicesMigrationPage]
class ServicesMigrationRoute extends PageRouteInfo<ServicesMigrationRouteArgs> {
ServicesMigrationRoute({
required List<Service> services,
required DiskStatus diskStatus,
required bool isMigration,
Key? key,
List<PageRouteInfo>? children,
}) : super(
ServicesMigrationRoute.name,
args: ServicesMigrationRouteArgs(
services: services,
diskStatus: diskStatus,
isMigration: isMigration,
key: key,
),
initialChildren: children,
);
static const String name = 'ServicesMigrationRoute';
static const PageInfo<ServicesMigrationRouteArgs> page =
PageInfo<ServicesMigrationRouteArgs>(name);
}
class ServicesMigrationRouteArgs {
const ServicesMigrationRouteArgs({
required this.services,
required this.diskStatus,
required this.isMigration,
this.key,
});
final List<Service> services;
final DiskStatus diskStatus;
final bool isMigration;
final Key? key;
@override
String toString() {
return 'ServicesMigrationRouteArgs{services: $services, diskStatus: $diskStatus, isMigration: $isMigration, key: $key}';
}
}
/// generated route for
/// [ExtendingVolumePage]
class ExtendingVolumeRoute extends PageRouteInfo<ExtendingVolumeRouteArgs> {
ExtendingVolumeRoute({
required DiskVolume diskVolumeToResize,
required DiskStatus diskStatus,
Key? key,
List<PageRouteInfo>? children,
}) : super(
ExtendingVolumeRoute.name,
args: ExtendingVolumeRouteArgs(
diskVolumeToResize: diskVolumeToResize,
diskStatus: diskStatus,
key: key,
),
initialChildren: children,
);
static const String name = 'ExtendingVolumeRoute';
static const PageInfo<ExtendingVolumeRouteArgs> page =
PageInfo<ExtendingVolumeRouteArgs>(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<ServerStorageRouteArgs> {
ServerStorageRoute({
required DiskStatus diskStatus,
Key? key,
List<PageRouteInfo>? children,
}) : super(
ServerStorageRoute.name,
args: ServerStorageRouteArgs(
diskStatus: diskStatus,
key: key,
),
initialChildren: children,
);
static const String name = 'ServerStorageRoute';
static const PageInfo<ServerStorageRouteArgs> page =
PageInfo<ServerStorageRouteArgs>(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 /// generated route for
/// [ServicePage] /// [ServicePage]
class ServiceRoute extends PageRouteInfo<ServiceRouteArgs> { class ServiceRoute extends PageRouteInfo<ServiceRouteArgs> {
@ -529,29 +260,15 @@ class ServicesRoute extends PageRouteInfo<void> {
} }
/// generated route for /// generated route for
/// [InitializingPage] /// [ServerDetailsScreen]
class InitializingRoute extends PageRouteInfo<void> { class ServerDetailsRoute extends PageRouteInfo<void> {
const InitializingRoute({List<PageRouteInfo>? children}) const ServerDetailsRoute({List<PageRouteInfo>? children})
: super( : super(
InitializingRoute.name, ServerDetailsRoute.name,
initialChildren: children, initialChildren: children,
); );
static const String name = 'InitializingRoute'; static const String name = 'ServerDetailsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [RecoveryRouting]
class RecoveryRoute extends PageRouteInfo<void> {
const RecoveryRoute({List<PageRouteInfo>? children})
: super(
RecoveryRoute.name,
initialChildren: children,
);
static const String name = 'RecoveryRoute';
static const PageInfo<void> page = PageInfo<void>(name); static const PageInfo<void> page = PageInfo<void>(name);
} }
@ -623,15 +340,298 @@ class UserDetailsRouteArgs {
} }
/// generated route for /// generated route for
/// [RootPage] /// [AppSettingsPage]
class RootRoute extends PageRouteInfo<void> { class AppSettingsRoute extends PageRouteInfo<void> {
const RootRoute({List<PageRouteInfo>? children}) const AppSettingsRoute({List<PageRouteInfo>? children})
: super( : super(
RootRoute.name, AppSettingsRoute.name,
initialChildren: children, initialChildren: children,
); );
static const String name = 'RootRoute'; static const String name = 'AppSettingsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [DeveloperSettingsPage]
class DeveloperSettingsRoute extends PageRouteInfo<void> {
const DeveloperSettingsRoute({List<PageRouteInfo>? children})
: super(
DeveloperSettingsRoute.name,
initialChildren: children,
);
static const String name = 'DeveloperSettingsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [MorePage]
class MoreRoute extends PageRouteInfo<void> {
const MoreRoute({List<PageRouteInfo>? children})
: super(
MoreRoute.name,
initialChildren: children,
);
static const String name = 'MoreRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [AboutApplicationPage]
class AboutApplicationRoute extends PageRouteInfo<void> {
const AboutApplicationRoute({List<PageRouteInfo>? children})
: super(
AboutApplicationRoute.name,
initialChildren: children,
);
static const String name = 'AboutApplicationRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ConsolePage]
class ConsoleRoute extends PageRouteInfo<void> {
const ConsoleRoute({List<PageRouteInfo>? children})
: super(
ConsoleRoute.name,
initialChildren: children,
);
static const String name = 'ConsoleRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ProvidersPage]
class ProvidersRoute extends PageRouteInfo<void> {
const ProvidersRoute({List<PageRouteInfo>? children})
: super(
ProvidersRoute.name,
initialChildren: children,
);
static const String name = 'ProvidersRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [RecoveryKeyPage]
class RecoveryKeyRoute extends PageRouteInfo<void> {
const RecoveryKeyRoute({List<PageRouteInfo>? children})
: super(
RecoveryKeyRoute.name,
initialChildren: children,
);
static const String name = 'RecoveryKeyRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [DnsDetailsPage]
class DnsDetailsRoute extends PageRouteInfo<void> {
const DnsDetailsRoute({List<PageRouteInfo>? children})
: super(
DnsDetailsRoute.name,
initialChildren: children,
);
static const String name = 'DnsDetailsRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [RecoveryRouting]
class RecoveryRoute extends PageRouteInfo<void> {
const RecoveryRoute({List<PageRouteInfo>? children})
: super(
RecoveryRoute.name,
initialChildren: children,
);
static const String name = 'RecoveryRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [InitializingPage]
class InitializingRoute extends PageRouteInfo<void> {
const InitializingRoute({List<PageRouteInfo>? children})
: super(
InitializingRoute.name,
initialChildren: children,
);
static const String name = 'InitializingRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [ServerStoragePage]
class ServerStorageRoute extends PageRouteInfo<ServerStorageRouteArgs> {
ServerStorageRoute({
required DiskStatus diskStatus,
Key? key,
List<PageRouteInfo>? children,
}) : super(
ServerStorageRoute.name,
args: ServerStorageRouteArgs(
diskStatus: diskStatus,
key: key,
),
initialChildren: children,
);
static const String name = 'ServerStorageRoute';
static const PageInfo<ServerStorageRouteArgs> page =
PageInfo<ServerStorageRouteArgs>(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
/// [ExtendingVolumePage]
class ExtendingVolumeRoute extends PageRouteInfo<ExtendingVolumeRouteArgs> {
ExtendingVolumeRoute({
required DiskVolume diskVolumeToResize,
required DiskStatus diskStatus,
Key? key,
List<PageRouteInfo>? children,
}) : super(
ExtendingVolumeRoute.name,
args: ExtendingVolumeRouteArgs(
diskVolumeToResize: diskVolumeToResize,
diskStatus: diskStatus,
key: key,
),
initialChildren: children,
);
static const String name = 'ExtendingVolumeRoute';
static const PageInfo<ExtendingVolumeRouteArgs> page =
PageInfo<ExtendingVolumeRouteArgs>(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
/// [ServicesMigrationPage]
class ServicesMigrationRoute extends PageRouteInfo<ServicesMigrationRouteArgs> {
ServicesMigrationRoute({
required List<Service> services,
required DiskStatus diskStatus,
required bool isMigration,
Key? key,
List<PageRouteInfo>? children,
}) : super(
ServicesMigrationRoute.name,
args: ServicesMigrationRouteArgs(
services: services,
diskStatus: diskStatus,
isMigration: isMigration,
key: key,
),
initialChildren: children,
);
static const String name = 'ServicesMigrationRoute';
static const PageInfo<ServicesMigrationRouteArgs> page =
PageInfo<ServicesMigrationRouteArgs>(name);
}
class ServicesMigrationRouteArgs {
const ServicesMigrationRouteArgs({
required this.services,
required this.diskStatus,
required this.isMigration,
this.key,
});
final List<Service> services;
final DiskStatus diskStatus;
final bool isMigration;
final Key? key;
@override
String toString() {
return 'ServicesMigrationRouteArgs{services: $services, diskStatus: $diskStatus, isMigration: $isMigration, key: $key}';
}
}
/// generated route for
/// [DevicesScreen]
class DevicesRoute extends PageRouteInfo<void> {
const DevicesRoute({List<PageRouteInfo>? children})
: super(
DevicesRoute.name,
initialChildren: children,
);
static const String name = 'DevicesRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [OnboardingPage]
class OnboardingRoute extends PageRouteInfo<void> {
const OnboardingRoute({List<PageRouteInfo>? children})
: super(
OnboardingRoute.name,
initialChildren: children,
);
static const String name = 'OnboardingRoute';
static const PageInfo<void> page = PageInfo<void>(name); static const PageInfo<void> page = PageInfo<void>(name);
} }

View File

@ -1,45 +1,5 @@
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:url_launcher/url_launcher.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,
);
}
DnsRecord? extractDkimRecord(final List<DnsRecord> records) { DnsRecord? extractDkimRecord(final List<DnsRecord> records) {
DnsRecord? dkimRecord; DnsRecord? dkimRecord;
@ -69,3 +29,15 @@ String getHostnameFromDomain(final String domain) {
return hostname; return hostname;
} }
void launchURL(final url) async {
try {
final Uri uri = Uri.parse(url);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}