mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-18 06:39:14 +00:00
Merge pull request 'refactor(api): Separate Rest API layer from business logic layer for DNS and Server Providers' (#213) from refactoring into master
Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/pulls/213 Reviewed-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
commit
d0366862c0
|
@ -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 |
|
@ -279,6 +279,8 @@
|
|||
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon."
|
||||
},
|
||||
"initializing": {
|
||||
"server_provider_description": "A place where your data and SelfPrivacy services will reside:",
|
||||
"dns_provider_description": "A service which lets your IP point towards domain names:",
|
||||
"connect_to_server": "Let's start with a server.",
|
||||
"select_provider": "Pick any provider from the following list, they all support SelfPrivacy",
|
||||
"select_provider_notice": "By 'Relatively small' we mean a machine with 2 cores of CPU and 2 gigabytes of RAM.",
|
||||
|
@ -313,8 +315,10 @@
|
|||
"choose_server_type_storage": "{} GB of system storage",
|
||||
"choose_server_type_payment_per_month": "{} per month",
|
||||
"no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.",
|
||||
"cloudflare_bad_key_error": "DNS Provider API key is invalid",
|
||||
"dns_provider_bad_key_error": "API key is invalid",
|
||||
"backblaze_bad_key_error": "Backblaze storage information is invalid",
|
||||
"connect_to_dns": "Connect the DNS provider",
|
||||
"connect_to_dns_provider_text": "With API token SelfPrivacy will manage all DNS entries",
|
||||
"select_dns": "Now let's select a DNS provider",
|
||||
"manage_domain_dns": "To manage your domain's DNS",
|
||||
"use_this_domain": "Use this domain?",
|
||||
|
@ -444,6 +448,7 @@
|
|||
"modals": {
|
||||
"dns_removal_error": "Couldn't remove DNS records.",
|
||||
"server_deletion_error": "Couldn't delete active server.",
|
||||
"volume_creation_error": "Couldn't create volume.",
|
||||
"server_validators_error": "Couldn't fetch available servers.",
|
||||
"already_exists": "Such server already exists.",
|
||||
"unexpected_error": "Unexpected error during placement from the provider side.",
|
||||
|
@ -488,7 +493,7 @@
|
|||
"required": "Required",
|
||||
"already_exist": "Already exists",
|
||||
"invalid_format": "Invalid format",
|
||||
"invalid_format_password": "Must not contain empty characters",
|
||||
"invalid_format_password": "Password must not contain spaces",
|
||||
"invalid_format_ssh": "Must follow the SSH key format",
|
||||
"root_name": "Cannot be 'root'",
|
||||
"length_not_equal": "Length is [], should be {}",
|
||||
|
@ -502,10 +507,12 @@
|
|||
"subtitle": "These settings are for debugging only. Don't change them unless you know what you're doing.",
|
||||
"server_setup": "Server setup",
|
||||
"use_staging_acme": "Use staging ACME server",
|
||||
"use_staging_acme_description": "Rebuild your app to change this value.",
|
||||
"use_staging_acme_description": "Applies when setting up a new server.",
|
||||
"ignore_tls": "Do not verify TLS certificates",
|
||||
"ignore_tls_description": "App will not verify TLS certificates when connecting to the server.",
|
||||
"routing": "App routing",
|
||||
"reset_onboarding": "Reset onboarding switch",
|
||||
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
|
||||
"cubit_statuses": "Cubit loading statuses"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,6 +273,7 @@
|
|||
"no_ssh_notice": "Для этого пользователя созданы только SSH и Email аккаунты. Единая авторизация для всех сервисов ещё не реализована."
|
||||
},
|
||||
"initializing": {
|
||||
"dns_provider_description": "Это позволит связать ваш домен с IP адресом:",
|
||||
"connect_to_server": "Начнём с сервера.",
|
||||
"select_provider": "Ниже подборка провайдеров, которых поддерживает SelfPrivacy",
|
||||
"select_provider_notice": "Под 'Небольшим сервером' имеется ввиду сервер с двумя потоками процессора и двумя гигабайтами оперативной памяти.",
|
||||
|
@ -307,8 +308,10 @@
|
|||
"choose_server_type_storage": "{} GB системного хранилища",
|
||||
"choose_server_type_payment_per_month": "{} в месяц",
|
||||
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
|
||||
"cloudflare_bad_key_error": "API ключ неверен",
|
||||
"dns_provider_bad_key_error": "API ключ неверен",
|
||||
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
|
||||
"connect_to_dns": "Подключите DNS провайдера",
|
||||
"connect_to_dns_provider_text": "С помощью API токена приложение SelfPrivacy настроит DNS записи",
|
||||
"manage_domain_dns": "Для управления DNS вашего домена",
|
||||
"use_this_domain": "Используем этот домен?",
|
||||
"use_this_domain_text": "Указанный вами токен даёт контроль над этим доменом",
|
||||
|
@ -469,10 +472,10 @@
|
|||
"required": "Обязательное поле",
|
||||
"already_exist": "Уже существует",
|
||||
"invalid_format": "Неверный формат",
|
||||
"invalid_format_password": "Должен не содержать пустые символы",
|
||||
"invalid_format_password": "Пароль не должен содержать пробелы",
|
||||
"invalid_format_ssh": "Должен следовать формату SSH ключей",
|
||||
"root_name": "Имя пользователя не может быть 'root'",
|
||||
"length_not_equal": "Длина строки [], должна быть равна {}",
|
||||
"length_longer": "Длина строки [], должна быть меньше либо равна {}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,9 @@ class HiveConfig {
|
|||
Hive.registerAdapter(BackblazeCredentialAdapter());
|
||||
Hive.registerAdapter(BackblazeBucketAdapter());
|
||||
Hive.registerAdapter(ServerVolumeAdapter());
|
||||
|
||||
Hive.registerAdapter(DnsProviderAdapter());
|
||||
Hive.registerAdapter(ServerProviderAdapter());
|
||||
Hive.registerAdapter(UserTypeAdapter());
|
||||
Hive.registerAdapter(DnsProviderTypeAdapter());
|
||||
Hive.registerAdapter(ServerProviderTypeAdapter());
|
||||
|
||||
await Hive.openBox(BNames.appSettingsBox);
|
||||
|
||||
|
@ -35,8 +34,8 @@ class HiveConfig {
|
|||
final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated);
|
||||
if (deprecatedUsers.isNotEmpty) {
|
||||
final Box<User> users = Hive.box<User>(BNames.usersBox);
|
||||
users.addAll(deprecatedUsers.values.toList());
|
||||
deprecatedUsers.clear();
|
||||
await users.addAll(deprecatedUsers.values.toList());
|
||||
await deprecatedUsers.clear();
|
||||
}
|
||||
|
||||
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class APIGenericResult<T> {
|
||||
APIGenericResult({
|
||||
class GenericResult<T> {
|
||||
GenericResult({
|
||||
required this.success,
|
||||
required this.data,
|
||||
this.message,
|
|
@ -3,7 +3,7 @@ import 'dart:io';
|
|||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||
import 'package:selfprivacy/logic/models/message.dart';
|
||||
|
||||
void _logToAppConsole<T>(final T objectToLog) {
|
||||
|
@ -53,10 +53,10 @@ class ResponseLoggingParser extends ResponseParser {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class ApiMap {
|
||||
abstract class GraphQLApiMap {
|
||||
Future<GraphQLClient> getClient() async {
|
||||
IOClient? ioClient;
|
||||
if (StagingOptions.stagingAcme || !StagingOptions.verifyCertificate) {
|
||||
if (TlsOptions.stagingAcme || !TlsOptions.verifyCertificate) {
|
||||
final HttpClient httpClient = HttpClient();
|
||||
httpClient.badCertificateCallback = (
|
||||
final cert,
|
|
@ -3,6 +3,7 @@ import 'package:gql/ast.dart';
|
|||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'package:selfprivacy/utils/scalars.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
|
||||
class Fragment$basicMutationReturnFields {
|
||||
Fragment$basicMutationReturnFields({
|
||||
|
|
|
@ -76,7 +76,8 @@ type DeviceApiTokenMutationReturn implements MutationReturnInterface {
|
|||
|
||||
enum DnsProvider {
|
||||
CLOUDFLARE,
|
||||
DESEC
|
||||
DESEC,
|
||||
DIGITALOCEAN
|
||||
}
|
||||
|
||||
type DnsRecord {
|
||||
|
|
|
@ -1096,7 +1096,7 @@ class _CopyWithStubImpl$Input$UserMutationInput<TRes>
|
|||
_res;
|
||||
}
|
||||
|
||||
enum Enum$DnsProvider { CLOUDFLARE, DESEC, $unknown }
|
||||
enum Enum$DnsProvider { CLOUDFLARE, DESEC, DIGITALOCEAN, $unknown }
|
||||
|
||||
String toJson$Enum$DnsProvider(Enum$DnsProvider e) {
|
||||
switch (e) {
|
||||
|
@ -1104,6 +1104,8 @@ String toJson$Enum$DnsProvider(Enum$DnsProvider e) {
|
|||
return r'CLOUDFLARE';
|
||||
case Enum$DnsProvider.DESEC:
|
||||
return r'DESEC';
|
||||
case Enum$DnsProvider.DIGITALOCEAN:
|
||||
return r'DIGITALOCEAN';
|
||||
case Enum$DnsProvider.$unknown:
|
||||
return r'$unknown';
|
||||
}
|
||||
|
@ -1115,6 +1117,8 @@ Enum$DnsProvider fromJson$Enum$DnsProvider(String value) {
|
|||
return Enum$DnsProvider.CLOUDFLARE;
|
||||
case r'DESEC':
|
||||
return Enum$DnsProvider.DESEC;
|
||||
case r'DIGITALOCEAN':
|
||||
return Enum$DnsProvider.DIGITALOCEAN;
|
||||
default:
|
||||
return Enum$DnsProvider.$unknown;
|
||||
}
|
||||
|
|
|
@ -80,7 +80,6 @@ query SystemDnsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
query GetApiTokens {
|
||||
api {
|
||||
devices {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'disk_volumes.graphql.dart';
|
||||
import 'package:gql/ast.dart';
|
||||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'package:selfprivacy/utils/scalars.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
|
||||
class Fragment$basicMutationReturnFields {
|
||||
Fragment$basicMutationReturnFields({
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'disk_volumes.graphql.dart';
|
||||
import 'package:gql/ast.dart';
|
||||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
|
||||
class Fragment$basicMutationReturnFields {
|
||||
Fragment$basicMutationReturnFields({
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'disk_volumes.graphql.dart';
|
||||
import 'package:gql/ast.dart';
|
||||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'package:selfprivacy/utils/scalars.dart';
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'disk_volumes.graphql.dart';
|
||||
import 'package:gql/ast.dart';
|
||||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
|
||||
class Fragment$basicMutationReturnFields {
|
||||
Fragment$basicMutationReturnFields({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
part of 'server_api.dart';
|
||||
|
||||
mixin JobsApi on ApiMap {
|
||||
mixin JobsApi on GraphQLApiMap {
|
||||
Future<List<ServerJob>> getServerJobs() async {
|
||||
QueryResult<Query$GetApiJobs> response;
|
||||
List<ServerJob> jobsList = [];
|
||||
|
@ -22,13 +22,13 @@ mixin JobsApi on ApiMap {
|
|||
return jobsList;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<bool>> removeApiJob(final String uid) async {
|
||||
Future<GenericResult<bool>> removeApiJob(final String uid) async {
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
final variables = Variables$Mutation$RemoveJob(jobId: uid);
|
||||
final mutation = Options$Mutation$RemoveJob(variables: variables);
|
||||
final response = await client.mutate$RemoveJob(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: response.parsedData?.removeJob.success ?? false,
|
||||
success: true,
|
||||
code: response.parsedData?.removeJob.code ?? 0,
|
||||
|
@ -36,7 +36,7 @@ mixin JobsApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: false,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
part of 'server_api.dart';
|
||||
|
||||
mixin ServerActionsApi on ApiMap {
|
||||
mixin ServerActionsApi on GraphQLApiMap {
|
||||
Future<bool> _commonBoolRequest(final Function graphQLMethod) async {
|
||||
QueryResult response;
|
||||
bool result = false;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:graphql/client.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/api_map.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/graphql_api_map.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_api.graphql.dart';
|
||||
|
@ -24,7 +24,7 @@ import 'package:selfprivacy/logic/models/service.dart';
|
|||
import 'package:selfprivacy/logic/models/ssh_settings.dart';
|
||||
import 'package:selfprivacy/logic/models/system_settings.dart';
|
||||
|
||||
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
|
||||
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
||||
|
||||
part 'jobs_api.dart';
|
||||
part 'server_actions_api.dart';
|
||||
|
@ -32,7 +32,7 @@ part 'services_api.dart';
|
|||
part 'users_api.dart';
|
||||
part 'volume_api.dart';
|
||||
|
||||
class ServerApi extends ApiMap
|
||||
class ServerApi extends GraphQLApiMap
|
||||
with VolumeApi, JobsApi, ServerActionsApi, ServicesApi, UsersApi {
|
||||
ServerApi({
|
||||
this.hasLogger = false,
|
||||
|
@ -69,9 +69,9 @@ class ServerApi extends ApiMap
|
|||
return apiVersion;
|
||||
}
|
||||
|
||||
Future<ServerProvider> getServerProviderType() async {
|
||||
Future<ServerProviderType> getServerProviderType() async {
|
||||
QueryResult<Query$SystemServerProvider> response;
|
||||
ServerProvider providerType = ServerProvider.unknown;
|
||||
ServerProviderType providerType = ServerProviderType.unknown;
|
||||
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
|
@ -79,7 +79,7 @@ class ServerApi extends ApiMap
|
|||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
}
|
||||
providerType = ServerProvider.fromGraphQL(
|
||||
providerType = ServerProviderType.fromGraphQL(
|
||||
response.parsedData!.system.provider.provider,
|
||||
);
|
||||
} catch (e) {
|
||||
|
@ -88,9 +88,9 @@ class ServerApi extends ApiMap
|
|||
return providerType;
|
||||
}
|
||||
|
||||
Future<DnsProvider> getDnsProviderType() async {
|
||||
Future<DnsProviderType> getDnsProviderType() async {
|
||||
QueryResult<Query$SystemDnsProvider> response;
|
||||
DnsProvider providerType = DnsProvider.unknown;
|
||||
DnsProviderType providerType = DnsProviderType.unknown;
|
||||
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
|
@ -98,7 +98,7 @@ class ServerApi extends ApiMap
|
|||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
}
|
||||
providerType = DnsProvider.fromGraphQL(
|
||||
providerType = DnsProviderType.fromGraphQL(
|
||||
response.parsedData!.system.domainInfo.provider,
|
||||
);
|
||||
} catch (e) {
|
||||
|
@ -205,7 +205,7 @@ class ServerApi extends ApiMap
|
|||
return settings;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
|
||||
Future<GenericResult<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
|
||||
RecoveryKeyStatus? key;
|
||||
QueryResult<Query$RecoveryKey> response;
|
||||
String? error;
|
||||
|
@ -222,18 +222,18 @@ class ServerApi extends ApiMap
|
|||
print(e);
|
||||
}
|
||||
|
||||
return APIGenericResult<RecoveryKeyStatus?>(
|
||||
return GenericResult<RecoveryKeyStatus?>(
|
||||
success: error == null,
|
||||
data: key,
|
||||
message: error,
|
||||
);
|
||||
}
|
||||
|
||||
Future<APIGenericResult<String>> generateRecoveryToken(
|
||||
Future<GenericResult<String>> generateRecoveryToken(
|
||||
final DateTime? expirationDate,
|
||||
final int? numberOfUses,
|
||||
) async {
|
||||
APIGenericResult<String> key;
|
||||
GenericResult<String> key;
|
||||
QueryResult<Mutation$GetNewRecoveryApiKey> response;
|
||||
|
||||
try {
|
||||
|
@ -254,19 +254,19 @@ class ServerApi extends ApiMap
|
|||
);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
key = APIGenericResult<String>(
|
||||
key = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: response.exception.toString(),
|
||||
);
|
||||
}
|
||||
key = APIGenericResult<String>(
|
||||
key = GenericResult<String>(
|
||||
success: true,
|
||||
data: response.parsedData!.getNewRecoveryApiKey.key!,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
key = APIGenericResult<String>(
|
||||
key = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: e.toString(),
|
||||
|
@ -299,8 +299,8 @@ class ServerApi extends ApiMap
|
|||
return records;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<List<ApiToken>>> getApiTokens() async {
|
||||
APIGenericResult<List<ApiToken>> tokens;
|
||||
Future<GenericResult<List<ApiToken>>> getApiTokens() async {
|
||||
GenericResult<List<ApiToken>> tokens;
|
||||
QueryResult<Query$GetApiTokens> response;
|
||||
|
||||
try {
|
||||
|
@ -309,7 +309,7 @@ class ServerApi extends ApiMap
|
|||
if (response.hasException) {
|
||||
final message = response.exception.toString();
|
||||
print(message);
|
||||
tokens = APIGenericResult<List<ApiToken>>(
|
||||
tokens = GenericResult<List<ApiToken>>(
|
||||
success: false,
|
||||
data: [],
|
||||
message: message,
|
||||
|
@ -323,13 +323,13 @@ class ServerApi extends ApiMap
|
|||
ApiToken.fromGraphQL(device),
|
||||
)
|
||||
.toList();
|
||||
tokens = APIGenericResult<List<ApiToken>>(
|
||||
tokens = GenericResult<List<ApiToken>>(
|
||||
success: true,
|
||||
data: parsed,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
tokens = APIGenericResult<List<ApiToken>>(
|
||||
tokens = GenericResult<List<ApiToken>>(
|
||||
success: false,
|
||||
data: [],
|
||||
message: e.toString(),
|
||||
|
@ -339,8 +339,8 @@ class ServerApi extends ApiMap
|
|||
return tokens;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<void>> deleteApiToken(final String name) async {
|
||||
APIGenericResult<void> returnable;
|
||||
Future<GenericResult<void>> deleteApiToken(final String name) async {
|
||||
GenericResult<void> returnable;
|
||||
QueryResult<Mutation$DeleteDeviceApiToken> response;
|
||||
|
||||
try {
|
||||
|
@ -357,19 +357,19 @@ class ServerApi extends ApiMap
|
|||
);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
returnable = APIGenericResult<void>(
|
||||
returnable = GenericResult<void>(
|
||||
success: false,
|
||||
data: null,
|
||||
message: response.exception.toString(),
|
||||
);
|
||||
}
|
||||
returnable = APIGenericResult<void>(
|
||||
returnable = GenericResult<void>(
|
||||
success: true,
|
||||
data: null,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
returnable = APIGenericResult<void>(
|
||||
returnable = GenericResult<void>(
|
||||
success: false,
|
||||
data: null,
|
||||
message: e.toString(),
|
||||
|
@ -379,8 +379,8 @@ class ServerApi extends ApiMap
|
|||
return returnable;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<String>> createDeviceToken() async {
|
||||
APIGenericResult<String> token;
|
||||
Future<GenericResult<String>> createDeviceToken() async {
|
||||
GenericResult<String> token;
|
||||
QueryResult<Mutation$GetNewDeviceApiKey> response;
|
||||
|
||||
try {
|
||||
|
@ -392,19 +392,19 @@ class ServerApi extends ApiMap
|
|||
);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: response.exception.toString(),
|
||||
);
|
||||
}
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: true,
|
||||
data: response.parsedData!.getNewDeviceApiKey.key!,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: e.toString(),
|
||||
|
@ -416,10 +416,10 @@ class ServerApi extends ApiMap
|
|||
|
||||
Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null;
|
||||
|
||||
Future<APIGenericResult<String>> authorizeDevice(
|
||||
Future<GenericResult<String>> authorizeDevice(
|
||||
final DeviceToken deviceToken,
|
||||
) async {
|
||||
APIGenericResult<String> token;
|
||||
GenericResult<String> token;
|
||||
QueryResult<Mutation$AuthorizeWithNewDeviceApiKey> response;
|
||||
|
||||
try {
|
||||
|
@ -441,19 +441,19 @@ class ServerApi extends ApiMap
|
|||
);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: response.exception.toString(),
|
||||
);
|
||||
}
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: true,
|
||||
data: response.parsedData!.authorizeWithNewDeviceApiKey.token!,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: e.toString(),
|
||||
|
@ -463,10 +463,10 @@ class ServerApi extends ApiMap
|
|||
return token;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<String>> useRecoveryToken(
|
||||
Future<GenericResult<String>> useRecoveryToken(
|
||||
final DeviceToken deviceToken,
|
||||
) async {
|
||||
APIGenericResult<String> token;
|
||||
GenericResult<String> token;
|
||||
QueryResult<Mutation$UseRecoveryApiKey> response;
|
||||
|
||||
try {
|
||||
|
@ -488,19 +488,19 @@ class ServerApi extends ApiMap
|
|||
);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: response.exception.toString(),
|
||||
);
|
||||
}
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: true,
|
||||
data: response.parsedData!.useRecoveryApiKey.token!,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
token = APIGenericResult<String>(
|
||||
token = GenericResult<String>(
|
||||
success: false,
|
||||
data: '',
|
||||
message: e.toString(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
part of 'server_api.dart';
|
||||
|
||||
mixin ServicesApi on ApiMap {
|
||||
mixin ServicesApi on GraphQLApiMap {
|
||||
Future<List<Service>> getAllServices() async {
|
||||
QueryResult<Query$AllServices> response;
|
||||
List<Service> services = [];
|
||||
|
@ -20,7 +20,7 @@ mixin ServicesApi on ApiMap {
|
|||
return services;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<bool>> enableService(
|
||||
Future<GenericResult<bool>> enableService(
|
||||
final String serviceId,
|
||||
) async {
|
||||
try {
|
||||
|
@ -28,7 +28,7 @@ mixin ServicesApi on ApiMap {
|
|||
final variables = Variables$Mutation$EnableService(serviceId: serviceId);
|
||||
final mutation = Options$Mutation$EnableService(variables: variables);
|
||||
final response = await client.mutate$EnableService(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: response.parsedData?.enableService.success ?? false,
|
||||
success: true,
|
||||
code: response.parsedData?.enableService.code ?? 0,
|
||||
|
@ -36,7 +36,7 @@ mixin ServicesApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: false,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -45,7 +45,7 @@ mixin ServicesApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<void>> disableService(
|
||||
Future<GenericResult<void>> disableService(
|
||||
final String serviceId,
|
||||
) async {
|
||||
try {
|
||||
|
@ -53,7 +53,7 @@ mixin ServicesApi on ApiMap {
|
|||
final variables = Variables$Mutation$DisableService(serviceId: serviceId);
|
||||
final mutation = Options$Mutation$DisableService(variables: variables);
|
||||
final response = await client.mutate$DisableService(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: response.parsedData?.disableService.success ?? false,
|
||||
code: response.parsedData?.disableService.code ?? 0,
|
||||
|
@ -61,7 +61,7 @@ mixin ServicesApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -70,7 +70,7 @@ mixin ServicesApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<bool>> stopService(
|
||||
Future<GenericResult<bool>> stopService(
|
||||
final String serviceId,
|
||||
) async {
|
||||
try {
|
||||
|
@ -78,7 +78,7 @@ mixin ServicesApi on ApiMap {
|
|||
final variables = Variables$Mutation$StopService(serviceId: serviceId);
|
||||
final mutation = Options$Mutation$StopService(variables: variables);
|
||||
final response = await client.mutate$StopService(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: response.parsedData?.stopService.success ?? false,
|
||||
success: true,
|
||||
code: response.parsedData?.stopService.code ?? 0,
|
||||
|
@ -86,7 +86,7 @@ mixin ServicesApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: false,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -95,13 +95,13 @@ mixin ServicesApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult> startService(final String serviceId) async {
|
||||
Future<GenericResult> startService(final String serviceId) async {
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
final variables = Variables$Mutation$StartService(serviceId: serviceId);
|
||||
final mutation = Options$Mutation$StartService(variables: variables);
|
||||
final response = await client.mutate$StartService(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: response.parsedData?.startService.success ?? false,
|
||||
code: response.parsedData?.startService.code ?? 0,
|
||||
|
@ -109,7 +109,7 @@ mixin ServicesApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -118,7 +118,7 @@ mixin ServicesApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<bool>> restartService(
|
||||
Future<GenericResult<bool>> restartService(
|
||||
final String serviceId,
|
||||
) async {
|
||||
try {
|
||||
|
@ -126,7 +126,7 @@ mixin ServicesApi on ApiMap {
|
|||
final variables = Variables$Mutation$RestartService(serviceId: serviceId);
|
||||
final mutation = Options$Mutation$RestartService(variables: variables);
|
||||
final response = await client.mutate$RestartService(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: response.parsedData?.restartService.success ?? false,
|
||||
success: true,
|
||||
code: response.parsedData?.restartService.code ?? 0,
|
||||
|
@ -134,7 +134,7 @@ mixin ServicesApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: false,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -143,7 +143,7 @@ mixin ServicesApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<ServerJob?>> moveService(
|
||||
Future<GenericResult<ServerJob?>> moveService(
|
||||
final String serviceId,
|
||||
final String destination,
|
||||
) async {
|
||||
|
@ -158,7 +158,7 @@ mixin ServicesApi on ApiMap {
|
|||
final mutation = Options$Mutation$MoveService(variables: variables);
|
||||
final response = await client.mutate$MoveService(mutation);
|
||||
final jobJson = response.parsedData?.moveService.job?.toJson();
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: true,
|
||||
code: response.parsedData?.moveService.code ?? 0,
|
||||
message: response.parsedData?.moveService.message,
|
||||
|
@ -166,7 +166,7 @@ mixin ServicesApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: false,
|
||||
code: 0,
|
||||
message: e.toString(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
part of 'server_api.dart';
|
||||
|
||||
mixin UsersApi on ApiMap {
|
||||
mixin UsersApi on GraphQLApiMap {
|
||||
Future<List<User>> getAllUsers() async {
|
||||
QueryResult<Query$AllUsers> response;
|
||||
List<User> users = [];
|
||||
|
@ -45,7 +45,7 @@ mixin UsersApi on ApiMap {
|
|||
return user;
|
||||
}
|
||||
|
||||
Future<APIGenericResult<User?>> createUser(
|
||||
Future<GenericResult<User?>> createUser(
|
||||
final String username,
|
||||
final String password,
|
||||
) async {
|
||||
|
@ -56,7 +56,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
final mutation = Options$Mutation$CreateUser(variables: variables);
|
||||
final response = await client.mutate$CreateUser(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: true,
|
||||
code: response.parsedData?.createUser.code ?? 500,
|
||||
message: response.parsedData?.createUser.message,
|
||||
|
@ -66,7 +66,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: false,
|
||||
code: 0,
|
||||
message: e.toString(),
|
||||
|
@ -75,7 +75,7 @@ mixin UsersApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<bool>> deleteUser(
|
||||
Future<GenericResult<bool>> deleteUser(
|
||||
final String username,
|
||||
) async {
|
||||
try {
|
||||
|
@ -83,7 +83,7 @@ mixin UsersApi on ApiMap {
|
|||
final variables = Variables$Mutation$DeleteUser(username: username);
|
||||
final mutation = Options$Mutation$DeleteUser(variables: variables);
|
||||
final response = await client.mutate$DeleteUser(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: response.parsedData?.deleteUser.success ?? false,
|
||||
success: true,
|
||||
code: response.parsedData?.deleteUser.code ?? 500,
|
||||
|
@ -91,7 +91,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: false,
|
||||
success: false,
|
||||
code: 500,
|
||||
|
@ -100,7 +100,7 @@ mixin UsersApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<User?>> updateUser(
|
||||
Future<GenericResult<User?>> updateUser(
|
||||
final String username,
|
||||
final String password,
|
||||
) async {
|
||||
|
@ -111,7 +111,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
final mutation = Options$Mutation$UpdateUser(variables: variables);
|
||||
final response = await client.mutate$UpdateUser(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: true,
|
||||
code: response.parsedData?.updateUser.code ?? 500,
|
||||
message: response.parsedData?.updateUser.message,
|
||||
|
@ -121,7 +121,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -130,7 +130,7 @@ mixin UsersApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<User?>> addSshKey(
|
||||
Future<GenericResult<User?>> addSshKey(
|
||||
final String username,
|
||||
final String sshKey,
|
||||
) async {
|
||||
|
@ -144,7 +144,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
final mutation = Options$Mutation$AddSshKey(variables: variables);
|
||||
final response = await client.mutate$AddSshKey(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: true,
|
||||
code: response.parsedData?.addSshKey.code ?? 500,
|
||||
message: response.parsedData?.addSshKey.message,
|
||||
|
@ -154,7 +154,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
@ -163,7 +163,7 @@ mixin UsersApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<User?>> removeSshKey(
|
||||
Future<GenericResult<User?>> removeSshKey(
|
||||
final String username,
|
||||
final String sshKey,
|
||||
) async {
|
||||
|
@ -177,7 +177,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
final mutation = Options$Mutation$RemoveSshKey(variables: variables);
|
||||
final response = await client.mutate$RemoveSshKey(mutation);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
success: response.parsedData?.removeSshKey.success ?? false,
|
||||
code: response.parsedData?.removeSshKey.code ?? 500,
|
||||
message: response.parsedData?.removeSshKey.message,
|
||||
|
@ -187,7 +187,7 @@ mixin UsersApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: false,
|
||||
code: 0,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
part of 'server_api.dart';
|
||||
|
||||
mixin VolumeApi on ApiMap {
|
||||
mixin VolumeApi on GraphQLApiMap {
|
||||
Future<List<ServerDiskVolume>> getServerDiskVolumes() async {
|
||||
QueryResult response;
|
||||
List<ServerDiskVolume> volumes = [];
|
||||
|
@ -57,10 +57,10 @@ mixin VolumeApi on ApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<APIGenericResult<String?>> migrateToBinds(
|
||||
Future<GenericResult<String?>> migrateToBinds(
|
||||
final Map<String, String> serviceToDisk,
|
||||
) async {
|
||||
APIGenericResult<String?>? mutation;
|
||||
GenericResult<String?>? mutation;
|
||||
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
|
@ -78,7 +78,7 @@ mixin VolumeApi on ApiMap {
|
|||
await client.mutate$MigrateToBinds(
|
||||
migrateMutation,
|
||||
);
|
||||
mutation = mutation = APIGenericResult(
|
||||
mutation = mutation = GenericResult(
|
||||
success: true,
|
||||
code: result.parsedData!.migrateToBinds.code,
|
||||
message: result.parsedData!.migrateToBinds.message,
|
||||
|
@ -86,7 +86,7 @@ mixin VolumeApi on ApiMap {
|
|||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
mutation = APIGenericResult(
|
||||
mutation = GenericResult(
|
||||
success: false,
|
||||
code: 0,
|
||||
message: e.toString(),
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@ import 'dart:io';
|
|||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||
|
||||
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
|
||||
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
||||
|
||||
class BackblazeApiAuth {
|
||||
BackblazeApiAuth({required this.authorizationToken, required this.apiUrl});
|
||||
|
@ -25,7 +25,7 @@ class BackblazeApplicationKey {
|
|||
final String applicationKey;
|
||||
}
|
||||
|
||||
class BackblazeApi extends ApiMap {
|
||||
class BackblazeApi extends RestApiMap {
|
||||
BackblazeApi({this.hasLogger = false, this.isWithToken = true});
|
||||
|
||||
@override
|
||||
|
@ -78,7 +78,7 @@ class BackblazeApi extends ApiMap {
|
|||
);
|
||||
}
|
||||
|
||||
Future<APIGenericResult<bool>> isApiTokenValid(
|
||||
Future<GenericResult<bool>> isApiTokenValid(
|
||||
final String encodedApiKey,
|
||||
) async {
|
||||
final Dio client = await getClient();
|
||||
|
@ -103,7 +103,7 @@ class BackblazeApi extends ApiMap {
|
|||
}
|
||||
} on DioError catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: false,
|
||||
success: false,
|
||||
message: e.toString(),
|
||||
|
@ -112,7 +112,7 @@ class BackblazeApi extends ApiMap {
|
|||
close(client);
|
||||
}
|
||||
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: isTokenValid,
|
||||
success: true,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
204
lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart
Normal file
204
lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
class ProviderApiSettings {
|
||||
const ProviderApiSettings({
|
||||
this.hasLogger = false,
|
||||
this.isWithToken = true,
|
||||
});
|
||||
final bool hasLogger;
|
||||
final bool isWithToken;
|
||||
}
|
|
@ -8,7 +8,7 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
|||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/models/message.dart';
|
||||
|
||||
abstract class ApiMap {
|
||||
abstract class RestApiMap {
|
||||
Future<Dio> getClient({final BaseOptions? customOptions}) async {
|
||||
final Dio dio = Dio(customOptions ?? (await options));
|
||||
if (hasLogger) {
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
/// Controls staging environment for network
|
||||
class StagingOptions {
|
||||
class TlsOptions {
|
||||
/// Whether we request for staging temprorary certificates.
|
||||
/// Hardcode to 'true' in the middle of testing to not
|
||||
/// get your domain banned by constant certificate renewal
|
||||
///
|
||||
/// If set to 'true', the 'verifyCertificate' becomes useless
|
||||
static bool get stagingAcme => false;
|
||||
static bool stagingAcme = false;
|
||||
|
||||
/// Should we consider CERTIFICATE_VERIFY_FAILED code an error
|
||||
/// For now it's just a global variable and DNS API
|
|
@ -35,7 +35,7 @@ class ApiDevicesCubit
|
|||
}
|
||||
|
||||
Future<List<ApiToken>?> _getApiTokens() async {
|
||||
final APIGenericResult<List<ApiToken>> response = await api.getApiTokens();
|
||||
final GenericResult<List<ApiToken>> response = await api.getApiTokens();
|
||||
if (response.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
|
@ -44,8 +44,7 @@ class ApiDevicesCubit
|
|||
}
|
||||
|
||||
Future<void> deleteDevice(final ApiToken device) async {
|
||||
final APIGenericResult<void> response =
|
||||
await api.deleteApiToken(device.name);
|
||||
final GenericResult<void> response = await api.deleteApiToken(device.name);
|
||||
if (response.success) {
|
||||
emit(
|
||||
ApiDevicesState(
|
||||
|
@ -60,7 +59,7 @@ class ApiDevicesCubit
|
|||
}
|
||||
|
||||
Future<String?> getNewDeviceKey() async {
|
||||
final APIGenericResult<String> response = await api.createDeviceToken();
|
||||
final GenericResult<String> response = await api.createDeviceToken();
|
||||
if (response.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
import 'package:selfprivacy/utils/network_utils.dart';
|
||||
|
||||
part 'dns_records_state.dart';
|
||||
|
@ -25,14 +25,13 @@ class DnsRecordsCubit
|
|||
emit(
|
||||
DnsRecordsState(
|
||||
dnsState: DnsRecordsStatus.refreshing,
|
||||
dnsRecords: ApiController.currentDnsProviderApiFactory
|
||||
?.getDnsProvider()
|
||||
.getDesiredDnsRecords(
|
||||
dnsRecords:
|
||||
ProvidersController.currentDnsProvider?.getDesiredDnsRecords(
|
||||
serverInstallationCubit.state.serverDomain?.domainName,
|
||||
'',
|
||||
'',
|
||||
) ??
|
||||
[],
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -45,13 +44,12 @@ class DnsRecordsCubit
|
|||
return;
|
||||
}
|
||||
|
||||
final foundRecords = await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.validateDnsRecords(
|
||||
domain!,
|
||||
ipAddress!,
|
||||
extractDkimRecord(await api.getDnsRecords())?.content ?? '',
|
||||
);
|
||||
final foundRecords =
|
||||
await ProvidersController.currentDnsProvider!.validateDnsRecords(
|
||||
domain!,
|
||||
ipAddress!,
|
||||
extractDkimRecord(await api.getDnsRecords())?.content ?? '',
|
||||
);
|
||||
|
||||
if (!foundRecords.success || foundRecords.data.isEmpty) {
|
||||
emit(const DnsRecordsState());
|
||||
|
@ -89,10 +87,10 @@ class DnsRecordsCubit
|
|||
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
|
||||
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
|
||||
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider();
|
||||
await dnsProviderApi.removeSimilarRecords(domain: domain!);
|
||||
await dnsProviderApi.createMultipleDnsRecords(
|
||||
await ProvidersController.currentDnsProvider!.removeDomainRecords(
|
||||
domain: domain!,
|
||||
);
|
||||
await ProvidersController.currentDnsProvider!.createDomainRecords(
|
||||
domain: domain,
|
||||
ip4: ipAddress,
|
||||
);
|
||||
|
@ -100,7 +98,10 @@ class DnsRecordsCubit
|
|||
final List<DnsRecord> records = await api.getDnsRecords();
|
||||
final DnsRecord? dkimRecord = extractDkimRecord(records);
|
||||
if (dkimRecord != null) {
|
||||
await dnsProviderApi.setDnsRecord(dkimRecord, domain);
|
||||
await ProvidersController.currentDnsProvider!.setDnsRecord(
|
||||
dkimRecord,
|
||||
domain,
|
||||
);
|
||||
}
|
||||
|
||||
await load();
|
||||
|
|
|
@ -40,7 +40,7 @@ class BackblazeFormCubit extends FormCubit {
|
|||
|
||||
@override
|
||||
FutureOr<bool> asyncValidation() async {
|
||||
late APIGenericResult<bool> backblazeResponse;
|
||||
late GenericResult<bool> backblazeResponse;
|
||||
final BackblazeApi apiClient = BackblazeApi(isWithToken: false);
|
||||
|
||||
try {
|
||||
|
@ -51,7 +51,7 @@ class BackblazeFormCubit extends FormCubit {
|
|||
backblazeResponse = await apiClient.isApiTokenValid(encodedApiKey);
|
||||
} catch (e) {
|
||||
addError(e);
|
||||
backblazeResponse = APIGenericResult(
|
||||
backblazeResponse = GenericResult(
|
||||
success: false,
|
||||
data: false,
|
||||
message: e.toString(),
|
||||
|
|
|
@ -41,7 +41,7 @@ class DnsProviderFormCubit extends FormCubit {
|
|||
}
|
||||
|
||||
if (!isKeyValid) {
|
||||
apiKey.setError('initializing.cloudflare_bad_key_error'.tr());
|
||||
apiKey.setError('initializing.dns_provider_bad_key_error'.tr());
|
||||
}
|
||||
|
||||
return isKeyValid;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
|
||||
class DomainSetupCubit extends Cubit<DomainSetupState> {
|
||||
DomainSetupCubit(this.serverInstallationCubit) : super(Initial());
|
||||
|
@ -10,36 +11,32 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
|
|||
|
||||
Future<void> load() async {
|
||||
emit(Loading(LoadingTypes.loadingDomain));
|
||||
final List<String> list = await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.domainList();
|
||||
if (list.isEmpty) {
|
||||
final GenericResult<List<String>> result =
|
||||
await ProvidersController.currentDnsProvider!.domainList();
|
||||
if (!result.success || result.data.isEmpty) {
|
||||
emit(Empty());
|
||||
} else if (list.length == 1) {
|
||||
emit(Loaded(list.first));
|
||||
} else if (result.data.length == 1) {
|
||||
emit(Loaded(result.data.first));
|
||||
} else {
|
||||
emit(MoreThenOne());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() => super.close();
|
||||
|
||||
Future<void> saveDomain() async {
|
||||
assert(state is Loaded, 'wrong state');
|
||||
final String domainName = (state as Loaded).domain;
|
||||
|
||||
emit(Loading(LoadingTypes.saving));
|
||||
|
||||
final String? zoneId = await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.getZoneId(domainName);
|
||||
final dnsProvider = ProvidersController.currentDnsProvider!;
|
||||
final GenericResult<String?> zoneIdResult =
|
||||
await dnsProvider.getZoneId(domainName);
|
||||
|
||||
if (zoneId != null) {
|
||||
if (zoneIdResult.success || zoneIdResult.data != null) {
|
||||
final ServerDomain domain = ServerDomain(
|
||||
domainName: domainName,
|
||||
zoneId: zoneId,
|
||||
provider: DnsProvider.cloudflare,
|
||||
zoneId: zoneIdResult.data!,
|
||||
provider: dnsProvider.type,
|
||||
);
|
||||
|
||||
serverInstallationCubit.setDomain(domain);
|
||||
|
|
|
@ -4,15 +4,12 @@ import 'package:cubit_form/cubit_form.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
|
||||
class ProviderFormCubit extends FormCubit {
|
||||
ProviderFormCubit(this.serverInstallationCubit) {
|
||||
//final int tokenLength =
|
||||
// serverInstallationCubit.serverProviderApiTokenValidation().length;
|
||||
class ServerProviderFormCubit extends FormCubit {
|
||||
ServerProviderFormCubit(this.serverInstallationCubit) {
|
||||
apiKey = FieldCubit(
|
||||
initalValue: '',
|
||||
validations: [
|
||||
RequiredStringValidation('validations.required'.tr()),
|
||||
//LengthStringNotEqualValidation(tokenLength),
|
||||
],
|
||||
);
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||
|
||||
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/metrics.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
|
||||
class MetricsLoadException implements Exception {
|
||||
MetricsLoadException(this.message);
|
||||
|
@ -12,8 +11,7 @@ class MetricsLoadException implements Exception {
|
|||
|
||||
class MetricsRepository {
|
||||
Future<MetricsLoaded> getMetrics(final Period period) async {
|
||||
final providerApiFactory = ApiController.currentServerProviderApiFactory;
|
||||
if (providerApiFactory == null) {
|
||||
if (ProvidersController.currentServerProvider == null) {
|
||||
throw MetricsLoadException('Server Provider data is null');
|
||||
}
|
||||
|
||||
|
@ -33,20 +31,19 @@ class MetricsRepository {
|
|||
}
|
||||
|
||||
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
|
||||
final ServerMetrics? metrics =
|
||||
await providerApiFactory.getServerProvider().getMetrics(
|
||||
serverId,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
final result = await ProvidersController.currentServerProvider!.getMetrics(
|
||||
serverId,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
|
||||
if (metrics == null) {
|
||||
if (result.data == null || !result.success) {
|
||||
throw MetricsLoadException('Metrics data is null');
|
||||
}
|
||||
|
||||
return MetricsLoaded(
|
||||
period: period,
|
||||
metrics: metrics,
|
||||
metrics: result.data!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_status.dart';
|
||||
import 'package:selfprivacy/logic/models/price.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
|
||||
part 'provider_volume_state.dart';
|
||||
|
||||
|
@ -20,50 +22,50 @@ class ApiProviderVolumeCubit
|
|||
@override
|
||||
Future<void> load() async {
|
||||
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||
_refetch();
|
||||
unawaited(_refetch());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Price?> getPricePerGb() async =>
|
||||
ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.getPricePerGb();
|
||||
(await ProvidersController.currentServerProvider!.getPricePerGb()).data;
|
||||
|
||||
Future<void> refresh() async {
|
||||
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
|
||||
_refetch();
|
||||
unawaited(_refetch());
|
||||
}
|
||||
|
||||
Future<void> _refetch() async {
|
||||
if (ApiController.currentVolumeProviderApiFactory == null) {
|
||||
if (ProvidersController.currentServerProvider == null) {
|
||||
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
|
||||
}
|
||||
|
||||
final List<ServerVolume> volumes = await ApiController
|
||||
.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.getVolumes();
|
||||
final volumesResult =
|
||||
await ProvidersController.currentServerProvider!.getVolumes();
|
||||
|
||||
if (volumes.isEmpty) {
|
||||
if (!volumesResult.success || volumesResult.data.isEmpty) {
|
||||
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
|
||||
}
|
||||
|
||||
emit(ApiProviderVolumeState(volumes, LoadingStatus.success, false));
|
||||
emit(
|
||||
ApiProviderVolumeState(
|
||||
volumesResult.data,
|
||||
LoadingStatus.success,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> attachVolume(final DiskVolume volume) async {
|
||||
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
|
||||
await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
await ProvidersController.currentServerProvider!
|
||||
.attachVolume(volume.providerVolume!, server.id);
|
||||
refresh();
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
||||
Future<void> detachVolume(final DiskVolume volume) async {
|
||||
await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
await ProvidersController.currentServerProvider!
|
||||
.detachVolume(volume.providerVolume!);
|
||||
refresh();
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
||||
Future<bool> resizeVolume(
|
||||
|
@ -75,14 +77,13 @@ class ApiProviderVolumeCubit
|
|||
'Starting resize',
|
||||
);
|
||||
emit(state.copyWith(isResizing: true));
|
||||
final bool resized = await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.resizeVolume(
|
||||
volume.providerVolume!,
|
||||
newSize,
|
||||
);
|
||||
final resizedResult =
|
||||
await ProvidersController.currentServerProvider!.resizeVolume(
|
||||
volume.providerVolume!,
|
||||
newSize,
|
||||
);
|
||||
|
||||
if (!resized) {
|
||||
if (!resizedResult.success || !resizedResult.data) {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'storage.extending_volume_error'.tr(),
|
||||
);
|
||||
|
@ -113,11 +114,8 @@ class ApiProviderVolumeCubit
|
|||
}
|
||||
|
||||
Future<void> createVolume() async {
|
||||
final ServerVolume? volume = (await ApiController
|
||||
.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.createVolume())
|
||||
.data;
|
||||
final ServerVolume? volume =
|
||||
(await ProvidersController.currentServerProvider!.createVolume()).data;
|
||||
|
||||
final diskVolume = DiskVolume(providerVolume: volume);
|
||||
await attachVolume(diskVolume);
|
||||
|
@ -125,14 +123,13 @@ class ApiProviderVolumeCubit
|
|||
await Future.delayed(const Duration(seconds: 10));
|
||||
|
||||
await ServerApi().mountVolume(volume!.name);
|
||||
refresh();
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
||||
Future<void> deleteVolume(final DiskVolume volume) async {
|
||||
await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
await ProvidersController.currentServerProvider!
|
||||
.deleteVolume(volume.providerVolume!);
|
||||
refresh();
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
|
@ -32,7 +34,7 @@ class RecoveryKeyCubit
|
|||
}
|
||||
|
||||
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
|
||||
final APIGenericResult<RecoveryKeyStatus?> response =
|
||||
final GenericResult<RecoveryKeyStatus?> response =
|
||||
await api.getRecoveryTokenStatus();
|
||||
if (response.success) {
|
||||
return response.data;
|
||||
|
@ -57,10 +59,10 @@ class RecoveryKeyCubit
|
|||
final DateTime? expirationDate,
|
||||
final int? numberOfUses,
|
||||
}) async {
|
||||
final APIGenericResult<String> response =
|
||||
final GenericResult<String> response =
|
||||
await api.generateRecoveryToken(expirationDate, numberOfUses);
|
||||
if (response.success) {
|
||||
refresh();
|
||||
unawaited(refresh());
|
||||
return response.data;
|
||||
} else {
|
||||
throw GenerationError(response.message ?? 'Unknown error');
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
|
||||
import 'package:selfprivacy/logic/models/server_metadata.dart';
|
||||
import 'package:selfprivacy/logic/models/timezone_settings.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
|
||||
class ServerDetailsRepository {
|
||||
ServerApi server = ServerApi();
|
||||
|
||||
Future<ServerDetailsRepositoryDto> load() async {
|
||||
final serverProviderApi = ApiController.currentServerProviderApiFactory;
|
||||
final serverProviderApi = ProvidersController.currentServerProvider;
|
||||
final settings = await server.getSystemSettings();
|
||||
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
|
||||
final metadata =
|
||||
await serverProviderApi!.getServerProvider().getMetadata(serverId);
|
||||
final metadata = await serverProviderApi?.getMetadata(serverId);
|
||||
|
||||
return ServerDetailsRepositoryDto(
|
||||
autoUpgradeSettings: settings.autoUpgradeSettings,
|
||||
metadata: metadata,
|
||||
metadata: metadata!.data,
|
||||
serverTimezone: TimeZoneSettings.fromString(
|
||||
settings.timezone,
|
||||
),
|
||||
|
|
|
@ -5,12 +5,11 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
||||
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
|
||||
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
|
||||
import 'package:selfprivacy/logic/providers/provider_settings.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
|
@ -20,6 +19,7 @@ import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
|||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
|
||||
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||
|
||||
export 'package:provider/provider.dart';
|
||||
|
||||
|
@ -58,45 +58,29 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
}
|
||||
}
|
||||
|
||||
void setServerProviderType(final ServerProvider providerType) async {
|
||||
void setServerProviderType(final ServerProviderType providerType) async {
|
||||
await repository.saveServerProviderType(providerType);
|
||||
ApiController.initServerProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
provider: providerType,
|
||||
),
|
||||
ProvidersController.initServerProvider(
|
||||
ServerProviderSettings(provider: providerType),
|
||||
);
|
||||
}
|
||||
|
||||
void setDnsProviderType(final DnsProvider providerType) async {
|
||||
void setDnsProviderType(final DnsProviderType providerType) async {
|
||||
await repository.saveDnsProviderType(providerType);
|
||||
ApiController.initDnsProviderApiFactory(
|
||||
DnsProviderApiFactorySettings(
|
||||
ProvidersController.initDnsProvider(
|
||||
DnsProviderSettings(
|
||||
provider: providerType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ProviderApiTokenValidation serverProviderApiTokenValidation() =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.getApiTokenValidation();
|
||||
|
||||
RegExp getDnsProviderApiTokenValidation() =>
|
||||
ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.getApiTokenValidation();
|
||||
|
||||
Future<bool?> isServerProviderApiTokenValid(
|
||||
final String providerToken,
|
||||
) async {
|
||||
final APIGenericResult<bool> apiResponse =
|
||||
await ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider(
|
||||
settings: const ServerProviderApiSettings(
|
||||
isWithToken: false,
|
||||
),
|
||||
)
|
||||
.isApiTokenValid(providerToken);
|
||||
final GenericResult<bool> apiResponse =
|
||||
await ProvidersController.currentServerProvider!.tryInitApiByToken(
|
||||
providerToken,
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
|
@ -111,12 +95,10 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
Future<bool?> isDnsProviderApiTokenValid(
|
||||
final String providerToken,
|
||||
) async {
|
||||
final APIGenericResult<bool> apiResponse =
|
||||
await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider(
|
||||
settings: const DnsProviderApiSettings(isWithToken: false),
|
||||
)
|
||||
.isApiTokenValid(providerToken);
|
||||
final GenericResult<bool> apiResponse =
|
||||
await ProvidersController.currentDnsProvider!.tryInitApiByToken(
|
||||
providerToken,
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
|
@ -129,35 +111,33 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
}
|
||||
|
||||
Future<List<ServerProviderLocation>> fetchAvailableLocations() async {
|
||||
if (ApiController.currentServerProviderApiFactory == null) {
|
||||
if (ProvidersController.currentServerProvider == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final APIGenericResult apiResult = await ApiController
|
||||
.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
final GenericResult apiResponse = await ProvidersController
|
||||
.currentServerProvider!
|
||||
.getAvailableLocations();
|
||||
|
||||
if (!apiResult.success) {
|
||||
if (!apiResponse.success) {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'initializing.could_not_connect'.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
return apiResult.data;
|
||||
return apiResponse.data;
|
||||
}
|
||||
|
||||
Future<List<ServerType>> fetchAvailableTypesByLocation(
|
||||
final ServerProviderLocation location,
|
||||
) async {
|
||||
if (ApiController.currentServerProviderApiFactory == null) {
|
||||
if (ProvidersController.currentServerProvider == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final APIGenericResult apiResult = await ApiController
|
||||
.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.getServerTypesByLocation(location: location);
|
||||
final GenericResult apiResult = await ProvidersController
|
||||
.currentServerProvider!
|
||||
.getServerTypes(location: location);
|
||||
|
||||
if (!apiResult.success) {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
|
@ -191,21 +171,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
void setServerType(final ServerType serverType) async {
|
||||
await repository.saveServerType(serverType);
|
||||
|
||||
ApiController.initServerProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
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,
|
||||
),
|
||||
);
|
||||
await ProvidersController.currentServerProvider!
|
||||
.trySetServerLocation(serverType.location.identifier);
|
||||
|
||||
emit(
|
||||
(state as ServerInstallationNotFinished).copyWith(
|
||||
|
@ -216,10 +183,10 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
|
||||
void setDnsApiToken(final String dnsApiToken) async {
|
||||
if (state is ServerInstallationRecovery) {
|
||||
setAndValidateDnsApiToken(dnsApiToken);
|
||||
await setAndValidateDnsApiToken(dnsApiToken);
|
||||
return;
|
||||
}
|
||||
await repository.saveDnsProviderKey(dnsApiToken);
|
||||
await repository.setDnsApiToken(dnsApiToken);
|
||||
|
||||
emit(
|
||||
(state as ServerInstallationNotFinished)
|
||||
|
@ -256,41 +223,53 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
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 {
|
||||
final ServerInstallationNotFinished stateCopy =
|
||||
state as ServerInstallationNotFinished;
|
||||
void onCancel() => emit(
|
||||
(state as ServerInstallationNotFinished).copyWith(isLoading: false),
|
||||
);
|
||||
emit((state as ServerInstallationNotFinished).copyWith(isLoading: true));
|
||||
|
||||
Future<void> onSuccess(final ServerHostingDetails serverDetails) async {
|
||||
await repository.createDnsRecords(
|
||||
serverDetails,
|
||||
state.serverDomain!,
|
||||
onCancel: onCancel,
|
||||
);
|
||||
final installationData = LaunchInstallationData(
|
||||
rootUser: state.rootUser!,
|
||||
dnsApiToken: state.dnsApiToken!,
|
||||
dnsProviderType: state.serverDomain!.provider,
|
||||
serverDomain: state.serverDomain!,
|
||||
serverTypeId: state.serverTypeIdentificator!,
|
||||
errorCallback: clearAppConfig,
|
||||
successCallback: onCreateServerSuccess,
|
||||
);
|
||||
|
||||
final result =
|
||||
await ProvidersController.currentServerProvider!.launchInstallation(
|
||||
installationData,
|
||||
);
|
||||
|
||||
if (!result.success && result.data != null) {
|
||||
emit(
|
||||
(state as ServerInstallationNotFinished).copyWith(
|
||||
isLoading: false,
|
||||
serverDetails: serverDetails,
|
||||
installationDialoguePopUp: result.data,
|
||||
),
|
||||
);
|
||||
runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null);
|
||||
}
|
||||
|
||||
try {
|
||||
emit((state as ServerInstallationNotFinished).copyWith(isLoading: true));
|
||||
await repository.createServer(
|
||||
state.rootUser!,
|
||||
state.serverDomain!.domainName,
|
||||
state.dnsApiToken!,
|
||||
state.backblazeCredential!,
|
||||
onCancel: onCancel,
|
||||
onSuccess: onSuccess,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(stateCopy);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -437,7 +416,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
emit(TimerState(dataState: dataState, isLoading: true));
|
||||
|
||||
final bool isServerWorking = await repository.isHttpServerWorking();
|
||||
StagingOptions.verifyCertificate = true;
|
||||
TlsOptions.verifyCertificate = true;
|
||||
|
||||
if (isServerWorking) {
|
||||
bool dkimCreated = true;
|
||||
|
@ -487,7 +466,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
void submitDomainForAccessRecovery(final String domain) async {
|
||||
final ServerDomain serverDomain = ServerDomain(
|
||||
domainName: domain,
|
||||
provider: DnsProvider.unknown,
|
||||
provider: DnsProviderType.unknown,
|
||||
zoneId: '',
|
||||
);
|
||||
final ServerRecoveryCapabilities recoveryCapabilities =
|
||||
|
@ -539,7 +518,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
token,
|
||||
dataState.recoveryCapabilities,
|
||||
);
|
||||
final ServerProvider provider = await ServerApi(
|
||||
final ServerProviderType serverProvider = await ServerApi(
|
||||
customToken: serverDetails.apiToken,
|
||||
isWithToken: true,
|
||||
).getServerProviderType();
|
||||
|
@ -547,15 +526,15 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
customToken: serverDetails.apiToken,
|
||||
isWithToken: true,
|
||||
).getDnsProviderType();
|
||||
if (provider == ServerProvider.unknown ||
|
||||
dnsProvider == DnsProvider.unknown) {
|
||||
if (serverProvider == ServerProviderType.unknown ||
|
||||
dnsProvider == DnsProviderType.unknown) {
|
||||
getIt<NavigationService>()
|
||||
.showSnackBar('recovering.generic_error'.tr());
|
||||
return;
|
||||
}
|
||||
await repository.saveServerDetails(serverDetails);
|
||||
await repository.saveDnsProviderType(dnsProvider);
|
||||
setServerProviderType(provider);
|
||||
setServerProviderType(serverProvider);
|
||||
setDnsProviderType(dnsProvider);
|
||||
emit(
|
||||
dataState.copyWith(
|
||||
|
@ -683,7 +662,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
linuxDevice: '',
|
||||
),
|
||||
apiToken: dataState.serverDetails!.apiToken,
|
||||
provider: ServerProvider.hetzner,
|
||||
provider: ServerProviderType.hetzner,
|
||||
);
|
||||
await repository.saveDomain(serverDomain);
|
||||
await repository.saveServerDetails(serverDetails);
|
||||
|
@ -720,13 +699,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
provider: dnsProviderType,
|
||||
),
|
||||
);
|
||||
await repository.saveDnsProviderKey(token);
|
||||
await repository.setDnsApiToken(token);
|
||||
emit(
|
||||
dataState.copyWith(
|
||||
serverDomain: ServerDomain(
|
||||
domainName: serverDomain.domainName,
|
||||
zoneId: zoneId,
|
||||
provider: DnsProvider.cloudflare,
|
||||
provider: dnsProviderType,
|
||||
),
|
||||
dnsApiToken: token,
|
||||
currentStep: RecoveryStep.backblazeToken,
|
||||
|
@ -754,13 +733,44 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
|
||||
@override
|
||||
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);
|
||||
}
|
||||
|
||||
void clearAppConfig() {
|
||||
closeTimer();
|
||||
ApiController.clearProviderApiFactories();
|
||||
StagingOptions.verifyCertificate = false;
|
||||
ProvidersController.clearProviders();
|
||||
TlsOptions.verifyCertificate = false;
|
||||
repository.clearAppConfig();
|
||||
emit(const ServerInstallationEmpty());
|
||||
}
|
||||
|
|
|
@ -1,30 +1,26 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/config/hive_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||
import 'package:selfprivacy/logic/providers/provider_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/json/device_token.dart';
|
||||
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||
import 'package:selfprivacy/logic/providers/providers_controller.dart';
|
||||
import 'package:selfprivacy/utils/network_utils.dart';
|
||||
|
||||
class IpNotFoundException implements Exception {
|
||||
|
@ -47,49 +43,39 @@ class ServerInstallationRepository {
|
|||
final String? dnsApiToken = getIt<ApiConfigModel>().dnsProviderKey;
|
||||
final String? serverTypeIdentificator = getIt<ApiConfigModel>().serverType;
|
||||
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
|
||||
final ServerProvider? serverProvider =
|
||||
final DnsProviderType? dnsProvider = getIt<ApiConfigModel>().dnsProvider;
|
||||
final ServerProviderType? serverProvider =
|
||||
getIt<ApiConfigModel>().serverProvider;
|
||||
final BackblazeCredential? backblazeCredential =
|
||||
getIt<ApiConfigModel>().backblazeCredential;
|
||||
final ServerHostingDetails? serverDetails =
|
||||
getIt<ApiConfigModel>().serverDetails;
|
||||
final DnsProvider? dnsProvider = getIt<ApiConfigModel>().dnsProvider;
|
||||
|
||||
if (serverProvider != null ||
|
||||
(serverDetails != null &&
|
||||
serverDetails.provider != ServerProvider.unknown)) {
|
||||
ApiController.initServerProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
provider: serverProvider ?? serverDetails!.provider,
|
||||
location: location,
|
||||
),
|
||||
);
|
||||
|
||||
// All current providers support volumes
|
||||
// so it's safe to hardcode for now
|
||||
ApiController.initVolumeProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
serverDetails.provider != ServerProviderType.unknown)) {
|
||||
ProvidersController.initServerProvider(
|
||||
ServerProviderSettings(
|
||||
provider: serverProvider ?? serverDetails!.provider,
|
||||
location: location,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (ApiController.currentDnsProviderApiFactory == null) {
|
||||
if (dnsProvider != null ||
|
||||
(serverDomain != null &&
|
||||
serverDomain.provider != DnsProvider.unknown)) {
|
||||
ApiController.initDnsProviderApiFactory(
|
||||
DnsProviderApiFactorySettings(
|
||||
provider: dnsProvider ?? serverDomain!.provider,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (dnsProvider != null ||
|
||||
(serverDomain != null &&
|
||||
serverDomain.provider != DnsProviderType.unknown)) {
|
||||
ProvidersController.initDnsProvider(
|
||||
DnsProviderSettings(
|
||||
provider: dnsProvider ?? serverDomain!.provider,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
|
||||
StagingOptions.verifyCertificate = true;
|
||||
TlsOptions.verifyCertificate = true;
|
||||
return ServerInstallationFinished(
|
||||
installationDialoguePopUp: null,
|
||||
providerApiToken: providerApiToken!,
|
||||
serverTypeIdentificator: serverTypeIdentificator ?? '',
|
||||
dnsApiToken: dnsApiToken!,
|
||||
|
@ -149,8 +135,8 @@ class ServerInstallationRepository {
|
|||
) {
|
||||
if (serverDetails != null) {
|
||||
if (serverProviderToken != null) {
|
||||
if (serverDetails.provider != ServerProvider.unknown) {
|
||||
if (serverDomain.provider != DnsProvider.unknown) {
|
||||
if (serverDetails.provider != ServerProviderType.unknown) {
|
||||
if (serverDomain.provider != DnsProviderType.unknown) {
|
||||
return RecoveryStep.backblazeToken;
|
||||
}
|
||||
return RecoveryStep.dnsProviderToken;
|
||||
|
@ -170,35 +156,26 @@ class ServerInstallationRepository {
|
|||
Future<ServerHostingDetails> startServer(
|
||||
final ServerHostingDetails server,
|
||||
) async {
|
||||
ServerHostingDetails serverDetails;
|
||||
final result = await ProvidersController.currentServerProvider!.powerOn(
|
||||
server.id,
|
||||
);
|
||||
|
||||
serverDetails = await ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.powerOn();
|
||||
if (result.success && result.data != null) {
|
||||
server.copyWith(startTime: result.data);
|
||||
}
|
||||
|
||||
return serverDetails;
|
||||
return server;
|
||||
}
|
||||
|
||||
Future<String?> getDomainId(final String token, final String domain) async {
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider(
|
||||
settings: DnsProviderApiSettings(
|
||||
isWithToken: false,
|
||||
customToken: token,
|
||||
),
|
||||
);
|
||||
|
||||
/// TODO: nvm it's because only Cloudflare uses Zone
|
||||
/// for other providers we need to implement a different kind of
|
||||
/// functionality here... but it's on refactoring, let it be here for now.
|
||||
final APIGenericResult<bool> apiResponse =
|
||||
await dnsProviderApi.isApiTokenValid(token);
|
||||
|
||||
String? domainId;
|
||||
if (apiResponse.success && apiResponse.data) {
|
||||
domainId = await dnsProviderApi.getZoneId(domain);
|
||||
}
|
||||
return domainId;
|
||||
final result =
|
||||
await ProvidersController.currentDnsProvider!.tryInitApiByToken(token);
|
||||
return result.success
|
||||
? (await ProvidersController.currentDnsProvider!.getZoneId(
|
||||
domain,
|
||||
))
|
||||
.data
|
||||
: null;
|
||||
}
|
||||
|
||||
Future<Map<String, bool>> isDnsAddressesMatch(
|
||||
|
@ -224,181 +201,7 @@ class ServerInstallationRepository {
|
|||
return matches;
|
||||
}
|
||||
|
||||
Future<void> createServer(
|
||||
final User rootUser,
|
||||
final String domainName,
|
||||
final String dnsApiToken,
|
||||
final BackblazeCredential 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 {
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider();
|
||||
final ServerApi api = ServerApi();
|
||||
|
||||
late DnsRecord record;
|
||||
|
@ -409,7 +212,10 @@ class ServerInstallationRepository {
|
|||
rethrow;
|
||||
}
|
||||
|
||||
await dnsProviderApi.setDnsRecord(record, cloudFlareDomain);
|
||||
await ProvidersController.currentDnsProvider!.setDnsRecord(
|
||||
record,
|
||||
cloudFlareDomain,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isHttpServerWorking() async {
|
||||
|
@ -417,15 +223,24 @@ class ServerInstallationRepository {
|
|||
return api.isHttpServerWorking();
|
||||
}
|
||||
|
||||
Future<ServerHostingDetails> restart() async =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.restart();
|
||||
Future<ServerHostingDetails> restart() async {
|
||||
final server = getIt<ApiConfigModel>().serverDetails!;
|
||||
|
||||
Future<ServerHostingDetails> powerOn() async =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.powerOn();
|
||||
final result = await ProvidersController.currentServerProvider!.restart(
|
||||
server.id,
|
||||
);
|
||||
|
||||
if (result.success && result.data != null) {
|
||||
server.copyWith(startTime: result.data);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
Future<ServerHostingDetails> powerOn() async {
|
||||
final server = getIt<ApiConfigModel>().serverDetails!;
|
||||
return startServer(server);
|
||||
}
|
||||
|
||||
Future<ServerRecoveryCapabilities> getRecoveryCapabilities(
|
||||
final ServerDomain serverDomain,
|
||||
|
@ -508,7 +323,7 @@ class ServerInstallationRepository {
|
|||
overrideDomain: serverDomain.domainName,
|
||||
);
|
||||
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),
|
||||
);
|
||||
|
||||
|
@ -522,7 +337,7 @@ class ServerInstallationRepository {
|
|||
serverId: 0,
|
||||
linuxDevice: '',
|
||||
),
|
||||
provider: ServerProvider.unknown,
|
||||
provider: ServerProviderType.unknown,
|
||||
id: 0,
|
||||
ip4: serverIp,
|
||||
startTime: null,
|
||||
|
@ -545,7 +360,7 @@ class ServerInstallationRepository {
|
|||
overrideDomain: serverDomain.domainName,
|
||||
);
|
||||
final String serverIp = await getServerIpFromDomain(serverDomain);
|
||||
final APIGenericResult<String> result = await serverApi.useRecoveryToken(
|
||||
final GenericResult<String> result = await serverApi.useRecoveryToken(
|
||||
DeviceToken(device: await getDeviceName(), token: recoveryKey),
|
||||
);
|
||||
|
||||
|
@ -559,7 +374,7 @@ class ServerInstallationRepository {
|
|||
serverId: 0,
|
||||
linuxDevice: '',
|
||||
),
|
||||
provider: ServerProvider.unknown,
|
||||
provider: ServerProviderType.unknown,
|
||||
id: 0,
|
||||
ip4: serverIp,
|
||||
startTime: null,
|
||||
|
@ -594,7 +409,7 @@ class ServerInstallationRepository {
|
|||
sizeByte: 0,
|
||||
linuxDevice: '',
|
||||
),
|
||||
provider: ServerProvider.unknown,
|
||||
provider: ServerProviderType.unknown,
|
||||
id: 0,
|
||||
ip4: serverIp,
|
||||
startTime: null,
|
||||
|
@ -606,9 +421,9 @@ class ServerInstallationRepository {
|
|||
);
|
||||
}
|
||||
}
|
||||
final APIGenericResult<String> deviceAuthKey =
|
||||
final GenericResult<String> deviceAuthKey =
|
||||
await serverApi.createDeviceToken();
|
||||
final APIGenericResult<String> result = await serverApi.authorizeDevice(
|
||||
final GenericResult<String> result = await serverApi.authorizeDevice(
|
||||
DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data),
|
||||
);
|
||||
|
||||
|
@ -622,7 +437,7 @@ class ServerInstallationRepository {
|
|||
serverId: 0,
|
||||
linuxDevice: '',
|
||||
),
|
||||
provider: ServerProvider.unknown,
|
||||
provider: ServerProviderType.unknown,
|
||||
id: 0,
|
||||
ip4: serverIp,
|
||||
startTime: null,
|
||||
|
@ -664,9 +479,7 @@ class ServerInstallationRepository {
|
|||
}
|
||||
|
||||
Future<List<ServerBasicInfo>> getServersOnProviderAccount() async =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.getServers();
|
||||
(await ProvidersController.currentServerProvider!.getServers()).data;
|
||||
|
||||
Future<void> saveServerDetails(
|
||||
final ServerHostingDetails serverDetails,
|
||||
|
@ -679,10 +492,14 @@ class ServerInstallationRepository {
|
|||
getIt<ApiConfigModel>().init();
|
||||
}
|
||||
|
||||
Future<void> saveServerProviderType(final ServerProvider type) async {
|
||||
Future<void> saveServerProviderType(final ServerProviderType type) async {
|
||||
await getIt<ApiConfigModel>().storeServerProviderType(type);
|
||||
}
|
||||
|
||||
Future<void> saveDnsProviderType(final DnsProviderType type) async {
|
||||
await getIt<ApiConfigModel>().storeDnsProviderType(type);
|
||||
}
|
||||
|
||||
Future<void> saveServerProviderKey(final String key) async {
|
||||
await getIt<ApiConfigModel>().storeServerProviderKey(key);
|
||||
}
|
||||
|
@ -701,10 +518,6 @@ class ServerInstallationRepository {
|
|||
getIt<ApiConfigModel>().init();
|
||||
}
|
||||
|
||||
Future<void> saveDnsProviderType(final DnsProvider type) async {
|
||||
await getIt<ApiConfigModel>().storeDnsProviderType(type);
|
||||
}
|
||||
|
||||
Future<void> saveBackblazeKey(
|
||||
final BackblazeCredential backblazeCredential,
|
||||
) async {
|
||||
|
@ -716,7 +529,7 @@ class ServerInstallationRepository {
|
|||
getIt<ApiConfigModel>().init();
|
||||
}
|
||||
|
||||
Future<void> saveDnsProviderKey(final String key) async {
|
||||
Future<void> setDnsApiToken(final String key) async {
|
||||
await getIt<ApiConfigModel>().storeDnsProviderKey(key);
|
||||
}
|
||||
|
||||
|
@ -759,12 +572,10 @@ class ServerInstallationRepository {
|
|||
}
|
||||
|
||||
Future<bool> deleteServer(final ServerDomain serverDomain) async {
|
||||
final APIGenericResult<bool> deletionResult = await ApiController
|
||||
.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.deleteServer(
|
||||
domainName: serverDomain.domainName,
|
||||
);
|
||||
final deletionResult =
|
||||
await ProvidersController.currentServerProvider!.deleteServer(
|
||||
serverDomain.domainName,
|
||||
);
|
||||
|
||||
if (!deletionResult.success) {
|
||||
getIt<NavigationService>()
|
||||
|
@ -772,12 +583,6 @@ class ServerInstallationRepository {
|
|||
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.isServerStarted, false);
|
||||
await box.put(BNames.isServerResetedFirstTime, false);
|
||||
|
@ -785,10 +590,9 @@ class ServerInstallationRepository {
|
|||
await box.put(BNames.isLoading, false);
|
||||
await box.put(BNames.serverDetails, null);
|
||||
|
||||
final APIGenericResult<void> removalResult = await ApiController
|
||||
.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.removeSimilarRecords(domain: serverDomain);
|
||||
final GenericResult<void> removalResult = await ProvidersController
|
||||
.currentDnsProvider!
|
||||
.removeDomainRecords(domain: serverDomain);
|
||||
|
||||
if (!removalResult.success) {
|
||||
getIt<NavigationService>().showSnackBar('modals.dns_removal_error'.tr());
|
||||
|
|
|
@ -12,6 +12,7 @@ abstract class ServerInstallationState extends Equatable {
|
|||
required this.isServerStarted,
|
||||
required this.isServerResetedFirstTime,
|
||||
required this.isServerResetedSecondTime,
|
||||
required this.installationDialoguePopUp,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -25,6 +26,7 @@ abstract class ServerInstallationState extends Equatable {
|
|||
serverDetails,
|
||||
isServerStarted,
|
||||
isServerResetedFirstTime,
|
||||
installationDialoguePopUp
|
||||
];
|
||||
|
||||
final String? providerApiToken;
|
||||
|
@ -37,6 +39,7 @@ abstract class ServerInstallationState extends Equatable {
|
|||
final bool isServerStarted;
|
||||
final bool isServerResetedFirstTime;
|
||||
final bool isServerResetedSecondTime;
|
||||
final CallbackDialogueBranching? installationDialoguePopUp;
|
||||
|
||||
bool get isServerProviderApiKeyFilled => providerApiToken != null;
|
||||
bool get isServerTypeFilled => serverTypeIdentificator != null;
|
||||
|
@ -96,6 +99,7 @@ class TimerState extends ServerInstallationNotFinished {
|
|||
isServerResetedFirstTime: dataState.isServerResetedFirstTime,
|
||||
isServerResetedSecondTime: dataState.isServerResetedSecondTime,
|
||||
dnsMatches: dataState.dnsMatches,
|
||||
installationDialoguePopUp: dataState.installationDialoguePopUp,
|
||||
);
|
||||
|
||||
final ServerInstallationNotFinished dataState;
|
||||
|
@ -138,6 +142,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
super.serverDomain,
|
||||
super.rootUser,
|
||||
super.serverDetails,
|
||||
super.installationDialoguePopUp,
|
||||
});
|
||||
final bool isLoading;
|
||||
final Map<String, bool>? dnsMatches;
|
||||
|
@ -155,6 +160,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
isServerResetedFirstTime,
|
||||
isLoading,
|
||||
dnsMatches,
|
||||
installationDialoguePopUp,
|
||||
];
|
||||
|
||||
ServerInstallationNotFinished copyWith({
|
||||
|
@ -170,6 +176,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
final bool? isServerResetedSecondTime,
|
||||
final bool? isLoading,
|
||||
final Map<String, bool>? dnsMatches,
|
||||
final CallbackDialogueBranching? installationDialoguePopUp,
|
||||
}) =>
|
||||
ServerInstallationNotFinished(
|
||||
providerApiToken: providerApiToken ?? this.providerApiToken,
|
||||
|
@ -187,6 +194,8 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
isServerResetedSecondTime ?? this.isServerResetedSecondTime,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
dnsMatches: dnsMatches ?? this.dnsMatches,
|
||||
installationDialoguePopUp:
|
||||
installationDialoguePopUp ?? this.installationDialoguePopUp,
|
||||
);
|
||||
|
||||
ServerInstallationFinished finish() => ServerInstallationFinished(
|
||||
|
@ -200,6 +209,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
isServerStarted: isServerStarted,
|
||||
isServerResetedFirstTime: isServerResetedFirstTime,
|
||||
isServerResetedSecondTime: isServerResetedSecondTime,
|
||||
installationDialoguePopUp: installationDialoguePopUp,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -218,6 +228,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
|
|||
isServerResetedSecondTime: false,
|
||||
isLoading: false,
|
||||
dnsMatches: null,
|
||||
installationDialoguePopUp: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -233,6 +244,7 @@ class ServerInstallationFinished extends ServerInstallationState {
|
|||
required super.isServerStarted,
|
||||
required super.isServerResetedFirstTime,
|
||||
required super.isServerResetedSecondTime,
|
||||
required super.installationDialoguePopUp,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -246,6 +258,7 @@ class ServerInstallationFinished extends ServerInstallationState {
|
|||
serverDetails,
|
||||
isServerStarted,
|
||||
isServerResetedFirstTime,
|
||||
installationDialoguePopUp,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -287,6 +300,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
isServerStarted: true,
|
||||
isServerResetedFirstTime: true,
|
||||
isServerResetedSecondTime: true,
|
||||
installationDialoguePopUp: null,
|
||||
);
|
||||
final RecoveryStep currentStep;
|
||||
final ServerRecoveryCapabilities recoveryCapabilities;
|
||||
|
@ -302,7 +316,8 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
serverDetails,
|
||||
isServerStarted,
|
||||
isServerResetedFirstTime,
|
||||
currentStep
|
||||
currentStep,
|
||||
installationDialoguePopUp
|
||||
];
|
||||
|
||||
ServerInstallationRecovery copyWith({
|
||||
|
@ -340,5 +355,6 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
isServerStarted: true,
|
||||
isServerResetedFirstTime: true,
|
||||
isServerResetedSecondTime: true,
|
||||
installationDialoguePopUp: null,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class ApiServerVolumeCubit
|
|||
@override
|
||||
Future<void> load() async {
|
||||
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||
reload();
|
||||
unawaited(reload());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
|
|||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
reload();
|
||||
unawaited(reload());
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -62,7 +62,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
|
|||
.toList(),
|
||||
),
|
||||
);
|
||||
reload();
|
||||
unawaited(reload());
|
||||
}
|
||||
|
||||
Future<void> moveService(
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
|
@ -39,7 +41,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
|
|||
);
|
||||
}
|
||||
|
||||
refresh();
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
|
@ -78,7 +80,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
|
|||
return;
|
||||
}
|
||||
// If API returned error, do nothing
|
||||
final APIGenericResult<User?> result =
|
||||
final GenericResult<User?> result =
|
||||
await api.createUser(user.login, password);
|
||||
if (result.data == null) {
|
||||
getIt<NavigationService>()
|
||||
|
@ -101,7 +103,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
|
|||
return;
|
||||
}
|
||||
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) {
|
||||
loadedUsers.removeWhere((final User u) => u.login == user.login);
|
||||
await box.clear();
|
||||
|
@ -128,7 +130,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
|
|||
.showSnackBar('users.could_not_change_password'.tr());
|
||||
return;
|
||||
}
|
||||
final APIGenericResult<User?> result =
|
||||
final GenericResult<User?> result =
|
||||
await api.updateUser(user.login, newPassword);
|
||||
if (result.data == null) {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
|
@ -138,7 +140,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
|
|||
}
|
||||
|
||||
Future<void> addSshKey(final User user, final String publicKey) async {
|
||||
final APIGenericResult<User?> result =
|
||||
final GenericResult<User?> result =
|
||||
await api.addSshKey(user.login, publicKey);
|
||||
if (result.data != null) {
|
||||
final User updatedUser = result.data!;
|
||||
|
@ -157,7 +159,7 @@ class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
|
|||
}
|
||||
|
||||
Future<void> deleteSshKey(final User user, final String publicKey) async {
|
||||
final APIGenericResult<User?> result =
|
||||
final GenericResult<User?> result =
|
||||
await api.removeSshKey(user.login, publicKey);
|
||||
if (result.data != null) {
|
||||
final User updatedUser = result.data!;
|
||||
|
|
|
@ -13,9 +13,8 @@ class ApiConfigModel {
|
|||
String? get serverLocation => _serverLocation;
|
||||
String? get serverType => _serverType;
|
||||
String? get dnsProviderKey => _dnsProviderKey;
|
||||
ServerProvider? get serverProvider => _serverProvider;
|
||||
DnsProvider? get dnsProvider => _dnsProvider;
|
||||
|
||||
ServerProviderType? get serverProvider => _serverProvider;
|
||||
DnsProviderType? get dnsProvider => _dnsProvider;
|
||||
BackblazeCredential? get backblazeCredential => _backblazeCredential;
|
||||
ServerDomain? get serverDomain => _serverDomain;
|
||||
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
||||
|
@ -24,19 +23,19 @@ class ApiConfigModel {
|
|||
String? _serverLocation;
|
||||
String? _dnsProviderKey;
|
||||
String? _serverType;
|
||||
ServerProvider? _serverProvider;
|
||||
DnsProvider? _dnsProvider;
|
||||
ServerProviderType? _serverProvider;
|
||||
DnsProviderType? _dnsProvider;
|
||||
ServerHostingDetails? _serverDetails;
|
||||
BackblazeCredential? _backblazeCredential;
|
||||
ServerDomain? _serverDomain;
|
||||
BackblazeBucket? _backblazeBucket;
|
||||
|
||||
Future<void> storeServerProviderType(final ServerProvider value) async {
|
||||
Future<void> storeServerProviderType(final ServerProviderType value) async {
|
||||
await _box.put(BNames.serverProvider, value);
|
||||
_serverProvider = value;
|
||||
}
|
||||
|
||||
Future<void> storeDnsProviderType(final DnsProvider value) async {
|
||||
Future<void> storeDnsProviderType(final DnsProviderType value) async {
|
||||
await _box.put(BNames.dnsProvider, value);
|
||||
_dnsProvider = value;
|
||||
}
|
||||
|
|
21
lib/logic/models/callback_dialogue_branching.dart
Normal file
21
lib/logic/models/callback_dialogue_branching.dart
Normal 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;
|
||||
}
|
|
@ -33,8 +33,8 @@ class ServerHostingDetails {
|
|||
@HiveField(5)
|
||||
final String apiToken;
|
||||
|
||||
@HiveField(6, defaultValue: ServerProvider.hetzner)
|
||||
final ServerProvider provider;
|
||||
@HiveField(6, defaultValue: ServerProviderType.hetzner)
|
||||
final ServerProviderType provider;
|
||||
|
||||
ServerHostingDetails copyWith({final DateTime? startTime}) =>
|
||||
ServerHostingDetails(
|
||||
|
@ -77,7 +77,7 @@ class ServerVolume {
|
|||
}
|
||||
|
||||
@HiveType(typeId: 101)
|
||||
enum ServerProvider {
|
||||
enum ServerProviderType {
|
||||
@HiveField(0)
|
||||
unknown,
|
||||
@HiveField(1)
|
||||
|
@ -85,7 +85,7 @@ enum ServerProvider {
|
|||
@HiveField(2)
|
||||
digitalOcean;
|
||||
|
||||
factory ServerProvider.fromGraphQL(final Enum$ServerProvider provider) {
|
||||
factory ServerProviderType.fromGraphQL(final Enum$ServerProvider provider) {
|
||||
switch (provider) {
|
||||
case Enum$ServerProvider.HETZNER:
|
||||
return hetzner;
|
||||
|
@ -98,9 +98,9 @@ enum ServerProvider {
|
|||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ServerProvider.hetzner:
|
||||
case ServerProviderType.hetzner:
|
||||
return 'Hetzner Cloud';
|
||||
case ServerProvider.digitalOcean:
|
||||
case ServerProviderType.digitalOcean:
|
||||
return 'Digital Ocean';
|
||||
default:
|
||||
return 'Unknown';
|
||||
|
|
|
@ -23,8 +23,8 @@ class ServerHostingDetailsAdapter extends TypeAdapter<ServerHostingDetails> {
|
|||
volume: fields[4] as ServerVolume,
|
||||
apiToken: fields[5] as String,
|
||||
provider: fields[6] == null
|
||||
? ServerProvider.hetzner
|
||||
: fields[6] as ServerProvider,
|
||||
? ServerProviderType.hetzner
|
||||
: fields[6] as ServerProviderType,
|
||||
startTime: fields[2] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
@ -109,34 +109,34 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
|
|||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
|
||||
class ServerProviderTypeAdapter extends TypeAdapter<ServerProviderType> {
|
||||
@override
|
||||
final int typeId = 101;
|
||||
|
||||
@override
|
||||
ServerProvider read(BinaryReader reader) {
|
||||
ServerProviderType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return ServerProvider.unknown;
|
||||
return ServerProviderType.unknown;
|
||||
case 1:
|
||||
return ServerProvider.hetzner;
|
||||
return ServerProviderType.hetzner;
|
||||
case 2:
|
||||
return ServerProvider.digitalOcean;
|
||||
return ServerProviderType.digitalOcean;
|
||||
default:
|
||||
return ServerProvider.unknown;
|
||||
return ServerProviderType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerProvider obj) {
|
||||
void write(BinaryWriter writer, ServerProviderType obj) {
|
||||
switch (obj) {
|
||||
case ServerProvider.unknown:
|
||||
case ServerProviderType.unknown:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case ServerProvider.hetzner:
|
||||
case ServerProviderType.hetzner:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case ServerProvider.digitalOcean:
|
||||
case ServerProviderType.digitalOcean:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
|
|||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ServerProviderAdapter &&
|
||||
other is ServerProviderTypeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
|
|
@ -17,28 +17,32 @@ class ServerDomain {
|
|||
@HiveField(1)
|
||||
final String zoneId;
|
||||
|
||||
@HiveField(2, defaultValue: DnsProvider.cloudflare)
|
||||
final DnsProvider provider;
|
||||
@HiveField(2, defaultValue: DnsProviderType.cloudflare)
|
||||
final DnsProviderType provider;
|
||||
|
||||
@override
|
||||
String toString() => '$domainName: $zoneId';
|
||||
}
|
||||
|
||||
@HiveType(typeId: 100)
|
||||
enum DnsProvider {
|
||||
enum DnsProviderType {
|
||||
@HiveField(0)
|
||||
unknown,
|
||||
@HiveField(1)
|
||||
cloudflare,
|
||||
@HiveField(2)
|
||||
desec;
|
||||
desec,
|
||||
@HiveField(3)
|
||||
digitalOcean;
|
||||
|
||||
factory DnsProvider.fromGraphQL(final Enum$DnsProvider provider) {
|
||||
factory DnsProviderType.fromGraphQL(final Enum$DnsProvider provider) {
|
||||
switch (provider) {
|
||||
case Enum$DnsProvider.CLOUDFLARE:
|
||||
return cloudflare;
|
||||
case Enum$DnsProvider.DESEC:
|
||||
return desec;
|
||||
case Enum$DnsProvider.DIGITALOCEAN:
|
||||
return digitalOcean;
|
||||
default:
|
||||
return unknown;
|
||||
}
|
||||
|
|
|
@ -19,8 +19,9 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
|
|||
return ServerDomain(
|
||||
domainName: fields[0] as String,
|
||||
zoneId: fields[1] as String,
|
||||
provider:
|
||||
fields[2] == null ? DnsProvider.cloudflare : fields[2] as DnsProvider,
|
||||
provider: fields[2] == null
|
||||
? DnsProviderType.cloudflare
|
||||
: fields[2] as DnsProviderType,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -47,36 +48,41 @@ class ServerDomainAdapter extends TypeAdapter<ServerDomain> {
|
|||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class DnsProviderAdapter extends TypeAdapter<DnsProvider> {
|
||||
class DnsProviderTypeAdapter extends TypeAdapter<DnsProviderType> {
|
||||
@override
|
||||
final int typeId = 100;
|
||||
|
||||
@override
|
||||
DnsProvider read(BinaryReader reader) {
|
||||
DnsProviderType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return DnsProvider.unknown;
|
||||
return DnsProviderType.unknown;
|
||||
case 1:
|
||||
return DnsProvider.cloudflare;
|
||||
return DnsProviderType.cloudflare;
|
||||
case 2:
|
||||
return DnsProvider.desec;
|
||||
return DnsProviderType.desec;
|
||||
case 3:
|
||||
return DnsProviderType.digitalOcean;
|
||||
default:
|
||||
return DnsProvider.unknown;
|
||||
return DnsProviderType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, DnsProvider obj) {
|
||||
void write(BinaryWriter writer, DnsProviderType obj) {
|
||||
switch (obj) {
|
||||
case DnsProvider.unknown:
|
||||
case DnsProviderType.unknown:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case DnsProvider.cloudflare:
|
||||
case DnsProviderType.cloudflare:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case DnsProvider.desec:
|
||||
case DnsProviderType.desec:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
case DnsProviderType.digitalOcean:
|
||||
writer.writeByte(3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +92,7 @@ class DnsProviderAdapter extends TypeAdapter<DnsProvider> {
|
|||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is DnsProviderAdapter &&
|
||||
other is DnsProviderTypeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
|
65
lib/logic/models/json/digital_ocean_server_info.dart
Normal file
65
lib/logic/models/json/digital_ocean_server_info.dart
Normal 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);
|
||||
}
|
61
lib/logic/models/json/digital_ocean_server_info.g.dart
Normal file
61
lib/logic/models/json/digital_ocean_server_info.g.dart
Normal 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,
|
||||
};
|
|
@ -9,6 +9,7 @@ class DnsRecord {
|
|||
required this.type,
|
||||
required this.name,
|
||||
required this.content,
|
||||
this.id,
|
||||
this.ttl = 3600,
|
||||
this.priority = 10,
|
||||
this.proxied = false,
|
||||
|
@ -31,5 +32,8 @@ class DnsRecord {
|
|||
final int priority;
|
||||
final bool proxied;
|
||||
|
||||
/// TODO: Refactoring refactoring refactoring refactoring >:c
|
||||
final int? id;
|
||||
|
||||
Map<String, dynamic> toJson() => _$DnsRecordToJson(this);
|
||||
}
|
||||
|
|
|
@ -13,4 +13,5 @@ Map<String, dynamic> _$DnsRecordToJson(DnsRecord instance) => <String, dynamic>{
|
|||
'ttl': instance.ttl,
|
||||
'priority': instance.priority,
|
||||
'proxied': instance.proxied,
|
||||
'id': instance.id,
|
||||
};
|
||||
|
|
|
@ -72,11 +72,21 @@ enum ServerStatus {
|
|||
|
||||
@JsonSerializable()
|
||||
class HetznerServerTypeInfo {
|
||||
HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices);
|
||||
HetznerServerTypeInfo(
|
||||
this.cores,
|
||||
this.memory,
|
||||
this.disk,
|
||||
this.prices,
|
||||
this.name,
|
||||
this.description,
|
||||
);
|
||||
final int cores;
|
||||
final num memory;
|
||||
final int disk;
|
||||
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
final List<HetznerPriceInfo> prices;
|
||||
|
||||
static HetznerServerTypeInfo fromJson(final Map<String, dynamic> json) =>
|
||||
|
@ -85,7 +95,11 @@ class HetznerServerTypeInfo {
|
|||
|
||||
@JsonSerializable()
|
||||
class HetznerPriceInfo {
|
||||
HetznerPriceInfo(this.hourly, this.monthly);
|
||||
HetznerPriceInfo(
|
||||
this.hourly,
|
||||
this.monthly,
|
||||
this.location,
|
||||
);
|
||||
|
||||
@JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice)
|
||||
final double hourly;
|
||||
|
@ -93,6 +107,8 @@ class HetznerPriceInfo {
|
|||
@JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice)
|
||||
final double monthly;
|
||||
|
||||
final String location;
|
||||
|
||||
static HetznerPriceInfo fromJson(final Map<String, dynamic> json) =>
|
||||
_$HetznerPriceInfoFromJson(json);
|
||||
|
||||
|
@ -102,7 +118,14 @@ class HetznerPriceInfo {
|
|||
|
||||
@JsonSerializable()
|
||||
class HetznerLocation {
|
||||
HetznerLocation(this.country, this.city, this.description, this.zone);
|
||||
HetznerLocation(
|
||||
this.country,
|
||||
this.city,
|
||||
this.description,
|
||||
this.zone,
|
||||
this.name,
|
||||
);
|
||||
final String name;
|
||||
final String country;
|
||||
final String city;
|
||||
final String description;
|
||||
|
@ -113,3 +136,24 @@ class HetznerLocation {
|
|||
static HetznerLocation fromJson(final Map<String, dynamic> 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);
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
|
|||
(json['prices'] as List<dynamic>)
|
||||
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
json['name'] as String,
|
||||
json['description'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HetznerServerTypeInfoToJson(
|
||||
|
@ -89,6 +91,8 @@ Map<String, dynamic> _$HetznerServerTypeInfoToJson(
|
|||
'cores': instance.cores,
|
||||
'memory': instance.memory,
|
||||
'disk': instance.disk,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'prices': instance.prices,
|
||||
};
|
||||
|
||||
|
@ -96,12 +100,14 @@ HetznerPriceInfo _$HetznerPriceInfoFromJson(Map<String, dynamic> json) =>
|
|||
HetznerPriceInfo(
|
||||
HetznerPriceInfo.getPrice(json['price_hourly'] as Map),
|
||||
HetznerPriceInfo.getPrice(json['price_monthly'] as Map),
|
||||
json['location'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HetznerPriceInfoToJson(HetznerPriceInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'price_hourly': instance.hourly,
|
||||
'price_monthly': instance.monthly,
|
||||
'location': instance.location,
|
||||
};
|
||||
|
||||
HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) =>
|
||||
|
@ -110,12 +116,32 @@ HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) =>
|
|||
json['city'] as String,
|
||||
json['description'] as String,
|
||||
json['network_zone'] as String,
|
||||
json['name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HetznerLocationToJson(HetznerLocation instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'country': instance.country,
|
||||
'city': instance.city,
|
||||
'description': instance.description,
|
||||
'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,
|
||||
};
|
||||
|
|
23
lib/logic/models/launch_installation_data.dart
Normal file
23
lib/logic/models/launch_installation_data.dart
Normal 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;
|
||||
}
|
|
@ -19,11 +19,11 @@ enum MetadataType {
|
|||
|
||||
class ServerMetadataEntity {
|
||||
ServerMetadataEntity({
|
||||
required this.name,
|
||||
required this.trId,
|
||||
required this.value,
|
||||
this.type = MetadataType.other,
|
||||
});
|
||||
final MetadataType type;
|
||||
final String name;
|
||||
final String trId;
|
||||
final String value;
|
||||
}
|
||||
|
|
356
lib/logic/providers/dns_providers/cloudflare.dart
Normal file
356
lib/logic/providers/dns_providers/cloudflare.dart
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,173 +1,113 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||
import 'package:selfprivacy/utils/network_utils.dart';
|
||||
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
|
||||
|
||||
class DesecApi extends DnsProviderApi {
|
||||
DesecApi({
|
||||
this.hasLogger = false,
|
||||
this.isWithToken = true,
|
||||
this.customToken,
|
||||
});
|
||||
@override
|
||||
final bool hasLogger;
|
||||
@override
|
||||
final bool isWithToken;
|
||||
class ApiAdapter {
|
||||
ApiAdapter({final bool isWithToken = true})
|
||||
: _api = DesecApi(
|
||||
isWithToken: isWithToken,
|
||||
);
|
||||
|
||||
final String? customToken;
|
||||
DesecApi api({final bool getInitialized = true}) => getInitialized
|
||||
? _api
|
||||
: DesecApi(
|
||||
isWithToken: false,
|
||||
);
|
||||
|
||||
final DesecApi _api;
|
||||
}
|
||||
|
||||
class DesecDnsProvider extends DnsProvider {
|
||||
DesecDnsProvider() : _adapter = ApiAdapter();
|
||||
DesecDnsProvider.load(
|
||||
final bool isAuthotized,
|
||||
) : _adapter = ApiAdapter(
|
||||
isWithToken: isAuthotized,
|
||||
);
|
||||
|
||||
ApiAdapter _adapter;
|
||||
|
||||
@override
|
||||
RegExp getApiTokenValidation() =>
|
||||
RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
|
||||
DnsProviderType get type => DnsProviderType.desec;
|
||||
|
||||
@override
|
||||
BaseOptions get options {
|
||||
final BaseOptions options = BaseOptions(
|
||||
baseUrl: rootAddress,
|
||||
contentType: Headers.jsonContentType,
|
||||
responseType: ResponseType.json,
|
||||
);
|
||||
if (isWithToken) {
|
||||
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
|
||||
assert(token != null);
|
||||
options.headers = {'Authorization': 'Token $token'};
|
||||
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;
|
||||
}
|
||||
|
||||
if (customToken != null) {
|
||||
options.headers = {'Authorization': 'Token $customToken'};
|
||||
}
|
||||
|
||||
if (validateStatus != null) {
|
||||
options.validateStatus = validateStatus!;
|
||||
}
|
||||
return options;
|
||||
_adapter = ApiAdapter(isWithToken: true);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
String rootAddress = 'https://desec.io/api/v1/domains/';
|
||||
|
||||
@override
|
||||
Future<APIGenericResult<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'},
|
||||
),
|
||||
Future<GenericResult<String?>> getZoneId(final String domain) async =>
|
||||
GenericResult(
|
||||
data: domain,
|
||||
success: true,
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
isValid = false;
|
||||
message = e.toString();
|
||||
} finally {
|
||||
close(client);
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
return APIGenericResult(
|
||||
data: isValid,
|
||||
success: false,
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode == HttpStatus.ok) {
|
||||
isValid = true;
|
||||
} else if (response.statusCode == HttpStatus.unauthorized) {
|
||||
isValid = false;
|
||||
} else {
|
||||
throw Exception('code: ${response.statusCode}');
|
||||
}
|
||||
|
||||
return APIGenericResult(
|
||||
data: isValid,
|
||||
success: true,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getZoneId(final String domain) async => domain;
|
||||
|
||||
@override
|
||||
Future<APIGenericResult<void>> removeSimilarRecords({
|
||||
Future<GenericResult<void>> removeDomainRecords({
|
||||
required final ServerDomain domain,
|
||||
final String? ip4,
|
||||
}) async {
|
||||
final String domainName = domain.domainName;
|
||||
final String url = '/$domainName/rrsets/';
|
||||
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
|
||||
final List<DnsRecord> listDnsRecords = projectDnsRecords(
|
||||
domain.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': [],
|
||||
},
|
||||
);
|
||||
}
|
||||
final List<dynamic> bulkRecords = [];
|
||||
for (final DnsRecord record in listDnsRecords) {
|
||||
bulkRecords.add(
|
||||
{
|
||||
'subname': 'selector._domainkey',
|
||||
'type': 'TXT',
|
||||
'ttl': 18000,
|
||||
'subname': record.name,
|
||||
'type': record.type,
|
||||
'ttl': record.ttl,
|
||||
'records': [],
|
||||
},
|
||||
);
|
||||
await client.put(url, data: bulkRecords);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
success: false,
|
||||
data: null,
|
||||
message: e.toString(),
|
||||
);
|
||||
} finally {
|
||||
close(client);
|
||||
}
|
||||
bulkRecords.add(
|
||||
{
|
||||
'subname': 'selector._domainkey',
|
||||
'type': 'TXT',
|
||||
'ttl': 18000,
|
||||
'records': [],
|
||||
},
|
||||
);
|
||||
|
||||
return APIGenericResult(success: true, data: null);
|
||||
return _adapter.api().updateRecords(
|
||||
domain: domain,
|
||||
records: bulkRecords,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DnsRecord>> getDnsRecords({
|
||||
Future<GenericResult<List<DnsRecord>>> getDnsRecords({
|
||||
required final ServerDomain domain,
|
||||
}) async {
|
||||
Response response;
|
||||
final String domainName = domain.domainName;
|
||||
final List<DnsRecord> allRecords = <DnsRecord>[];
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
final String url = '/$domainName/rrsets/';
|
||||
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
response = await client.get(url);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
final List records = response.data;
|
||||
|
||||
for (final record in records) {
|
||||
for (final record in result.data) {
|
||||
final String? content = (record['records'] is List<dynamic>)
|
||||
? record['records'][0]
|
||||
: record['records'];
|
||||
allRecords.add(
|
||||
records.add(
|
||||
DnsRecord(
|
||||
name: record['subname'],
|
||||
type: record['type'],
|
||||
|
@ -178,54 +118,14 @@ class DesecApi extends DnsProviderApi {
|
|||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
close(client);
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<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(
|
||||
return GenericResult(
|
||||
success: false,
|
||||
data: null,
|
||||
data: records,
|
||||
message: e.toString(),
|
||||
);
|
||||
} finally {
|
||||
close(client);
|
||||
}
|
||||
|
||||
return APIGenericResult(success: true, data: null);
|
||||
return GenericResult(success: true, data: records);
|
||||
}
|
||||
|
||||
List<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? content = record.content;
|
||||
if (record.type == 'TXT' && content != null && !content.startsWith('"')) {
|
||||
|
@ -285,60 +236,47 @@ class DesecApi extends DnsProviderApi {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> setDnsRecord(
|
||||
final DnsRecord record,
|
||||
final ServerDomain domain,
|
||||
) async {
|
||||
final String url = '/${domain.domainName}/rrsets/';
|
||||
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
await client.post(
|
||||
url,
|
||||
data: {
|
||||
'subname': record.name,
|
||||
'type': record.type,
|
||||
'ttl': record.ttl,
|
||||
'records': [extractContent(record)],
|
||||
},
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
close(client);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> domainList() async {
|
||||
Future<GenericResult<List<String>>> domainList() async {
|
||||
List<String> domains = [];
|
||||
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
final Response response = await client.get(
|
||||
'',
|
||||
final result = await _adapter.api().getDomains();
|
||||
if (result.data.isEmpty || !result.success) {
|
||||
return GenericResult(
|
||||
success: result.success,
|
||||
data: domains,
|
||||
code: result.code,
|
||||
message: result.message,
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
domains = response.data
|
||||
.map<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
|
||||
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
|
||||
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
|
||||
final ServerDomain domain,
|
||||
final String ip4,
|
||||
final String dkimPublicKey,
|
||||
) 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 = [];
|
||||
try {
|
||||
final List<DesiredDnsRecord> desiredRecords =
|
||||
|
@ -384,13 +322,13 @@ class DesecApi extends DnsProviderApi {
|
|||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: [],
|
||||
success: false,
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
return APIGenericResult(
|
||||
return GenericResult(
|
||||
data: foundRecords,
|
||||
success: true,
|
||||
);
|
359
lib/logic/providers/dns_providers/digital_ocean_dns.dart
Normal file
359
lib/logic/providers/dns_providers/digital_ocean_dns.dart
Normal 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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
37
lib/logic/providers/dns_providers/dns_provider.dart
Normal file
37
lib/logic/providers/dns_providers/dns_provider.dart
Normal 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,
|
||||
);
|
||||
}
|
28
lib/logic/providers/dns_providers/dns_provider_factory.dart
Normal file
28
lib/logic/providers/dns_providers/dns_provider_factory.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,20 @@
|
|||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
|
||||
class ServerProviderApiFactorySettings {
|
||||
ServerProviderApiFactorySettings({
|
||||
class ServerProviderSettings {
|
||||
ServerProviderSettings({
|
||||
required this.provider,
|
||||
this.location,
|
||||
});
|
||||
|
||||
final ServerProvider provider;
|
||||
final ServerProviderType provider;
|
||||
final String? location;
|
||||
}
|
||||
|
||||
class DnsProviderApiFactorySettings {
|
||||
DnsProviderApiFactorySettings({
|
||||
class DnsProviderSettings {
|
||||
DnsProviderSettings({
|
||||
required this.provider,
|
||||
});
|
||||
|
||||
final DnsProvider provider;
|
||||
final DnsProviderType provider;
|
||||
}
|
34
lib/logic/providers/providers_controller.dart
Normal file
34
lib/logic/providers/providers_controller.dart
Normal 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;
|
||||
}
|
812
lib/logic/providers/server_providers/digital_ocean.dart
Normal file
812
lib/logic/providers/server_providers/digital_ocean.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
807
lib/logic/providers/server_providers/hetzner.dart
Normal file
807
lib/logic/providers/server_providers/hetzner.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
57
lib/logic/providers/server_providers/server_provider.dart
Normal file
57
lib/logic/providers/server_providers/server_provider.dart
Normal 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);
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,7 +72,10 @@ class ServiceConsumptionTitle extends StatelessWidget {
|
|||
service.svgIcon,
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
|
|
@ -2,12 +2,12 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||
import 'package:selfprivacy/utils/network_utils.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DnsDetailsPage extends StatefulWidget {
|
||||
|
@ -111,9 +111,12 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
|
|||
heroIcon: BrandIcons.globe,
|
||||
heroTitle: 'domain.screen_title'.tr(),
|
||||
children: <Widget>[
|
||||
_getStateCard(dnsCubit.dnsState, () {
|
||||
context.read<DnsRecordsCubit>().fix();
|
||||
}),
|
||||
_getStateCard(
|
||||
dnsCubit.dnsState,
|
||||
() {
|
||||
context.read<DnsRecordsCubit>().fix();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
ListTile(
|
||||
title: Text(
|
||||
|
@ -152,7 +155,7 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
|
|||
dnsRecord.description.tr(),
|
||||
),
|
||||
subtitle: Text(
|
||||
dnsRecord.name,
|
||||
dnsRecord.displayName ?? dnsRecord.name,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
|
||||
|
@ -37,8 +37,19 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
|
|||
title: Text('developer_settings.use_staging_acme'.tr()),
|
||||
subtitle:
|
||||
Text('developer_settings.use_staging_acme_description'.tr()),
|
||||
value: StagingOptions.stagingAcme,
|
||||
onChanged: null,
|
||||
value: TlsOptions.stagingAcme,
|
||||
onChanged: (final bool value) => setState(
|
||||
() => TlsOptions.stagingAcme = value,
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('developer_settings.ignore_tls'.tr()),
|
||||
subtitle:
|
||||
Text('developer_settings.ignore_tls_description'.tr()),
|
||||
value: TlsOptions.verifyCertificate,
|
||||
onChanged: (final bool value) => setState(
|
||||
() => TlsOptions.verifyCertificate = value,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
|
@ -49,7 +49,8 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
paused ? Icons.play_arrow_outlined : Icons.pause_outlined),
|
||||
paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
|
||||
),
|
||||
onPressed: () => setState(() => paused = !paused),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -26,7 +26,7 @@ class _TextDetails extends StatelessWidget {
|
|||
...details.metadata.map(
|
||||
(final metadata) => ListTileOnSurfaceVariant(
|
||||
leadingIcon: metadata.type.icon,
|
||||
title: metadata.name,
|
||||
title: metadata.trId.tr(),
|
||||
subtitle: metadata.value,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -133,7 +133,10 @@ class ServerConsumptionListTile extends StatelessWidget {
|
|||
service.svgIcon,
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
rightSideText: service.storageUsage.used.toString(),
|
||||
percentage: service.storageUsage.used.byte / volume.sizeTotal.byte,
|
||||
|
|
|
@ -2,14 +2,15 @@ import 'package:cubit_form/cubit_form.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:selfprivacy/config/brand_theme.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
|
||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
|
||||
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
|
||||
import 'package:selfprivacy/utils/launch_url.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class DnsProviderPicker extends StatefulWidget {
|
||||
const DnsProviderPicker({
|
||||
|
@ -26,9 +27,9 @@ class DnsProviderPicker extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DnsProviderPickerState extends State<DnsProviderPicker> {
|
||||
DnsProvider selectedProvider = DnsProvider.unknown;
|
||||
DnsProviderType selectedProvider = DnsProviderType.unknown;
|
||||
|
||||
void setProvider(final DnsProvider provider) {
|
||||
void setProvider(final DnsProviderType provider) {
|
||||
setState(() {
|
||||
selectedProvider = provider;
|
||||
});
|
||||
|
@ -37,35 +38,36 @@ class _DnsProviderPickerState extends State<DnsProviderPicker> {
|
|||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
switch (selectedProvider) {
|
||||
case DnsProvider.unknown:
|
||||
case DnsProviderType.unknown:
|
||||
return ProviderSelectionPage(
|
||||
serverInstallationCubit: widget.serverInstallationCubit,
|
||||
callback: setProvider,
|
||||
);
|
||||
|
||||
case DnsProvider.cloudflare:
|
||||
case DnsProviderType.cloudflare:
|
||||
return ProviderInputDataPage(
|
||||
providerCubit: widget.formCubit,
|
||||
providerInfo: ProviderPageInfo(
|
||||
providerType: DnsProvider.cloudflare,
|
||||
providerInfo: const ProviderPageInfo(
|
||||
providerType: DnsProviderType.cloudflare,
|
||||
pathToHow: 'how_cloudflare',
|
||||
image: Image.asset(
|
||||
'assets/images/logos/cloudflare.svg',
|
||||
width: 150,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case DnsProvider.desec:
|
||||
case DnsProviderType.digitalOcean:
|
||||
return ProviderInputDataPage(
|
||||
providerCubit: widget.formCubit,
|
||||
providerInfo: ProviderPageInfo(
|
||||
providerType: DnsProvider.desec,
|
||||
providerInfo: const ProviderPageInfo(
|
||||
providerType: DnsProviderType.digitalOcean,
|
||||
pathToHow: 'how_digital_ocean_dns',
|
||||
),
|
||||
);
|
||||
|
||||
case DnsProviderType.desec:
|
||||
return ProviderInputDataPage(
|
||||
providerCubit: widget.formCubit,
|
||||
providerInfo: const ProviderPageInfo(
|
||||
providerType: DnsProviderType.desec,
|
||||
pathToHow: 'how_desec',
|
||||
image: Image.asset(
|
||||
'assets/images/logos/desec.svg',
|
||||
width: 150,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -76,12 +78,10 @@ class ProviderPageInfo {
|
|||
const ProviderPageInfo({
|
||||
required this.providerType,
|
||||
required this.pathToHow,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
final String pathToHow;
|
||||
final Image image;
|
||||
final DnsProvider providerType;
|
||||
final DnsProviderType providerType;
|
||||
}
|
||||
|
||||
class ProviderInputDataPage extends StatelessWidget {
|
||||
|
@ -99,7 +99,7 @@ class ProviderInputDataPage extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'initializing.cloudflare_api_token'.tr(),
|
||||
'initializing.connect_to_dns'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
@ -124,12 +124,22 @@ class ProviderInputDataPage extends StatelessWidget {
|
|||
const SizedBox(height: 10),
|
||||
BrandOutlinedButton(
|
||||
child: Text('initializing.how'.tr()),
|
||||
onPressed: () {
|
||||
context.read<SupportSystemCubit>().showArticle(
|
||||
article: providerInfo.pathToHow,
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
onPressed: () => showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (final BuildContext context) => Padding(
|
||||
padding: paddingH15V0,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
children: [
|
||||
BrandMarkdown(
|
||||
fileName: providerInfo.pathToHow,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -150,6 +160,8 @@ class ProviderSelectionPage extends StatelessWidget {
|
|||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
/// TODO: Remove obvious repetition
|
||||
children: [
|
||||
Text(
|
||||
'initializing.select_dns'.tr(),
|
||||
|
@ -202,13 +214,13 @@ class ProviderSelectionPage extends StatelessWidget {
|
|||
text: 'basis.select'.tr(),
|
||||
onPressed: () {
|
||||
serverInstallationCubit
|
||||
.setDnsProviderType(DnsProvider.desec);
|
||||
callback(DnsProvider.desec);
|
||||
.setDnsProviderType(DnsProviderType.desec);
|
||||
callback(DnsProviderType.desec);
|
||||
},
|
||||
),
|
||||
// Outlined button that will open website
|
||||
BrandOutlinedButton(
|
||||
onPressed: () => launchURL('https://desec.io/'),
|
||||
onPressed: () => launchUrlString('https://desec.io/'),
|
||||
title: 'initializing.select_provider_site_button'.tr(),
|
||||
),
|
||||
],
|
||||
|
@ -257,14 +269,70 @@ class ProviderSelectionPage extends StatelessWidget {
|
|||
text: 'basis.select'.tr(),
|
||||
onPressed: () {
|
||||
serverInstallationCubit
|
||||
.setDnsProviderType(DnsProvider.cloudflare);
|
||||
callback(DnsProvider.cloudflare);
|
||||
.setDnsProviderType(DnsProviderType.cloudflare);
|
||||
callback(DnsProviderType.cloudflare);
|
||||
},
|
||||
),
|
||||
// Outlined button that will open website
|
||||
BrandOutlinedButton(
|
||||
onPressed: () =>
|
||||
launchURL('https://dash.cloudflare.com/'),
|
||||
launchUrlString('https://dash.cloudflare.com/'),
|
||||
title: 'initializing.select_provider_site_button'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
OutlinedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: const Color.fromARGB(255, 1, 126, 251),
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/logos/digital_ocean.svg',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Digital Ocean',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'initializing.select_provider_price_title'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
Text(
|
||||
'initializing.select_provider_price_free'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BrandButton.rised(
|
||||
text: 'basis.select'.tr(),
|
||||
onPressed: () {
|
||||
serverInstallationCubit
|
||||
.setDnsProviderType(DnsProviderType.digitalOcean);
|
||||
callback(DnsProviderType.digitalOcean);
|
||||
},
|
||||
),
|
||||
// Outlined button that will open website
|
||||
BrandOutlinedButton(
|
||||
onPressed: () =>
|
||||
launchUrlString('https://cloud.digitalocean.com/'),
|
||||
title: 'initializing.select_provider_site_button'.tr(),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
|
||||
|
@ -211,10 +211,11 @@ class InitializingPage extends StatelessWidget {
|
|||
final ServerInstallationCubit serverInstallationCubit,
|
||||
) =>
|
||||
BlocProvider(
|
||||
create: (final context) => ProviderFormCubit(serverInstallationCubit),
|
||||
create: (final context) =>
|
||||
ServerProviderFormCubit(serverInstallationCubit),
|
||||
child: Builder(
|
||||
builder: (final context) {
|
||||
final providerCubit = context.watch<ProviderFormCubit>();
|
||||
final providerCubit = context.watch<ServerProviderFormCubit>();
|
||||
return ServerProviderPicker(
|
||||
formCubit: providerCubit,
|
||||
serverInstallationCubit: serverInstallationCubit,
|
||||
|
@ -227,7 +228,8 @@ class InitializingPage extends StatelessWidget {
|
|||
final ServerInstallationCubit serverInstallationCubit,
|
||||
) =>
|
||||
BlocProvider(
|
||||
create: (final context) => ProviderFormCubit(serverInstallationCubit),
|
||||
create: (final context) =>
|
||||
ServerProviderFormCubit(serverInstallationCubit),
|
||||
child: Builder(
|
||||
builder: (final context) => ServerTypePicker(
|
||||
serverInstallationCubit: serverInstallationCubit,
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||
|
@ -20,7 +20,7 @@ class ServerProviderPicker extends StatefulWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
final ProviderFormCubit formCubit;
|
||||
final ServerProviderFormCubit formCubit;
|
||||
final ServerInstallationCubit serverInstallationCubit;
|
||||
|
||||
@override
|
||||
|
@ -28,9 +28,9 @@ class ServerProviderPicker extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ServerProviderPickerState extends State<ServerProviderPicker> {
|
||||
ServerProvider selectedProvider = ServerProvider.unknown;
|
||||
ServerProviderType selectedProvider = ServerProviderType.unknown;
|
||||
|
||||
void setProvider(final ServerProvider provider) {
|
||||
void setProvider(final ServerProviderType provider) {
|
||||
setState(() {
|
||||
selectedProvider = provider;
|
||||
});
|
||||
|
@ -39,17 +39,17 @@ class _ServerProviderPickerState extends State<ServerProviderPicker> {
|
|||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
switch (selectedProvider) {
|
||||
case ServerProvider.unknown:
|
||||
case ServerProviderType.unknown:
|
||||
return ProviderSelectionPage(
|
||||
serverInstallationCubit: widget.serverInstallationCubit,
|
||||
callback: setProvider,
|
||||
);
|
||||
|
||||
case ServerProvider.hetzner:
|
||||
case ServerProviderType.hetzner:
|
||||
return ProviderInputDataPage(
|
||||
providerCubit: widget.formCubit,
|
||||
providerInfo: ProviderPageInfo(
|
||||
providerType: ServerProvider.hetzner,
|
||||
providerType: ServerProviderType.hetzner,
|
||||
pathToHow: 'how_hetzner',
|
||||
image: Image.asset(
|
||||
'assets/images/logos/hetzner.png',
|
||||
|
@ -58,11 +58,11 @@ class _ServerProviderPickerState extends State<ServerProviderPicker> {
|
|||
),
|
||||
);
|
||||
|
||||
case ServerProvider.digitalOcean:
|
||||
case ServerProviderType.digitalOcean:
|
||||
return ProviderInputDataPage(
|
||||
providerCubit: widget.formCubit,
|
||||
providerInfo: ProviderPageInfo(
|
||||
providerType: ServerProvider.digitalOcean,
|
||||
providerType: ServerProviderType.digitalOcean,
|
||||
pathToHow: 'how_digital_ocean',
|
||||
image: Image.asset(
|
||||
'assets/images/logos/digital_ocean.png',
|
||||
|
@ -83,7 +83,7 @@ class ProviderPageInfo {
|
|||
|
||||
final String pathToHow;
|
||||
final Image image;
|
||||
final ServerProvider providerType;
|
||||
final ServerProviderType providerType;
|
||||
}
|
||||
|
||||
class ProviderInputDataPage extends StatelessWidget {
|
||||
|
@ -94,7 +94,7 @@ class ProviderInputDataPage extends StatelessWidget {
|
|||
});
|
||||
|
||||
final ProviderPageInfo providerInfo;
|
||||
final ProviderFormCubit providerCubit;
|
||||
final ServerProviderFormCubit providerCubit;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => ResponsiveLayoutWithInfobox(
|
||||
|
@ -238,9 +238,10 @@ class ProviderSelectionPage extends StatelessWidget {
|
|||
BrandButton.filled(
|
||||
child: Text('basis.select'.tr()),
|
||||
onPressed: () {
|
||||
serverInstallationCubit
|
||||
.setServerProviderType(ServerProvider.hetzner);
|
||||
callback(ServerProvider.hetzner);
|
||||
serverInstallationCubit.setServerProviderType(
|
||||
ServerProviderType.hetzner,
|
||||
);
|
||||
callback(ServerProviderType.hetzner);
|
||||
},
|
||||
),
|
||||
// Outlined button that will open website
|
||||
|
@ -313,9 +314,9 @@ class ProviderSelectionPage extends StatelessWidget {
|
|||
child: Text('basis.select'.tr()),
|
||||
onPressed: () {
|
||||
serverInstallationCubit.setServerProviderType(
|
||||
ServerProvider.digitalOcean,
|
||||
ServerProviderType.digitalOcean,
|
||||
);
|
||||
callback(ServerProvider.digitalOcean);
|
||||
callback(ServerProviderType.digitalOcean);
|
||||
},
|
||||
),
|
||||
// Outlined button that will open website
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
|
||||
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
|
@ -16,52 +16,48 @@ class RecoveryServerProviderConnected extends StatelessWidget {
|
|||
context.watch<ServerInstallationCubit>();
|
||||
|
||||
return BlocProvider(
|
||||
create: (final BuildContext context) => ProviderFormCubit(appConfig),
|
||||
create: (final BuildContext context) =>
|
||||
ServerProviderFormCubit(appConfig),
|
||||
child: Builder(
|
||||
builder: (final BuildContext context) {
|
||||
final FormCubitState formCubitState =
|
||||
context.watch<ProviderFormCubit>().state;
|
||||
|
||||
return BrandHeroScreen(
|
||||
heroTitle: 'recovering.server_provider_connected'.tr(),
|
||||
heroSubtitle: 'recovering.server_provider_connected_description'.tr(
|
||||
args: [appConfig.state.serverDomain?.domainName ?? 'your domain'],
|
||||
builder: (final BuildContext context) => BrandHeroScreen(
|
||||
heroTitle: 'recovering.server_provider_connected'.tr(),
|
||||
heroSubtitle: 'recovering.server_provider_connected_description'.tr(
|
||||
args: [appConfig.state.serverDomain?.domainName ?? 'your domain'],
|
||||
),
|
||||
hasBackButton: true,
|
||||
hasFlashButton: false,
|
||||
ignoreBreakpoints: true,
|
||||
hasSupportDrawer: true,
|
||||
onBackButtonPressed: () {
|
||||
Navigator.of(context).popUntil((final route) => route.isFirst);
|
||||
},
|
||||
children: [
|
||||
CubitFormTextField(
|
||||
formFieldCubit: context.read<ServerProviderFormCubit>().apiKey,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText:
|
||||
'recovering.server_provider_connected_placeholder'.tr(),
|
||||
),
|
||||
),
|
||||
hasBackButton: true,
|
||||
hasFlashButton: false,
|
||||
ignoreBreakpoints: true,
|
||||
hasSupportDrawer: true,
|
||||
onBackButtonPressed: () {
|
||||
Navigator.of(context).popUntil((final route) => route.isFirst);
|
||||
},
|
||||
children: [
|
||||
CubitFormTextField(
|
||||
formFieldCubit: context.read<ProviderFormCubit>().apiKey,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText:
|
||||
'recovering.server_provider_connected_placeholder'.tr(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BrandButton.filled(
|
||||
onPressed: () =>
|
||||
context.read<ServerProviderFormCubit>().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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,52 +15,16 @@ abstract class _$RootRouter extends RootStackRouter {
|
|||
|
||||
@override
|
||||
final Map<String, PageFactory> pagesMap = {
|
||||
AppSettingsRoute.name: (routeData) {
|
||||
BackupDetailsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const AppSettingsPage(),
|
||||
child: const BackupDetailsPage(),
|
||||
);
|
||||
},
|
||||
DeveloperSettingsRoute.name: (routeData) {
|
||||
RootRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const DeveloperSettingsPage(),
|
||||
);
|
||||
},
|
||||
ConsoleRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const ConsolePage(),
|
||||
);
|
||||
},
|
||||
MoreRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const MorePage(),
|
||||
);
|
||||
},
|
||||
AboutApplicationRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const AboutApplicationPage(),
|
||||
);
|
||||
},
|
||||
OnboardingRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const OnboardingPage(),
|
||||
);
|
||||
},
|
||||
ProvidersRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const ProvidersPage(),
|
||||
);
|
||||
},
|
||||
ServerDetailsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const ServerDetailsScreen(),
|
||||
child: WrappedRoute(child: const RootPage()),
|
||||
);
|
||||
},
|
||||
ServiceRoute.name: (routeData) {
|
||||
|
@ -79,6 +43,12 @@ abstract class _$RootRouter extends RootStackRouter {
|
|||
child: const ServicesPage(),
|
||||
);
|
||||
},
|
||||
ServerDetailsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const ServerDetailsScreen(),
|
||||
);
|
||||
},
|
||||
UsersRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
|
@ -101,10 +71,46 @@ abstract class _$RootRouter extends RootStackRouter {
|
|||
),
|
||||
);
|
||||
},
|
||||
BackupDetailsRoute.name: (routeData) {
|
||||
AppSettingsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const BackupDetailsPage(),
|
||||
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) {
|
||||
|
@ -125,26 +131,12 @@ abstract class _$RootRouter extends RootStackRouter {
|
|||
child: const InitializingPage(),
|
||||
);
|
||||
},
|
||||
RecoveryKeyRoute.name: (routeData) {
|
||||
ServerStorageRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<ServerStorageRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const RecoveryKeyPage(),
|
||||
);
|
||||
},
|
||||
DevicesRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const DevicesScreen(),
|
||||
);
|
||||
},
|
||||
ServicesMigrationRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: ServicesMigrationPage(
|
||||
services: args.services,
|
||||
child: ServerStoragePage(
|
||||
diskStatus: args.diskStatus,
|
||||
isMigration: args.isMigration,
|
||||
key: args.key,
|
||||
),
|
||||
);
|
||||
|
@ -160,133 +152,57 @@ abstract class _$RootRouter extends RootStackRouter {
|
|||
),
|
||||
);
|
||||
},
|
||||
ServerStorageRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<ServerStorageRouteArgs>();
|
||||
ServicesMigrationRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: ServerStoragePage(
|
||||
child: ServicesMigrationPage(
|
||||
services: args.services,
|
||||
diskStatus: args.diskStatus,
|
||||
isMigration: args.isMigration,
|
||||
key: args.key,
|
||||
),
|
||||
);
|
||||
},
|
||||
RootRoute.name: (routeData) {
|
||||
DevicesRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: WrappedRoute(child: const RootPage()),
|
||||
child: const DevicesScreen(),
|
||||
);
|
||||
},
|
||||
OnboardingRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const OnboardingPage(),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AppSettingsPage]
|
||||
class AppSettingsRoute extends PageRouteInfo<void> {
|
||||
const AppSettingsRoute({List<PageRouteInfo>? children})
|
||||
/// [BackupDetailsPage]
|
||||
class BackupDetailsRoute extends PageRouteInfo<void> {
|
||||
const BackupDetailsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
AppSettingsRoute.name,
|
||||
BackupDetailsRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AppSettingsRoute';
|
||||
static const String name = 'BackupDetailsRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [DeveloperSettingsPage]
|
||||
class DeveloperSettingsRoute extends PageRouteInfo<void> {
|
||||
const DeveloperSettingsRoute({List<PageRouteInfo>? children})
|
||||
/// [RootPage]
|
||||
class RootRoute extends PageRouteInfo<void> {
|
||||
const RootRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
DeveloperSettingsRoute.name,
|
||||
RootRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'DeveloperSettingsRoute';
|
||||
|
||||
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
|
||||
/// [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
|
||||
/// [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
|
||||
/// [ServerDetailsScreen]
|
||||
class ServerDetailsRoute extends PageRouteInfo<void> {
|
||||
const ServerDetailsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
ServerDetailsRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'ServerDetailsRoute';
|
||||
static const String name = 'RootRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
@ -343,6 +259,20 @@ class ServicesRoute extends PageRouteInfo<void> {
|
|||
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
|
||||
/// [UsersPage]
|
||||
class UsersRoute extends PageRouteInfo<void> {
|
||||
|
@ -410,15 +340,99 @@ class UserDetailsRouteArgs {
|
|||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BackupDetailsPage]
|
||||
class BackupDetailsRoute extends PageRouteInfo<void> {
|
||||
const BackupDetailsRoute({List<PageRouteInfo>? children})
|
||||
/// [AppSettingsPage]
|
||||
class AppSettingsRoute extends PageRouteInfo<void> {
|
||||
const AppSettingsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
BackupDetailsRoute.name,
|
||||
AppSettingsRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'BackupDetailsRoute';
|
||||
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);
|
||||
}
|
||||
|
@ -466,31 +480,84 @@ class InitializingRoute extends PageRouteInfo<void> {
|
|||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RecoveryKeyPage]
|
||||
class RecoveryKeyRoute extends PageRouteInfo<void> {
|
||||
const RecoveryKeyRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
RecoveryKeyRoute.name,
|
||||
/// [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 = 'RecoveryKeyRoute';
|
||||
static const String name = 'ServerStorageRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
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
|
||||
/// [DevicesScreen]
|
||||
class DevicesRoute extends PageRouteInfo<void> {
|
||||
const DevicesRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
DevicesRoute.name,
|
||||
/// [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 = 'DevicesRoute';
|
||||
static const String name = 'ExtendingVolumeRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
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
|
||||
|
@ -542,96 +609,29 @@ class ServicesMigrationRouteArgs {
|
|||
}
|
||||
|
||||
/// 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
|
||||
/// [RootPage]
|
||||
class RootRoute extends PageRouteInfo<void> {
|
||||
const RootRoute({List<PageRouteInfo>? children})
|
||||
/// [DevicesScreen]
|
||||
class DevicesRoute extends PageRouteInfo<void> {
|
||||
const DevicesRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
RootRoute.name,
|
||||
DevicesRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'RootRoute';
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,45 +1,5 @@
|
|||
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||
|
||||
enum DnsRecordsCategory {
|
||||
services,
|
||||
email,
|
||||
other,
|
||||
}
|
||||
|
||||
class DesiredDnsRecord {
|
||||
const DesiredDnsRecord({
|
||||
required this.name,
|
||||
required this.content,
|
||||
this.type = 'A',
|
||||
this.description = '',
|
||||
this.category = DnsRecordsCategory.services,
|
||||
this.isSatisfied = false,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String type;
|
||||
final String content;
|
||||
final String description;
|
||||
final DnsRecordsCategory category;
|
||||
final bool isSatisfied;
|
||||
|
||||
DesiredDnsRecord copyWith({
|
||||
final String? name,
|
||||
final String? type,
|
||||
final String? content,
|
||||
final String? description,
|
||||
final DnsRecordsCategory? category,
|
||||
final bool? isSatisfied,
|
||||
}) =>
|
||||
DesiredDnsRecord(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
content: content ?? this.content,
|
||||
description: description ?? this.description,
|
||||
category: category ?? this.category,
|
||||
isSatisfied: isSatisfied ?? this.isSatisfied,
|
||||
);
|
||||
}
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
DnsRecord? extractDkimRecord(final List<DnsRecord> records) {
|
||||
DnsRecord? dkimRecord;
|
||||
|
@ -69,3 +29,15 @@ String getHostnameFromDomain(final String domain) {
|
|||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
void launchURL(final url) async {
|
||||
try {
|
||||
final Uri uri = Uri.parse(url);
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue