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:
NaiJi ✨ 2023-06-19 23:03:55 +03:00
commit d0366862c0
99 changed files with 5779 additions and 3885 deletions

View file

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

Before

Width:  |  Height:  |  Size: 922 B

After

Width:  |  Height:  |  Size: 923 B

View file

@ -279,6 +279,8 @@
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon."
},
"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"
}
}
}

View file

@ -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": "Длина строки [], должна быть меньше либо равна {}"
}
}
}

View file

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

View file

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

View file

@ -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,

View file

@ -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({

View file

@ -76,7 +76,8 @@ type DeviceApiTokenMutationReturn implements MutationReturnInterface {
enum DnsProvider {
CLOUDFLARE,
DESEC
DESEC,
DIGITALOCEAN
}
type DnsRecord {

View file

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

View file

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

View file

@ -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({

View file

@ -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({

View file

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

View file

@ -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({

View file

@ -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,

View file

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

View file

@ -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(),

View file

@ -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(),

View file

@ -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,

View file

@ -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(),

View file

@ -1,44 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
class ApiController {
static VolumeProviderApiFactory? get currentVolumeProviderApiFactory =>
_volumeProviderApiFactory;
static DnsProviderApiFactory? get currentDnsProviderApiFactory =>
_dnsProviderApiFactory;
static ServerProviderApiFactory? get currentServerProviderApiFactory =>
_serverProviderApiFactory;
static void initVolumeProviderApiFactory(
final ServerProviderApiFactorySettings settings,
) {
_volumeProviderApiFactory =
VolumeApiFactoryCreator.createVolumeProviderApiFactory(settings);
}
static void initDnsProviderApiFactory(
final DnsProviderApiFactorySettings settings,
) {
_dnsProviderApiFactory =
ApiFactoryCreator.createDnsProviderApiFactory(settings);
}
static void initServerProviderApiFactory(
final ServerProviderApiFactorySettings settings,
) {
_serverProviderApiFactory =
ApiFactoryCreator.createServerProviderApiFactory(settings);
}
static void clearProviderApiFactories() {
_volumeProviderApiFactory = null;
_dnsProviderApiFactory = null;
_serverProviderApiFactory = null;
}
static VolumeProviderApiFactory? _volumeProviderApiFactory;
static DnsProviderApiFactory? _dnsProviderApiFactory;
static ServerProviderApiFactory? _serverProviderApiFactory;
}

View file

@ -1,57 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
class UnknownApiProviderException implements Exception {
UnknownApiProviderException(this.message);
final String message;
}
class ApiFactoryCreator {
static ServerProviderApiFactory createServerProviderApiFactory(
final ServerProviderApiFactorySettings settings,
) {
switch (settings.provider) {
case ServerProvider.hetzner:
return HetznerApiFactory(region: settings.location);
case ServerProvider.digitalOcean:
return DigitalOceanApiFactory(region: settings.location);
case ServerProvider.unknown:
throw UnknownApiProviderException('Unknown server provider');
}
}
static DnsProviderApiFactory createDnsProviderApiFactory(
final DnsProviderApiFactorySettings settings,
) {
switch (settings.provider) {
case DnsProvider.desec:
return DesecApiFactory();
case DnsProvider.cloudflare:
return CloudflareApiFactory();
case DnsProvider.unknown:
throw UnknownApiProviderException('Unknown DNS provider');
}
}
}
class VolumeApiFactoryCreator {
static VolumeProviderApiFactory createVolumeProviderApiFactory(
final ServerProviderApiFactorySettings settings,
) {
switch (settings.provider) {
case ServerProvider.hetzner:
return HetznerApiFactory();
case ServerProvider.digitalOcean:
return DigitalOceanApiFactory();
case ServerProvider.unknown:
throw UnknownApiProviderException('Unknown volume provider');
}
}
}

View file

@ -2,11 +2,11 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package: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,
);

View file

@ -1,468 +0,0 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class CloudflareApi extends DnsProviderApi {
CloudflareApi({
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
@override
final bool hasLogger;
@override
final bool isWithToken;
final String? customToken;
@override
RegExp getApiTokenValidation() =>
RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (customToken != null) {
options.headers = {'Authorization': 'Bearer $customToken'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String rootAddress = 'https://api.cloudflare.com/client/v4';
@override
Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/user/tokens/verify',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return APIGenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return APIGenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
@override
Future<String?> getZoneId(final String domain) async {
String? zoneId;
final Dio client = await getClient();
try {
final Response response = await client.get(
'/zones',
queryParameters: {'name': domain},
);
zoneId = response.data['result'][0]['id'];
} catch (e) {
print(e);
} finally {
close(client);
}
return zoneId;
}
@override
Future<APIGenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final String url = '/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
final Response response = await client.get(url);
final List records = response.data['result'] ?? [];
final List<Future> allDeleteFutures = <Future>[];
for (final record in records) {
if (record['zone_name'] == domainName) {
allDeleteFutures.add(
client.delete('$url/${record["id"]}'),
);
}
}
await Future.wait(allDeleteFutures);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(success: true, data: null);
}
@override
Future<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response response;
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List<DnsRecord> allRecords = <DnsRecord>[];
final String url = '/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
response = await client.get(url);
final List records = response.data['result'] ?? [];
for (final record in records) {
if (record['zone_name'] == domainName) {
allRecords.add(
DnsRecord(
name: record['name'],
type: record['type'],
content: record['content'],
ttl: record['ttl'],
proxied: record['proxied'],
),
);
}
}
} catch (e) {
print(e);
} finally {
close(client);
}
return allRecords;
}
@override
Future<APIGenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final List<Future> allCreateFutures = <Future>[];
final Dio client = await getClient();
try {
for (final DnsRecord record in listDnsRecords) {
allCreateFutures.add(
client.post(
'/zones/$domainZoneId/dns_records',
data: record.toJson(),
),
);
}
await Future.wait(allCreateFutures);
} on DioError catch (e) {
print(e.message);
rethrow;
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(success: true, data: null);
}
List<DnsRecord> projectDnsRecords(
final String? domainName,
final String? ip4,
) {
final DnsRecord domainA =
DnsRecord(type: 'A', name: domainName, content: ip4);
final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName);
final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4);
final DnsRecord passwordA =
DnsRecord(type: 'A', name: 'password', content: ip4);
final DnsRecord socialA =
DnsRecord(type: 'A', name: 'social', content: ip4);
final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4);
final DnsRecord txt1 = DnsRecord(
type: 'TXT',
name: '_dmarc',
content: 'v=DMARC1; p=none',
ttl: 18000,
);
final DnsRecord txt2 = DnsRecord(
type: 'TXT',
name: domainName,
content: 'v=spf1 a mx ip4:$ip4 -all',
ttl: 18000,
);
return <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
@override
Future<void> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async {
final String domainZoneId = domain.zoneId;
final String url = '$rootAddress/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
await client.post(
url,
data: record.toJson(),
);
} catch (e) {
print(e);
} finally {
close(client);
}
}
@override
Future<List<String>> domainList() async {
final String url = '$rootAddress/zones';
List<String> domains = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
url,
queryParameters: {'per_page': 50},
);
domains = response.data['result']
.map<String>((final el) => el['name'] as String)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
}
return domains;
}
@override
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final List<DnsRecord> records = await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> desiredRecords =
getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey);
for (final DesiredDnsRecord record in desiredRecords) {
if (record.description == 'record.dkim') {
final DnsRecord foundRecord = records.firstWhere(
(final r) => (r.name == record.name) && r.type == record.type,
orElse: () => DnsRecord(
name: record.name,
type: record.type,
content: '',
ttl: 800,
proxied: false,
),
);
// remove all spaces and tabulators from
// the foundRecord.content and the record.content
// to compare them
final String? foundContent =
foundRecord.content?.replaceAll(RegExp(r'\s+'), '');
final String content = record.content.replaceAll(RegExp(r'\s+'), '');
if (foundContent == content) {
foundRecords.add(record.copyWith(isSatisfied: true));
} else {
foundRecords.add(record.copyWith(isSatisfied: false));
}
} else {
if (records.any(
(final r) =>
(r.name == record.name) &&
r.type == record.type &&
r.content == record.content,
)) {
foundRecords.add(record.copyWith(isSatisfied: true));
} else {
foundRecords.add(record.copyWith(isSatisfied: false));
}
}
}
} catch (e) {
print(e);
return APIGenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return APIGenericResult(
data: foundRecords,
success: true,
);
}
@override
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
) {
if (domainName == null || ip4 == null) {
return [];
}
return [
DesiredDnsRecord(
name: domainName,
content: ip4,
description: 'record.root',
),
DesiredDnsRecord(
name: 'api.$domainName',
content: ip4,
description: 'record.api',
),
DesiredDnsRecord(
name: 'cloud.$domainName',
content: ip4,
description: 'record.cloud',
),
DesiredDnsRecord(
name: 'git.$domainName',
content: ip4,
description: 'record.git',
),
DesiredDnsRecord(
name: 'meet.$domainName',
content: ip4,
description: 'record.meet',
),
DesiredDnsRecord(
name: 'social.$domainName',
content: ip4,
description: 'record.social',
),
DesiredDnsRecord(
name: 'password.$domainName',
content: ip4,
description: 'record.password',
),
DesiredDnsRecord(
name: 'vpn.$domainName',
content: ip4,
description: 'record.vpn',
),
DesiredDnsRecord(
name: domainName,
content: domainName,
description: 'record.mx',
type: 'MX',
category: DnsRecordsCategory.email,
),
DesiredDnsRecord(
name: '_dmarc.$domainName',
content: 'v=DMARC1; p=none',
description: 'record.dmarc',
type: 'TXT',
category: DnsRecordsCategory.email,
),
DesiredDnsRecord(
name: domainName,
content: 'v=spf1 a mx ip4:$ip4 -all',
description: 'record.spf',
type: 'TXT',
category: DnsRecordsCategory.email,
),
if (dkimPublicKey != null)
DesiredDnsRecord(
name: 'selector._domainkey.$domainName',
content: dkimPublicKey,
description: 'record.dkim',
type: 'TXT',
category: DnsRecordsCategory.email,
),
];
}
}

View file

@ -0,0 +1,252 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
class CloudflareApi extends RestApiMap {
CloudflareApi({
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
@override
final bool hasLogger;
@override
final bool isWithToken;
final String? customToken;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (customToken != null) {
options.headers = {'Authorization': 'Bearer $customToken'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String rootAddress = 'https://api.cloudflare.com/client/v4';
Future<GenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/user/tokens/verify',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return GenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return GenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
Future<GenericResult<List<dynamic>>> getZones(final String domain) async {
List zones = [];
late final Response? response;
final Dio client = await getClient();
try {
response = await client.get(
'/zones',
queryParameters: {'name': domain},
);
zones = response.data['result'];
} catch (e) {
print(e);
GenericResult(
success: false,
data: zones,
code: response?.statusCode,
message: response?.statusMessage,
);
} finally {
close(client);
}
return GenericResult(success: true, data: zones);
}
Future<GenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
required final List records,
}) async {
final String domainZoneId = domain.zoneId;
final String url = '/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
final List<Future> allDeleteFutures = <Future>[];
for (final record in records) {
allDeleteFutures.add(
client.delete('$url/${record["id"]}'),
);
}
await Future.wait(allDeleteFutures);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response response;
final String domainName = domain.domainName;
final String domainZoneId = domain.zoneId;
final List allRecords = [];
final String url = '/zones/$domainZoneId/dns_records';
final Dio client = await getClient();
try {
response = await client.get(url);
final List records = response.data['result'] ?? [];
for (final record in records) {
if (record['zone_name'] == domainName) {
allRecords.add(record);
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: allRecords, success: true);
}
Future<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
required final List<DnsRecord> records,
}) async {
final String domainZoneId = domain.zoneId;
final List<Future> allCreateFutures = <Future>[];
final Dio client = await getClient();
try {
for (final DnsRecord record in records) {
allCreateFutures.add(
client.post(
'/zones/$domainZoneId/dns_records',
data: record.toJson(),
),
);
}
await Future.wait(allCreateFutures);
} on DioError catch (e) {
print(e.message);
rethrow;
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> getDomains() async {
final String url = '$rootAddress/zones';
List domains = [];
late final Response? response;
final Dio client = await getClient();
try {
response = await client.get(
url,
queryParameters: {'per_page': 50},
);
domains = response.data['result'];
} catch (e) {
print(e);
return GenericResult(
success: false,
data: domains,
code: response?.statusCode,
message: response?.statusMessage,
);
} finally {
close(client);
}
return GenericResult(
success: true,
data: domains,
code: response.statusCode,
message: response.statusMessage,
);
}
}

View file

@ -1,16 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
class CloudflareApiFactory extends DnsProviderApiFactory {
@override
DnsProviderApi getDnsProvider({
final DnsProviderApiSettings settings = const DnsProviderApiSettings(),
}) =>
CloudflareApi(
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
customToken: settings.customToken,
);
}

View file

@ -0,0 +1,204 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
class DesecApi extends RestApiMap {
DesecApi({
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
@override
final bool hasLogger;
@override
final bool isWithToken;
final String? customToken;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Token $token'};
}
if (customToken != null) {
options.headers = {'Authorization': 'Token $customToken'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String rootAddress = 'https://desec.io/api/v1/domains/';
Future<GenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Token $token'},
),
);
await Future.delayed(const Duration(seconds: 1));
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return GenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return GenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
Future<GenericResult<void>> updateRecords({
required final ServerDomain domain,
required final List<dynamic> records,
}) async {
final String domainName = domain.domainName;
final String url = '/$domainName/rrsets/';
final Dio client = await getClient();
try {
await client.put(url, data: records);
await Future.delayed(const Duration(seconds: 1));
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List<dynamic>>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response? response;
final String domainName = domain.domainName;
List allRecords = [];
final String url = '/$domainName/rrsets/';
final Dio client = await getClient();
try {
response = await client.get(url);
await Future.delayed(const Duration(seconds: 1));
allRecords = response.data;
} catch (e) {
print(e);
return GenericResult(
data: allRecords,
success: false,
message: e.toString(),
code: response?.statusCode,
);
} finally {
close(client);
}
return GenericResult(data: allRecords, success: true);
}
Future<GenericResult<void>> createRecords({
required final ServerDomain domain,
required final List<dynamic> records,
}) async {
final String domainName = domain.domainName;
final String url = '/$domainName/rrsets/';
final Dio client = await getClient();
try {
await client.post(url, data: records);
await Future.delayed(const Duration(seconds: 1));
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> getDomains() async {
List domains = [];
late final Response? response;
final Dio client = await getClient();
try {
response = await client.get(
'',
);
await Future.delayed(const Duration(seconds: 1));
domains = response.data;
} catch (e) {
print(e);
return GenericResult(
success: false,
data: domains,
code: response?.statusCode,
message: response?.statusMessage,
);
} finally {
close(client);
}
return GenericResult(
success: true,
data: domains,
code: response.statusCode,
message: response.statusMessage,
);
}
}

View file

@ -1,16 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
class DesecApiFactory extends DnsProviderApiFactory {
@override
DnsProviderApi getDnsProvider({
final DnsProviderApiSettings settings = const DnsProviderApiSettings(),
}) =>
DesecApi(
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
customToken: settings.customToken,
);
}

View file

@ -0,0 +1,44 @@
enum DnsRecordsCategory {
services,
email,
other,
}
class DesiredDnsRecord {
const DesiredDnsRecord({
required this.name,
required this.content,
this.type = 'A',
this.description = '',
this.category = DnsRecordsCategory.services,
this.isSatisfied = false,
this.displayName,
});
final String name;
final String type;
final String content;
final String description;
final String? displayName;
final DnsRecordsCategory category;
final bool isSatisfied;
DesiredDnsRecord copyWith({
final String? name,
final String? type,
final String? content,
final String? description,
final String? displayName,
final DnsRecordsCategory? category,
final bool? isSatisfied,
}) =>
DesiredDnsRecord(
name: name ?? this.name,
type: type ?? this.type,
content: content ?? this.content,
description: description ?? this.description,
category: category ?? this.category,
isSatisfied: isSatisfied ?? this.isSatisfied,
displayName: displayName ?? this.displayName,
);
}

View file

@ -0,0 +1,217 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
class DigitalOceanDnsApi extends RestApiMap {
DigitalOceanDnsApi({
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
@override
final bool hasLogger;
@override
final bool isWithToken;
final String? customToken;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().dnsProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (customToken != null) {
options.headers = {'Authorization': 'Bearer $customToken'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String rootAddress = 'https://api.digitalocean.com/v2';
Future<GenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/account',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return GenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return GenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
Future<GenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
required final List records,
}) async {
final String domainName = domain.domainName;
final Dio client = await getClient();
try {
final List<Future> allDeleteFutures = [];
for (final record in records) {
allDeleteFutures.add(
client.delete("/domains/$domainName/records/${record['id']}"),
);
}
await Future.wait(allDeleteFutures);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> getDnsRecords({
required final ServerDomain domain,
}) async {
Response response;
final String domainName = domain.domainName;
List allRecords = [];
/// Default amount is 20, but we will eventually overflow it,
/// so I hardcode it to the maximum available amount in advance just in case
///
/// https://docs.digitalocean.com/reference/api/api-reference/#operation/domains_list_records
const int amountPerPage = 200;
final String url = '/domains/$domainName/records?per_page=$amountPerPage';
final Dio client = await getClient();
try {
response = await client.get(url);
allRecords = response.data['domain_records'] ?? [];
} catch (e) {
print(e);
GenericResult(
data: allRecords,
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: allRecords, success: true);
}
Future<GenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
required final List<DnsRecord> records,
}) async {
final String domainName = domain.domainName;
final List<Future> allCreateFutures = <Future>[];
final Dio client = await getClient();
try {
for (final DnsRecord record in records) {
allCreateFutures.add(
client.post(
'/domains/$domainName/records',
data: {
'type': record.type,
'name': record.name,
'data': record.content,
'ttl': record.ttl,
'priority': record.priority,
},
),
);
}
await Future.wait(allCreateFutures);
} on DioError catch (e) {
print(e.message);
rethrow;
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> domainList() async {
List domains = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/domains');
domains = response.data['domains'];
} catch (e) {
print(e);
return GenericResult(
data: domains,
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: domains, success: true);
}
}

View file

@ -1,44 +0,0 @@
import 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/utils/network_utils.dart';
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
class DomainNotFoundException implements Exception {
DomainNotFoundException(this.message);
final String message;
}
abstract class DnsProviderApi extends ApiMap {
Future<List<DnsRecord>> getDnsRecords({
required final ServerDomain domain,
});
Future<APIGenericResult<void>> removeSimilarRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<APIGenericResult<void>> createMultipleDnsRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<void> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
);
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
);
Future<String?> getZoneId(final String domain);
Future<List<String>> domainList();
Future<APIGenericResult<bool>> isApiTokenValid(final String token);
RegExp getApiTokenValidation();
}

View file

@ -1,10 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
class DnsProviderApiSettings extends ProviderApiSettings {
const DnsProviderApiSettings({
super.hasLogger = false,
super.isWithToken = true,
this.customToken,
});
final String? customToken;
}

View file

@ -1,8 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
abstract class DnsProviderApiFactory {
DnsProviderApi getDnsProvider({
final DnsProviderApiSettings settings,
});
}

View file

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

View file

@ -8,7 +8,7 @@ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/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) {

View file

@ -1,866 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
DigitalOceanApi({
required this.region,
this.hasLogger = true,
this.isWithToken = true,
});
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String get rootAddress => 'https://api.digitalocean.com/v2';
@override
String get infectProviderName => 'digitalocean';
@override
String get displayProviderName => 'Digital Ocean';
@override
Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/account',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return APIGenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return APIGenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
/// Hardcoded on their documentation and there is no pricing API at all
/// Probably we should scrap the doc page manually
@override
Future<Price?> getPricePerGb() async => Price(
value: 0.10,
currency: 'USD',
);
@override
Future<APIGenericResult<ServerVolume?>> createVolume() async {
ServerVolume? volume;
Response? createVolumeResponse;
final Dio client = await getClient();
try {
final List<ServerVolume> volumes = await getVolumes();
await Future.delayed(const Duration(seconds: 6));
createVolumeResponse = await client.post(
'/volumes',
data: {
'size_gigabytes': 10,
'name': 'volume${StringGenerators.storageName()}',
'labels': {'labelkey': 'value'},
'region': region,
'filesystem_type': 'ext4',
},
);
final volumeId = createVolumeResponse.data['volume']['id'];
final volumeSize = createVolumeResponse.data['volume']['size_gigabytes'];
final volumeName = createVolumeResponse.data['volume']['name'];
volume = ServerVolume(
id: volumes.length,
name: volumeName,
sizeByte: volumeSize,
serverId: null,
linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName',
uuid: volumeId,
);
} catch (e) {
print(e);
return APIGenericResult(
data: null,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return APIGenericResult(
data: volume,
success: true,
code: createVolumeResponse.statusCode,
message: createVolumeResponse.statusMessage,
);
}
@override
Future<List<ServerVolume>> getVolumes({final String? status}) async {
final List<ServerVolume> volumes = [];
final Response getVolumesResponse;
final Dio client = await getClient();
try {
getVolumesResponse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
final List<dynamic> rawVolumes = getVolumesResponse.data['volumes'];
int id = 0;
for (final rawVolume in rawVolumes) {
final volumeId = rawVolume['id'];
final int volumeSize = rawVolume['size_gigabytes'] * 1024 * 1024 * 1024;
final volumeDropletIds = rawVolume['droplet_ids'];
final String volumeName = rawVolume['name'];
final volume = ServerVolume(
id: id++,
name: volumeName,
sizeByte: volumeSize,
serverId: volumeDropletIds.isNotEmpty ? volumeDropletIds[0] : null,
linuxDevice: 'scsi-0DO_Volume_$volumeName',
uuid: volumeId,
);
volumes.add(volume);
}
} catch (e) {
print(e);
} finally {
client.close();
}
return volumes;
}
Future<ServerVolume?> getVolume(final String volumeUuid) async {
ServerVolume? requestedVolume;
final List<ServerVolume> volumes = await getVolumes();
for (final volume in volumes) {
if (volume.uuid == volumeUuid) {
requestedVolume = volume;
}
}
return requestedVolume;
}
@override
Future<void> deleteVolume(final ServerVolume volume) async {
final Dio client = await getClient();
try {
await client.delete('/volumes/${volume.uuid}');
} catch (e) {
print(e);
} finally {
client.close();
}
}
@override
Future<APIGenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async {
bool success = false;
Response? attachVolumeResponse;
final Dio client = await getClient();
try {
attachVolumeResponse = await client.post(
'/volumes/actions',
data: {
'type': 'attach',
'volume_name': volume.name,
'region': region,
'droplet_id': serverId,
},
);
success =
attachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
return APIGenericResult(
data: false,
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(
data: success,
success: true,
code: attachVolumeResponse.statusCode,
message: attachVolumeResponse.statusMessage,
);
}
@override
Future<bool> detachVolume(final ServerVolume volume) async {
bool success = false;
final Response detachVolumeResponse;
final Dio client = await getClient();
try {
detachVolumeResponse = await client.post(
'/volumes/actions',
data: {
'type': 'detach',
'volume_name': volume.name,
'droplet_id': volume.serverId,
'region': region,
},
);
success =
detachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<bool> resizeVolume(
final ServerVolume volume,
final DiskSize size,
) async {
bool success = false;
final Response resizeVolumeResponse;
final Dio client = await getClient();
try {
resizeVolumeResponse = await client.post(
'/volumes/actions',
data: {
'type': 'resize',
'volume_name': volume.name,
'size_gigabytes': size.gibibyte,
'region': region,
},
);
success =
resizeVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final String serverType,
required final DnsProvider dnsProvider,
}) async {
ServerHostingDetails? serverDetails;
final String dbPassword = StringGenerators.dbPassword();
final String apiToken = StringGenerators.apiToken();
final String base64Password =
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
final String formattedHostname = getHostnameFromDomain(domainName);
const String infectBranch = 'providers/digital-ocean';
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
final String dnsProviderType = dnsProviderToInfectName(dnsProvider);
final String userdataString =
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | DNS_PROVIDER_TYPE=$dnsProviderType PROVIDER=$infectProviderName STAGING_ACME='$stagingAcme' DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$formattedHostname bash 2>&1 | tee /tmp/infect.log";
print(userdataString);
Response? serverCreateResponse;
final Dio client = await getClient();
try {
final Map<String, Object> data = {
'name': formattedHostname,
'size': serverType,
'image': 'ubuntu-20-04-x64',
'user_data': userdataString,
'region': region!,
};
print('Decoded data: $data');
serverCreateResponse = await client.post(
'/droplets',
data: data,
);
final int serverId = serverCreateResponse.data['droplet']['id'];
final ServerVolume? newVolume = (await createVolume()).data;
final bool attachedVolume =
(await attachVolume(newVolume!, serverId)).data;
String? ipv4;
int attempts = 0;
while (attempts < 5 && ipv4 == null) {
await Future.delayed(const Duration(seconds: 20));
final List<ServerBasicInfo> servers = await getServers();
for (final server in servers) {
if (server.name == formattedHostname && server.ip != '0.0.0.0') {
ipv4 = server.ip;
break;
}
}
++attempts;
}
if (attachedVolume && ipv4 != null) {
serverDetails = ServerHostingDetails(
id: serverId,
ip4: ipv4,
createTime: DateTime.now(),
volume: newVolume,
apiToken: apiToken,
provider: ServerProvider.digitalOcean,
);
}
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(
data: serverDetails,
success: true,
code: serverCreateResponse.statusCode,
message: serverCreateResponse.statusMessage,
);
}
@override
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
}) async {
final Dio client = await getClient();
final String hostname = getHostnameFromDomain(domainName);
final servers = await getServers();
final ServerBasicInfo serverToRemove;
try {
serverToRemove = servers.firstWhere(
(final el) => el.name == hostname,
);
} catch (e) {
print(e);
return APIGenericResult(
data: false,
success: false,
message: e.toString(),
);
}
final volumes = await getVolumes();
final ServerVolume volumeToRemove;
try {
volumeToRemove = volumes.firstWhere(
(final el) => el.serverId == serverToRemove.id,
);
} catch (e) {
print(e);
return APIGenericResult(
data: false,
success: false,
message: e.toString(),
);
}
final List<Future> laterFutures = <Future>[];
await detachVolume(volumeToRemove);
await Future.delayed(const Duration(seconds: 10));
try {
laterFutures.add(deleteVolume(volumeToRemove));
laterFutures.add(client.delete('/droplets/${serverToRemove.id}'));
await Future.wait(laterFutures);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(
success: true,
data: true,
);
}
@override
Future<ServerHostingDetails> restart() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
final Dio client = await getClient();
try {
await client.post(
'/droplets/${server.id}/actions',
data: {
'type': 'reboot',
},
);
} catch (e) {
print(e);
} finally {
close(client);
}
return server.copyWith(startTime: DateTime.now());
}
@override
Future<ServerHostingDetails> powerOn() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
final Dio client = await getClient();
try {
await client.post(
'/droplets/${server.id}/actions',
data: {
'type': 'power_on',
},
);
} catch (e) {
print(e);
} finally {
close(client);
}
return server.copyWith(startTime: DateTime.now());
}
/// Digital Ocean returns a map of lists of /proc/stat values,
/// so here we are trying to implement average CPU
/// load calculation for each point in time on a given interval.
///
/// For each point of time:
///
/// `Average Load = 100 * (1 - (Idle Load / Total Load))`
///
/// For more info please proceed to read:
/// https://rosettacode.org/wiki/Linux_CPU_utilization
List<TimeSeriesData> calculateCpuLoadMetrics(final List rawProcStatMetrics) {
final List<TimeSeriesData> cpuLoads = [];
final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length;
for (int i = 0; i < pointsInTime; ++i) {
double currentMetricLoad = 0.0;
double? currentMetricIdle;
for (final rawProcStat in rawProcStatMetrics) {
final String rawProcValue = rawProcStat['values'][i][1];
// Converting MBit into bit
final double procValue = double.parse(rawProcValue) * 1000000;
currentMetricLoad += procValue;
if (currentMetricIdle == null &&
rawProcStat['metric']['mode'] == 'idle') {
currentMetricIdle = procValue;
}
}
currentMetricIdle ??= 0.0;
currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad));
cpuLoads.add(
TimeSeriesData(
rawProcStatMetrics[0]['values'][i][0],
currentMetricLoad,
),
);
}
return cpuLoads;
}
@override
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
const int step = 15;
final Dio client = await getClient();
try {
Response response = await client.get(
'/monitoring/metrics/droplet/bandwidth',
queryParameters: {
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
'host_id': '$serverId',
'interface': 'public',
'direction': 'inbound',
},
);
final List inbound = response.data['data']['result'][0]['values'];
response = await client.get(
'/monitoring/metrics/droplet/bandwidth',
queryParameters: {
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
'host_id': '$serverId',
'interface': 'public',
'direction': 'outbound',
},
);
final List outbound = response.data['data']['result'][0]['values'];
response = await client.get(
'/monitoring/metrics/droplet/cpu',
queryParameters: {
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
'host_id': '$serverId',
},
);
metrics = ServerMetrics(
bandwidthIn: inbound
.map(
(final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000),
)
.toList(),
bandwidthOut: outbound
.map(
(final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000),
)
.toList(),
cpu: calculateCpuLoadMetrics(response.data['data']['result']),
start: start,
end: end,
stepsInSecond: step,
);
} catch (e) {
print(e);
} finally {
close(client);
}
return metrics;
}
@override
Future<List<ServerMetadataEntity>> getMetadata(final int serverId) async {
List<ServerMetadataEntity> metadata = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/droplets/$serverId');
final droplet = response.data!['droplet'];
metadata = [
ServerMetadataEntity(
type: MetadataType.id,
name: 'server.server_id'.tr(),
value: droplet['id'].toString(),
),
ServerMetadataEntity(
type: MetadataType.status,
name: 'server.status'.tr(),
value: droplet['status'].toString().capitalize(),
),
ServerMetadataEntity(
type: MetadataType.cpu,
name: 'server.cpu'.tr(),
value: 'server.core_count'.plural(droplet['vcpus']),
),
ServerMetadataEntity(
type: MetadataType.ram,
name: 'server.ram'.tr(),
value: "${droplet['memory'].toString()} MB",
),
ServerMetadataEntity(
type: MetadataType.cost,
name: 'server.monthly_cost'.tr(),
value: droplet['size']['price_monthly'].toString(),
),
ServerMetadataEntity(
type: MetadataType.location,
name: 'server.location'.tr(),
value:
'${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}',
),
ServerMetadataEntity(
type: MetadataType.other,
name: 'server.provider'.tr(),
value: displayProviderName,
),
];
} catch (e) {
print(e);
} finally {
close(client);
}
return metadata;
}
@override
Future<List<ServerBasicInfo>> getServers() async {
List<ServerBasicInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/droplets');
servers = response.data!['droplets'].map<ServerBasicInfo>(
(final server) {
String ipv4 = '0.0.0.0';
if (server['networks']['v4'].isNotEmpty) {
for (final v4 in server['networks']['v4']) {
if (v4['type'].toString() == 'public') {
ipv4 = v4['ip_address'].toString();
}
}
}
return ServerBasicInfo(
id: server['id'],
reverseDns: server['name'],
created: DateTime.now(),
ip: ipv4,
name: server['name'],
);
},
).toList();
} catch (e) {
print(e);
} finally {
close(client);
}
print(servers);
return servers;
}
String? getEmojiFlag(final String query) {
String? emoji;
switch (query.toLowerCase().substring(0, 3)) {
case 'fra':
emoji = '🇩🇪';
break;
case 'ams':
emoji = '🇳🇱';
break;
case 'sgp':
emoji = '🇸🇬';
break;
case 'lon':
emoji = '🇬🇧';
break;
case 'tor':
emoji = '🇨🇦';
break;
case 'blr':
emoji = '🇮🇳';
break;
case 'nyc':
case 'sfo':
emoji = '🇺🇸';
break;
}
return emoji;
}
@override
Future<APIGenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
List<ServerProviderLocation> locations = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/regions',
);
locations = response.data!['regions']
.map<ServerProviderLocation>(
(final location) => ServerProviderLocation(
title: location['slug'],
description: location['name'],
flag: getEmojiFlag(location['slug']),
identifier: location['slug'],
),
)
.toList();
} catch (e) {
print(e);
return APIGenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(data: locations, success: true);
}
@override
Future<APIGenericResult<List<ServerType>>> getServerTypesByLocation({
required final ServerProviderLocation location,
}) async {
final List<ServerType> types = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/sizes',
);
final rawSizes = response.data!['sizes'];
for (final rawSize in rawSizes) {
for (final rawRegion in rawSize['regions']) {
final ramMb = rawSize['memory'].toDouble();
if (rawRegion.toString() == location.identifier && ramMb > 1024) {
types.add(
ServerType(
title: rawSize['description'],
identifier: rawSize['slug'],
ram: ramMb / 1024,
cores: rawSize['vcpus'],
disk: DiskSize(byte: rawSize['disk'] * 1024 * 1024 * 1024),
price: Price(
value: rawSize['price_monthly'],
currency: 'USD',
),
location: location,
),
);
}
}
}
} catch (e) {
print(e);
return APIGenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(data: types, success: true);
}
@override
Future<APIGenericResult<void>> createReverseDns({
required final ServerHostingDetails serverDetails,
required final ServerDomain domain,
}) async {
/// TODO remove from provider interface
const bool success = true;
return APIGenericResult(success: success, data: null);
}
@override
ProviderApiTokenValidation getApiTokenValidation() =>
ProviderApiTokenValidation(
regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'),
length: 71,
);
}

View file

@ -0,0 +1,561 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/digital_ocean_server_info.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class DigitalOceanApi extends RestApiMap {
DigitalOceanApi({
required this.region,
this.hasLogger = true,
this.isWithToken = true,
});
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String get rootAddress => 'https://api.digitalocean.com/v2';
String get infectProviderName => 'digitalocean';
String get displayProviderName => 'Digital Ocean';
Future<GenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/account',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return GenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return GenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
Future<GenericResult<DigitalOceanVolume?>> createVolume() async {
DigitalOceanVolume? volume;
Response? createVolumeResponse;
final Dio client = await getClient();
try {
await Future.delayed(const Duration(seconds: 6));
createVolumeResponse = await client.post(
'/volumes',
data: {
'size_gigabytes': 10,
'name': 'volume${StringGenerators.storageName()}',
'labels': {'labelkey': 'value'},
'region': region,
'filesystem_type': 'ext4',
},
);
volume = DigitalOceanVolume.fromJson(createVolumeResponse.data['volume']);
} catch (e) {
print(e);
return GenericResult(
data: null,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: volume,
success: true,
code: createVolumeResponse.statusCode,
message: createVolumeResponse.statusMessage,
);
}
Future<GenericResult<List<DigitalOceanVolume>>> getVolumes({
final String? status,
}) async {
final List<DigitalOceanVolume> volumes = [];
Response? getVolumesResponse;
final Dio client = await getClient();
try {
getVolumesResponse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
for (final volume in getVolumesResponse.data['volumes']) {
volumes.add(DigitalOceanVolume.fromJson(volume));
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: volumes,
success: true,
);
}
Future<GenericResult<void>> deleteVolume(final String uuid) async {
final Dio client = await getClient();
try {
await client.delete('/volumes/$uuid');
} catch (e) {
print(e);
return GenericResult(
data: null,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: null,
success: true,
);
}
Future<GenericResult<bool>> attachVolume(
final String name,
final int serverId,
) async {
bool success = false;
Response? attachVolumeResponse;
final Dio client = await getClient();
try {
attachVolumeResponse = await client.post(
'/volumes/actions',
data: {
'type': 'attach',
'volume_name': name,
'region': region,
'droplet_id': serverId,
},
);
success =
attachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
return GenericResult(
data: false,
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(
data: success,
success: true,
code: attachVolumeResponse.statusCode,
message: attachVolumeResponse.statusMessage,
);
}
Future<GenericResult<bool>> detachVolume(
final String name,
final int serverId,
) async {
bool success = false;
final Response detachVolumeResponse;
final Dio client = await getClient();
try {
detachVolumeResponse = await client.post(
'/volumes/actions',
data: {
'type': 'detach',
'volume_name': name,
'droplet_id': serverId,
'region': region,
},
);
success =
detachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
return GenericResult(
data: false,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: success,
success: true,
);
}
Future<GenericResult<bool>> resizeVolume(
final String name,
final DiskSize size,
) async {
bool success = false;
final Response resizeVolumeResponse;
final Dio client = await getClient();
try {
resizeVolumeResponse = await client.post(
'/volumes/actions',
data: {
'type': 'resize',
'volume_name': name,
'size_gigabytes': size.gibibyte,
'region': region,
},
);
success =
resizeVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
return GenericResult(
data: false,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: success,
success: true,
);
}
Future<GenericResult<int?>> createServer({
required final String dnsApiToken,
required final String dnsProviderType,
required final String serverApiToken,
required final User rootUser,
required final String base64Password,
required final String databasePassword,
required final String domainName,
required final String hostName,
required final String serverType,
}) async {
final String stagingAcme = TlsOptions.stagingAcme ? 'true' : 'false';
int? dropletId;
Response? serverCreateResponse;
final Dio client = await getClient();
try {
final Map<String, Object> data = {
'name': hostName,
'size': serverType,
'image': 'ubuntu-20-04-x64',
'user_data': '#cloud-config\n'
'runcmd:\n'
'- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/providers/digital-ocean/nixos-infect | '
"PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType STAGING_ACME='$stagingAcme' DOMAIN='$domainName' "
"LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$databasePassword "
'API_TOKEN=$serverApiToken HOSTNAME=$hostName bash 2>&1 | tee /tmp/infect.log',
'region': region!,
};
print('Decoded data: $data');
serverCreateResponse = await client.post(
'/droplets',
data: data,
);
dropletId = serverCreateResponse.data['droplet']['id'];
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(
data: dropletId,
success: true,
code: serverCreateResponse.statusCode,
message: serverCreateResponse.statusMessage,
);
}
Future<GenericResult<void>> deleteServer(final int serverId) async {
final Dio client = await getClient();
try {
await client.delete('/droplets/$serverId');
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<void>> restart(final int serverId) async {
final Dio client = await getClient();
try {
await client.post(
'/droplets/$serverId/actions',
data: {
'type': 'reboot',
},
);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<void>> powerOn(final int serverId) async {
final Dio client = await getClient();
try {
await client.post(
'/droplets/$serverId/actions',
data: {
'type': 'power_on',
},
);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<List>> getMetricsCpu(
final int serverId,
final DateTime start,
final DateTime end,
) async {
List metrics = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/monitoring/metrics/droplet/cpu',
queryParameters: {
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
'host_id': '$serverId',
},
);
metrics = response.data['data']['result'];
} catch (e) {
print(e);
return GenericResult(
success: false,
data: [],
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: metrics);
}
Future<GenericResult<List>> getMetricsBandwidth(
final int serverId,
final DateTime start,
final DateTime end,
final bool isInbound,
) async {
List metrics = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/monitoring/metrics/droplet/bandwidth',
queryParameters: {
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
'host_id': '$serverId',
'interface': 'public',
'direction': isInbound ? 'inbound' : 'outbound',
},
);
metrics = response.data['data']['result'][0]['values'];
} catch (e) {
print(e);
return GenericResult(
success: false,
data: [],
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: metrics);
}
Future<GenericResult<List>> getServers() async {
List servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/droplets');
servers = response.data['droplets'];
} catch (e) {
print(e);
return GenericResult(
success: false,
data: servers,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: servers);
}
Future<GenericResult<List<DigitalOceanLocation>>>
getAvailableLocations() async {
final List<DigitalOceanLocation> locations = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/regions',
);
for (final region in response.data!['regions']) {
locations.add(DigitalOceanLocation.fromJson(region));
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: locations, success: true);
}
Future<GenericResult<List<DigitalOceanServerType>>>
getAvailableServerTypes() async {
final List<DigitalOceanServerType> types = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/sizes',
);
for (final size in response.data!['sizes']) {
types.add(DigitalOceanServerType.fromJson(size));
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: types, success: true);
}
}

View file

@ -1,34 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
class DigitalOceanApiFactory extends ServerProviderApiFactory
with VolumeProviderApiFactory {
DigitalOceanApiFactory({this.region});
final String? region;
@override
ServerProviderApi getServerProvider({
final ServerProviderApiSettings settings =
const ServerProviderApiSettings(),
}) =>
DigitalOceanApi(
region: settings.region ?? region,
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
);
@override
VolumeProviderApi getVolumeProvider({
final ServerProviderApiSettings settings =
const ServerProviderApiSettings(),
}) =>
DigitalOceanApi(
region: settings.region ?? region,
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
);
}

View file

@ -1,850 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends ServerProviderApi with VolumeProviderApi {
HetznerApi({
this.region,
this.hasLogger = true,
this.isWithToken = true,
});
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String get rootAddress => 'https://api.hetzner.cloud/v1';
@override
String get infectProviderName => 'hetzner';
@override
String get displayProviderName => 'Hetzner';
@override
Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/servers',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return APIGenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return APIGenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
@override
ProviderApiTokenValidation getApiTokenValidation() =>
ProviderApiTokenValidation(
regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'),
length: 64,
);
@override
Future<Price?> getPricePerGb() async {
double? price;
final Response pricingResponse;
final Dio client = await getClient();
try {
pricingResponse = await client.get('/pricing');
final volume = pricingResponse.data['pricing']['volume'];
final volumePrice = volume['price_per_gb_month']['gross'];
price = double.parse(volumePrice);
} catch (e) {
print(e);
} finally {
client.close();
}
return price == null
? null
: Price(
value: price,
currency: 'EUR',
);
}
@override
Future<APIGenericResult<ServerVolume?>> createVolume() async {
ServerVolume? volume;
Response? createVolumeResponse;
final Dio client = await getClient();
try {
createVolumeResponse = await client.post(
'/volumes',
data: {
'size': 10,
'name': StringGenerators.storageName(),
'labels': {'labelkey': 'value'},
'location': region,
'automount': false,
'format': 'ext4'
},
);
final volumeId = createVolumeResponse.data['volume']['id'];
final volumeSize = createVolumeResponse.data['volume']['size'];
final volumeServer = createVolumeResponse.data['volume']['server'];
final volumeName = createVolumeResponse.data['volume']['name'];
final volumeDevice = createVolumeResponse.data['volume']['linux_device'];
volume = ServerVolume(
id: volumeId,
name: volumeName,
sizeByte: volumeSize,
serverId: volumeServer,
linuxDevice: volumeDevice,
);
} catch (e) {
print(e);
return APIGenericResult(
data: null,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return APIGenericResult(
data: volume,
success: true,
code: createVolumeResponse.statusCode,
message: createVolumeResponse.statusMessage,
);
}
@override
Future<List<ServerVolume>> getVolumes({final String? status}) async {
final List<ServerVolume> volumes = [];
final Response getVolumesResonse;
final Dio client = await getClient();
try {
getVolumesResonse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
final List<dynamic> rawVolumes = getVolumesResonse.data['volumes'];
for (final rawVolume in rawVolumes) {
final int volumeId = rawVolume['id'];
final int volumeSize = rawVolume['size'] * 1024 * 1024 * 1024;
final volumeServer = rawVolume['server'];
final String volumeName = rawVolume['name'];
final volumeDevice = rawVolume['linux_device'];
final volume = ServerVolume(
id: volumeId,
name: volumeName,
sizeByte: volumeSize,
serverId: volumeServer,
linuxDevice: volumeDevice,
);
volumes.add(volume);
}
} catch (e) {
print(e);
} finally {
client.close();
}
return volumes;
}
Future<ServerVolume?> getVolume(
final String volumeId,
) async {
ServerVolume? volume;
final Response getVolumeResponse;
final Dio client = await getClient();
try {
getVolumeResponse = await client.get('/volumes/$volumeId');
final int responseVolumeId = getVolumeResponse.data['volume']['id'];
final int volumeSize = getVolumeResponse.data['volume']['size'];
final int volumeServer = getVolumeResponse.data['volume']['server'];
final String volumeName = getVolumeResponse.data['volume']['name'];
final volumeDevice = getVolumeResponse.data['volume']['linux_device'];
volume = ServerVolume(
id: responseVolumeId,
name: volumeName,
sizeByte: volumeSize,
serverId: volumeServer,
linuxDevice: volumeDevice,
);
} catch (e) {
print(e);
} finally {
client.close();
}
return volume;
}
@override
Future<void> deleteVolume(final ServerVolume volume) async {
final Dio client = await getClient();
try {
await client.delete('/volumes/${volume.id}');
} catch (e) {
print(e);
} finally {
client.close();
}
}
@override
Future<APIGenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async {
bool success = false;
Response? attachVolumeResponse;
final Dio client = await getClient();
try {
attachVolumeResponse = await client.post(
'/volumes/${volume.id}/actions/attach',
data: {
'automount': true,
'server': serverId,
},
);
success =
attachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return APIGenericResult(
data: success,
success: true,
code: attachVolumeResponse?.statusCode,
message: attachVolumeResponse?.statusMessage,
);
}
@override
Future<bool> detachVolume(final ServerVolume volume) async {
bool success = false;
final Response detachVolumeResponse;
final Dio client = await getClient();
try {
detachVolumeResponse = await client.post(
'/volumes/${volume.id}/actions/detach',
);
success =
detachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<bool> resizeVolume(
final ServerVolume volume,
final DiskSize size,
) async {
bool success = false;
final Response resizeVolumeResponse;
final Dio client = await getClient();
try {
resizeVolumeResponse = await client.post(
'/volumes/${volume.id}/actions/resize',
data: {
'size': size.gibibyte,
},
);
success =
resizeVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return success;
}
@override
Future<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final String serverType,
required final DnsProvider dnsProvider,
}) async {
final APIGenericResult<ServerVolume?> newVolumeResponse =
await createVolume();
if (!newVolumeResponse.success || newVolumeResponse.data == null) {
return APIGenericResult(
data: null,
success: false,
message: newVolumeResponse.message,
code: newVolumeResponse.code,
);
}
return createServerWithVolume(
dnsApiToken: dnsApiToken,
rootUser: rootUser,
domainName: domainName,
volume: newVolumeResponse.data!,
serverType: serverType,
dnsProvider: dnsProvider,
);
}
Future<APIGenericResult<ServerHostingDetails?>> createServerWithVolume({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final ServerVolume volume,
required final String serverType,
required final DnsProvider dnsProvider,
}) async {
final Dio client = await getClient();
final String dbPassword = StringGenerators.dbPassword();
final int volumeId = volume.id;
final String apiToken = StringGenerators.apiToken();
final String hostname = getHostnameFromDomain(domainName);
const String infectBranch = 'providers/hetzner';
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
final String base64Password =
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
final String dnsProviderType = dnsProviderToInfectName(dnsProvider);
final String userdataString =
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | DNS_PROVIDER_TYPE=$dnsProviderType STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log";
Response? serverCreateResponse;
ServerHostingDetails? serverDetails;
DioError? hetznerError;
bool success = false;
try {
final Map<String, Object> data = {
'name': hostname,
'server_type': serverType,
'start_after_create': false,
'image': 'ubuntu-20.04',
'volumes': [volumeId],
'networks': [],
'user_data': userdataString,
'labels': {},
'automount': true,
'location': region!,
};
print('Decoded data: $data');
serverCreateResponse = await client.post(
'/servers',
data: data,
);
print(serverCreateResponse.data);
serverDetails = ServerHostingDetails(
id: serverCreateResponse.data['server']['id'],
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(),
volume: volume,
apiToken: apiToken,
provider: ServerProvider.hetzner,
);
success = true;
} on DioError catch (e) {
print(e);
hetznerError = e;
} catch (e) {
print(e);
} finally {
client.close();
}
if (!success) {
await Future.delayed(const Duration(seconds: 10));
await deleteVolume(volume);
}
String? apiResultMessage = serverCreateResponse?.statusMessage;
if (hetznerError != null &&
hetznerError.response!.data['error']['code'] == 'uniqueness_error') {
apiResultMessage = 'uniqueness_error';
}
return APIGenericResult(
data: serverDetails,
success: success && hetznerError == null,
code: serverCreateResponse?.statusCode ??
hetznerError?.response?.statusCode,
message: apiResultMessage,
);
}
@override
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
}) async {
final Dio client = await getClient();
try {
final String hostname = getHostnameFromDomain(domainName);
final Response serversReponse = await client.get('/servers');
final List servers = serversReponse.data['servers'];
final Map server =
servers.firstWhere((final el) => el['name'] == hostname);
final List volumes = server['volumes'];
final List<Future> laterFutures = <Future>[];
for (final volumeId in volumes) {
await client.post('/volumes/$volumeId/actions/detach');
}
await Future.delayed(const Duration(seconds: 10));
for (final volumeId in volumes) {
laterFutures.add(client.delete('/volumes/$volumeId'));
}
laterFutures.add(client.delete('/servers/${server['id']}'));
await Future.wait(laterFutures);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(
success: true,
data: true,
);
}
@override
Future<ServerHostingDetails> restart() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
final Dio client = await getClient();
try {
await client.post('/servers/${server.id}/actions/reset');
} catch (e) {
print(e);
} finally {
close(client);
}
return server.copyWith(startTime: DateTime.now());
}
@override
Future<ServerHostingDetails> powerOn() async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
final Dio client = await getClient();
try {
await client.post('/servers/${server.id}/actions/poweron');
} catch (e) {
print(e);
} finally {
close(client);
}
return server.copyWith(startTime: DateTime.now());
}
Future<Map<String, dynamic>> requestRawMetrics(
final int serverId,
final DateTime start,
final DateTime end,
final String type,
) async {
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
final Map<String, dynamic> queryParameters = {
'start': start.toUtc().toIso8601String(),
'end': end.toUtc().toIso8601String(),
'type': type
};
final Response res = await client.get(
'/servers/$serverId/metrics',
queryParameters: queryParameters,
);
metrics = res.data['metrics'];
} catch (e) {
print(e);
} finally {
close(client);
}
return metrics;
}
List<TimeSeriesData> serializeTimeSeries(
final Map<String, dynamic> json,
final String type,
) {
final List list = json['time_series'][type]['values'];
return list
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
.toList();
}
@override
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
final Map<String, dynamic> rawCpuMetrics = await requestRawMetrics(
serverId,
start,
end,
'cpu',
);
final Map<String, dynamic> rawNetworkMetrics = await requestRawMetrics(
serverId,
start,
end,
'network',
);
if (rawNetworkMetrics.isEmpty || rawCpuMetrics.isEmpty) {
return metrics;
}
metrics = ServerMetrics(
cpu: serializeTimeSeries(
rawCpuMetrics,
'cpu',
),
bandwidthIn: serializeTimeSeries(
rawNetworkMetrics,
'network.0.bandwidth.in',
),
bandwidthOut: serializeTimeSeries(
rawNetworkMetrics,
'network.0.bandwidth.out',
),
end: end,
start: start,
stepsInSecond: rawCpuMetrics['step'],
);
return metrics;
}
@override
Future<List<ServerMetadataEntity>> getMetadata(final int serverId) async {
List<ServerMetadataEntity> metadata = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/servers/$serverId');
final hetznerInfo = HetznerServerInfo.fromJson(response.data!['server']);
metadata = [
ServerMetadataEntity(
type: MetadataType.id,
name: 'server.server_id'.tr(),
value: hetznerInfo.id.toString(),
),
ServerMetadataEntity(
type: MetadataType.status,
name: 'server.status'.tr(),
value: hetznerInfo.status.toString().split('.')[1].capitalize(),
),
ServerMetadataEntity(
type: MetadataType.cpu,
name: 'server.cpu'.tr(),
value: 'server.core_count'.plural(hetznerInfo.serverType.cores),
),
ServerMetadataEntity(
type: MetadataType.ram,
name: 'server.ram'.tr(),
value: '${hetznerInfo.serverType.memory.toString()} GB',
),
ServerMetadataEntity(
type: MetadataType.cost,
name: 'server.monthly_cost'.tr(),
value: hetznerInfo.serverType.prices[1].monthly.toStringAsFixed(2),
),
ServerMetadataEntity(
type: MetadataType.location,
name: 'server.location'.tr(),
value:
'${hetznerInfo.location.city}, ${hetznerInfo.location.country}',
),
ServerMetadataEntity(
type: MetadataType.other,
name: 'server.provider'.tr(),
value: displayProviderName,
),
];
} catch (e) {
print(e);
} finally {
close(client);
}
return metadata;
}
@override
Future<List<ServerBasicInfo>> getServers() async {
List<ServerBasicInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/servers');
servers = response.data!['servers']
.map<HetznerServerInfo>(
(final e) => HetznerServerInfo.fromJson(e),
)
.toList()
.where(
(final server) => server.publicNet.ipv4 != null,
)
.map<ServerBasicInfo>(
(final server) => ServerBasicInfo(
id: server.id,
name: server.name,
ip: server.publicNet.ipv4.ip,
reverseDns: server.publicNet.ipv4.reverseDns,
created: server.created,
),
)
.toList();
} catch (e) {
print(e);
} finally {
close(client);
}
print(servers);
return servers;
}
String? getEmojiFlag(final String query) {
String? emoji;
switch (query.toLowerCase()) {
case 'de':
emoji = '🇩🇪';
break;
case 'fi':
emoji = '🇫🇮';
break;
case 'us':
emoji = '🇺🇸';
break;
}
return emoji;
}
@override
Future<APIGenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
List<ServerProviderLocation> locations = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/locations',
);
locations = response.data!['locations']
.map<ServerProviderLocation>(
(final location) => ServerProviderLocation(
title: location['city'],
description: location['description'],
flag: getEmojiFlag(location['country']),
identifier: location['name'],
),
)
.toList();
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: [],
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(success: true, data: locations);
}
@override
Future<APIGenericResult<List<ServerType>>> getServerTypesByLocation({
required final ServerProviderLocation location,
}) async {
final List<ServerType> types = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/server_types',
);
final rawTypes = response.data!['server_types'];
for (final rawType in rawTypes) {
for (final rawPrice in rawType['prices']) {
if (rawPrice['location'].toString() == location.identifier) {
types.add(
ServerType(
title: rawType['description'],
identifier: rawType['name'],
ram: rawType['memory'],
cores: rawType['cores'],
disk: DiskSize(byte: rawType['disk'] * 1024 * 1024 * 1024),
price: Price(
value: double.parse(rawPrice['price_monthly']['gross']),
currency: 'EUR',
),
location: location,
),
);
}
}
}
} catch (e) {
print(e);
return APIGenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(data: types, success: true);
}
@override
Future<APIGenericResult<void>> createReverseDns({
required final ServerHostingDetails serverDetails,
required final ServerDomain domain,
}) async {
final Dio client = await getClient();
try {
await client.post(
'/servers/${serverDetails.id}/actions/change_dns_ptr',
data: {
'ip': serverDetails.ip4,
'dns_ptr': domain.domainName,
},
);
} catch (e) {
print(e);
return APIGenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return APIGenericResult(success: true, data: null);
}
}

View file

@ -0,0 +1,595 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends RestApiMap {
HetznerApi({
this.region,
this.hasLogger = true,
this.isWithToken = true,
});
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(
baseUrl: rootAddress,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String get rootAddress => 'https://api.hetzner.cloud/v1';
String get infectProviderName => 'hetzner';
String get displayProviderName => 'Hetzner';
Future<GenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
String message = '';
final Dio client = await getClient();
try {
response = await client.get(
'/servers',
options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
message = e.toString();
} finally {
close(client);
}
if (response == null) {
return GenericResult(
data: isValid,
success: false,
message: message,
);
}
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
return GenericResult(
data: isValid,
success: true,
message: response.statusMessage,
);
}
Future<GenericResult<double?>> getPricePerGb() async {
double? price;
final Response pricingResponse;
final Dio client = await getClient();
try {
pricingResponse = await client.get('/pricing');
final volume = pricingResponse.data['pricing']['volume'];
final volumePrice = volume['price_per_gb_month']['gross'];
price = double.parse(volumePrice);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: price,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(success: true, data: price);
}
Future<GenericResult<HetznerVolume?>> createVolume() async {
Response? createVolumeResponse;
HetznerVolume? volume;
final Dio client = await getClient();
try {
createVolumeResponse = await client.post(
'/volumes',
data: {
'size': 10,
'name': StringGenerators.storageName(),
'labels': {'labelkey': 'value'},
'location': region,
'automount': false,
'format': 'ext4'
},
);
volume = HetznerVolume.fromJson(createVolumeResponse.data['volume']);
} catch (e) {
print(e);
return GenericResult(
data: null,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: volume,
success: true,
code: createVolumeResponse.statusCode,
message: createVolumeResponse.statusMessage,
);
}
Future<GenericResult<List<HetznerVolume>>> getVolumes({
final String? status,
}) async {
final List<HetznerVolume> volumes = [];
Response? getVolumesResonse;
final Dio client = await getClient();
try {
getVolumesResonse = await client.get(
'/volumes',
queryParameters: {
'status': status,
},
);
for (final volume in getVolumesResonse.data['volumes']) {
volumes.add(HetznerVolume.fromJson(volume));
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: volumes,
success: true,
code: getVolumesResonse.statusCode,
message: getVolumesResonse.statusMessage,
);
}
Future<GenericResult<HetznerVolume?>> getVolume(
final String volumeId,
) async {
HetznerVolume? volume;
final Response getVolumeResponse;
final Dio client = await getClient();
try {
getVolumeResponse = await client.get('/volumes/$volumeId');
volume = HetznerVolume.fromJson(getVolumeResponse.data['volume']);
} catch (e) {
print(e);
return GenericResult(
data: null,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: volume,
success: true,
);
}
Future<GenericResult<bool>> deleteVolume(final int volumeId) async {
final Dio client = await getClient();
try {
await client.delete('/volumes/$volumeId');
} catch (e) {
print(e);
return GenericResult(
success: false,
data: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
success: true,
data: true,
);
}
Future<GenericResult<bool>> attachVolume(
final HetznerVolume volume,
final int serverId,
) async {
bool success = false;
Response? attachVolumeResponse;
final Dio client = await getClient();
try {
attachVolumeResponse = await client.post(
'/volumes/${volume.id}/actions/attach',
data: {
'automount': true,
'server': serverId,
},
);
success =
attachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
} finally {
client.close();
}
return GenericResult(
data: success,
success: true,
code: attachVolumeResponse?.statusCode,
message: attachVolumeResponse?.statusMessage,
);
}
Future<GenericResult<bool>> detachVolume(final int volumeId) async {
bool success = false;
final Response detachVolumeResponse;
final Dio client = await getClient();
try {
detachVolumeResponse = await client.post(
'/volumes/$volumeId/actions/detach',
);
success =
detachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
return GenericResult(
success: false,
data: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
success: false,
data: success,
);
}
Future<GenericResult<bool>> resizeVolume(
final HetznerVolume volume,
final DiskSize size,
) async {
bool success = false;
final Response resizeVolumeResponse;
final Dio client = await getClient();
try {
resizeVolumeResponse = await client.post(
'/volumes/${volume.id}/actions/resize',
data: {
'size': size.gibibyte,
},
);
success =
resizeVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) {
print(e);
return GenericResult(
data: false,
success: false,
message: e.toString(),
);
} finally {
client.close();
}
return GenericResult(
data: success,
success: true,
);
}
Future<GenericResult<HetznerServerInfo?>> createServer({
required final String dnsApiToken,
required final String dnsProviderType,
required final String serverApiToken,
required final User rootUser,
required final String base64Password,
required final String databasePassword,
required final String domainName,
required final String hostName,
required final int volumeId,
required final String serverType,
}) async {
final String stagingAcme = TlsOptions.stagingAcme ? 'true' : 'false';
Response? serverCreateResponse;
HetznerServerInfo? serverInfo;
DioError? hetznerError;
bool success = false;
final Dio client = await getClient();
try {
final Map<String, Object> data = {
'name': hostName,
'server_type': serverType,
'start_after_create': false,
'image': 'ubuntu-20.04',
'volumes': [volumeId],
'networks': [],
'user_data': '#cloud-config\n'
'runcmd:\n'
'- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/providers/hetzner/nixos-infect | '
"STAGING_ACME='$stagingAcme' PROVIDER=$infectProviderName DNS_PROVIDER_TYPE=$dnsProviderType "
"NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' "
'CF_TOKEN=$dnsApiToken DB_PASSWORD=$databasePassword API_TOKEN=$serverApiToken HOSTNAME=$hostName bash 2>&1 | '
'tee /tmp/infect.log',
'labels': {},
'automount': true,
'location': region!,
};
print('Decoded data: $data');
serverCreateResponse = await client.post('/servers', data: data);
serverInfo = HetznerServerInfo.fromJson(
serverCreateResponse.data['server'],
);
success = true;
} on DioError catch (e) {
print(e);
hetznerError = e;
} catch (e) {
print(e);
} finally {
close(client);
}
String? apiResultMessage = serverCreateResponse?.statusMessage;
if (hetznerError != null &&
hetznerError.response!.data['error']['code'] == 'uniqueness_error') {
apiResultMessage = 'uniqueness_error';
}
return GenericResult(
data: serverInfo,
success: success && hetznerError == null,
code: serverCreateResponse?.statusCode ??
hetznerError?.response?.statusCode,
message: apiResultMessage,
);
}
Future<GenericResult<void>> deleteServer({
required final int serverId,
}) async {
final Dio client = await getClient();
try {
await client.delete('/servers/$serverId');
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<void>> restart(final int serverId) async {
final Dio client = await getClient();
try {
await client.post('/servers/$serverId/actions/reset');
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<void>> powerOn(final int serverId) async {
final Dio client = await getClient();
try {
await client.post('/servers/$serverId/actions/poweron');
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
Future<GenericResult<Map<String, dynamic>>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
final String type,
) async {
Map<String, dynamic> metrics = {};
final Dio client = await getClient();
try {
final Map<String, dynamic> queryParameters = {
'start': start.toUtc().toIso8601String(),
'end': end.toUtc().toIso8601String(),
'type': type
};
final Response res = await client.get(
'/servers/$serverId/metrics',
queryParameters: queryParameters,
);
metrics = res.data['metrics'];
} catch (e) {
print(e);
return GenericResult(
success: false,
data: {},
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: metrics, success: true);
}
Future<GenericResult<List<HetznerServerInfo>>> getServers() async {
List<HetznerServerInfo> servers = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/servers');
servers = response.data!['servers']
.map<HetznerServerInfo>(
(final e) => HetznerServerInfo.fromJson(e),
)
.toList();
} catch (e) {
print(e);
return GenericResult(
success: false,
data: [],
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: servers, success: true);
}
Future<GenericResult<List<HetznerLocation>>> getAvailableLocations() async {
final List<HetznerLocation> locations = [];
final Dio client = await getClient();
try {
final Response response = await client.get('/locations');
for (final location in response.data!['locations']) {
locations.add(HetznerLocation.fromJson(location));
}
} catch (e) {
print(e);
return GenericResult(
success: false,
data: [],
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: locations);
}
Future<GenericResult<List<HetznerServerTypeInfo>>>
getAvailableServerTypes() async {
final List<HetznerServerTypeInfo> types = [];
final Dio client = await getClient();
try {
final Response response = await client.get(
'/server_types',
);
for (final type in response.data!['server_types']) {
types.add(HetznerServerTypeInfo.fromJson(type));
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(data: types, success: true);
}
Future<GenericResult<void>> createReverseDns({
required final int serverId,
required final String ip4,
required final String dnsPtr,
}) async {
final Dio client = await getClient();
try {
await client.post(
'/servers/$serverId/actions/change_dns_ptr',
data: {
'ip': ip4,
'dns_ptr': dnsPtr,
},
);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
} finally {
close(client);
}
return GenericResult(success: true, data: null);
}
}

View file

@ -1,34 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
class HetznerApiFactory extends ServerProviderApiFactory
with VolumeProviderApiFactory {
HetznerApiFactory({this.region});
final String? region;
@override
ServerProviderApi getServerProvider({
final ServerProviderApiSettings settings =
const ServerProviderApiSettings(),
}) =>
HetznerApi(
region: settings.region ?? region,
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
);
@override
VolumeProviderApi getVolumeProvider({
final ServerProviderApiSettings settings =
const ServerProviderApiSettings(),
}) =>
HetznerApi(
region: settings.region ?? region,
hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken,
);
}

View file

@ -1,79 +0,0 @@
import 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
class ProviderApiTokenValidation {
ProviderApiTokenValidation({
required this.length,
required this.regexp,
});
final int length;
final RegExp regexp;
}
abstract class ServerProviderApi extends ApiMap {
Future<List<ServerBasicInfo>> getServers();
Future<APIGenericResult<List<ServerProviderLocation>>>
getAvailableLocations();
Future<APIGenericResult<List<ServerType>>> getServerTypesByLocation({
required final ServerProviderLocation location,
});
Future<ServerHostingDetails> restart();
Future<ServerHostingDetails> powerOn();
Future<APIGenericResult<bool>> deleteServer({
required final String domainName,
});
Future<APIGenericResult<ServerHostingDetails?>> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final String serverType,
required final DnsProvider dnsProvider,
});
Future<APIGenericResult<void>> createReverseDns({
required final ServerHostingDetails serverDetails,
required final ServerDomain domain,
});
Future<APIGenericResult<bool>> isApiTokenValid(final String token);
ProviderApiTokenValidation getApiTokenValidation();
Future<List<ServerMetadataEntity>> getMetadata(final int serverId);
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
);
String dnsProviderToInfectName(final DnsProvider dnsProvider) {
String dnsProviderType;
switch (dnsProvider) {
case DnsProvider.desec:
dnsProviderType = 'DESEC';
break;
case DnsProvider.cloudflare:
default:
dnsProviderType = 'CLOUDFLARE';
break;
}
return dnsProviderType;
}
/// Provider name key which lets infect understand what kind of installation
/// it requires, for example 'digitaloceal' for Digital Ocean
String get infectProviderName;
/// Actual provider name to render on information page for user,
/// for example 'Digital Ocean' for Digital Ocean
String get displayProviderName;
}

View file

@ -1,11 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
class ServerProviderApiSettings extends ProviderApiSettings {
const ServerProviderApiSettings({
this.region,
super.hasLogger = false,
super.isWithToken = true,
});
final String? region;
}

View file

@ -1,15 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
abstract class ServerProviderApiFactory {
ServerProviderApi getServerProvider({
final ServerProviderApiSettings settings,
});
}
mixin VolumeProviderApiFactory {
VolumeProviderApi getVolumeProvider({
final ServerProviderApiSettings settings,
});
}

View file

@ -1,20 +0,0 @@
import 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/price.dart';
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
mixin VolumeProviderApi on ApiMap {
Future<APIGenericResult<ServerVolume?>> createVolume();
Future<List<ServerVolume>> getVolumes({final String? status});
Future<APIGenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
);
Future<bool> detachVolume(final ServerVolume volume);
Future<bool> resizeVolume(final ServerVolume volume, final DiskSize size);
Future<void> deleteVolume(final ServerVolume volume);
Future<Price?> getPricePerGb();
}

View file

@ -1,11 +1,11 @@
/// Controls staging environment for network
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

View file

@ -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 {

View file

@ -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();

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/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');

View file

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

View file

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

View file

@ -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());

View file

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

View file

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

View file

@ -53,7 +53,7 @@ class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
}
await Future.delayed(const Duration(seconds: 2));
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(

View file

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

View file

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

View file

@ -0,0 +1,21 @@
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
class CallbackDialogueBranching {
CallbackDialogueBranching({
required this.title,
required this.description,
required this.choices,
});
final String title;
final String description;
final List<CallbackDialogueChoice> choices;
}
class CallbackDialogueChoice {
CallbackDialogueChoice({
required this.title,
required this.callback,
});
final String title;
final Future<GenericResult<CallbackDialogueBranching?>> Function()? callback;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,65 @@
import 'package:json_annotation/json_annotation.dart';
part 'digital_ocean_server_info.g.dart';
@JsonSerializable()
class DigitalOceanVolume {
DigitalOceanVolume(
this.id,
this.name,
this.sizeGigabytes,
this.dropletIds,
);
final String id;
final String name;
@JsonKey(name: 'droplet_ids')
final List<int>? dropletIds;
@JsonKey(name: 'size_gigabytes')
final int sizeGigabytes;
static DigitalOceanVolume fromJson(final Map<String, dynamic> json) =>
_$DigitalOceanVolumeFromJson(json);
}
@JsonSerializable()
class DigitalOceanLocation {
DigitalOceanLocation(
this.slug,
this.name,
);
final String slug;
final String name;
static DigitalOceanLocation fromJson(final Map<String, dynamic> json) =>
_$DigitalOceanLocationFromJson(json);
}
@JsonSerializable()
class DigitalOceanServerType {
DigitalOceanServerType(
this.regions,
this.memory,
this.description,
this.disk,
this.priceMonthly,
this.slug,
this.vcpus,
);
final List<String> regions;
final double memory;
final String slug;
final String description;
final int vcpus;
final int disk;
@JsonKey(name: 'price_monthly')
final double priceMonthly;
static DigitalOceanServerType fromJson(final Map<String, dynamic> json) =>
_$DigitalOceanServerTypeFromJson(json);
}

View file

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

View file

@ -9,6 +9,7 @@ class DnsRecord {
required this.type,
required this.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);
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,23 @@
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
class LaunchInstallationData {
LaunchInstallationData({
required this.rootUser,
required this.dnsApiToken,
required this.dnsProviderType,
required this.serverDomain,
required this.serverTypeId,
required this.errorCallback,
required this.successCallback,
});
final User rootUser;
final String dnsApiToken;
final ServerDomain serverDomain;
final DnsProviderType dnsProviderType;
final String serverTypeId;
final Function() errorCallback;
final Function(ServerHostingDetails details) successCallback;
}

View file

@ -19,11 +19,11 @@ enum MetadataType {
class ServerMetadataEntity {
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;
}

View file

@ -0,0 +1,356 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
: _api = CloudflareApi(
isWithToken: isWithToken,
);
CloudflareApi api({final bool getInitialized = true}) => getInitialized
? _api
: CloudflareApi(
isWithToken: false,
);
final CloudflareApi _api;
}
class CloudflareDnsProvider extends DnsProvider {
CloudflareDnsProvider() : _adapter = ApiAdapter();
CloudflareDnsProvider.load(
final bool isAuthotized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
);
ApiAdapter _adapter;
@override
DnsProviderType get type => DnsProviderType.cloudflare;
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);
final result = await api.isApiTokenValid(token);
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(isWithToken: true);
return result;
}
@override
Future<GenericResult<String?>> getZoneId(final String domain) async {
String? id;
final result = await _adapter.api().getZones(domain);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: id,
code: result.code,
message: result.message,
);
}
id = result.data[0]['id'];
return GenericResult(success: true, data: id);
}
@override
Future<GenericResult<void>> removeDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final result = await _adapter.api().getDnsRecords(domain: domain);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: null,
code: result.code,
message: result.message,
);
}
return _adapter.api().removeSimilarRecords(
domain: domain,
records: result.data,
);
}
@override
Future<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
}) async {
final List<DnsRecord> records = [];
final result = await _adapter.api().getDnsRecords(domain: domain);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: records,
code: result.code,
message: result.message,
);
}
for (final rawRecord in result.data) {
records.add(
DnsRecord(
name: rawRecord['name'],
type: rawRecord['type'],
content: rawRecord['content'],
ttl: rawRecord['ttl'],
proxied: rawRecord['proxied'],
),
);
}
return GenericResult(
success: result.success,
data: records,
);
}
@override
Future<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) {
final records = getProjectDnsRecords(domain.domainName, ip4);
return _adapter.api().createMultipleDnsRecords(
domain: domain,
records: records,
);
}
@override
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: [record],
);
@override
Future<GenericResult<List<String>>> domainList() async {
List<String> domains = [];
final result = await _adapter.api().getDomains();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: domains,
code: result.code,
message: result.message,
);
}
domains = result.data
.map<String>(
(final el) => el['name'] as String,
)
.toList();
return GenericResult(
success: true,
data: domains,
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final GenericResult<List<DnsRecord>> records =
await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> desiredRecords =
getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey);
for (final DesiredDnsRecord record in desiredRecords) {
if (record.description == 'record.dkim') {
final DnsRecord foundRecord = records.data.firstWhere(
(final r) => (r.name == record.name) && r.type == record.type,
orElse: () => DnsRecord(
name: record.name,
type: record.type,
content: '',
ttl: 800,
proxied: false,
),
);
// remove all spaces and tabulators from
// the foundRecord.content and the record.content
// to compare them
final String? foundContent =
foundRecord.content?.replaceAll(RegExp(r'\s+'), '');
final String content = record.content.replaceAll(RegExp(r'\s+'), '');
if (foundContent == content) {
foundRecords.add(record.copyWith(isSatisfied: true));
} else {
foundRecords.add(record.copyWith(isSatisfied: false));
}
} else {
if (records.data.any(
(final r) =>
(r.name == record.name) &&
r.type == record.type &&
r.content == record.content,
)) {
foundRecords.add(record.copyWith(isSatisfied: true));
} else {
foundRecords.add(record.copyWith(isSatisfied: false));
}
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: foundRecords,
success: true,
);
}
@override
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
) {
if (domainName == null || ip4 == null) {
return [];
}
return [
DesiredDnsRecord(
name: domainName,
content: ip4,
description: 'record.root',
),
DesiredDnsRecord(
name: 'api.$domainName',
content: ip4,
description: 'record.api',
),
DesiredDnsRecord(
name: 'cloud.$domainName',
content: ip4,
description: 'record.cloud',
),
DesiredDnsRecord(
name: 'git.$domainName',
content: ip4,
description: 'record.git',
),
DesiredDnsRecord(
name: 'meet.$domainName',
content: ip4,
description: 'record.meet',
),
DesiredDnsRecord(
name: 'social.$domainName',
content: ip4,
description: 'record.social',
),
DesiredDnsRecord(
name: 'password.$domainName',
content: ip4,
description: 'record.password',
),
DesiredDnsRecord(
name: 'vpn.$domainName',
content: ip4,
description: 'record.vpn',
),
DesiredDnsRecord(
name: domainName,
content: domainName,
description: 'record.mx',
type: 'MX',
category: DnsRecordsCategory.email,
),
DesiredDnsRecord(
name: '_dmarc.$domainName',
content: 'v=DMARC1; p=none',
description: 'record.dmarc',
type: 'TXT',
category: DnsRecordsCategory.email,
),
DesiredDnsRecord(
name: domainName,
content: 'v=spf1 a mx ip4:$ip4 -all',
description: 'record.spf',
type: 'TXT',
category: DnsRecordsCategory.email,
),
if (dkimPublicKey != null)
DesiredDnsRecord(
name: 'selector._domainkey.$domainName',
content: dkimPublicKey,
description: 'record.dkim',
type: 'TXT',
category: DnsRecordsCategory.email,
),
];
}
List<DnsRecord> getProjectDnsRecords(
final String? domainName,
final String? ip4,
) {
final DnsRecord domainA =
DnsRecord(type: 'A', name: domainName, content: ip4);
final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName);
final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4);
final DnsRecord passwordA =
DnsRecord(type: 'A', name: 'password', content: ip4);
final DnsRecord socialA =
DnsRecord(type: 'A', name: 'social', content: ip4);
final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4);
final DnsRecord txt1 = DnsRecord(
type: 'TXT',
name: '_dmarc',
content: 'v=DMARC1; p=none',
ttl: 18000,
);
final DnsRecord txt2 = DnsRecord(
type: 'TXT',
name: domainName,
content: 'v=spf1 a mx ip4:$ip4 -all',
ttl: 18000,
);
return <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
}

View file

@ -1,173 +1,113 @@
import 'dart:io';
import 'package: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,
);

View file

@ -0,0 +1,359 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
: _api = DigitalOceanDnsApi(
isWithToken: isWithToken,
);
DigitalOceanDnsApi api({final bool getInitialized = true}) => getInitialized
? _api
: DigitalOceanDnsApi(
isWithToken: false,
);
final DigitalOceanDnsApi _api;
}
class DigitalOceanDnsProvider extends DnsProvider {
DigitalOceanDnsProvider() : _adapter = ApiAdapter();
DigitalOceanDnsProvider.load(
final bool isAuthotized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
);
ApiAdapter _adapter;
@override
DnsProviderType get type => DnsProviderType.digitalOcean;
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);
final result = await api.isApiTokenValid(token);
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(isWithToken: true);
return result;
}
@override
Future<GenericResult<String?>> getZoneId(final String domain) async =>
GenericResult(
data: domain,
success: true,
);
@override
Future<GenericResult<void>> removeDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) async {
final result = await _adapter.api().getDnsRecords(domain: domain);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: null,
code: result.code,
message: result.message,
);
}
const ignoreType = 'SOA';
final filteredRecords = [];
for (final record in result.data) {
if (record['type'] != ignoreType) {
filteredRecords.add(record);
}
}
return _adapter.api().removeSimilarRecords(
domain: domain,
records: filteredRecords,
);
}
@override
Future<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
}) async {
final List<DnsRecord> records = [];
final result = await _adapter.api().getDnsRecords(domain: domain);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: records,
code: result.code,
message: result.message,
);
}
for (final rawRecord in result.data) {
records.add(
DnsRecord(
id: rawRecord['id'],
name: rawRecord['name'],
type: rawRecord['type'],
content: rawRecord['data'],
ttl: rawRecord['ttl'],
proxied: false,
),
);
}
return GenericResult(data: records, success: true);
}
@override
Future<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
}) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: getProjectDnsRecords(
domain.domainName,
ip4,
),
);
@override
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
) async =>
_adapter.api().createMultipleDnsRecords(
domain: domain,
records: [record],
);
@override
Future<GenericResult<List<String>>> domainList() async {
List<String> domains = [];
final result = await _adapter.api().domainList();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: domains,
code: result.code,
message: result.message,
);
}
domains = result.data
.map<String>(
(final el) => el['name'] as String,
)
.toList();
return GenericResult(
success: true,
data: domains,
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final GenericResult<List<DnsRecord>> records =
await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> desiredRecords =
getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey);
for (final DesiredDnsRecord record in desiredRecords) {
if (record.description == 'record.dkim') {
final DnsRecord foundRecord = records.data.firstWhere(
(final r) => (r.name == record.name) && r.type == record.type,
orElse: () => DnsRecord(
name: record.name,
type: record.type,
content: '',
ttl: 800,
proxied: false,
),
);
// remove all spaces and tabulators from
// the foundRecord.content and the record.content
// to compare them
final String? foundContent =
foundRecord.content?.replaceAll(RegExp(r'\s+'), '');
final String content = record.content.replaceAll(RegExp(r'\s+'), '');
if (foundContent == content) {
foundRecords.add(record.copyWith(isSatisfied: true));
} else {
foundRecords.add(record.copyWith(isSatisfied: false));
}
} else {
if (records.data.any(
(final r) =>
(r.name == record.name) &&
r.type == record.type &&
r.content == record.content,
)) {
foundRecords.add(record.copyWith(isSatisfied: true));
} else {
foundRecords.add(record.copyWith(isSatisfied: false));
}
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: foundRecords,
success: true,
);
}
List<DnsRecord> getProjectDnsRecords(
final String? domainName,
final String? ip4,
) {
final DnsRecord domainA = DnsRecord(type: 'A', name: '@', content: ip4);
final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: '@');
final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4);
final DnsRecord passwordA =
DnsRecord(type: 'A', name: 'password', content: ip4);
final DnsRecord socialA =
DnsRecord(type: 'A', name: 'social', content: ip4);
final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4);
final DnsRecord txt1 = DnsRecord(
type: 'TXT',
name: '_dmarc',
content: 'v=DMARC1; p=none',
ttl: 18000,
);
final DnsRecord txt2 = DnsRecord(
type: 'TXT',
name: '@',
content: 'v=spf1 a mx ip4:$ip4 -all',
ttl: 18000,
);
return <DnsRecord>[
domainA,
apiA,
cloudA,
gitA,
meetA,
passwordA,
socialA,
mx,
txt1,
txt2,
vpn
];
}
@override
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
) {
if (domainName == null || ip4 == null) {
return [];
}
return [
DesiredDnsRecord(
name: '@',
content: ip4,
description: 'record.root',
displayName: domainName,
),
DesiredDnsRecord(
name: 'api',
content: ip4,
description: 'record.api',
displayName: 'api.$domainName',
),
DesiredDnsRecord(
name: 'cloud',
content: ip4,
description: 'record.cloud',
displayName: 'cloud.$domainName',
),
DesiredDnsRecord(
name: 'git',
content: ip4,
description: 'record.git',
displayName: 'git.$domainName',
),
DesiredDnsRecord(
name: 'meet',
content: ip4,
description: 'record.meet',
displayName: 'meet.$domainName',
),
DesiredDnsRecord(
name: 'social',
content: ip4,
description: 'record.social',
displayName: 'social.$domainName',
),
DesiredDnsRecord(
name: 'password',
content: ip4,
description: 'record.password',
displayName: 'password.$domainName',
),
DesiredDnsRecord(
name: 'vpn',
content: ip4,
description: 'record.vpn',
displayName: 'vpn.$domainName',
),
const DesiredDnsRecord(
name: '@',
content: '@',
description: 'record.mx',
type: 'MX',
category: DnsRecordsCategory.email,
),
const DesiredDnsRecord(
name: '_dmarc',
content: 'v=DMARC1; p=none',
description: 'record.dmarc',
type: 'TXT',
category: DnsRecordsCategory.email,
),
DesiredDnsRecord(
name: '@',
content: 'v=spf1 a mx ip4:$ip4 -all',
description: 'record.spf',
type: 'TXT',
category: DnsRecordsCategory.email,
),
if (dkimPublicKey != null)
DesiredDnsRecord(
name: 'selector._domainkey',
content: dkimPublicKey,
description: 'record.dkim',
type: 'TXT',
category: DnsRecordsCategory.email,
),
];
}
}

View file

@ -0,0 +1,37 @@
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
abstract class DnsProvider {
DnsProviderType get type;
Future<GenericResult<bool>> tryInitApiByToken(final String token);
Future<GenericResult<String?>> getZoneId(final String domain);
Future<GenericResult<void>> removeDomainRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<GenericResult<List<DnsRecord>>> getDnsRecords({
required final ServerDomain domain,
});
Future<GenericResult<void>> createDomainRecords({
required final ServerDomain domain,
final String? ip4,
});
Future<GenericResult<void>> setDnsRecord(
final DnsRecord record,
final ServerDomain domain,
);
Future<GenericResult<List<String>>> domainList();
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
);
}

View file

@ -0,0 +1,28 @@
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/providers/dns_providers/cloudflare.dart';
import 'package:selfprivacy/logic/providers/dns_providers/desec.dart';
import 'package:selfprivacy/logic/providers/dns_providers/digital_ocean_dns.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
class UnknownProviderException implements Exception {
UnknownProviderException(this.message);
final String message;
}
class DnsProviderFactory {
static DnsProvider createDnsProviderInterface(
final DnsProviderSettings settings,
) {
switch (settings.provider) {
case DnsProviderType.cloudflare:
return CloudflareDnsProvider();
case DnsProviderType.digitalOcean:
return DigitalOceanDnsProvider();
case DnsProviderType.desec:
return DesecDnsProvider();
case DnsProviderType.unknown:
throw UnknownProviderException('Unknown server provider');
}
}
}

View file

@ -1,20 +1,20 @@
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_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;
}

View file

@ -0,0 +1,34 @@
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider_factory.dart';
class ProvidersController {
static ServerProvider? get currentServerProvider => _serverProvider;
static DnsProvider? get currentDnsProvider => _dnsProvider;
static void initServerProvider(
final ServerProviderSettings settings,
) {
_serverProvider = ServerProviderFactory.createServerProviderInterface(
settings,
);
}
static void initDnsProvider(
final DnsProviderSettings settings,
) {
_dnsProvider = DnsProviderFactory.createDnsProviderInterface(
settings,
);
}
static void clearProviders() {
_serverProvider = null;
_dnsProvider = null;
}
static ServerProvider? _serverProvider;
static DnsProvider? _dnsProvider;
}

View file

@ -0,0 +1,812 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/digital_ocean_server_info.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class ApiAdapter {
ApiAdapter({final String? region, final bool isWithToken = true})
: _api = DigitalOceanApi(
region: region,
isWithToken: isWithToken,
);
DigitalOceanApi api({final bool getInitialized = true}) => getInitialized
? _api
: DigitalOceanApi(
region: _api.region,
isWithToken: false,
);
final DigitalOceanApi _api;
}
class DigitalOceanServerProvider extends ServerProvider {
DigitalOceanServerProvider() : _adapter = ApiAdapter();
DigitalOceanServerProvider.load(
final ServerType serverType,
final bool isAuthotized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
region: serverType.location.identifier,
);
ApiAdapter _adapter;
final String currency = 'USD';
@override
ServerProviderType get type => ServerProviderType.digitalOcean;
@override
Future<GenericResult<bool>> trySetServerLocation(
final String location,
) async {
final bool apiInitialized = _adapter.api().isWithToken;
if (!apiInitialized) {
return GenericResult(
success: true,
data: false,
message: 'Not authorized!',
);
}
_adapter = ApiAdapter(
isWithToken: true,
region: location,
);
return success;
}
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);
final result = await api.isApiTokenValid(token);
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(region: api.region, isWithToken: true);
return result;
}
String? getEmojiFlag(final String query) {
String? emoji;
switch (query.toLowerCase().substring(0, 3)) {
case 'fra':
emoji = '🇩🇪';
break;
case 'ams':
emoji = '🇳🇱';
break;
case 'sgp':
emoji = '🇸🇬';
break;
case 'lon':
emoji = '🇬🇧';
break;
case 'tor':
emoji = '🇨🇦';
break;
case 'blr':
emoji = '🇮🇳';
break;
case 'nyc':
case 'sfo':
emoji = '🇺🇸';
break;
}
return emoji;
}
String dnsProviderToInfectName(final DnsProviderType dnsProvider) {
String dnsProviderType;
switch (dnsProvider) {
case DnsProviderType.digitalOcean:
dnsProviderType = 'DIGITALOCEAN';
break;
case DnsProviderType.cloudflare:
default:
dnsProviderType = 'CLOUDFLARE';
break;
}
return dnsProviderType;
}
@override
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,
) async {
ServerHostingDetails? serverDetails;
final serverApiToken = StringGenerators.apiToken();
final hostname = getHostnameFromDomain(
installationData.serverDomain.domainName,
);
final serverResult = await _adapter.api().createServer(
dnsApiToken: installationData.dnsApiToken,
rootUser: installationData.rootUser,
domainName: installationData.serverDomain.domainName,
serverType: installationData.serverTypeId,
dnsProviderType:
dnsProviderToInfectName(installationData.dnsProviderType),
hostName: hostname,
base64Password: base64.encode(
utf8.encode(installationData.rootUser.password ?? 'PASS'),
),
databasePassword: StringGenerators.dbPassword(),
serverApiToken: serverApiToken,
);
if (!serverResult.success || serverResult.data == null) {
GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: () async => await installationData.errorCallback(),
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async => launchInstallation(installationData),
),
],
description: serverResult.message ?? 'recovering.generic_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: serverResult.message,
code: serverResult.code,
);
}
try {
final int dropletId = serverResult.data!;
final newVolume = (await createVolume()).data;
final bool attachedVolume = (await _adapter.api().attachVolume(
newVolume!.name,
dropletId,
))
.data;
String? ipv4;
int attempts = 0;
while (attempts < 5 && ipv4 == null) {
await Future.delayed(const Duration(seconds: 20));
final servers = await getServers();
for (final server in servers.data) {
if (server.name == hostname && server.ip != '0.0.0.0') {
ipv4 = server.ip;
break;
}
}
++attempts;
}
if (attachedVolume && ipv4 != null) {
serverDetails = ServerHostingDetails(
id: dropletId,
ip4: ipv4,
createTime: DateTime.now(),
volume: newVolume,
apiToken: serverApiToken,
provider: ServerProviderType.digitalOcean,
);
}
} catch (e) {
return GenericResult(
success: false,
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async {
await Future.delayed(const Duration(seconds: 5));
final deletion = await deleteServer(hostname);
return deletion.success
? await launchInstallation(installationData)
: deletion;
},
),
],
description: 'modals.try_again'.tr(),
title: 'modals.server_deletion_error'.tr(),
),
message: e.toString(),
);
}
await installationData.successCallback(serverDetails!);
return GenericResult(success: true, data: null);
}
@override
Future<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
final List<ServerProviderLocation> locations = [];
final result = await _adapter.api().getAvailableLocations();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: locations,
code: result.code,
message: result.message,
);
}
final List<DigitalOceanLocation> rawLocations = result.data;
for (final rawLocation in rawLocations) {
ServerProviderLocation? location;
try {
location = ServerProviderLocation(
title: rawLocation.slug,
description: rawLocation.name,
flag: getEmojiFlag(rawLocation.slug),
identifier: rawLocation.slug,
);
} catch (e) {
continue;
}
locations.add(location);
}
return GenericResult(success: true, data: locations);
}
@override
Future<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
}) async {
final List<ServerType> types = [];
final result = await _adapter.api().getAvailableServerTypes();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: types,
code: result.code,
message: result.message,
);
}
final List<DigitalOceanServerType> rawSizes = result.data;
for (final rawSize in rawSizes) {
for (final rawRegion in rawSize.regions) {
if (rawRegion == location.identifier && rawSize.memory > 1024) {
types.add(
ServerType(
title: rawSize.description,
identifier: rawSize.slug,
ram: rawSize.memory / 1024,
cores: rawSize.vcpus,
disk: DiskSize(byte: rawSize.disk * 1024 * 1024 * 1024),
price: Price(
value: rawSize.priceMonthly,
currency: currency,
),
location: location,
),
);
}
}
}
return GenericResult(success: true, data: types);
}
@override
Future<GenericResult<List<ServerBasicInfo>>> getServers() async {
List<ServerBasicInfo> servers = [];
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: servers,
code: result.code,
message: result.message,
);
}
final List rawServers = result.data;
servers = rawServers.map<ServerBasicInfo>(
(final server) {
String ipv4 = '0.0.0.0';
if (server['networks']['v4'].isNotEmpty) {
for (final v4 in server['networks']['v4']) {
if (v4['type'].toString() == 'public') {
ipv4 = v4['ip_address'].toString();
}
}
}
return ServerBasicInfo(
id: server['id'],
reverseDns: server['name'],
created: DateTime.now(),
ip: ipv4,
name: server['name'],
);
},
).toList();
return GenericResult(success: true, data: servers);
}
@override
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
) async {
List<ServerMetadataEntity> metadata = [];
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: false,
data: metadata,
code: result.code,
message: result.message,
);
}
final List servers = result.data;
try {
final droplet = servers.firstWhere(
(final server) => server['id'] == serverId,
);
metadata = [
ServerMetadataEntity(
type: MetadataType.id,
trId: 'server.server_id',
value: droplet['id'].toString(),
),
ServerMetadataEntity(
type: MetadataType.status,
trId: 'server.status',
value: droplet['status'].toString().capitalize(),
),
ServerMetadataEntity(
type: MetadataType.cpu,
trId: 'server.cpu',
value: droplet['vcpus'].toString(),
),
ServerMetadataEntity(
type: MetadataType.ram,
trId: 'server.ram',
value: "${droplet['memory'].toString()} MB",
),
ServerMetadataEntity(
type: MetadataType.cost,
trId: 'server.monthly_cost',
value: '${droplet['size']['price_monthly']} $currency',
),
ServerMetadataEntity(
type: MetadataType.location,
trId: 'server.location',
value:
'${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}',
),
ServerMetadataEntity(
type: MetadataType.other,
trId: 'server.provider',
value: _adapter.api().displayProviderName,
),
];
} catch (e) {
return GenericResult(
success: false,
data: [],
message: e.toString(),
);
}
return GenericResult(success: true, data: metadata);
}
/// Digital Ocean returns a map of lists of /proc/stat values,
/// so here we are trying to implement average CPU
/// load calculation for each point in time on a given interval.
///
/// For each point of time:
///
/// `Average Load = 100 * (1 - (Idle Load / Total Load))`
///
/// For more info please proceed to read:
/// https://rosettacode.org/wiki/Linux_CPU_utilization
List<TimeSeriesData> calculateCpuLoadMetrics(final List rawProcStatMetrics) {
final List<TimeSeriesData> cpuLoads = [];
final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length;
for (int i = 0; i < pointsInTime; ++i) {
double currentMetricLoad = 0.0;
double? currentMetricIdle;
for (final rawProcStat in rawProcStatMetrics) {
final String rawProcValue = rawProcStat['values'][i][1];
// Converting MBit into bit
final double procValue = double.parse(rawProcValue) * 1000000;
currentMetricLoad += procValue;
if (currentMetricIdle == null &&
rawProcStat['metric']['mode'] == 'idle') {
currentMetricIdle = procValue;
}
}
currentMetricIdle ??= 0.0;
currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad));
cpuLoads.add(
TimeSeriesData(
rawProcStatMetrics[0]['values'][i][0],
currentMetricLoad,
),
);
}
return cpuLoads;
}
@override
Future<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
const int step = 15;
final inboundResult = await _adapter.api().getMetricsBandwidth(
serverId,
start,
end,
true,
);
if (inboundResult.data.isEmpty || !inboundResult.success) {
return GenericResult(
success: false,
data: null,
code: inboundResult.code,
message: inboundResult.message,
);
}
final outboundResult = await _adapter.api().getMetricsBandwidth(
serverId,
start,
end,
false,
);
if (outboundResult.data.isEmpty || !outboundResult.success) {
return GenericResult(
success: false,
data: null,
code: outboundResult.code,
message: outboundResult.message,
);
}
final cpuResult = await _adapter.api().getMetricsCpu(serverId, start, end);
if (cpuResult.data.isEmpty || !cpuResult.success) {
return GenericResult(
success: false,
data: null,
code: cpuResult.code,
message: cpuResult.message,
);
}
metrics = ServerMetrics(
bandwidthIn: inboundResult.data
.map(
(final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000),
)
.toList(),
bandwidthOut: outboundResult.data
.map(
(final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000),
)
.toList(),
cpu: calculateCpuLoadMetrics(cpuResult.data),
start: start,
end: end,
stepsInSecond: step,
);
return GenericResult(success: true, data: metrics);
}
@override
Future<GenericResult<DateTime?>> restart(final int serverId) async {
DateTime? timestamp;
final result = await _adapter.api().restart(serverId);
if (!result.success) {
return GenericResult(
success: false,
data: timestamp,
code: result.code,
message: result.message,
);
}
timestamp = DateTime.now();
return GenericResult(
success: true,
data: timestamp,
);
}
@override
Future<GenericResult<CallbackDialogueBranching?>> deleteServer(
final String hostname,
) async {
final String deletionName = getHostnameFromDomain(hostname);
final serversResult = await getServers();
try {
final servers = serversResult.data;
ServerBasicInfo? foundServer;
for (final server in servers) {
if (server.name == deletionName) {
foundServer = server;
break;
}
}
final volumes = await getVolumes();
final ServerVolume volumeToRemove;
volumeToRemove = volumes.data.firstWhere(
(final el) => el.serverId == foundServer!.id,
);
await _adapter.api().detachVolume(
volumeToRemove.name,
volumeToRemove.serverId!,
);
await Future.delayed(const Duration(seconds: 10));
final List<Future> laterFutures = <Future>[];
laterFutures.add(_adapter.api().deleteVolume(volumeToRemove.uuid!));
laterFutures.add(_adapter.api().deleteServer(foundServer!.id));
await Future.wait(laterFutures);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async {
await Future.delayed(const Duration(seconds: 5));
return deleteServer(hostname);
},
),
],
description: 'modals.try_again'.tr(),
title: 'modals.server_deletion_error'.tr(),
),
message: e.toString(),
);
}
return GenericResult(
success: true,
data: null,
);
}
@override
Future<GenericResult<List<ServerVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> volumes = [];
final result = await _adapter.api().getVolumes();
if (!result.success || result.data.isEmpty) {
return GenericResult(
data: [],
success: false,
code: result.code,
message: result.message,
);
}
try {
int id = 0;
for (final rawVolume in result.data) {
final String volumeName = rawVolume.name;
final volume = ServerVolume(
id: id++,
name: volumeName,
sizeByte: rawVolume.sizeGigabytes * 1024 * 1024 * 1024,
serverId:
(rawVolume.dropletIds != null && rawVolume.dropletIds!.isNotEmpty)
? rawVolume.dropletIds![0]
: null,
linuxDevice: 'scsi-0DO_Volume_$volumeName',
uuid: rawVolume.id,
);
volumes.add(volume);
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: volumes,
success: true,
);
}
@override
Future<GenericResult<ServerVolume?>> createVolume() async {
ServerVolume? volume;
final result = await _adapter.api().createVolume();
if (!result.success || result.data == null) {
return GenericResult(
data: null,
success: false,
code: result.code,
message: result.message,
);
}
final getVolumesResult = await _adapter.api().getVolumes();
if (!getVolumesResult.success || getVolumesResult.data.isEmpty) {
return GenericResult(
data: null,
success: false,
code: result.code,
message: result.message,
);
}
final String volumeName = result.data!.name;
volume = ServerVolume(
id: getVolumesResult.data.length,
name: volumeName,
sizeByte: result.data!.sizeGigabytes,
serverId: null,
linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName',
uuid: result.data!.id,
);
return GenericResult(
data: volume,
success: true,
);
}
Future<GenericResult<ServerVolume?>> getVolume(
final String volumeUuid,
) async {
ServerVolume? requestedVolume;
final result = await getVolumes();
if (!result.success || result.data.isEmpty) {
return GenericResult(
data: null,
success: false,
code: result.code,
message: result.message,
);
}
for (final volume in result.data) {
if (volume.uuid == volumeUuid) {
requestedVolume = volume;
}
}
return GenericResult(
data: requestedVolume,
success: true,
);
}
@override
Future<GenericResult<void>> deleteVolume(
final ServerVolume volume,
) async =>
_adapter.api().deleteVolume(
volume.uuid!,
);
@override
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
volume.name,
serverId,
);
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
) async =>
_adapter.api().detachVolume(
volume.name,
volume.serverId!,
);
@override
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
) async =>
_adapter.api().resizeVolume(
volume.name,
size,
);
/// Hardcoded on their documentation and there is no pricing API at all
/// Probably we should scrap the doc page manually
@override
Future<GenericResult<Price?>> getPricePerGb() async => GenericResult(
success: true,
data: Price(
value: 0.10,
currency: currency,
),
);
@override
Future<GenericResult<DateTime?>> powerOn(final int serverId) async {
DateTime? timestamp;
final result = await _adapter.api().powerOn(serverId);
if (!result.success) {
return GenericResult(
success: false,
data: timestamp,
code: result.code,
message: result.message,
);
}
timestamp = DateTime.now();
return GenericResult(
success: true,
data: timestamp,
);
}
}

View file

@ -0,0 +1,807 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class ApiAdapter {
ApiAdapter({final String? region, final bool isWithToken = true})
: _api = HetznerApi(
region: region,
isWithToken: isWithToken,
);
HetznerApi api({final bool getInitialized = true}) => getInitialized
? _api
: HetznerApi(
region: _api.region,
isWithToken: false,
);
final HetznerApi _api;
}
class HetznerServerProvider extends ServerProvider {
HetznerServerProvider() : _adapter = ApiAdapter();
HetznerServerProvider.load(
final ServerType serverType,
final bool isAuthotized,
) : _adapter = ApiAdapter(
isWithToken: isAuthotized,
region: serverType.location.identifier,
);
ApiAdapter _adapter;
final String currency = 'EUR';
@override
ServerProviderType get type => ServerProviderType.hetzner;
@override
Future<GenericResult<bool>> trySetServerLocation(
final String location,
) async {
final bool apiInitialized = _adapter.api().isWithToken;
if (!apiInitialized) {
return GenericResult(
success: true,
data: false,
message: 'Not authorized!',
);
}
_adapter = ApiAdapter(
isWithToken: true,
region: location,
);
return success;
}
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);
final result = await api.isApiTokenValid(token);
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(region: api.region, isWithToken: true);
return result;
}
String? getEmojiFlag(final String query) {
String? emoji;
switch (query.toLowerCase()) {
case 'de':
emoji = '🇩🇪';
break;
case 'fi':
emoji = '🇫🇮';
break;
case 'us':
emoji = '🇺🇸';
break;
}
return emoji;
}
@override
Future<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
final List<ServerProviderLocation> locations = [];
final result = await _adapter.api().getAvailableLocations();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: locations,
code: result.code,
message: result.message,
);
}
final List<HetznerLocation> rawLocations = result.data;
for (final rawLocation in rawLocations) {
ServerProviderLocation? location;
try {
location = ServerProviderLocation(
title: rawLocation.city,
description: rawLocation.description,
flag: getEmojiFlag(rawLocation.country),
identifier: rawLocation.name,
);
} catch (e) {
continue;
}
locations.add(location);
}
return GenericResult(success: true, data: locations);
}
@override
Future<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
}) async {
final List<ServerType> types = [];
final result = await _adapter.api().getAvailableServerTypes();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: types,
code: result.code,
message: result.message,
);
}
final rawTypes = result.data;
for (final rawType in rawTypes) {
for (final rawPrice in rawType.prices) {
if (rawPrice.location == location.identifier) {
types.add(
ServerType(
title: rawType.description,
identifier: rawType.name,
ram: rawType.memory.toDouble(),
cores: rawType.cores,
disk: DiskSize(byte: rawType.disk * 1024 * 1024 * 1024),
price: Price(
value: rawPrice.monthly,
currency: currency,
),
location: location,
),
);
}
}
}
return GenericResult(success: true, data: types);
}
@override
Future<GenericResult<List<ServerBasicInfo>>> getServers() async {
final List<ServerBasicInfo> servers = [];
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: servers,
code: result.code,
message: result.message,
);
}
final List<HetznerServerInfo> hetznerServers = result.data;
for (final hetznerServer in hetznerServers) {
if (hetznerServer.publicNet.ipv4 == null) {
continue;
}
ServerBasicInfo? server;
try {
server = ServerBasicInfo(
id: hetznerServer.id,
name: hetznerServer.name,
ip: hetznerServer.publicNet.ipv4!.ip,
reverseDns: hetznerServer.publicNet.ipv4!.reverseDns,
created: hetznerServer.created,
);
} catch (e) {
continue;
}
servers.add(server);
}
return GenericResult(success: true, data: servers);
}
@override
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
) async {
List<ServerMetadataEntity> metadata = [];
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: false,
data: metadata,
code: result.code,
message: result.message,
);
}
final List<HetznerServerInfo> servers = result.data;
try {
final HetznerServerInfo server = servers.firstWhere(
(final server) => server.id == serverId,
);
metadata = [
ServerMetadataEntity(
type: MetadataType.id,
trId: 'server.server_id',
value: server.id.toString(),
),
ServerMetadataEntity(
type: MetadataType.status,
trId: 'server.status',
value: server.status.toString().split('.')[1].capitalize(),
),
ServerMetadataEntity(
type: MetadataType.cpu,
trId: 'server.cpu',
value: server.serverType.cores.toString(),
),
ServerMetadataEntity(
type: MetadataType.ram,
trId: 'server.ram',
value: '${server.serverType.memory.toString()} GB',
),
ServerMetadataEntity(
type: MetadataType.cost,
trId: 'server.monthly_cost',
value:
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} $currency',
),
ServerMetadataEntity(
type: MetadataType.location,
trId: 'server.location',
value: '${server.location.city}, ${server.location.country}',
),
ServerMetadataEntity(
type: MetadataType.other,
trId: 'server.provider',
value: _adapter.api().displayProviderName,
),
];
} catch (e) {
return GenericResult(
success: false,
data: [],
message: e.toString(),
);
}
return GenericResult(success: true, data: metadata);
}
@override
Future<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
List<TimeSeriesData> serializeTimeSeries(
final Map<String, dynamic> json,
final String type,
) {
final List list = json['time_series'][type]['values'];
return list
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
.toList();
}
final cpuResult = await _adapter.api().getMetrics(
serverId,
start,
end,
'cpu',
);
if (cpuResult.data.isEmpty || !cpuResult.success) {
return GenericResult(
success: false,
data: metrics,
code: cpuResult.code,
message: cpuResult.message,
);
}
final netResult = await _adapter.api().getMetrics(
serverId,
start,
end,
'network',
);
if (cpuResult.data.isEmpty || !netResult.success) {
return GenericResult(
success: false,
data: metrics,
code: netResult.code,
message: netResult.message,
);
}
metrics = ServerMetrics(
cpu: serializeTimeSeries(
cpuResult.data,
'cpu',
),
bandwidthIn: serializeTimeSeries(
netResult.data,
'network.0.bandwidth.in',
),
bandwidthOut: serializeTimeSeries(
netResult.data,
'network.0.bandwidth.out',
),
end: end,
start: start,
stepsInSecond: cpuResult.data['step'],
);
return GenericResult(data: metrics, success: true);
}
@override
Future<GenericResult<DateTime?>> restart(final int serverId) async {
DateTime? timestamp;
final result = await _adapter.api().restart(serverId);
if (!result.success) {
return GenericResult(
success: false,
data: timestamp,
code: result.code,
message: result.message,
);
}
timestamp = DateTime.now();
return GenericResult(
success: true,
data: timestamp,
);
}
@override
Future<GenericResult<DateTime?>> powerOn(final int serverId) async {
DateTime? timestamp;
final result = await _adapter.api().powerOn(serverId);
if (!result.success) {
return GenericResult(
success: false,
data: timestamp,
code: result.code,
message: result.message,
);
}
timestamp = DateTime.now();
return GenericResult(
success: true,
data: timestamp,
);
}
String dnsProviderToInfectName(final DnsProviderType dnsProvider) {
String dnsProviderType;
switch (dnsProvider) {
case DnsProviderType.digitalOcean:
dnsProviderType = 'DIGITALOCEAN';
break;
case DnsProviderType.desec:
dnsProviderType = 'DESEC';
break;
case DnsProviderType.cloudflare:
default:
dnsProviderType = 'CLOUDFLARE';
break;
}
return dnsProviderType;
}
@override
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,
) async {
final volumeResult = await _adapter.api().createVolume();
if (!volumeResult.success || volumeResult.data == null) {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: () async => await installationData.errorCallback(),
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async => launchInstallation(installationData),
),
],
description:
volumeResult.message ?? 'modals.volume_creation_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
}
final volume = volumeResult.data!;
final serverApiToken = StringGenerators.apiToken();
final hostname = getHostnameFromDomain(
installationData.serverDomain.domainName,
);
final serverResult = await _adapter.api().createServer(
dnsApiToken: installationData.dnsApiToken,
rootUser: installationData.rootUser,
domainName: installationData.serverDomain.domainName,
serverType: installationData.serverTypeId,
dnsProviderType:
dnsProviderToInfectName(installationData.dnsProviderType),
hostName: hostname,
volumeId: volume.id,
base64Password: base64.encode(
utf8.encode(installationData.rootUser.password ?? 'PASS'),
),
databasePassword: StringGenerators.dbPassword(),
serverApiToken: serverApiToken,
);
if (!serverResult.success || serverResult.data == null) {
await _adapter.api().deleteVolume(volume.id);
await Future.delayed(const Duration(seconds: 5));
if (serverResult.message != null &&
serverResult.message == 'uniqueness_error') {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: () async => installationData.errorCallback(),
),
CallbackDialogueChoice(
title: 'modals.yes'.tr(),
callback: () async {
final deleting = await deleteServer(hostname);
if (deleting.success) {
return launchInstallation(installationData);
}
return deleting;
},
),
],
description: 'modals.destroy_server'.tr(),
title: 'modals.already_exists'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
} else {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: () async {
final deletion = await deleteServer(hostname);
if (deletion.success) {
installationData.errorCallback();
}
return deletion;
},
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async => launchInstallation(installationData),
),
],
description:
volumeResult.message ?? 'recovering.generic_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
}
}
final serverDetails = ServerHostingDetails(
id: serverResult.data!.id,
ip4: serverResult.data!.publicNet.ipv4!.ip,
createTime: DateTime.now(),
volume: ServerVolume(
id: volume.id,
name: volume.name,
sizeByte: volume.size * 1024 * 1024 * 1024,
serverId: volume.serverId,
linuxDevice: volume.linuxDevice,
),
apiToken: serverApiToken,
provider: ServerProviderType.hetzner,
);
final createDnsResult = await _adapter.api().createReverseDns(
serverId: serverDetails.id,
ip4: serverDetails.ip4,
dnsPtr: installationData.serverDomain.domainName,
);
if (!createDnsResult.success) {
return GenericResult(
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: () async {
final deletion = await deleteServer(hostname);
if (deletion.success) {
installationData.errorCallback();
}
return deletion;
},
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async {
await _adapter.api().deleteVolume(volume.id);
await Future.delayed(const Duration(seconds: 5));
final deletion = await deleteServer(hostname);
if (deletion.success) {
await Future.delayed(const Duration(seconds: 5));
return launchInstallation(installationData);
}
return deletion;
},
),
],
description: volumeResult.message ?? 'recovering.generic_error'.tr(),
title: 'modals.unexpected_error'.tr(),
),
success: false,
message: volumeResult.message,
code: volumeResult.code,
);
}
await installationData.successCallback(serverDetails);
return GenericResult(success: true, data: null);
}
@override
Future<GenericResult<CallbackDialogueBranching?>> deleteServer(
final String hostname,
) async {
final serversResult = await _adapter.api().getServers();
try {
final servers = serversResult.data;
HetznerServerInfo? foundServer;
for (final server in servers) {
if (server.name == hostname) {
foundServer = server;
break;
}
}
for (final volumeId in foundServer!.volumes) {
await _adapter.api().detachVolume(volumeId);
}
await Future.delayed(const Duration(seconds: 10));
final List<Future> laterFutures = <Future>[];
for (final volumeId in foundServer.volumes) {
laterFutures.add(_adapter.api().deleteVolume(volumeId));
}
laterFutures.add(_adapter.api().deleteServer(serverId: foundServer.id));
await Future.wait(laterFutures);
} catch (e) {
print(e);
return GenericResult(
success: false,
data: CallbackDialogueBranching(
choices: [
CallbackDialogueChoice(
title: 'basis.cancel'.tr(),
callback: null,
),
CallbackDialogueChoice(
title: 'modals.try_again'.tr(),
callback: () async {
await Future.delayed(const Duration(seconds: 5));
return deleteServer(hostname);
},
),
],
description: 'modals.try_again'.tr(),
title: 'modals.server_deletion_error'.tr(),
),
message: e.toString(),
);
}
return GenericResult(
success: true,
data: null,
);
}
@override
Future<GenericResult<ServerVolume?>> createVolume() async {
ServerVolume? volume;
final result = await _adapter.api().createVolume();
if (!result.success || result.data == null) {
return GenericResult(
data: null,
success: false,
message: result.message,
code: result.code,
);
}
try {
volume = ServerVolume(
id: result.data!.id,
name: result.data!.name,
sizeByte: result.data!.size * 1024 * 1024 * 1024,
serverId: result.data!.serverId,
linuxDevice: result.data!.linuxDevice,
);
} catch (e) {
print(e);
return GenericResult(
data: null,
success: false,
message: e.toString(),
);
}
return GenericResult(
data: volume,
success: true,
code: result.code,
message: result.message,
);
}
@override
Future<GenericResult<List<ServerVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> volumes = [];
final result = await _adapter.api().getVolumes();
if (!result.success) {
return GenericResult(
data: [],
success: false,
message: result.message,
code: result.code,
);
}
try {
for (final rawVolume in result.data) {
final int volumeId = rawVolume.id;
final int volumeSize = rawVolume.size * 1024 * 1024 * 1024;
final volumeServer = rawVolume.serverId;
final String volumeName = rawVolume.name;
final volumeDevice = rawVolume.linuxDevice;
final volume = ServerVolume(
id: volumeId,
name: volumeName,
sizeByte: volumeSize,
serverId: volumeServer,
linuxDevice: volumeDevice,
);
volumes.add(volume);
}
} catch (e) {
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: volumes,
success: true,
code: result.code,
message: result.message,
);
}
@override
Future<GenericResult<void>> deleteVolume(final ServerVolume volume) async =>
_adapter.api().deleteVolume(volume.id);
@override
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
HetznerVolume(
volume.id,
volume.sizeByte,
volume.serverId,
volume.name,
volume.linuxDevice,
),
serverId,
);
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
) async =>
_adapter.api().detachVolume(
volume.id,
);
@override
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
) async =>
_adapter.api().resizeVolume(
HetznerVolume(
volume.id,
volume.sizeByte,
volume.serverId,
volume.name,
volume.linuxDevice,
),
size,
);
@override
Future<GenericResult<Price?>> getPricePerGb() async {
final result = await _adapter.api().getPricePerGb();
if (!result.success || result.data == null) {
return GenericResult(
data: null,
success: false,
message: result.message,
code: result.code,
);
}
return GenericResult(
success: true,
data: Price(
value: result.data!,
currency: currency,
),
);
}
}

View file

@ -0,0 +1,57 @@
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/launch_installation_data.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
export 'package:selfprivacy/logic/models/launch_installation_data.dart';
abstract class ServerProvider {
ServerProviderType get type;
Future<GenericResult<List<ServerBasicInfo>>> getServers();
Future<GenericResult<bool>> trySetServerLocation(final String location);
Future<GenericResult<bool>> tryInitApiByToken(final String token);
Future<GenericResult<List<ServerProviderLocation>>> getAvailableLocations();
Future<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
});
Future<GenericResult<CallbackDialogueBranching?>> deleteServer(
final String hostname,
);
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,
);
Future<GenericResult<DateTime?>> powerOn(final int serverId);
Future<GenericResult<DateTime?>> restart(final int serverId);
Future<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
);
Future<GenericResult<Price?>> getPricePerGb();
Future<GenericResult<List<ServerVolume>>> getVolumes({final String? status});
Future<GenericResult<ServerVolume?>> createVolume();
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
);
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
);
Future<GenericResult<bool>> detachVolume(final ServerVolume volume);
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
);
GenericResult<bool> get success => GenericResult(success: true, data: true);
}

View file

@ -0,0 +1,25 @@
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/providers/server_providers/digital_ocean.dart';
import 'package:selfprivacy/logic/providers/server_providers/hetzner.dart';
class UnknownProviderException implements Exception {
UnknownProviderException(this.message);
final String message;
}
class ServerProviderFactory {
static ServerProvider createServerProviderInterface(
final ServerProviderSettings settings,
) {
switch (settings.provider) {
case ServerProviderType.hetzner:
return HetznerServerProvider();
case ServerProviderType.digitalOcean:
return DigitalOceanServerProvider();
case ServerProviderType.unknown:
throw UnknownProviderException('Unknown server provider');
}
}
}

View file

@ -72,7 +72,10 @@ class ServiceConsumptionTitle extends StatelessWidget {
service.svgIcon,
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),

View file

@ -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,
),
),
],

View file

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

View file

@ -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),
),
],

View file

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

View file

@ -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,

View file

@ -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(),
),
],

View file

@ -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,

View file

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

View file

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

View file

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

View file

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