chore: Merge digital-ocean into master

Reviewed-on: kherel/selfprivacy.org.app#140
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
pull/151/head
NaiJi ✨ 2022-11-23 15:28:32 +02:00
commit 58ce0f0f8b
96 changed files with 3086 additions and 812 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -31,7 +31,8 @@
"remove": "Remove", "remove": "Remove",
"apply": "Apply", "apply": "Apply",
"done": "Done", "done": "Done",
"continue": "Continue" "continue": "Continue",
"alert": "Alert"
}, },
"more_page": { "more_page": {
"configuration_wizard": "Setup wizard", "configuration_wizard": "Setup wizard",
@ -109,6 +110,7 @@
"disk": "Disk local", "disk": "Disk local",
"monthly_cost": "Monthly cost", "monthly_cost": "Monthly cost",
"location": "Location", "location": "Location",
"provider": "Provider",
"core_count": { "core_count": {
"one": "{} core", "one": "{} core",
"two": "{} cores", "two": "{} cores",
@ -267,9 +269,14 @@
}, },
"initializing": { "initializing": {
"connect_to_server": "Connect a server", "connect_to_server": "Connect a server",
"select_provider": "Select your provider",
"place_where_data": "A place where your data and SelfPrivacy services will reside:", "place_where_data": "A place where your data and SelfPrivacy services will reside:",
"how": "How to obtain API token", "how": "How to obtain API token",
"hetzner_bad_key_error": "Hetzner API key is invalid", "provider_bad_key_error": "Provider API key is invalid",
"choose_location_type": "Choose your server location and type:",
"back_to_locations": "Go back to available locations!",
"no_locations_found": "No available locations found. Make sure your account is accessible.",
"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": "Cloudflare API key is invalid", "cloudflare_bad_key_error": "Cloudflare API key is invalid",
"backblaze_bad_key_error": "Backblaze storage information is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid",
"connect_cloudflare": "Connect CloudFlare", "connect_cloudflare": "Connect CloudFlare",
@ -295,6 +302,7 @@
"checks": "Checks have been completed \n{} out of {}" "checks": "Checks have been completed \n{} out of {}"
}, },
"recovering": { "recovering": {
"generic_error": "Operation failed, please try again.",
"recovery_main_header": "Connect to an existing server", "recovery_main_header": "Connect to an existing server",
"domain_recovery_description": "Enter a server domain you want to get access for:", "domain_recovery_description": "Enter a server domain you want to get access for:",
"domain_recover_placeholder": "Your domain", "domain_recover_placeholder": "Your domain",
@ -314,9 +322,9 @@
"fallback_select_provider_console": "Access to the server console of my prodiver.", "fallback_select_provider_console": "Access to the server console of my prodiver.",
"authorization_failed": "Couldn't log in with this key", "authorization_failed": "Couldn't log in with this key",
"fallback_select_provider_console_hint": "For example: Hetzner.", "fallback_select_provider_console_hint": "For example: Hetzner.",
"hetzner_connected": "Connect to Hetzner", "server_provider_connected": "Connect to your Server Provider",
"hetzner_connected_description": "Communication established. Enter Hetzner token with access to {}:", "server_provider_connected_description": "Communication established. Enter you token with access to {}:",
"hetzner_connected_placeholder": "Hetzner token", "server_provider_connected_placeholder": "Server Provider token",
"confirm_server": "Confirm server", "confirm_server": "Confirm server",
"confirm_server_description": "Found your server! Confirm it is the right one:", "confirm_server_description": "Found your server! Confirm it is the right one:",
"confirm_server_accept": "Yes! That's it", "confirm_server_accept": "Yes! That's it",

View File

@ -31,7 +31,8 @@
"remove": "Удалить", "remove": "Удалить",
"apply": "Применить", "apply": "Применить",
"done": "Готово", "done": "Готово",
"continue": "Продолжить" "continue": "Продолжить",
"alert": "Уведомление"
}, },
"more_page": { "more_page": {
"configuration_wizard": "Мастер настройки", "configuration_wizard": "Мастер настройки",
@ -109,6 +110,7 @@
"disk": "Диск", "disk": "Диск",
"monthly_cost": "Ежемесячная стоимость", "monthly_cost": "Ежемесячная стоимость",
"location": "Размещение", "location": "Размещение",
"provider": "Провайдер",
"core_count": { "core_count": {
"one": "{} ядро", "one": "{} ядро",
"two": "{} ядра", "two": "{} ядра",
@ -269,7 +271,11 @@
"connect_to_server": "Подключите сервер", "connect_to_server": "Подключите сервер",
"place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:", "place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:",
"how": "Как получить API Token", "how": "Как получить API Token",
"hetzner_bad_key_error": "Hetzner API ключ неверен", "provider_bad_key_error": "API ключ провайдера неверен",
"choose_location_type": "Выберите локацию и тип вашего сервера:",
"back_to_locations": "Назад к доступным локациям!",
"no_locations_found": "Не найдено локаций. Убедитесь, что ваш аккаунт доступен.",
"no_server_types_found": "Не удалось получить список серверов. Убедитесь, что ваш аккаунт доступен и попытайтесь сменить локацию сервера.",
"cloudflare_bad_key_error": "Cloudflare API ключ неверен", "cloudflare_bad_key_error": "Cloudflare API ключ неверен",
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
"connect_cloudflare": "Подключите CloudFlare", "connect_cloudflare": "Подключите CloudFlare",
@ -295,6 +301,7 @@
"checks": "Проверок выполнено: \n{} / {}" "checks": "Проверок выполнено: \n{} / {}"
}, },
"recovering": { "recovering": {
"generic_error": "Ошибка проведения операции, попробуйте ещё раз.",
"recovery_main_header": "Подключиться к существующему серверу", "recovery_main_header": "Подключиться к существующему серверу",
"domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:", "domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:",
"domain_recover_placeholder": "Домен", "domain_recover_placeholder": "Домен",

View File

@ -87,9 +87,18 @@ class BNames {
/// A String field of [serverInstallationBox] box. /// A String field of [serverInstallationBox] box.
static String hetznerKey = 'hetznerKey'; static String hetznerKey = 'hetznerKey';
/// A String field of [serverInstallationBox] box.
static String serverProvider = 'serverProvider';
/// A String field of [serverLocation] box.
static String serverLocation = 'serverLocation';
/// A String field of [serverInstallationBox] box. /// A String field of [serverInstallationBox] box.
static String cloudFlareKey = 'cloudFlareKey'; static String cloudFlareKey = 'cloudFlareKey';
/// A String field of [serverTypeIdentifier] box.
static String serverTypeIdentifier = 'serverTypeIdentifier';
/// A [User] field of [serverInstallationBox] box. /// A [User] field of [serverInstallationBox] box.
static String rootUser = 'rootUser'; static String rootUser = 'rootUser';

View File

@ -1,10 +1,27 @@
import 'dart:io';
import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:http/io_client.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
abstract class ApiMap { abstract class ApiMap {
Future<GraphQLClient> getClient() async { Future<GraphQLClient> getClient() async {
IOClient? ioClient;
if (StagingOptions.stagingAcme) {
final HttpClient httpClient = HttpClient();
httpClient.badCertificateCallback = (
final cert,
final host,
final port,
) =>
true;
ioClient = IOClient(httpClient);
}
final httpLink = HttpLink( final httpLink = HttpLink(
'https://api.$rootAddress/graphql', 'https://api.$rootAddress/graphql',
httpClient: ioClient,
); );
final String token = _getApiToken(); final String token = _getApiToken();

View File

@ -4,7 +4,7 @@ import 'package:graphql/client.dart' as graphql;
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/utils/scalars.dart'; import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart'; import 'server_api.graphql.dart';
part 'disk_volumes.graphql.g.dart'; part 'disk_volumes.graphql.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)

View File

@ -173,6 +173,7 @@ input RecoveryKeyLimitsInput {
enum ServerProvider { enum ServerProvider {
HETZNER HETZNER
DIGITALOCEAN
} }
type Service { type Service {

View File

@ -693,6 +693,8 @@ enum Enum$DnsProvider {
enum Enum$ServerProvider { enum Enum$ServerProvider {
@JsonValue('HETZNER') @JsonValue('HETZNER')
HETZNER, HETZNER,
@JsonValue('DIGITALOCEAN')
DIGITALOCEAN,
$unknown $unknown
} }

View File

@ -64,6 +64,14 @@ mutation RebootSystem {
} }
} }
query SystemServerProvider {
system {
provider {
provider
}
}
}
query GetApiTokens { query GetApiTokens {
api { api {
devices { devices {

View File

@ -4,7 +4,6 @@ import 'package:graphql/client.dart' as graphql;
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/utils/scalars.dart'; import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart';
part 'server_api.graphql.g.dart'; part 'server_api.graphql.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
@ -3178,6 +3177,425 @@ class _CopyWithStubImpl$Mutation$RebootSystem$rebootSystem<TRes>
_res; _res;
} }
@JsonSerializable(explicitToJson: true)
class Query$SystemServerProvider {
Query$SystemServerProvider({required this.system, required this.$__typename});
@override
factory Query$SystemServerProvider.fromJson(Map<String, dynamic> json) =>
_$Query$SystemServerProviderFromJson(json);
final Query$SystemServerProvider$system system;
@JsonKey(name: '__typename')
final String $__typename;
Map<String, dynamic> toJson() => _$Query$SystemServerProviderToJson(this);
int get hashCode {
final l$system = system;
final l$$__typename = $__typename;
return Object.hashAll([l$system, l$$__typename]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (!(other is Query$SystemServerProvider) ||
runtimeType != other.runtimeType) return false;
final l$system = system;
final lOther$system = other.system;
if (l$system != lOther$system) return false;
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) return false;
return true;
}
}
extension UtilityExtension$Query$SystemServerProvider
on Query$SystemServerProvider {
CopyWith$Query$SystemServerProvider<Query$SystemServerProvider>
get copyWith => CopyWith$Query$SystemServerProvider(this, (i) => i);
}
abstract class CopyWith$Query$SystemServerProvider<TRes> {
factory CopyWith$Query$SystemServerProvider(
Query$SystemServerProvider instance,
TRes Function(Query$SystemServerProvider) then) =
_CopyWithImpl$Query$SystemServerProvider;
factory CopyWith$Query$SystemServerProvider.stub(TRes res) =
_CopyWithStubImpl$Query$SystemServerProvider;
TRes call({Query$SystemServerProvider$system? system, String? $__typename});
CopyWith$Query$SystemServerProvider$system<TRes> get system;
}
class _CopyWithImpl$Query$SystemServerProvider<TRes>
implements CopyWith$Query$SystemServerProvider<TRes> {
_CopyWithImpl$Query$SystemServerProvider(this._instance, this._then);
final Query$SystemServerProvider _instance;
final TRes Function(Query$SystemServerProvider) _then;
static const _undefined = {};
TRes call({Object? system = _undefined, Object? $__typename = _undefined}) =>
_then(Query$SystemServerProvider(
system: system == _undefined || system == null
? _instance.system
: (system as Query$SystemServerProvider$system),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String)));
CopyWith$Query$SystemServerProvider$system<TRes> get system {
final local$system = _instance.system;
return CopyWith$Query$SystemServerProvider$system(
local$system, (e) => call(system: e));
}
}
class _CopyWithStubImpl$Query$SystemServerProvider<TRes>
implements CopyWith$Query$SystemServerProvider<TRes> {
_CopyWithStubImpl$Query$SystemServerProvider(this._res);
TRes _res;
call({Query$SystemServerProvider$system? system, String? $__typename}) =>
_res;
CopyWith$Query$SystemServerProvider$system<TRes> get system =>
CopyWith$Query$SystemServerProvider$system.stub(_res);
}
const documentNodeQuerySystemServerProvider = DocumentNode(definitions: [
OperationDefinitionNode(
type: OperationType.query,
name: NameNode(value: 'SystemServerProvider'),
variableDefinitions: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'system'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'provider'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'provider'),
alias: null,
arguments: [],
directives: [],
selectionSet: null),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null)
])),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null)
])),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null)
])),
]);
Query$SystemServerProvider _parserFn$Query$SystemServerProvider(
Map<String, dynamic> data) =>
Query$SystemServerProvider.fromJson(data);
class Options$Query$SystemServerProvider
extends graphql.QueryOptions<Query$SystemServerProvider> {
Options$Query$SystemServerProvider(
{String? operationName,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Duration? pollInterval,
graphql.Context? context})
: super(
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
optimisticResult: optimisticResult,
pollInterval: pollInterval,
context: context,
document: documentNodeQuerySystemServerProvider,
parserFn: _parserFn$Query$SystemServerProvider);
}
class WatchOptions$Query$SystemServerProvider
extends graphql.WatchQueryOptions<Query$SystemServerProvider> {
WatchOptions$Query$SystemServerProvider(
{String? operationName,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
graphql.Context? context,
Duration? pollInterval,
bool? eagerlyFetchResults,
bool carryForwardDataOnException = true,
bool fetchResults = false})
: super(
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
optimisticResult: optimisticResult,
context: context,
document: documentNodeQuerySystemServerProvider,
pollInterval: pollInterval,
eagerlyFetchResults: eagerlyFetchResults,
carryForwardDataOnException: carryForwardDataOnException,
fetchResults: fetchResults,
parserFn: _parserFn$Query$SystemServerProvider);
}
class FetchMoreOptions$Query$SystemServerProvider
extends graphql.FetchMoreOptions {
FetchMoreOptions$Query$SystemServerProvider(
{required graphql.UpdateQuery updateQuery})
: super(
updateQuery: updateQuery,
document: documentNodeQuerySystemServerProvider);
}
extension ClientExtension$Query$SystemServerProvider on graphql.GraphQLClient {
Future<graphql.QueryResult<Query$SystemServerProvider>>
query$SystemServerProvider(
[Options$Query$SystemServerProvider? options]) async =>
await this.query(options ?? Options$Query$SystemServerProvider());
graphql.ObservableQuery<Query$SystemServerProvider>
watchQuery$SystemServerProvider(
[WatchOptions$Query$SystemServerProvider? options]) =>
this.watchQuery(options ?? WatchOptions$Query$SystemServerProvider());
void writeQuery$SystemServerProvider(
{required Query$SystemServerProvider data, bool broadcast = true}) =>
this.writeQuery(
graphql.Request(
operation: graphql.Operation(
document: documentNodeQuerySystemServerProvider)),
data: data.toJson(),
broadcast: broadcast);
Query$SystemServerProvider? readQuery$SystemServerProvider(
{bool optimistic = true}) {
final result = this.readQuery(
graphql.Request(
operation: graphql.Operation(
document: documentNodeQuerySystemServerProvider)),
optimistic: optimistic);
return result == null ? null : Query$SystemServerProvider.fromJson(result);
}
}
@JsonSerializable(explicitToJson: true)
class Query$SystemServerProvider$system {
Query$SystemServerProvider$system(
{required this.provider, required this.$__typename});
@override
factory Query$SystemServerProvider$system.fromJson(
Map<String, dynamic> json) =>
_$Query$SystemServerProvider$systemFromJson(json);
final Query$SystemServerProvider$system$provider provider;
@JsonKey(name: '__typename')
final String $__typename;
Map<String, dynamic> toJson() =>
_$Query$SystemServerProvider$systemToJson(this);
int get hashCode {
final l$provider = provider;
final l$$__typename = $__typename;
return Object.hashAll([l$provider, l$$__typename]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (!(other is Query$SystemServerProvider$system) ||
runtimeType != other.runtimeType) return false;
final l$provider = provider;
final lOther$provider = other.provider;
if (l$provider != lOther$provider) return false;
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) return false;
return true;
}
}
extension UtilityExtension$Query$SystemServerProvider$system
on Query$SystemServerProvider$system {
CopyWith$Query$SystemServerProvider$system<Query$SystemServerProvider$system>
get copyWith =>
CopyWith$Query$SystemServerProvider$system(this, (i) => i);
}
abstract class CopyWith$Query$SystemServerProvider$system<TRes> {
factory CopyWith$Query$SystemServerProvider$system(
Query$SystemServerProvider$system instance,
TRes Function(Query$SystemServerProvider$system) then) =
_CopyWithImpl$Query$SystemServerProvider$system;
factory CopyWith$Query$SystemServerProvider$system.stub(TRes res) =
_CopyWithStubImpl$Query$SystemServerProvider$system;
TRes call(
{Query$SystemServerProvider$system$provider? provider,
String? $__typename});
CopyWith$Query$SystemServerProvider$system$provider<TRes> get provider;
}
class _CopyWithImpl$Query$SystemServerProvider$system<TRes>
implements CopyWith$Query$SystemServerProvider$system<TRes> {
_CopyWithImpl$Query$SystemServerProvider$system(this._instance, this._then);
final Query$SystemServerProvider$system _instance;
final TRes Function(Query$SystemServerProvider$system) _then;
static const _undefined = {};
TRes call(
{Object? provider = _undefined, Object? $__typename = _undefined}) =>
_then(Query$SystemServerProvider$system(
provider: provider == _undefined || provider == null
? _instance.provider
: (provider as Query$SystemServerProvider$system$provider),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String)));
CopyWith$Query$SystemServerProvider$system$provider<TRes> get provider {
final local$provider = _instance.provider;
return CopyWith$Query$SystemServerProvider$system$provider(
local$provider, (e) => call(provider: e));
}
}
class _CopyWithStubImpl$Query$SystemServerProvider$system<TRes>
implements CopyWith$Query$SystemServerProvider$system<TRes> {
_CopyWithStubImpl$Query$SystemServerProvider$system(this._res);
TRes _res;
call(
{Query$SystemServerProvider$system$provider? provider,
String? $__typename}) =>
_res;
CopyWith$Query$SystemServerProvider$system$provider<TRes> get provider =>
CopyWith$Query$SystemServerProvider$system$provider.stub(_res);
}
@JsonSerializable(explicitToJson: true)
class Query$SystemServerProvider$system$provider {
Query$SystemServerProvider$system$provider(
{required this.provider, required this.$__typename});
@override
factory Query$SystemServerProvider$system$provider.fromJson(
Map<String, dynamic> json) =>
_$Query$SystemServerProvider$system$providerFromJson(json);
@JsonKey(unknownEnumValue: Enum$ServerProvider.$unknown)
final Enum$ServerProvider provider;
@JsonKey(name: '__typename')
final String $__typename;
Map<String, dynamic> toJson() =>
_$Query$SystemServerProvider$system$providerToJson(this);
int get hashCode {
final l$provider = provider;
final l$$__typename = $__typename;
return Object.hashAll([l$provider, l$$__typename]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (!(other is Query$SystemServerProvider$system$provider) ||
runtimeType != other.runtimeType) return false;
final l$provider = provider;
final lOther$provider = other.provider;
if (l$provider != lOther$provider) return false;
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) return false;
return true;
}
}
extension UtilityExtension$Query$SystemServerProvider$system$provider
on Query$SystemServerProvider$system$provider {
CopyWith$Query$SystemServerProvider$system$provider<
Query$SystemServerProvider$system$provider>
get copyWith =>
CopyWith$Query$SystemServerProvider$system$provider(this, (i) => i);
}
abstract class CopyWith$Query$SystemServerProvider$system$provider<TRes> {
factory CopyWith$Query$SystemServerProvider$system$provider(
Query$SystemServerProvider$system$provider instance,
TRes Function(Query$SystemServerProvider$system$provider) then) =
_CopyWithImpl$Query$SystemServerProvider$system$provider;
factory CopyWith$Query$SystemServerProvider$system$provider.stub(TRes res) =
_CopyWithStubImpl$Query$SystemServerProvider$system$provider;
TRes call({Enum$ServerProvider? provider, String? $__typename});
}
class _CopyWithImpl$Query$SystemServerProvider$system$provider<TRes>
implements CopyWith$Query$SystemServerProvider$system$provider<TRes> {
_CopyWithImpl$Query$SystemServerProvider$system$provider(
this._instance, this._then);
final Query$SystemServerProvider$system$provider _instance;
final TRes Function(Query$SystemServerProvider$system$provider) _then;
static const _undefined = {};
TRes call(
{Object? provider = _undefined, Object? $__typename = _undefined}) =>
_then(Query$SystemServerProvider$system$provider(
provider: provider == _undefined || provider == null
? _instance.provider
: (provider as Enum$ServerProvider),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String)));
}
class _CopyWithStubImpl$Query$SystemServerProvider$system$provider<TRes>
implements CopyWith$Query$SystemServerProvider$system$provider<TRes> {
_CopyWithStubImpl$Query$SystemServerProvider$system$provider(this._res);
TRes _res;
call({Enum$ServerProvider? provider, String? $__typename}) => _res;
}
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class Query$GetApiTokens { class Query$GetApiTokens {
Query$GetApiTokens({required this.api, required this.$__typename}); Query$GetApiTokens({required this.api, required this.$__typename});

View File

@ -330,6 +330,58 @@ Map<String, dynamic> _$Mutation$RebootSystem$rebootSystemToJson(
'__typename': instance.$__typename, '__typename': instance.$__typename,
}; };
Query$SystemServerProvider _$Query$SystemServerProviderFromJson(
Map<String, dynamic> json) =>
Query$SystemServerProvider(
system: Query$SystemServerProvider$system.fromJson(
json['system'] as Map<String, dynamic>),
$__typename: json['__typename'] as String,
);
Map<String, dynamic> _$Query$SystemServerProviderToJson(
Query$SystemServerProvider instance) =>
<String, dynamic>{
'system': instance.system.toJson(),
'__typename': instance.$__typename,
};
Query$SystemServerProvider$system _$Query$SystemServerProvider$systemFromJson(
Map<String, dynamic> json) =>
Query$SystemServerProvider$system(
provider: Query$SystemServerProvider$system$provider.fromJson(
json['provider'] as Map<String, dynamic>),
$__typename: json['__typename'] as String,
);
Map<String, dynamic> _$Query$SystemServerProvider$systemToJson(
Query$SystemServerProvider$system instance) =>
<String, dynamic>{
'provider': instance.provider.toJson(),
'__typename': instance.$__typename,
};
Query$SystemServerProvider$system$provider
_$Query$SystemServerProvider$system$providerFromJson(
Map<String, dynamic> json) =>
Query$SystemServerProvider$system$provider(
provider: $enumDecode(_$Enum$ServerProviderEnumMap, json['provider'],
unknownValue: Enum$ServerProvider.$unknown),
$__typename: json['__typename'] as String,
);
Map<String, dynamic> _$Query$SystemServerProvider$system$providerToJson(
Query$SystemServerProvider$system$provider instance) =>
<String, dynamic>{
'provider': _$Enum$ServerProviderEnumMap[instance.provider]!,
'__typename': instance.$__typename,
};
const _$Enum$ServerProviderEnumMap = {
Enum$ServerProvider.HETZNER: 'HETZNER',
Enum$ServerProvider.DIGITALOCEAN: 'DIGITALOCEAN',
Enum$ServerProvider.$unknown: r'$unknown',
};
Query$GetApiTokens _$Query$GetApiTokensFromJson(Map<String, dynamic> json) => Query$GetApiTokens _$Query$GetApiTokensFromJson(Map<String, dynamic> json) =>
Query$GetApiTokens( Query$GetApiTokens(
api: Query$GetApiTokens$api.fromJson(json['api'] as Map<String, dynamic>), api: Query$GetApiTokens$api.fromJson(json['api'] as Map<String, dynamic>),

View File

@ -3,7 +3,7 @@ import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart'; import 'server_api.graphql.dart';
part 'server_settings.graphql.g.dart'; part 'server_settings.graphql.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)

View File

@ -4,6 +4,7 @@ import 'package:graphql/client.dart' as graphql;
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/utils/scalars.dart'; import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'server_api.graphql.dart';
part 'services.graphql.g.dart'; part 'services.graphql.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)

View File

@ -3,7 +3,7 @@ import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql; import 'package:graphql/client.dart' as graphql;
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'schema.graphql.dart'; import 'schema.graphql.dart';
import 'services.graphql.dart'; import 'server_api.graphql.dart';
part 'users.graphql.g.dart'; part 'users.graphql.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)

View File

@ -1,4 +1,4 @@
part of 'server.dart'; part of 'server_api.dart';
mixin JobsApi on ApiMap { mixin JobsApi on ApiMap {
Future<List<ServerJob>> getServerJobs() async { Future<List<ServerJob>> getServerJobs() async {

View File

@ -1,4 +1,4 @@
part of 'server.dart'; part of 'server_api.dart';
mixin ServerActionsApi on ApiMap { mixin ServerActionsApi on ApiMap {
Future<bool> _commonBoolRequest(final Function graphQLMethod) async { Future<bool> _commonBoolRequest(final Function graphQLMethod) async {

View File

@ -9,6 +9,7 @@ import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/services.graphql.
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/users.graphql.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/users.graphql.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/logic/models/json/backup.dart'; import 'package:selfprivacy/logic/models/json/backup.dart';
@ -88,6 +89,25 @@ class ServerApi extends ApiMap
return apiVersion; return apiVersion;
} }
Future<ServerProvider> getServerProviderType() async {
QueryResult<Query$SystemServerProvider> response;
ServerProvider providerType = ServerProvider.unknown;
try {
final GraphQLClient client = await getClient();
response = await client.query$SystemServerProvider();
if (response.hasException) {
print(response.exception.toString());
}
providerType = ServerProvider.fromGraphQL(
response.parsedData!.system.provider.provider,
);
} catch (e) {
print(e);
}
return providerType;
}
Future<bool> isUsingBinds() async { Future<bool> isUsingBinds() async {
QueryResult response; QueryResult response;
bool usesBinds = false; bool usesBinds = false;

View File

@ -1,4 +1,4 @@
part of 'server.dart'; part of 'server_api.dart';
mixin ServicesApi on ApiMap { mixin ServicesApi on ApiMap {
Future<List<Service>> getAllServices() async { Future<List<Service>> getAllServices() async {

View File

@ -1,4 +1,4 @@
part of 'server.dart'; part of 'server_api.dart';
mixin UsersApi on ApiMap { mixin UsersApi on ApiMap {
Future<List<User>> getAllUsers() async { Future<List<User>> getAllUsers() async {

View File

@ -1,4 +1,4 @@
part of 'server.dart'; part of 'server_api.dart';
mixin VolumeApi on ApiMap { mixin VolumeApi on ApiMap {
Future<List<ServerDiskVolume>> getServerDiskVolumes() async { Future<List<ServerDiskVolume>> getServerDiskVolumes() async {

View File

@ -0,0 +1,44 @@
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,5 +1,7 @@
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/cloudflare/cloudflare_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_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/hetzner/hetzner_factory.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/server_provider_factory.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
@ -12,20 +14,22 @@ class UnknownApiProviderException implements Exception {
class ApiFactoryCreator { class ApiFactoryCreator {
static ServerProviderApiFactory createServerProviderApiFactory( static ServerProviderApiFactory createServerProviderApiFactory(
final ServerProvider provider, final ServerProviderApiFactorySettings settings,
) { ) {
switch (provider) { switch (settings.provider) {
case ServerProvider.hetzner: case ServerProvider.hetzner:
return HetznerApiFactory(); return HetznerApiFactory(region: settings.location);
case ServerProvider.digitalOcean:
return DigitalOceanApiFactory(region: settings.location);
case ServerProvider.unknown: case ServerProvider.unknown:
throw UnknownApiProviderException('Unknown server provider'); throw UnknownApiProviderException('Unknown server provider');
} }
} }
static DnsProviderApiFactory createDnsProviderApiFactory( static DnsProviderApiFactory createDnsProviderApiFactory(
final DnsProvider provider, final DnsProviderApiFactorySettings settings,
) { ) {
switch (provider) { switch (settings.provider) {
case DnsProvider.cloudflare: case DnsProvider.cloudflare:
return CloudflareApiFactory(); return CloudflareApiFactory();
case DnsProvider.unknown: case DnsProvider.unknown:
@ -36,11 +40,13 @@ class ApiFactoryCreator {
class VolumeApiFactoryCreator { class VolumeApiFactoryCreator {
static VolumeProviderApiFactory createVolumeProviderApiFactory( static VolumeProviderApiFactory createVolumeProviderApiFactory(
final ServerProvider provider, final ServerProviderApiFactorySettings settings,
) { ) {
switch (provider) { switch (settings.provider) {
case ServerProvider.hetzner: case ServerProvider.hetzner:
return HetznerApiFactory(); return HetznerApiFactory();
case ServerProvider.digitalOcean:
return DigitalOceanApiFactory();
case ServerProvider.unknown: case ServerProvider.unknown:
throw UnknownApiProviderException('Unknown volume provider'); throw UnknownApiProviderException('Unknown volume provider');
} }

View File

@ -0,0 +1,20 @@
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
class ServerProviderApiFactorySettings {
ServerProviderApiFactorySettings({
required this.provider,
this.location,
});
final ServerProvider provider;
final String? location;
}
class DnsProviderApiFactorySettings {
DnsProviderApiFactorySettings({
required this.provider,
});
final DnsProvider provider;
}

View File

@ -9,8 +9,8 @@ import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/message.dart';
abstract class ApiMap { abstract class ApiMap {
Future<Dio> getClient() async { Future<Dio> getClient({final BaseOptions? customOptions}) async {
final Dio dio = Dio(await options); final Dio dio = Dio(customOptions ?? (await options));
if (hasLogger) { if (hasLogger) {
dio.interceptors.add(PrettyDioLogger()); dio.interceptors.add(PrettyDioLogger());
} }
@ -41,7 +41,8 @@ abstract class ApiMap {
FutureOr<BaseOptions> get options; FutureOr<BaseOptions> get options;
abstract final String rootAddress; String get rootAddress;
abstract final bool hasLogger; abstract final bool hasLogger;
abstract final bool isWithToken; abstract final bool isWithToken;

View File

@ -1,5 +1,6 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart'; 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.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'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
class CloudflareApiFactory extends DnsProviderApiFactory { class CloudflareApiFactory extends DnsProviderApiFactory {

View File

@ -0,0 +1,10 @@
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,17 +1,8 @@
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.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
class DnsProviderApiSettings extends ProviderApiSettings {
const DnsProviderApiSettings({
super.hasLogger = false,
super.isWithToken = true,
this.customToken,
});
final String? customToken;
}
abstract class DnsProviderApiFactory { abstract class DnsProviderApiFactory {
DnsProviderApi getDnsProvider({ DnsProviderApi getDnsProvider({
final DnsProviderApiSettings settings = const DnsProviderApiSettings(), final DnsProviderApiSettings settings,
}); });
} }

View File

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

View File

@ -0,0 +1,780 @@
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/password_generator.dart';
class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
DigitalOceanApi({
required this.region,
this.hasLogger = false,
this.isWithToken = true,
});
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
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<bool> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
final Dio client = await getClient();
try {
response = await client.get(
'/account',
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
} finally {
close(client);
}
if (response != null) {
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
}
return isValid;
}
/// 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<ServerVolume?> createVolume() async {
ServerVolume? volume;
final 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);
} finally {
client.close();
}
return volume;
}
@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<bool> attachVolume(
final ServerVolume volume,
final int serverId,
) async {
bool success = false;
final 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);
} finally {
close(client);
}
return success;
}
@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;
}
static String getHostnameFromDomain(final String domain) {
// Replace all non-alphanumeric characters with an underscore
String hostname =
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
if (hostname.endsWith('-')) {
hostname = hostname.substring(0, hostname.length - 1);
}
if (hostname.startsWith('-')) {
hostname = hostname.substring(1);
}
if (hostname.isEmpty) {
hostname = 'selfprivacy-server';
}
return hostname;
}
@override
Future<ServerHostingDetails?> createServer({
required final String dnsApiToken,
required final User rootUser,
required final String domainName,
required final String serverType,
}) 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 userdataString =
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | 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);
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');
final Response serverCreateResponse = await client.post(
'/droplets',
data: data,
);
final int serverId = serverCreateResponse.data['droplet']['id'];
final ServerVolume? newVolume = await createVolume();
final bool attachedVolume = await attachVolume(newVolume!, serverId);
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);
} finally {
close(client);
}
return serverDetails;
}
@override
Future<void> deleteServer({
required final String domainName,
}) async {
final Dio client = await getClient();
final ServerBasicInfo serverToRemove = (await getServers()).firstWhere(
(final el) => el.name == domainName,
);
final ServerVolume volumeToRemove = (await getVolumes()).firstWhere(
(final el) => el.serverId == serverToRemove.id,
);
final 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);
} finally {
close(client);
}
}
@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<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);
} finally {
close(client);
}
return locations;
}
@override
Future<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']) {
if (rawRegion.toString() == location.identifier) {
types.add(
ServerType(
title: rawSize['description'],
identifier: rawSize['slug'],
ram: rawSize['memory'].toDouble(),
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);
} finally {
close(client);
}
return types;
}
@override
Future<void> createReverseDns({
required final ServerHostingDetails serverDetails,
required final ServerDomain domain,
}) async {
/// TODO remove from provider interface
}
@override
ProviderApiTokenValidation getApiTokenValidation() =>
ProviderApiTokenValidation(
regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'),
length: 71,
);
}

View File

@ -0,0 +1,34 @@
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

@ -2,28 +2,43 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.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/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_domain.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.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/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.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_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/password_generator.dart'; import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends ServerProviderApi with VolumeProviderApi { class HetznerApi extends ServerProviderApi with VolumeProviderApi {
HetznerApi({this.hasLogger = false, this.isWithToken = true}); HetznerApi({
this.region,
this.hasLogger = false,
this.isWithToken = true,
});
@override @override
bool hasLogger; bool hasLogger;
@override @override
bool isWithToken; bool isWithToken;
final String? region;
@override @override
BaseOptions get options { BaseOptions get options {
final BaseOptions options = BaseOptions(baseUrl: rootAddress); final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) { if (isWithToken) {
final String? token = getIt<ApiConfigModel>().hetznerKey; final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null); assert(token != null);
options.headers = {'Authorization': 'Bearer $token'}; options.headers = {'Authorization': 'Bearer $token'};
} }
@ -36,7 +51,13 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
} }
@override @override
String rootAddress = 'https://api.hetzner.cloud/v1'; String get rootAddress => 'https://api.hetzner.cloud/v1';
@override
String get infectProviderName => 'hetzner';
@override
String get displayProviderName => 'Hetzner';
@override @override
Future<bool> isApiTokenValid(final String token) async { Future<bool> isApiTokenValid(final String token) async {
@ -71,19 +92,22 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
} }
@override @override
RegExp getApiTokenValidation() => ProviderApiTokenValidation getApiTokenValidation() =>
RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); ProviderApiTokenValidation(
regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'),
length: 64,
);
@override @override
Future<double?> getPricePerGb() async { Future<Price?> getPricePerGb() async {
double? price; double? price;
final Response dbGetResponse; final Response pricingResponse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbGetResponse = await client.get('/pricing'); pricingResponse = await client.get('/pricing');
final volume = dbGetResponse.data['pricing']['volume']; final volume = pricingResponse.data['pricing']['volume'];
final volumePrice = volume['price_per_gb_month']['gross']; final volumePrice = volume['price_per_gb_month']['gross'];
price = double.parse(volumePrice); price = double.parse(volumePrice);
} catch (e) { } catch (e) {
@ -92,38 +116,43 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
client.close(); client.close();
} }
return price; return price == null
? null
: Price(
value: price,
currency: 'EUR',
);
} }
@override @override
Future<ServerVolume?> createVolume() async { Future<ServerVolume?> createVolume() async {
ServerVolume? volume; ServerVolume? volume;
final Response dbCreateResponse; final Response createVolumeResponse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbCreateResponse = await client.post( createVolumeResponse = await client.post(
'/volumes', '/volumes',
data: { data: {
'size': 10, 'size': 10,
'name': StringGenerators.dbStorageName(), 'name': StringGenerators.storageName(),
'labels': {'labelkey': 'value'}, 'labels': {'labelkey': 'value'},
'location': 'fsn1', 'location': region,
'automount': false, 'automount': false,
'format': 'ext4' 'format': 'ext4'
}, },
); );
final dbId = dbCreateResponse.data['volume']['id']; final volumeId = createVolumeResponse.data['volume']['id'];
final dbSize = dbCreateResponse.data['volume']['size']; final volumeSize = createVolumeResponse.data['volume']['size'];
final dbServer = dbCreateResponse.data['volume']['server']; final volumeServer = createVolumeResponse.data['volume']['server'];
final dbName = dbCreateResponse.data['volume']['name']; final volumeName = createVolumeResponse.data['volume']['name'];
final dbDevice = dbCreateResponse.data['volume']['linux_device']; final volumeDevice = createVolumeResponse.data['volume']['linux_device'];
volume = ServerVolume( volume = ServerVolume(
id: dbId, id: volumeId,
name: dbName, name: volumeName,
sizeByte: dbSize, sizeByte: volumeSize,
serverId: dbServer, serverId: volumeServer,
linuxDevice: dbDevice, linuxDevice: volumeDevice,
); );
} catch (e) { } catch (e) {
print(e); print(e);
@ -138,28 +167,28 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
Future<List<ServerVolume>> getVolumes({final String? status}) async { Future<List<ServerVolume>> getVolumes({final String? status}) async {
final List<ServerVolume> volumes = []; final List<ServerVolume> volumes = [];
final Response dbGetResponse; final Response getVolumesResonse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbGetResponse = await client.get( getVolumesResonse = await client.get(
'/volumes', '/volumes',
queryParameters: { queryParameters: {
'status': status, 'status': status,
}, },
); );
final List<dynamic> rawVolumes = dbGetResponse.data['volumes']; final List<dynamic> rawVolumes = getVolumesResonse.data['volumes'];
for (final rawVolume in rawVolumes) { for (final rawVolume in rawVolumes) {
final int dbId = rawVolume['id']; final int volumeId = rawVolume['id'];
final int dbSize = rawVolume['size'] * 1024 * 1024 * 1024; final int volumeSize = rawVolume['size'] * 1024 * 1024 * 1024;
final dbServer = rawVolume['server']; final volumeServer = rawVolume['server'];
final String dbName = rawVolume['name']; final String volumeName = rawVolume['name'];
final dbDevice = rawVolume['linux_device']; final volumeDevice = rawVolume['linux_device'];
final volume = ServerVolume( final volume = ServerVolume(
id: dbId, id: volumeId,
name: dbName, name: volumeName,
sizeByte: dbSize, sizeByte: volumeSize,
serverId: dbServer, serverId: volumeServer,
linuxDevice: dbDevice, linuxDevice: volumeDevice,
); );
volumes.add(volume); volumes.add(volume);
} }
@ -172,25 +201,26 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return volumes; return volumes;
} }
@override Future<ServerVolume?> getVolume(
Future<ServerVolume?> getVolume(final int id) async { final String volumeId,
) async {
ServerVolume? volume; ServerVolume? volume;
final Response dbGetResponse; final Response getVolumeResponse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbGetResponse = await client.get('/volumes/$id'); getVolumeResponse = await client.get('/volumes/$volumeId');
final int dbId = dbGetResponse.data['volume']['id']; final int responseVolumeId = getVolumeResponse.data['volume']['id'];
final int dbSize = dbGetResponse.data['volume']['size']; final int volumeSize = getVolumeResponse.data['volume']['size'];
final int dbServer = dbGetResponse.data['volume']['server']; final int volumeServer = getVolumeResponse.data['volume']['server'];
final String dbName = dbGetResponse.data['volume']['name']; final String volumeName = getVolumeResponse.data['volume']['name'];
final dbDevice = dbGetResponse.data['volume']['linux_device']; final volumeDevice = getVolumeResponse.data['volume']['linux_device'];
volume = ServerVolume( volume = ServerVolume(
id: dbId, id: responseVolumeId,
name: dbName, name: volumeName,
sizeByte: dbSize, sizeByte: volumeSize,
serverId: dbServer, serverId: volumeServer,
linuxDevice: dbDevice, linuxDevice: volumeDevice,
); );
} catch (e) { } catch (e) {
print(e); print(e);
@ -202,10 +232,10 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
} }
@override @override
Future<void> deleteVolume(final int id) async { Future<void> deleteVolume(final ServerVolume volume) async {
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
await client.delete('/volumes/$id'); await client.delete('/volumes/${volume.id}');
} catch (e) { } catch (e) {
print(e); print(e);
} finally { } finally {
@ -214,20 +244,24 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
} }
@override @override
Future<bool> attachVolume(final int volumeId, final int serverId) async { Future<bool> attachVolume(
final ServerVolume volume,
final int serverId,
) async {
bool success = false; bool success = false;
final Response dbPostResponse; final Response attachVolumeResponse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbPostResponse = await client.post( attachVolumeResponse = await client.post(
'/volumes/$volumeId/actions/attach', '/volumes/${volume.id}/actions/attach',
data: { data: {
'automount': true, 'automount': true,
'server': serverId, 'server': serverId,
}, },
); );
success = dbPostResponse.data['action']['status'].toString() != 'error'; success =
attachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) { } catch (e) {
print(e); print(e);
} finally { } finally {
@ -238,14 +272,17 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
} }
@override @override
Future<bool> detachVolume(final int volumeId) async { Future<bool> detachVolume(final ServerVolume volume) async {
bool success = false; bool success = false;
final Response dbPostResponse; final Response detachVolumeResponse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbPostResponse = await client.post('/volumes/$volumeId/actions/detach'); detachVolumeResponse = await client.post(
success = dbPostResponse.data['action']['status'].toString() != 'error'; '/volumes/${volume.id}/actions/detach',
);
success =
detachVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) { } catch (e) {
print(e); print(e);
} finally { } finally {
@ -256,19 +293,23 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
} }
@override @override
Future<bool> resizeVolume(final int volumeId, final int sizeGb) async { Future<bool> resizeVolume(
final ServerVolume volume,
final DiskSize size,
) async {
bool success = false; bool success = false;
final Response dbPostResponse; final Response resizeVolumeResponse;
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
dbPostResponse = await client.post( resizeVolumeResponse = await client.post(
'/volumes/$volumeId/actions/resize', '/volumes/${volume.id}/actions/resize',
data: { data: {
'size': sizeGb, 'size': size.gibibyte,
}, },
); );
success = dbPostResponse.data['action']['status'].toString() != 'error'; success =
resizeVolumeResponse.data['action']['status'].toString() != 'error';
} catch (e) { } catch (e) {
print(e); print(e);
} finally { } finally {
@ -283,6 +324,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
required final String dnsApiToken, required final String dnsApiToken,
required final User rootUser, required final User rootUser,
required final String domainName, required final String domainName,
required final String serverType,
}) async { }) async {
ServerHostingDetails? details; ServerHostingDetails? details;
@ -295,7 +337,8 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
dnsApiToken: dnsApiToken, dnsApiToken: dnsApiToken,
rootUser: rootUser, rootUser: rootUser,
domainName: domainName, domainName: domainName,
dataBase: newVolume, volume: newVolume,
serverType: serverType,
); );
return details; return details;
@ -305,48 +348,43 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
required final String dnsApiToken, required final String dnsApiToken,
required final User rootUser, required final User rootUser,
required final String domainName, required final String domainName,
required final ServerVolume dataBase, required final ServerVolume volume,
required final String serverType,
}) async { }) async {
final Dio client = await getClient(); final Dio client = await getClient();
final String dbPassword = StringGenerators.dbPassword(); final String dbPassword = StringGenerators.dbPassword();
final int dbId = dataBase.id; final int volumeId = volume.id;
final String apiToken = StringGenerators.apiToken(); final String apiToken = StringGenerators.apiToken();
final String hostname = getHostnameFromDomain(domainName); final String hostname = getHostnameFromDomain(domainName);
const String infectBranch = 'providers/hetzner';
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
final String base64Password = final String base64Password =
base64.encode(utf8.encode(rootUser.password ?? 'PASS')); base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
print('hostname: $hostname');
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
/// check the branch name, it could be "development" or "master".
///
final String userdataString = final String userdataString =
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner 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"; "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | 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";
print(userdataString);
final Map<String, Object> data = {
'name': hostname,
'server_type': 'cx11',
'start_after_create': false,
'image': 'ubuntu-20.04',
'volumes': [dbId],
'networks': [],
'user_data': userdataString,
'labels': {},
'automount': true,
'location': 'fsn1'
};
print('Decoded data: $data');
ServerHostingDetails? serverDetails; ServerHostingDetails? serverDetails;
DioError? hetznerError; DioError? hetznerError;
bool success = false; bool success = false;
try { 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');
final Response serverCreateResponse = await client.post( final Response serverCreateResponse = await client.post(
'/servers', '/servers',
data: data, data: data,
@ -356,7 +394,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
id: serverCreateResponse.data['server']['id'], id: serverCreateResponse.data['server']['id'],
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(), createTime: DateTime.now(),
volume: dataBase, volume: volume,
apiToken: apiToken, apiToken: apiToken,
provider: ServerProvider.hetzner, provider: ServerProvider.hetzner,
); );
@ -372,7 +410,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
if (!success) { if (!success) {
await Future.delayed(const Duration(seconds: 10)); await Future.delayed(const Duration(seconds: 10));
await deleteVolume(dbId); await deleteVolume(volume);
} }
if (hetznerError != null) { if (hetznerError != null) {
@ -459,14 +497,12 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return server.copyWith(startTime: DateTime.now()); return server.copyWith(startTime: DateTime.now());
} }
Future<Map<String, dynamic>> getMetrics( Future<Map<String, dynamic>> requestRawMetrics(
final int serverId,
final DateTime start, final DateTime start,
final DateTime end, final DateTime end,
final String type, final String type,
) async { ) async {
final ServerHostingDetails? hetznerServer =
getIt<ApiConfigModel>().serverDetails;
Map<String, dynamic> metrics = {}; Map<String, dynamic> metrics = {};
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
@ -476,10 +512,10 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
'type': type 'type': type
}; };
final Response res = await client.get( final Response res = await client.get(
'/servers/${hetznerServer!.id}/metrics', '/servers/$serverId/metrics',
queryParameters: queryParameters, queryParameters: queryParameters,
); );
metrics = res.data; metrics = res.data['metrics'];
} catch (e) { } catch (e) {
print(e); print(e);
} finally { } finally {
@ -489,14 +525,115 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return metrics; return metrics;
} }
Future<HetznerServerInfo> getInfo() async { List<TimeSeriesData> serializeTimeSeries(
final ServerHostingDetails? hetznerServer = final Map<String, dynamic> json,
getIt<ApiConfigModel>().serverDetails; final String type,
final Dio client = await getClient(); ) {
final Response response = await client.get('/servers/${hetznerServer!.id}'); final List list = json['time_series'][type]['values'];
close(client); return list
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
.toList();
}
return HetznerServerInfo.fromJson(response.data!['server']); @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 @override
@ -521,7 +658,6 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
ip: server.publicNet.ipv4.ip, ip: server.publicNet.ipv4.ip,
reverseDns: server.publicNet.ipv4.reverseDns, reverseDns: server.publicNet.ipv4.reverseDns,
created: server.created, created: server.created,
volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0,
), ),
) )
.toList(); .toList();
@ -535,6 +671,96 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
return 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<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);
} finally {
close(client);
}
return locations;
}
@override
Future<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);
} finally {
close(client);
}
return types;
}
@override @override
Future<void> createReverseDns({ Future<void> createReverseDns({
required final ServerHostingDetails serverDetails, required final ServerHostingDetails serverDetails,

View File

@ -1,25 +1,33 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart'; 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.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/server_provider_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
class HetznerApiFactory extends ServerProviderApiFactory class HetznerApiFactory extends ServerProviderApiFactory
with VolumeProviderApiFactory { with VolumeProviderApiFactory {
HetznerApiFactory({this.region});
final String? region;
@override @override
ServerProviderApi getServerProvider({ ServerProviderApi getServerProvider({
final ProviderApiSettings settings = const ProviderApiSettings(), final ServerProviderApiSettings settings =
const ServerProviderApiSettings(),
}) => }) =>
HetznerApi( HetznerApi(
region: settings.region ?? region,
hasLogger: settings.hasLogger, hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken, isWithToken: settings.isWithToken,
); );
@override @override
VolumeProviderApi getVolumeProvider({ VolumeProviderApi getVolumeProvider({
final ProviderApiSettings settings = const ProviderApiSettings(), final ServerProviderApiSettings settings =
const ServerProviderApiSettings(),
}) => }) =>
HetznerApi( HetznerApi(
region: settings.region ?? region,
hasLogger: settings.hasLogger, hasLogger: settings.hasLogger,
isWithToken: settings.isWithToken, isWithToken: settings.isWithToken,
); );

View File

@ -2,10 +2,27 @@ 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_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
class ProviderApiTokenValidation {
ProviderApiTokenValidation({
required this.length,
required this.regexp,
});
final int length;
final RegExp regexp;
}
abstract class ServerProviderApi extends ApiMap { abstract class ServerProviderApi extends ApiMap {
Future<List<ServerBasicInfo>> getServers(); Future<List<ServerBasicInfo>> getServers();
Future<List<ServerProviderLocation>> getAvailableLocations();
Future<List<ServerType>> getServerTypesByLocation({
required final ServerProviderLocation location,
});
Future<ServerHostingDetails> restart(); Future<ServerHostingDetails> restart();
Future<ServerHostingDetails> powerOn(); Future<ServerHostingDetails> powerOn();
@ -15,6 +32,7 @@ abstract class ServerProviderApi extends ApiMap {
required final String dnsApiToken, required final String dnsApiToken,
required final User rootUser, required final User rootUser,
required final String domainName, required final String domainName,
required final String serverType,
}); });
Future<void> createReverseDns({ Future<void> createReverseDns({
required final ServerHostingDetails serverDetails, required final ServerHostingDetails serverDetails,
@ -22,5 +40,19 @@ abstract class ServerProviderApi extends ApiMap {
}); });
Future<bool> isApiTokenValid(final String token); Future<bool> isApiTokenValid(final String token);
RegExp getApiTokenValidation(); ProviderApiTokenValidation getApiTokenValidation();
Future<List<ServerMetadataEntity>> getMetadata(final int serverId);
Future<ServerMetrics?> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
);
/// 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

@ -0,0 +1,11 @@
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 +1,15 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/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.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'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
abstract class ServerProviderApiFactory { abstract class ServerProviderApiFactory {
ServerProviderApi getServerProvider({ ServerProviderApi getServerProvider({
final ProviderApiSettings settings = const ProviderApiSettings(), final ServerProviderApiSettings settings,
}); });
} }
mixin VolumeProviderApiFactory { mixin VolumeProviderApiFactory {
VolumeProviderApi getVolumeProvider({ VolumeProviderApi getVolumeProvider({
final ProviderApiSettings settings = const ProviderApiSettings(), final ServerProviderApiSettings settings,
}); });
} }

View File

@ -1,13 +1,14 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.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/hive/server_details.dart';
import 'package:selfprivacy/logic/models/price.dart';
mixin VolumeProviderApi on ApiMap { mixin VolumeProviderApi on ApiMap {
Future<ServerVolume?> createVolume(); Future<ServerVolume?> createVolume();
Future<List<ServerVolume>> getVolumes({final String? status}); Future<List<ServerVolume>> getVolumes({final String? status});
Future<ServerVolume?> getVolume(final int id); Future<bool> attachVolume(final ServerVolume volume, final int serverId);
Future<bool> attachVolume(final int volumeId, final int serverId); Future<bool> detachVolume(final ServerVolume volume);
Future<bool> detachVolume(final int volumeId); Future<bool> resizeVolume(final ServerVolume volume, final DiskSize size);
Future<bool> resizeVolume(final int volumeId, final int sizeGb); Future<void> deleteVolume(final ServerVolume volume);
Future<void> deleteVolume(final int id); Future<Price?> getPricePerGb();
Future<double?> getPricePerGb();
} }

View File

@ -0,0 +1,8 @@
/// Controls staging environment for network, is used during manual
/// integration testing and such
class StagingOptions {
/// 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
static bool get stagingAcme => false;
}

View File

@ -5,17 +5,6 @@ enum LoadingStatus {
error, error,
} }
enum InitializingSteps {
setHetznerKey,
setCloudFlareKey,
setDomainName,
setRootUser,
createServer,
checkCloudFlareDns,
startServer,
checkSystemDnsAndDkimSet,
}
enum Period { enum Period {
hour, hour,
day, day,

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/json/backup.dart'; import 'package:selfprivacy/logic/models/json/backup.dart';

View File

@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/job.dart';

View File

@ -1,5 +1,5 @@
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart';

View File

@ -1,12 +1,11 @@
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.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/dns_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/network_utils.dart';
part 'dns_records_state.dart'; part 'dns_records_state.dart';
@ -19,11 +18,6 @@ class DnsRecordsCubit
const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing), const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing),
); );
DnsProviderApiFactory? dnsProviderApiFactory =
ApiFactoryCreator.createDnsProviderApiFactory(
DnsProvider.cloudflare, // TODO: HARDCODE FOR NOW!!!
); // TODO: Remove when provider selection is implemented.
final ServerApi api = ServerApi(); final ServerApi api = ServerApi();
@override @override
@ -44,7 +38,8 @@ class DnsRecordsCubit
final String? ipAddress = final String? ipAddress =
serverInstallationCubit.state.serverDetails?.ip4; serverInstallationCubit.state.serverDetails?.ip4;
if (domain != null && ipAddress != null) { if (domain != null && ipAddress != null) {
final List<DnsRecord> records = await dnsProviderApiFactory! final List<DnsRecord> records = await ApiController
.currentDnsProviderApiFactory!
.getDnsProvider() .getDnsProvider()
.getDnsRecords(domain: domain); .getDnsRecords(domain: domain);
final String? dkimPublicKey = final String? dkimPublicKey =
@ -124,7 +119,7 @@ class DnsRecordsCubit
final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
final DnsProviderApi dnsProviderApi = final DnsProviderApi dnsProviderApi =
dnsProviderApiFactory!.getDnsProvider(); ApiController.currentDnsProviderApiFactory!.getDnsProvider();
await dnsProviderApi.removeSimilarRecords(domain: domain!); await dnsProviderApi.removeSimilarRecords(domain: domain!);
await dnsProviderApi.createMultipleDnsRecords( await dnsProviderApi.createMultipleDnsRecords(
domain: domain, domain: domain,

View File

@ -7,15 +7,10 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
class DnsProviderFormCubit extends FormCubit { class DnsProviderFormCubit extends FormCubit {
DnsProviderFormCubit(this.initializingCubit) { DnsProviderFormCubit(this.initializingCubit) {
final RegExp regExp = initializingCubit.getDnsProviderApiTokenValidation();
apiKey = FieldCubit( apiKey = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('validations.required'.tr()), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
regExp.hasMatch,
'validations.invalid_format'.tr(),
),
LengthStringNotEqualValidation(40) LengthStringNotEqualValidation(40)
], ],
); );

View File

@ -1,4 +1,5 @@
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
@ -9,8 +10,7 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
Future<void> load() async { Future<void> load() async {
emit(Loading(LoadingTypes.loadingDomain)); emit(Loading(LoadingTypes.loadingDomain));
final List<String> list = await serverInstallationCubit final List<String> list = await ApiController.currentDnsProviderApiFactory!
.repository.dnsProviderApiFactory!
.getDnsProvider() .getDnsProvider()
.domainList(); .domainList();
if (list.isEmpty) { if (list.isEmpty) {
@ -31,8 +31,7 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
emit(Loading(LoadingTypes.saving)); emit(Loading(LoadingTypes.saving));
final String? zoneId = await serverInstallationCubit final String? zoneId = await ApiController.currentDnsProviderApiFactory!
.repository.dnsProviderApiFactory!
.getDnsProvider() .getDnsProvider()
.getZoneId(domainName); .getZoneId(domainName);

View File

@ -3,21 +3,16 @@ import 'dart:async';
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
class ProviderFormCubit extends FormCubit { class ProviderFormCubit extends FormCubit {
ProviderFormCubit(this.serverInstallationCubit) { ProviderFormCubit(this.serverInstallationCubit) {
final RegExp regExp = //final int tokenLength =
serverInstallationCubit.getServerProviderApiTokenValidation(); // serverInstallationCubit.serverProviderApiTokenValidation().length;
apiKey = FieldCubit( apiKey = FieldCubit(
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('validations.required'.tr()), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( //LengthStringNotEqualValidation(tokenLength),
regExp.hasMatch,
'validations.invalid_format'.tr(),
),
LengthStringNotEqualValidation(64)
], ],
); );
@ -26,7 +21,7 @@ class ProviderFormCubit extends FormCubit {
@override @override
FutureOr<void> onSubmit() async { FutureOr<void> onSubmit() async {
serverInstallationCubit.setHetznerKey(apiKey.state.value); serverInstallationCubit.setServerProviderKey(apiKey.state.value);
} }
final ServerInstallationCubit serverInstallationCubit; final ServerInstallationCubit serverInstallationCubit;
@ -45,7 +40,7 @@ class ProviderFormCubit extends FormCubit {
} }
if (!isKeyValid) { if (!isKeyValid) {
apiKey.setError('initializing.hetzner_bad_key_error'.tr()); apiKey.setError('initializing.provider_bad_key_error'.tr());
return false; return false;
} }

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';

View File

@ -1,69 +0,0 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart';
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart';
class MetricsLoadException implements Exception {
MetricsLoadException(this.message);
final String message;
}
class HetznerMetricsRepository {
Future<HetznerMetricsLoaded> getMetrics(final Period period) async {
final DateTime end = DateTime.now();
DateTime start;
switch (period) {
case Period.hour:
start = end.subtract(const Duration(hours: 1));
break;
case Period.day:
start = end.subtract(const Duration(days: 1));
break;
case Period.month:
start = end.subtract(const Duration(days: 15));
break;
}
final HetznerApi api = HetznerApi(hasLogger: false);
final List<Map<String, dynamic>> results = await Future.wait([
api.getMetrics(start, end, 'cpu'),
api.getMetrics(start, end, 'network'),
]);
final cpuMetricsData = results[0]['metrics'];
final networkMetricsData = results[1]['metrics'];
if (cpuMetricsData == null || networkMetricsData == null) {
throw MetricsLoadException('Metrics data is null');
}
return HetznerMetricsLoaded(
period: period,
start: start,
end: end,
stepInSeconds: cpuMetricsData['step'],
cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'),
ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'),
ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'),
bandwidthIn:
timeSeriesSerializer(networkMetricsData, 'network.0.bandwidth.in'),
bandwidthOut: timeSeriesSerializer(
networkMetricsData,
'network.0.bandwidth.out',
),
);
}
}
List<TimeSeriesData> timeSeriesSerializer(
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();
}

View File

@ -1,45 +0,0 @@
part of 'hetzner_metrics_cubit.dart';
abstract class HetznerMetricsState extends Equatable {
const HetznerMetricsState();
abstract final Period period;
}
class HetznerMetricsLoading extends HetznerMetricsState {
const HetznerMetricsLoading(this.period);
@override
final Period period;
@override
List<Object?> get props => [period];
}
class HetznerMetricsLoaded extends HetznerMetricsState {
const HetznerMetricsLoaded({
required this.period,
required this.start,
required this.end,
required this.stepInSeconds,
required this.cpu,
required this.ppsIn,
required this.ppsOut,
required this.bandwidthIn,
required this.bandwidthOut,
});
@override
final Period period;
final DateTime start;
final DateTime end;
final num stepInSeconds;
final List<TimeSeriesData> cpu;
final List<TimeSeriesData> ppsIn;
final List<TimeSeriesData> ppsOut;
final List<TimeSeriesData> bandwidthIn;
final List<TimeSeriesData> bandwidthOut;
@override
List<Object?> get props => [period, start, end];
}

View File

@ -3,16 +3,16 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart'; import 'package:selfprivacy/logic/cubit/metrics/metrics_repository.dart';
part 'hetzner_metrics_state.dart'; part 'metrics_state.dart';
class HetznerMetricsCubit extends Cubit<HetznerMetricsState> { class MetricsCubit extends Cubit<MetricsState> {
HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day)); MetricsCubit() : super(const MetricsLoading(Period.day));
final HetznerMetricsRepository repository = HetznerMetricsRepository(); final MetricsRepository repository = MetricsRepository();
Timer? timer; Timer? timer;
@ -30,7 +30,7 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
void changePeriod(final Period period) async { void changePeriod(final Period period) async {
closeTimer(); closeTimer();
emit(HetznerMetricsLoading(period)); emit(MetricsLoading(period));
load(period); load(period);
} }
@ -40,14 +40,14 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
void load(final Period period) async { void load(final Period period) async {
try { try {
final HetznerMetricsLoaded newState = await repository.getMetrics(period); final MetricsLoaded newState = await repository.getMetrics(period);
timer = Timer( timer = Timer(
Duration(seconds: newState.stepInSeconds.toInt()), Duration(seconds: newState.metrics.stepsInSecond.toInt()),
() => load(newState.period), () => load(newState.period),
); );
emit(newState); emit(newState);
} on StateError { } on StateError {
print('Tried to emit Hetzner metrics when cubit is closed'); print('Tried to emit metrics when cubit is closed');
} on MetricsLoadException { } on MetricsLoadException {
timer = Timer( timer = Timer(
Duration(seconds: state.period.stepPeriodInSeconds), Duration(seconds: state.period.stepPeriodInSeconds),

View File

@ -0,0 +1,52 @@
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';
class MetricsLoadException implements Exception {
MetricsLoadException(this.message);
final String message;
}
class MetricsRepository {
Future<MetricsLoaded> getMetrics(final Period period) async {
final providerApiFactory = ApiController.currentServerProviderApiFactory;
if (providerApiFactory == null) {
throw MetricsLoadException('Server Provider data is null');
}
final DateTime end = DateTime.now();
DateTime start;
switch (period) {
case Period.hour:
start = end.subtract(const Duration(hours: 1));
break;
case Period.day:
start = end.subtract(const Duration(days: 1));
break;
case Period.month:
start = end.subtract(const Duration(days: 15));
break;
}
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
final ServerMetrics? metrics =
await providerApiFactory.getServerProvider().getMetrics(
serverId,
start,
end,
);
if (metrics == null) {
throw MetricsLoadException('Metrics data is null');
}
return MetricsLoaded(
period: period,
metrics: metrics,
);
}
}

View File

@ -0,0 +1,31 @@
part of 'metrics_cubit.dart';
abstract class MetricsState extends Equatable {
const MetricsState();
abstract final Period period;
}
class MetricsLoading extends MetricsState {
const MetricsLoading(this.period);
@override
final Period period;
@override
List<Object?> get props => [period];
}
class MetricsLoaded extends MetricsState {
const MetricsLoaded({
required this.period,
required this.metrics,
});
@override
final Period period;
final ServerMetrics metrics;
@override
List<Object?> get props => [period, metrics];
}

View File

@ -1,12 +1,13 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/disk_status.dart'; import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/price.dart';
part 'provider_volume_state.dart'; part 'provider_volume_state.dart';
@ -14,26 +15,19 @@ class ApiProviderVolumeCubit
extends ServerInstallationDependendCubit<ApiProviderVolumeState> { extends ServerInstallationDependendCubit<ApiProviderVolumeState> {
ApiProviderVolumeCubit(final ServerInstallationCubit serverInstallationCubit) ApiProviderVolumeCubit(final ServerInstallationCubit serverInstallationCubit)
: super(serverInstallationCubit, const ApiProviderVolumeState.initial()); : super(serverInstallationCubit, const ApiProviderVolumeState.initial());
VolumeProviderApiFactory? providerApi;
final ServerApi serverApi = ServerApi(); final ServerApi serverApi = ServerApi();
@override @override
Future<void> load() async { Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) { if (serverInstallationCubit.state is ServerInstallationFinished) {
final serverDetails = getIt<ApiConfigModel>().serverDetails;
providerApi = serverDetails == null
? null
: VolumeApiFactoryCreator.createVolumeProviderApiFactory(
getIt<ApiConfigModel>().serverDetails!.provider,
);
_refetch(); _refetch();
} }
} }
Future<double?> getPricePerGb() async => Future<Price?> getPricePerGb() async =>
providerApi!.getVolumeProvider().getPricePerGb(); ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider()
.getPricePerGb();
Future<void> refresh() async { Future<void> refresh() async {
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
@ -41,12 +35,14 @@ class ApiProviderVolumeCubit
} }
Future<void> _refetch() async { Future<void> _refetch() async {
if (providerApi == null) { if (ApiController.currentVolumeProviderApiFactory == null) {
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
} }
final List<ServerVolume> volumes = final List<ServerVolume> volumes = await ApiController
await providerApi!.getVolumeProvider().getVolumes(); .currentVolumeProviderApiFactory!
.getVolumeProvider()
.getVolumes();
if (volumes.isEmpty) { if (volumes.isEmpty) {
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
@ -57,31 +53,33 @@ class ApiProviderVolumeCubit
Future<void> attachVolume(final DiskVolume volume) async { Future<void> attachVolume(final DiskVolume volume) async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!; final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
await providerApi! await ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider() .getVolumeProvider()
.attachVolume(volume.providerVolume!.id, server.id); .attachVolume(volume.providerVolume!, server.id);
refresh(); refresh();
} }
Future<void> detachVolume(final DiskVolume volume) async { Future<void> detachVolume(final DiskVolume volume) async {
await providerApi! await ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider() .getVolumeProvider()
.detachVolume(volume.providerVolume!.id); .detachVolume(volume.providerVolume!);
refresh(); refresh();
} }
Future<bool> resizeVolume( Future<bool> resizeVolume(
final DiskVolume volume, final DiskVolume volume,
final int newSizeGb, final DiskSize newSize,
final Function() callback, final Function() callback,
) async { ) async {
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
'Starting resize', 'Starting resize',
); );
emit(state.copyWith(isResizing: true)); emit(state.copyWith(isResizing: true));
final bool resized = await providerApi!.getVolumeProvider().resizeVolume( final bool resized = await ApiController.currentVolumeProviderApiFactory!
volume.providerVolume!.id, .getVolumeProvider()
newSizeGb, .resizeVolume(
volume.providerVolume!,
newSize,
); );
if (!resized) { if (!resized) {
@ -93,13 +91,13 @@ class ApiProviderVolumeCubit
} }
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
'Hetzner resized, waiting 10 seconds', 'Provider volume resized, waiting 10 seconds',
); );
await Future.delayed(const Duration(seconds: 10)); await Future.delayed(const Duration(seconds: 10));
await ServerApi().resizeVolume(volume.name); await ServerApi().resizeVolume(volume.name);
getIt<NavigationService>().showSnackBar( getIt<NavigationService>().showSnackBar(
'Server api resized, waiting 20 seconds', 'Server volume resized, waiting 20 seconds',
); );
await Future.delayed(const Duration(seconds: 20)); await Future.delayed(const Duration(seconds: 20));
@ -115,8 +113,10 @@ class ApiProviderVolumeCubit
} }
Future<void> createVolume() async { Future<void> createVolume() async {
final ServerVolume? volume = final ServerVolume? volume = await ApiController
await providerApi!.getVolumeProvider().createVolume(); .currentVolumeProviderApiFactory!
.getVolumeProvider()
.createVolume();
final diskVolume = DiskVolume(providerVolume: volume); final diskVolume = DiskVolume(providerVolume: volume);
await attachVolume(diskVolume); await attachVolume(diskVolume);
@ -128,9 +128,9 @@ class ApiProviderVolumeCubit
} }
Future<void> deleteVolume(final DiskVolume volume) async { Future<void> deleteVolume(final DiskVolume volume) async {
await providerApi! await ApiController.currentVolumeProviderApiFactory!
.getVolumeProvider() .getVolumeProvider()
.deleteVolume(volume.providerVolume!.id); .deleteVolume(volume.providerVolume!);
refresh(); refresh();
} }

View File

@ -1,4 +1,4 @@
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';

View File

@ -2,7 +2,7 @@ import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart';
part 'server_detailed_info_state.dart'; part 'server_detailed_info_state.dart';
@ -22,7 +22,7 @@ class ServerDetailsCubit
final ServerDetailsRepositoryDto data = await repository.load(); final ServerDetailsRepositoryDto data = await repository.load();
emit( emit(
Loaded( Loaded(
serverInfo: data.hetznerServerInfo, metadata: data.metadata,
autoUpgradeSettings: data.autoUpgradeSettings, autoUpgradeSettings: data.autoUpgradeSettings,
serverTimezone: data.serverTimezone, serverTimezone: data.serverTimezone,
checkTime: DateTime.now(), checkTime: DateTime.now(),

View File

@ -1,18 +1,23 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart';
class ServerDetailsRepository { class ServerDetailsRepository {
HetznerApi hetzner = HetznerApi();
ServerApi server = ServerApi(); ServerApi server = ServerApi();
Future<ServerDetailsRepositoryDto> load() async { Future<ServerDetailsRepositoryDto> load() async {
final serverProviderApi = ApiController.currentServerProviderApiFactory;
final settings = await server.getSystemSettings(); final settings = await server.getSystemSettings();
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
final metadata =
await serverProviderApi!.getServerProvider().getMetadata(serverId);
return ServerDetailsRepositoryDto( return ServerDetailsRepositoryDto(
autoUpgradeSettings: settings.autoUpgradeSettings, autoUpgradeSettings: settings.autoUpgradeSettings,
hetznerServerInfo: await hetzner.getInfo(), metadata: metadata,
serverTimezone: TimeZoneSettings.fromString( serverTimezone: TimeZoneSettings.fromString(
settings.timezone, settings.timezone,
), ),
@ -36,13 +41,11 @@ class ServerDetailsRepository {
class ServerDetailsRepositoryDto { class ServerDetailsRepositoryDto {
ServerDetailsRepositoryDto({ ServerDetailsRepositoryDto({
required this.hetznerServerInfo, required this.metadata,
required this.serverTimezone, required this.serverTimezone,
required this.autoUpgradeSettings, required this.autoUpgradeSettings,
}); });
final HetznerServerInfo hetznerServerInfo; final List<ServerMetadataEntity> metadata;
final TimeZoneSettings serverTimezone; final TimeZoneSettings serverTimezone;
final AutoUpgradeSettings autoUpgradeSettings; final AutoUpgradeSettings autoUpgradeSettings;
} }

View File

@ -17,21 +17,19 @@ class Loading extends ServerDetailsState {}
class Loaded extends ServerDetailsState { class Loaded extends ServerDetailsState {
const Loaded({ const Loaded({
required this.serverInfo, required this.metadata,
required this.serverTimezone, required this.serverTimezone,
required this.autoUpgradeSettings, required this.autoUpgradeSettings,
required this.checkTime, required this.checkTime,
}); });
final HetznerServerInfo serverInfo; final List<ServerMetadataEntity> metadata;
final TimeZoneSettings serverTimezone; final TimeZoneSettings serverTimezone;
final AutoUpgradeSettings autoUpgradeSettings; final AutoUpgradeSettings autoUpgradeSettings;
final DateTime checkTime; final DateTime checkTime;
@override @override
List<Object> get props => [ List<Object> get props => [
serverInfo, metadata,
serverTimezone, serverTimezone,
autoUpgradeSettings, autoUpgradeSettings,
checkTime, checkTime,

View File

@ -4,8 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.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/models/hive/backblaze_credential.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_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
@ -13,6 +17,8 @@ import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
export 'package:provider/provider.dart'; export 'package:provider/provider.dart';
@ -51,40 +57,85 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
} }
} }
RegExp getServerProviderApiTokenValidation() => void setServerProviderType(final ServerProvider providerType) async {
repository.serverProviderApiFactory! await repository.saveServerProviderType(providerType);
ApiController.initServerProviderApiFactory(
ServerProviderApiFactorySettings(
provider: providerType,
),
);
}
ProviderApiTokenValidation serverProviderApiTokenValidation() =>
ApiController.currentServerProviderApiFactory!
.getServerProvider() .getServerProvider()
.getApiTokenValidation(); .getApiTokenValidation();
RegExp getDnsProviderApiTokenValidation() => repository.dnsProviderApiFactory! RegExp getDnsProviderApiTokenValidation() =>
.getDnsProvider() ApiController.currentDnsProviderApiFactory!
.getApiTokenValidation(); .getDnsProvider()
.getApiTokenValidation();
Future<bool> isServerProviderApiTokenValid( Future<bool> isServerProviderApiTokenValid(
final String providerToken, final String providerToken,
) async => ) async =>
repository.serverProviderApiFactory! ApiController.currentServerProviderApiFactory!
.getServerProvider( .getServerProvider(
settings: const ProviderApiSettings(isWithToken: false), settings: const ServerProviderApiSettings(
isWithToken: false,
),
) )
.isApiTokenValid(providerToken); .isApiTokenValid(providerToken);
Future<bool> isDnsProviderApiTokenValid( Future<bool> isDnsProviderApiTokenValid(
final String providerToken, final String providerToken,
) async => ) async {
repository.dnsProviderApiFactory! if (ApiController.currentDnsProviderApiFactory == null) {
.getDnsProvider( // No other DNS provider is supported for now,
settings: const DnsProviderApiSettings(isWithToken: false), // so it's safe to hardcode Cloudflare
) ApiController.initDnsProviderApiFactory(
.isApiTokenValid(providerToken); DnsProviderApiFactorySettings(
provider: DnsProvider.cloudflare,
),
);
}
void setHetznerKey(final String hetznerKey) async { return ApiController.currentDnsProviderApiFactory!
await repository.saveHetznerKey(hetznerKey); .getDnsProvider(
settings: const DnsProviderApiSettings(isWithToken: false),
)
.isApiTokenValid(providerToken);
}
Future<List<ServerProviderLocation>> fetchAvailableLocations() async {
if (ApiController.currentServerProviderApiFactory == null) {
return [];
}
return ApiController.currentServerProviderApiFactory!
.getServerProvider()
.getAvailableLocations();
}
Future<List<ServerType>> fetchAvailableTypesByLocation(
final ServerProviderLocation location,
) async {
if (ApiController.currentServerProviderApiFactory == null) {
return [];
}
return ApiController.currentServerProviderApiFactory!
.getServerProvider()
.getServerTypesByLocation(location: location);
}
void setServerProviderKey(final String serverProviderKey) async {
await repository.saveServerProviderKey(serverProviderKey);
if (state is ServerInstallationRecovery) { if (state is ServerInstallationRecovery) {
emit( emit(
(state as ServerInstallationRecovery).copyWith( (state as ServerInstallationRecovery).copyWith(
providerApiToken: hetznerKey, providerApiToken: serverProviderKey,
currentStep: RecoveryStep.serverSelection, currentStep: RecoveryStep.serverSelection,
), ),
); );
@ -93,7 +144,33 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
emit( emit(
(state as ServerInstallationNotFinished).copyWith( (state as ServerInstallationNotFinished).copyWith(
providerApiToken: hetznerKey, providerApiToken: serverProviderKey,
),
);
}
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,
),
);
emit(
(state as ServerInstallationNotFinished).copyWith(
serverTypeIdentificator: serverType.identifier,
), ),
); );
} }
@ -104,6 +181,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
return; return;
} }
await repository.saveCloudFlareKey(cloudFlareKey); await repository.saveCloudFlareKey(cloudFlareKey);
emit( emit(
(state as ServerInstallationNotFinished) (state as ServerInstallationNotFinished)
.copyWith(cloudFlareKey: cloudFlareKey), .copyWith(cloudFlareKey: cloudFlareKey),
@ -248,14 +326,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
), ),
); );
timer = Timer(pauseDuration, () async { timer = Timer(pauseDuration, () async {
final ServerHostingDetails hetznerServerDetails = final ServerHostingDetails serverDetails = await repository.restart();
await repository.restart();
await repository.saveIsServerResetedFirstTime(true); await repository.saveIsServerResetedFirstTime(true);
await repository.saveServerDetails(hetznerServerDetails); await repository.saveServerDetails(serverDetails);
final ServerInstallationNotFinished newState = dataState.copyWith( final ServerInstallationNotFinished newState = dataState.copyWith(
isServerResetedFirstTime: true, isServerResetedFirstTime: true,
serverDetails: hetznerServerDetails, serverDetails: serverDetails,
isLoading: false, isLoading: false,
); );
@ -290,14 +367,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
), ),
); );
timer = Timer(pauseDuration, () async { timer = Timer(pauseDuration, () async {
final ServerHostingDetails hetznerServerDetails = final ServerHostingDetails serverDetails = await repository.restart();
await repository.restart();
await repository.saveIsServerResetedSecondTime(true); await repository.saveIsServerResetedSecondTime(true);
await repository.saveServerDetails(hetznerServerDetails); await repository.saveServerDetails(serverDetails);
final ServerInstallationNotFinished newState = dataState.copyWith( final ServerInstallationNotFinished newState = dataState.copyWith(
isServerResetedSecondTime: true, isServerResetedSecondTime: true,
serverDetails: hetznerServerDetails, serverDetails: serverDetails,
isLoading: false, isLoading: false,
); );
@ -423,11 +499,21 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
token, token,
dataState.recoveryCapabilities, dataState.recoveryCapabilities,
); );
final ServerProvider provider = await ServerApi(
customToken: serverDetails.apiToken,
isWithToken: true,
).getServerProviderType();
if (provider == ServerProvider.unknown) {
getIt<NavigationService>()
.showSnackBar('recovering.generic_error'.tr());
return;
}
await repository.saveServerDetails(serverDetails); await repository.saveServerDetails(serverDetails);
setServerProviderType(provider);
emit( emit(
dataState.copyWith( dataState.copyWith(
serverDetails: serverDetails, serverDetails: serverDetails,
currentStep: RecoveryStep.hetznerToken, currentStep: RecoveryStep.serverProviderToken,
), ),
); );
} on ServerAuthorizationException { } on ServerAuthorizationException {
@ -503,8 +589,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
} }
} }
Future<List<ServerBasicInfoWithValidators>> Future<List<ServerBasicInfoWithValidators>> getAvailableServers() async {
getServersOnHetznerAccount() async {
final ServerInstallationRecovery dataState = final ServerInstallationRecovery dataState =
state as ServerInstallationRecovery; state as ServerInstallationRecovery;
final List<ServerBasicInfo> servers = final List<ServerBasicInfo> servers =
@ -515,7 +600,9 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
serverBasicInfo: server, serverBasicInfo: server,
isIpValid: server.ip == dataState.serverDetails?.ip4, isIpValid: server.ip == dataState.serverDetails?.ip4,
isReverseDnsValid: isReverseDnsValid:
server.reverseDns == dataState.serverDomain?.domainName, server.reverseDns == dataState.serverDomain?.domainName ||
server.reverseDns ==
dataState.serverDomain?.domainName.split('.')[0],
), ),
); );
return validated.toList(); return validated.toList();
@ -533,7 +620,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
id: server.id, id: server.id,
createTime: server.created, createTime: server.created,
volume: ServerVolume( volume: ServerVolume(
id: server.volumeId, id: 0,
name: 'recovered_volume', name: 'recovered_volume',
sizeByte: 0, sizeByte: 0,
serverId: server.id, serverId: server.id,
@ -612,7 +699,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
void clearAppConfig() { void clearAppConfig() {
closeTimer(); closeTimer();
ApiController.clearProviderApiFactories();
repository.clearAppConfig(); repository.clearAppConfig();
emit(const ServerInstallationEmpty()); emit(const ServerInstallationEmpty());
} }

View File

@ -9,12 +9,12 @@ import 'package:hive/hive.dart';
import 'package:pub_semver/pub_semver.dart'; import 'package:pub_semver/pub_semver.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.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.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.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/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_credential.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_details.dart';
@ -24,8 +24,8 @@ import 'package:selfprivacy/logic/models/json/device_token.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/message.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/utils/network_utils.dart'; import 'package:selfprivacy/utils/network_utils.dart';
class IpNotFoundException implements Exception { class IpNotFoundException implements Exception {
@ -41,41 +41,52 @@ class ServerAuthorizationException implements Exception {
class ServerInstallationRepository { class ServerInstallationRepository {
Box box = Hive.box(BNames.serverInstallationBox); Box box = Hive.box(BNames.serverInstallationBox);
Box<User> usersBox = Hive.box(BNames.usersBox); Box<User> usersBox = Hive.box(BNames.usersBox);
ServerProviderApiFactory? serverProviderApiFactory =
ApiFactoryCreator.createServerProviderApiFactory(
ServerProvider.hetzner, // TODO: HARDCODE FOR NOW!!!
); // TODO: Remove when provider selection is implemented.
DnsProviderApiFactory? dnsProviderApiFactory =
ApiFactoryCreator.createDnsProviderApiFactory(
DnsProvider.cloudflare, // TODO: HARDCODE FOR NOW!!!
);
Future<ServerInstallationState> load() async { Future<ServerInstallationState> load() async {
final String? providerApiToken = getIt<ApiConfigModel>().hetznerKey; final String? providerApiToken = getIt<ApiConfigModel>().serverProviderKey;
final String? location = getIt<ApiConfigModel>().serverLocation;
final String? cloudflareToken = getIt<ApiConfigModel>().cloudFlareKey; final String? cloudflareToken = getIt<ApiConfigModel>().cloudFlareKey;
final String? serverTypeIdentificator = getIt<ApiConfigModel>().serverType;
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain; final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
final ServerProvider? serverProvider =
getIt<ApiConfigModel>().serverProvider;
final BackblazeCredential? backblazeCredential = final BackblazeCredential? backblazeCredential =
getIt<ApiConfigModel>().backblazeCredential; getIt<ApiConfigModel>().backblazeCredential;
final ServerHostingDetails? serverDetails = final ServerHostingDetails? serverDetails =
getIt<ApiConfigModel>().serverDetails; getIt<ApiConfigModel>().serverDetails;
if (serverDetails != null && if (serverProvider != null ||
serverDetails.provider != ServerProvider.unknown) { (serverDetails != null &&
serverProviderApiFactory = serverDetails.provider != ServerProvider.unknown)) {
ApiFactoryCreator.createServerProviderApiFactory( ApiController.initServerProviderApiFactory(
serverDetails.provider, ServerProviderApiFactorySettings(
provider: serverProvider ?? serverDetails!.provider,
location: location,
),
);
// All current providers support volumes
// so it's safe to hardcode for now
ApiController.initVolumeProviderApiFactory(
ServerProviderApiFactorySettings(
provider: serverProvider ?? serverDetails!.provider,
location: location,
),
); );
} }
if (serverDomain != null && serverDomain.provider != DnsProvider.unknown) { if (serverDomain != null && serverDomain.provider != DnsProvider.unknown) {
dnsProviderApiFactory = ApiFactoryCreator.createDnsProviderApiFactory( ApiController.initDnsProviderApiFactory(
serverDomain.provider, DnsProviderApiFactorySettings(
provider: serverDomain.provider,
),
); );
} }
if (box.get(BNames.hasFinalChecked, defaultValue: false)) { if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
return ServerInstallationFinished( return ServerInstallationFinished(
providerApiToken: providerApiToken!, providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '',
cloudFlareKey: cloudflareToken!, cloudFlareKey: cloudflareToken!,
serverDomain: serverDomain!, serverDomain: serverDomain!,
backblazeCredential: backblazeCredential!, backblazeCredential: backblazeCredential!,
@ -126,13 +137,13 @@ class ServerInstallationRepository {
} }
RecoveryStep _getCurrentRecoveryStep( RecoveryStep _getCurrentRecoveryStep(
final String? hetznerToken, final String? serverProviderToken,
final String? cloudflareToken, final String? cloudflareToken,
final ServerDomain serverDomain, final ServerDomain serverDomain,
final ServerHostingDetails? serverDetails, final ServerHostingDetails? serverDetails,
) { ) {
if (serverDetails != null) { if (serverDetails != null) {
if (hetznerToken != null) { if (serverProviderToken != null) {
if (serverDetails.provider != ServerProvider.unknown) { if (serverDetails.provider != ServerProvider.unknown) {
if (serverDomain.provider != DnsProvider.unknown) { if (serverDomain.provider != DnsProvider.unknown) {
return RecoveryStep.backblazeToken; return RecoveryStep.backblazeToken;
@ -141,7 +152,7 @@ class ServerInstallationRepository {
} }
return RecoveryStep.serverSelection; return RecoveryStep.serverSelection;
} }
return RecoveryStep.hetznerToken; return RecoveryStep.serverProviderToken;
} }
return RecoveryStep.selecting; return RecoveryStep.selecting;
} }
@ -152,18 +163,20 @@ class ServerInstallationRepository {
} }
Future<ServerHostingDetails> startServer( Future<ServerHostingDetails> startServer(
final ServerHostingDetails hetznerServer, final ServerHostingDetails server,
) async { ) async {
ServerHostingDetails serverDetails; ServerHostingDetails serverDetails;
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); serverDetails = await ApiController.currentServerProviderApiFactory!
serverDetails = await api.powerOn(); .getServerProvider()
.powerOn();
return serverDetails; return serverDetails;
} }
Future<String?> getDomainId(final String token, final String domain) async { Future<String?> getDomainId(final String token, final String domain) async {
final DnsProviderApi dnsProviderApi = dnsProviderApiFactory!.getDnsProvider( final DnsProviderApi dnsProviderApi =
ApiController.currentDnsProviderApiFactory!.getDnsProvider(
settings: DnsProviderApiSettings( settings: DnsProviderApiSettings(
isWithToken: false, isWithToken: false,
customToken: token, customToken: token,
@ -232,12 +245,14 @@ class ServerInstallationRepository {
required final Future<void> Function(ServerHostingDetails serverDetails) required final Future<void> Function(ServerHostingDetails serverDetails)
onSuccess, onSuccess,
}) async { }) async {
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); final ServerProviderApi api =
ApiController.currentServerProviderApiFactory!.getServerProvider();
try { try {
final ServerHostingDetails? serverDetails = await api.createServer( final ServerHostingDetails? serverDetails = await api.createServer(
dnsApiToken: cloudFlareKey, dnsApiToken: cloudFlareKey,
rootUser: rootUser, rootUser: rootUser,
domainName: domainName, domainName: domainName,
serverType: getIt<ApiConfigModel>().serverType!,
); );
if (serverDetails == null) { if (serverDetails == null) {
@ -248,82 +263,62 @@ class ServerInstallationRepository {
onSuccess(serverDetails); onSuccess(serverDetails);
} on DioError catch (e) { } on DioError catch (e) {
if (e.response!.data['error']['code'] == 'uniqueness_error') { if (e.response!.data['error']['code'] == 'uniqueness_error') {
final NavigationService nav = getIt.get<NavigationService>(); showPopUpAlert(
nav.showPopUpDialog( alertTitle: 'modals.already_exists'.tr(),
BrandAlert( description: 'modals.destroy_server'.tr(),
title: 'modals.already_exists'.tr(), actionButtonTitle: 'modals.yes'.tr(),
contentText: 'modals.destroy_server'.tr(), actionButtonOnPressed: () async {
actions: [ await api.deleteServer(
ActionButton( domainName: domainName,
text: 'basis.delete'.tr(), );
isRed: true,
onPressed: () async {
await api.deleteServer(
domainName: domainName,
);
ServerHostingDetails? serverDetails; ServerHostingDetails? serverDetails;
try { try {
serverDetails = await api.createServer( serverDetails = await api.createServer(
dnsApiToken: cloudFlareKey, dnsApiToken: cloudFlareKey,
rootUser: rootUser, rootUser: rootUser,
domainName: domainName, domainName: domainName,
); serverType: getIt<ApiConfigModel>().serverType!,
} catch (e) { );
print(e); } catch (e) {
} print(e);
}
if (serverDetails == null) { if (serverDetails == null) {
print('Server is not initialized!'); print('Server is not initialized!');
return; return;
} }
await saveServerDetails(serverDetails); await saveServerDetails(serverDetails);
onSuccess(serverDetails); onSuccess(serverDetails);
}, },
), cancelButtonOnPressed: onCancel,
ActionButton(
text: 'basis.cancel'.tr(),
onPressed: onCancel,
),
],
),
); );
} else { } else {
final NavigationService nav = getIt.get<NavigationService>(); showPopUpAlert(
nav.showPopUpDialog( alertTitle: 'modals.unexpected_error'.tr(),
BrandAlert( description: 'modals.try_again'.tr(),
title: 'modals.unexpected_error'.tr(), actionButtonTitle: 'modals.yes'.tr(),
contentText: 'modals.try_again'.tr(), actionButtonOnPressed: () async {
actions: [ ServerHostingDetails? serverDetails;
ActionButton( try {
text: 'modals.yes'.tr(), serverDetails = await api.createServer(
isRed: true, dnsApiToken: cloudFlareKey,
onPressed: () async { rootUser: rootUser,
ServerHostingDetails? serverDetails; domainName: domainName,
try { serverType: getIt<ApiConfigModel>().serverType!,
serverDetails = await api.createServer( );
dnsApiToken: cloudFlareKey, } catch (e) {
rootUser: rootUser, print(e);
domainName: domainName, }
);
} catch (e) {
print(e);
}
if (serverDetails == null) { if (serverDetails == null) {
print('Server is not initialized!'); print('Server is not initialized!');
return; return;
} }
await saveServerDetails(serverDetails); await saveServerDetails(serverDetails);
onSuccess(serverDetails); onSuccess(serverDetails);
}, },
), cancelButtonOnPressed: onCancel,
ActionButton(
text: 'basis.cancel'.tr(),
onPressed: onCancel,
),
],
),
); );
} }
} }
@ -335,9 +330,9 @@ class ServerInstallationRepository {
required final void Function() onCancel, required final void Function() onCancel,
}) async { }) async {
final DnsProviderApi dnsProviderApi = final DnsProviderApi dnsProviderApi =
dnsProviderApiFactory!.getDnsProvider(); ApiController.currentDnsProviderApiFactory!.getDnsProvider();
final ServerProviderApi serverApi = final ServerProviderApi serverApi =
serverProviderApiFactory!.getServerProvider(); ApiController.currentServerProviderApiFactory!.getServerProvider();
await dnsProviderApi.removeSimilarRecords( await dnsProviderApi.removeSimilarRecords(
ip4: serverDetails.ip4, ip4: serverDetails.ip4,
@ -350,31 +345,19 @@ class ServerInstallationRepository {
domain: domain, domain: domain,
); );
} on DioError catch (e) { } on DioError catch (e) {
final NavigationService nav = getIt.get<NavigationService>(); showPopUpAlert(
nav.showPopUpDialog( alertTitle: e.response!.data['errors'][0]['code'] == 1038
BrandAlert( ? 'modals.you_cant_use_this_api'.tr()
title: e.response!.data['errors'][0]['code'] == 1038 : 'domain.error'.tr(),
? 'modals.you_cant_use_this_api'.tr() description: 'modals.delete_server_volume'.tr(),
: 'domain.error'.tr(), cancelButtonOnPressed: onCancel,
contentText: 'modals.delete_server_volume'.tr(), actionButtonTitle: 'basis.delete'.tr(),
actions: [ actionButtonOnPressed: () async {
ActionButton( await serverApi.deleteServer(
text: 'basis.delete'.tr(), domainName: domain.domainName,
isRed: true, );
onPressed: () async { onCancel();
await serverApi.deleteServer( },
domainName: domain.domainName,
);
onCancel();
},
),
ActionButton(
text: 'basis.cancel'.tr(),
onPressed: onCancel,
),
],
),
); );
return false; return false;
} }
@ -389,7 +372,7 @@ class ServerInstallationRepository {
Future<void> createDkimRecord(final ServerDomain cloudFlareDomain) async { Future<void> createDkimRecord(final ServerDomain cloudFlareDomain) async {
final DnsProviderApi dnsProviderApi = final DnsProviderApi dnsProviderApi =
dnsProviderApiFactory!.getDnsProvider(); ApiController.currentDnsProviderApiFactory!.getDnsProvider();
final ServerApi api = ServerApi(); final ServerApi api = ServerApi();
late DnsRecord record; late DnsRecord record;
@ -408,15 +391,15 @@ class ServerInstallationRepository {
return api.isHttpServerWorking(); return api.isHttpServerWorking();
} }
Future<ServerHostingDetails> restart() async { Future<ServerHostingDetails> restart() async =>
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); ApiController.currentServerProviderApiFactory!
return api.restart(); .getServerProvider()
} .restart();
Future<ServerHostingDetails> powerOn() async { Future<ServerHostingDetails> powerOn() async =>
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); ApiController.currentServerProviderApiFactory!
return api.powerOn(); .getServerProvider()
} .powerOn();
Future<ServerRecoveryCapabilities> getRecoveryCapabilities( Future<ServerRecoveryCapabilities> getRecoveryCapabilities(
final ServerDomain serverDomain, final ServerDomain serverDomain,
@ -651,10 +634,10 @@ class ServerInstallationRepository {
} }
} }
Future<List<ServerBasicInfo>> getServersOnProviderAccount() async { Future<List<ServerBasicInfo>> getServersOnProviderAccount() async =>
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); ApiController.currentServerProviderApiFactory!
return api.getServers(); .getServerProvider()
} .getServers();
Future<void> saveServerDetails( Future<void> saveServerDetails(
final ServerHostingDetails serverDetails, final ServerHostingDetails serverDetails,
@ -667,12 +650,24 @@ class ServerInstallationRepository {
getIt<ApiConfigModel>().init(); getIt<ApiConfigModel>().init();
} }
Future<void> saveHetznerKey(final String key) async { Future<void> saveServerProviderType(final ServerProvider type) async {
print('saved'); await getIt<ApiConfigModel>().storeServerProviderType(type);
await getIt<ApiConfigModel>().storeHetznerKey(key);
} }
Future<void> deleteHetznerKey() async { Future<void> saveServerProviderKey(final String key) async {
await getIt<ApiConfigModel>().storeServerProviderKey(key);
}
Future<void> saveServerType(final ServerType serverType) async {
await getIt<ApiConfigModel>().storeServerTypeIdentifier(
serverType.identifier,
);
await getIt<ApiConfigModel>().storeServerLocation(
serverType.location.identifier,
);
}
Future<void> deleteServerProviderKey() async {
await box.delete(BNames.hetznerKey); await box.delete(BNames.hetznerKey);
getIt<ApiConfigModel>().init(); getIt<ApiConfigModel>().init();
} }
@ -731,13 +726,11 @@ class ServerInstallationRepository {
} }
Future<void> deleteServer(final ServerDomain serverDomain) async { Future<void> deleteServer(final ServerDomain serverDomain) async {
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); await ApiController.currentServerProviderApiFactory!
final DnsProviderApi dnsProviderApi = .getServerProvider()
dnsProviderApiFactory!.getDnsProvider(); .deleteServer(
domainName: serverDomain.domainName,
await api.deleteServer( );
domainName: serverDomain.domainName,
);
await box.put(BNames.hasFinalChecked, false); await box.put(BNames.hasFinalChecked, false);
await box.put(BNames.isServerStarted, false); await box.put(BNames.isServerStarted, false);
@ -746,7 +739,9 @@ class ServerInstallationRepository {
await box.put(BNames.isLoading, false); await box.put(BNames.isLoading, false);
await box.put(BNames.serverDetails, null); await box.put(BNames.serverDetails, null);
await dnsProviderApi.removeSimilarRecords(domain: serverDomain); await ApiController.currentDnsProviderApiFactory!
.getDnsProvider()
.removeSimilarRecords(domain: serverDomain);
} }
Future<void> deleteServerRelatedRecords() async { Future<void> deleteServerRelatedRecords() async {

View File

@ -3,6 +3,7 @@ part of '../server_installation/server_installation_cubit.dart';
abstract class ServerInstallationState extends Equatable { abstract class ServerInstallationState extends Equatable {
const ServerInstallationState({ const ServerInstallationState({
required this.providerApiToken, required this.providerApiToken,
required this.serverTypeIdentificator,
required this.cloudFlareKey, required this.cloudFlareKey,
required this.backblazeCredential, required this.backblazeCredential,
required this.serverDomain, required this.serverDomain,
@ -16,6 +17,7 @@ abstract class ServerInstallationState extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
providerApiToken, providerApiToken,
serverTypeIdentificator,
cloudFlareKey, cloudFlareKey,
backblazeCredential, backblazeCredential,
serverDomain, serverDomain,
@ -27,6 +29,7 @@ abstract class ServerInstallationState extends Equatable {
final String? providerApiToken; final String? providerApiToken;
final String? cloudFlareKey; final String? cloudFlareKey;
final String? serverTypeIdentificator;
final BackblazeCredential? backblazeCredential; final BackblazeCredential? backblazeCredential;
final ServerDomain? serverDomain; final ServerDomain? serverDomain;
final User? rootUser; final User? rootUser;
@ -35,7 +38,8 @@ abstract class ServerInstallationState extends Equatable {
final bool isServerResetedFirstTime; final bool isServerResetedFirstTime;
final bool isServerResetedSecondTime; final bool isServerResetedSecondTime;
bool get isServerProviderFilled => providerApiToken != null; bool get isServerProviderApiKeyFilled => providerApiToken != null;
bool get isServerTypeFilled => serverTypeIdentificator != null;
bool get isDnsProviderFilled => cloudFlareKey != null; bool get isDnsProviderFilled => cloudFlareKey != null;
bool get isBackupsProviderFilled => backblazeCredential != null; bool get isBackupsProviderFilled => backblazeCredential != null;
bool get isDomainSelected => serverDomain != null; bool get isDomainSelected => serverDomain != null;
@ -58,7 +62,8 @@ abstract class ServerInstallationState extends Equatable {
List<bool?> get _fulfilementList { List<bool?> get _fulfilementList {
final List<bool> res = [ final List<bool> res = [
isServerProviderFilled, isServerProviderApiKeyFilled,
isServerTypeFilled,
isDnsProviderFilled, isDnsProviderFilled,
isBackupsProviderFilled, isBackupsProviderFilled,
isDomainSelected, isDomainSelected,
@ -81,6 +86,7 @@ class TimerState extends ServerInstallationNotFinished {
this.duration, this.duration,
}) : super( }) : super(
providerApiToken: dataState.providerApiToken, providerApiToken: dataState.providerApiToken,
serverTypeIdentificator: dataState.serverTypeIdentificator,
cloudFlareKey: dataState.cloudFlareKey, cloudFlareKey: dataState.cloudFlareKey,
backblazeCredential: dataState.backblazeCredential, backblazeCredential: dataState.backblazeCredential,
serverDomain: dataState.serverDomain, serverDomain: dataState.serverDomain,
@ -106,7 +112,8 @@ class TimerState extends ServerInstallationNotFinished {
enum ServerSetupProgress { enum ServerSetupProgress {
nothingYet, nothingYet,
hetznerFilled, serverProviderFilled,
servertTypeFilled,
cloudFlareFilled, cloudFlareFilled,
backblazeFilled, backblazeFilled,
domainFilled, domainFilled,
@ -125,6 +132,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
required this.isLoading, required this.isLoading,
required this.dnsMatches, required this.dnsMatches,
super.providerApiToken, super.providerApiToken,
super.serverTypeIdentificator,
super.cloudFlareKey, super.cloudFlareKey,
super.backblazeCredential, super.backblazeCredential,
super.serverDomain, super.serverDomain,
@ -137,6 +145,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
@override @override
List<Object?> get props => [ List<Object?> get props => [
providerApiToken, providerApiToken,
serverTypeIdentificator,
cloudFlareKey, cloudFlareKey,
backblazeCredential, backblazeCredential,
serverDomain, serverDomain,
@ -150,6 +159,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
ServerInstallationNotFinished copyWith({ ServerInstallationNotFinished copyWith({
final String? providerApiToken, final String? providerApiToken,
final String? serverTypeIdentificator,
final String? cloudFlareKey, final String? cloudFlareKey,
final BackblazeCredential? backblazeCredential, final BackblazeCredential? backblazeCredential,
final ServerDomain? serverDomain, final ServerDomain? serverDomain,
@ -163,6 +173,8 @@ class ServerInstallationNotFinished extends ServerInstallationState {
}) => }) =>
ServerInstallationNotFinished( ServerInstallationNotFinished(
providerApiToken: providerApiToken ?? this.providerApiToken, providerApiToken: providerApiToken ?? this.providerApiToken,
serverTypeIdentificator:
serverTypeIdentificator ?? this.serverTypeIdentificator,
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
backblazeCredential: backblazeCredential ?? this.backblazeCredential, backblazeCredential: backblazeCredential ?? this.backblazeCredential,
serverDomain: serverDomain ?? this.serverDomain, serverDomain: serverDomain ?? this.serverDomain,
@ -179,6 +191,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
ServerInstallationFinished finish() => ServerInstallationFinished( ServerInstallationFinished finish() => ServerInstallationFinished(
providerApiToken: providerApiToken!, providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '',
cloudFlareKey: cloudFlareKey!, cloudFlareKey: cloudFlareKey!,
backblazeCredential: backblazeCredential!, backblazeCredential: backblazeCredential!,
serverDomain: serverDomain!, serverDomain: serverDomain!,
@ -194,6 +207,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
const ServerInstallationEmpty() const ServerInstallationEmpty()
: super( : super(
providerApiToken: null, providerApiToken: null,
serverTypeIdentificator: null,
cloudFlareKey: null, cloudFlareKey: null,
backblazeCredential: null, backblazeCredential: null,
serverDomain: null, serverDomain: null,
@ -210,6 +224,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
class ServerInstallationFinished extends ServerInstallationState { class ServerInstallationFinished extends ServerInstallationState {
const ServerInstallationFinished({ const ServerInstallationFinished({
required String super.providerApiToken, required String super.providerApiToken,
required String super.serverTypeIdentificator,
required String super.cloudFlareKey, required String super.cloudFlareKey,
required BackblazeCredential super.backblazeCredential, required BackblazeCredential super.backblazeCredential,
required ServerDomain super.serverDomain, required ServerDomain super.serverDomain,
@ -223,6 +238,7 @@ class ServerInstallationFinished extends ServerInstallationState {
@override @override
List<Object?> get props => [ List<Object?> get props => [
providerApiToken, providerApiToken,
serverTypeIdentificator,
cloudFlareKey, cloudFlareKey,
backblazeCredential, backblazeCredential,
serverDomain, serverDomain,
@ -238,7 +254,7 @@ enum RecoveryStep {
recoveryKey, recoveryKey,
newDeviceKey, newDeviceKey,
oldToken, oldToken,
hetznerToken, serverProviderToken,
serverSelection, serverSelection,
cloudflareToken, cloudflareToken,
backblazeToken, backblazeToken,
@ -261,6 +277,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
required this.currentStep, required this.currentStep,
required this.recoveryCapabilities, required this.recoveryCapabilities,
super.providerApiToken, super.providerApiToken,
super.serverTypeIdentificator,
super.cloudFlareKey, super.cloudFlareKey,
super.backblazeCredential, super.backblazeCredential,
super.serverDomain, super.serverDomain,
@ -277,6 +294,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
@override @override
List<Object?> get props => [ List<Object?> get props => [
providerApiToken, providerApiToken,
serverTypeIdentificator,
cloudFlareKey, cloudFlareKey,
backblazeCredential, backblazeCredential,
serverDomain, serverDomain,
@ -289,6 +307,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
ServerInstallationRecovery copyWith({ ServerInstallationRecovery copyWith({
final String? providerApiToken, final String? providerApiToken,
final String? serverTypeIdentificator,
final String? cloudFlareKey, final String? cloudFlareKey,
final BackblazeCredential? backblazeCredential, final BackblazeCredential? backblazeCredential,
final ServerDomain? serverDomain, final ServerDomain? serverDomain,
@ -299,6 +318,8 @@ class ServerInstallationRecovery extends ServerInstallationState {
}) => }) =>
ServerInstallationRecovery( ServerInstallationRecovery(
providerApiToken: providerApiToken ?? this.providerApiToken, providerApiToken: providerApiToken ?? this.providerApiToken,
serverTypeIdentificator:
serverTypeIdentificator ?? this.serverTypeIdentificator,
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
backblazeCredential: backblazeCredential ?? this.backblazeCredential, backblazeCredential: backblazeCredential ?? this.backblazeCredential,
serverDomain: serverDomain ?? this.serverDomain, serverDomain: serverDomain ?? this.serverDomain,
@ -310,6 +331,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
ServerInstallationFinished finish() => ServerInstallationFinished( ServerInstallationFinished finish() => ServerInstallationFinished(
providerApiToken: providerApiToken!, providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator ?? '',
cloudFlareKey: cloudFlareKey!, cloudFlareKey: cloudFlareKey!,
backblazeCredential: backblazeCredential!, backblazeCredential: backblazeCredential!,
serverDomain: serverDomain!, serverDomain: serverDomain!,

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart'; import 'package:selfprivacy/logic/models/json/server_job.dart';

View File

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/service.dart';

View File

@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/hive/user.dart';

View File

@ -9,22 +9,33 @@ class ApiConfigModel {
final Box _box = Hive.box(BNames.serverInstallationBox); final Box _box = Hive.box(BNames.serverInstallationBox);
ServerHostingDetails? get serverDetails => _serverDetails; ServerHostingDetails? get serverDetails => _serverDetails;
String? get hetznerKey => _hetznerKey; String? get serverProviderKey => _serverProviderKey;
String? get serverLocation => _serverLocation;
String? get serverType => _serverType;
String? get cloudFlareKey => _cloudFlareKey; String? get cloudFlareKey => _cloudFlareKey;
ServerProvider? get serverProvider => _serverProvider;
BackblazeCredential? get backblazeCredential => _backblazeCredential; BackblazeCredential? get backblazeCredential => _backblazeCredential;
ServerDomain? get serverDomain => _serverDomain; ServerDomain? get serverDomain => _serverDomain;
BackblazeBucket? get backblazeBucket => _backblazeBucket; BackblazeBucket? get backblazeBucket => _backblazeBucket;
String? _hetznerKey; String? _serverProviderKey;
String? _serverLocation;
String? _cloudFlareKey; String? _cloudFlareKey;
String? _serverType;
ServerProvider? _serverProvider;
ServerHostingDetails? _serverDetails; ServerHostingDetails? _serverDetails;
BackblazeCredential? _backblazeCredential; BackblazeCredential? _backblazeCredential;
ServerDomain? _serverDomain; ServerDomain? _serverDomain;
BackblazeBucket? _backblazeBucket; BackblazeBucket? _backblazeBucket;
Future<void> storeHetznerKey(final String value) async { Future<void> storeServerProviderType(final ServerProvider value) async {
await _box.put(BNames.serverProvider, value);
_serverProvider = value;
}
Future<void> storeServerProviderKey(final String value) async {
await _box.put(BNames.hetznerKey, value); await _box.put(BNames.hetznerKey, value);
_hetznerKey = value; _serverProviderKey = value;
} }
Future<void> storeCloudFlareKey(final String value) async { Future<void> storeCloudFlareKey(final String value) async {
@ -32,6 +43,16 @@ class ApiConfigModel {
_cloudFlareKey = value; _cloudFlareKey = value;
} }
Future<void> storeServerTypeIdentifier(final String typeIdentifier) async {
await _box.put(BNames.serverTypeIdentifier, typeIdentifier);
_serverType = typeIdentifier;
}
Future<void> storeServerLocation(final String serverLocation) async {
await _box.put(BNames.serverLocation, serverLocation);
_serverLocation = serverLocation;
}
Future<void> storeBackblazeCredential(final BackblazeCredential value) async { Future<void> storeBackblazeCredential(final BackblazeCredential value) async {
await _box.put(BNames.backblazeCredential, value); await _box.put(BNames.backblazeCredential, value);
_backblazeCredential = value; _backblazeCredential = value;
@ -53,20 +74,26 @@ class ApiConfigModel {
} }
void clear() { void clear() {
_hetznerKey = null; _serverProviderKey = null;
_serverLocation = null;
_cloudFlareKey = null; _cloudFlareKey = null;
_backblazeCredential = null; _backblazeCredential = null;
_serverDomain = null; _serverDomain = null;
_serverDetails = null; _serverDetails = null;
_backblazeBucket = null; _backblazeBucket = null;
_serverType = null;
_serverProvider = null;
} }
void init() { void init() {
_hetznerKey = _box.get(BNames.hetznerKey); _serverProviderKey = _box.get(BNames.hetznerKey);
_serverLocation = _box.get(BNames.serverLocation);
_cloudFlareKey = _box.get(BNames.cloudFlareKey); _cloudFlareKey = _box.get(BNames.cloudFlareKey);
_backblazeCredential = _box.get(BNames.backblazeCredential); _backblazeCredential = _box.get(BNames.backblazeCredential);
_serverDomain = _box.get(BNames.serverDomain); _serverDomain = _box.get(BNames.serverDomain);
_serverDetails = _box.get(BNames.serverDetails); _serverDetails = _box.get(BNames.serverDetails);
_backblazeBucket = _box.get(BNames.backblazeBucket); _backblazeBucket = _box.get(BNames.backblazeBucket);
_serverType = _box.get(BNames.serverTypeIdentifier);
_serverProvider = _box.get(BNames.serverProvider);
} }
} }

View File

@ -1,11 +0,0 @@
class TimeSeriesData {
TimeSeriesData(
this.secondsSinceEpoch,
this.value,
);
final int secondsSinceEpoch;
DateTime get time =>
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
final double value;
}

View File

@ -1,4 +1,5 @@
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart';
part 'server_details.g.dart'; part 'server_details.g.dart';
@ -58,6 +59,7 @@ class ServerVolume {
required this.sizeByte, required this.sizeByte,
required this.serverId, required this.serverId,
required this.linuxDevice, required this.linuxDevice,
this.uuid,
}); });
@HiveField(1) @HiveField(1)
@ -70,6 +72,8 @@ class ServerVolume {
int? serverId; int? serverId;
@HiveField(5, defaultValue: null) @HiveField(5, defaultValue: null)
String? linuxDevice; String? linuxDevice;
@HiveField(6, defaultValue: null)
String? uuid;
} }
@HiveType(typeId: 101) @HiveType(typeId: 101)
@ -78,4 +82,17 @@ enum ServerProvider {
unknown, unknown,
@HiveField(1) @HiveField(1)
hetzner, hetzner,
@HiveField(2)
digitalOcean;
factory ServerProvider.fromGraphQL(final Enum$ServerProvider provider) {
switch (provider) {
case Enum$ServerProvider.HETZNER:
return hetzner;
case Enum$ServerProvider.DIGITALOCEAN:
return digitalOcean;
default:
return unknown;
}
}
} }

View File

@ -76,13 +76,14 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
sizeByte: fields[3] == null ? 10737418240 : fields[3] as int, sizeByte: fields[3] == null ? 10737418240 : fields[3] as int,
serverId: fields[4] as int?, serverId: fields[4] as int?,
linuxDevice: fields[5] as String?, linuxDevice: fields[5] as String?,
uuid: fields[6] as String?,
); );
} }
@override @override
void write(BinaryWriter writer, ServerVolume obj) { void write(BinaryWriter writer, ServerVolume obj) {
writer writer
..writeByte(5) ..writeByte(6)
..writeByte(1) ..writeByte(1)
..write(obj.id) ..write(obj.id)
..writeByte(2) ..writeByte(2)
@ -92,7 +93,9 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
..writeByte(4) ..writeByte(4)
..write(obj.serverId) ..write(obj.serverId)
..writeByte(5) ..writeByte(5)
..write(obj.linuxDevice); ..write(obj.linuxDevice)
..writeByte(6)
..write(obj.uuid);
} }
@override @override
@ -117,6 +120,8 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
return ServerProvider.unknown; return ServerProvider.unknown;
case 1: case 1:
return ServerProvider.hetzner; return ServerProvider.hetzner;
case 2:
return ServerProvider.digitalOcean;
default: default:
return ServerProvider.unknown; return ServerProvider.unknown;
} }
@ -131,6 +136,9 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
case ServerProvider.hetzner: case ServerProvider.hetzner:
writer.writeByte(1); writer.writeByte(1);
break; break;
case ServerProvider.digitalOcean:
writer.writeByte(2);
break;
} }
} }

View File

@ -0,0 +1,30 @@
class TimeSeriesData {
TimeSeriesData(
this.secondsSinceEpoch,
this.value,
);
final int secondsSinceEpoch;
DateTime get time =>
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
final double value;
}
class ServerMetrics {
ServerMetrics({
required this.stepsInSecond,
required this.cpu,
required this.bandwidthIn,
required this.bandwidthOut,
required this.start,
required this.end,
});
final num stepsInSecond;
final List<TimeSeriesData> cpu;
final List<TimeSeriesData> bandwidthIn;
final List<TimeSeriesData> bandwidthOut;
final DateTime start;
final DateTime end;
}

View File

@ -0,0 +1,9 @@
class Price {
Price({
required this.value,
required this.currency,
});
double value;
String currency;
}

View File

@ -5,14 +5,12 @@ class ServerBasicInfo {
required this.reverseDns, required this.reverseDns,
required this.ip, required this.ip,
required this.created, required this.created,
required this.volumeId,
}); });
final int id; final int id;
final String name; final String name;
final String reverseDns; final String reverseDns;
final String ip; final String ip;
final DateTime created; final DateTime created;
final int volumeId;
} }
class ServerBasicInfoWithValidators extends ServerBasicInfo { class ServerBasicInfoWithValidators extends ServerBasicInfo {
@ -26,7 +24,6 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo {
reverseDns: serverBasicInfo.reverseDns, reverseDns: serverBasicInfo.reverseDns,
ip: serverBasicInfo.ip, ip: serverBasicInfo.ip,
created: serverBasicInfo.created, created: serverBasicInfo.created,
volumeId: serverBasicInfo.volumeId,
isIpValid: isIpValid, isIpValid: isIpValid,
isReverseDnsValid: isReverseDnsValid, isReverseDnsValid: isReverseDnsValid,
); );
@ -37,7 +34,6 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo {
required super.reverseDns, required super.reverseDns,
required super.ip, required super.ip,
required super.created, required super.created,
required super.volumeId,
required this.isIpValid, required this.isIpValid,
required this.isReverseDnsValid, required this.isReverseDnsValid,
}); });

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
enum MetadataType {
id(icon: Icons.numbers_outlined),
status(icon: Icons.mode_standby_outlined),
cpu(icon: Icons.memory_outlined),
ram(icon: Icons.memory_outlined),
cost(icon: Icons.payments_outlined),
location(icon: Icons.location_on_outlined),
other(icon: Icons.info_outlined);
const MetadataType({
required this.icon,
});
final IconData icon;
}
class ServerMetadataEntity {
ServerMetadataEntity({
required this.name,
required this.value,
this.type = MetadataType.other,
});
final MetadataType type;
final String name;
final String value;
}

View File

@ -0,0 +1,15 @@
class ServerProviderLocation {
ServerProviderLocation({
required this.title,
required this.identifier,
this.description,
this.flag,
});
final String title;
final String identifier;
final String? description;
/// as emoji
final String? flag;
}

View File

@ -0,0 +1,22 @@
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
class ServerType {
ServerType({
required this.title,
required this.identifier,
required this.ram,
required this.cores,
required this.disk,
required this.price,
required this.location,
});
final String title;
final String identifier;
final double ram;
final DiskSize disk;
final int cores;
final Price price;
final ServerProviderLocation location;
}

View File

@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';

View File

@ -2,18 +2,16 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart'; import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart'; import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
class JobsContent extends StatelessWidget { class JobsContent extends StatelessWidget {
const JobsContent({super.key}); const JobsContent({super.key});
@ -47,26 +45,16 @@ class JobsContent extends StatelessWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
BrandButton.text( BrandButton.text(
title: 'jobs.reboot_server'.tr(),
onPressed: () { onPressed: () {
final NavigationService nav = getIt<NavigationService>(); showPopUpAlert(
nav.showPopUpDialog( alertTitle: 'jobs.reboot_server'.tr(),
BrandAlert( description: 'modals.are_you_sure'.tr(),
title: 'jobs.reboot_server'.tr(), actionButtonTitle: 'modals.reboot'.tr(),
contentText: 'modals.are_you_sure'.tr(), actionButtonOnPressed: () =>
actions: [ {context.read<JobsCubit>().rebootServer()},
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () =>
{context.read<JobsCubit>().rebootServer()},
text: 'modals.reboot'.tr(),
)
],
),
); );
}, },
title: 'jobs.reboot_server'.tr(),
), ),
]; ];
} }

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';

View File

@ -1,5 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
Future<T?> showBrandBottomSheet<T>({ Future<T?> showBrandBottomSheet<T>({
required final BuildContext context, required final BuildContext context,
@ -12,3 +16,30 @@ Future<T?> showBrandBottomSheet<T>({
shadow: const BoxShadow(color: Colors.transparent), shadow: const BoxShadow(color: Colors.transparent),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
); );
void showPopUpAlert({
required final String description,
required final String actionButtonTitle,
required final void Function() actionButtonOnPressed,
final void Function()? cancelButtonOnPressed,
final String? alertTitle,
final String? cancelButtonTitle,
}) {
getIt.get<NavigationService>().showPopUpDialog(
BrandAlert(
title: alertTitle ?? 'basis.alert'.tr(),
contentText: description,
actions: [
ActionButton(
text: actionButtonTitle,
isRed: true,
onPressed: actionButtonOnPressed,
),
ActionButton(
text: cancelButtonTitle ?? 'basis.cancel'.tr(),
onPressed: cancelButtonOnPressed,
),
],
),
);
}

View File

@ -1,17 +1,15 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/models/json/backup.dart'; import 'package:selfprivacy/logic/models/json/backup.dart';
import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart'; import 'package:selfprivacy/ui/components/brand_cards/outlined_card.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -157,28 +155,17 @@ class _BackupDetailsState extends State<BackupDetails>
onTap: preventActions onTap: preventActions
? null ? null
: () { : () {
final NavigationService nav = showPopUpAlert(
getIt<NavigationService>(); alertTitle: 'backup.restoring'.tr(),
nav.showPopUpDialog( description: 'backup.restore_alert'.tr(
BrandAlert( args: [backup.time.toString()],
title: 'backup.restoring'.tr(),
contentText: 'backup.restore_alert'.tr(
args: [backup.time.toString()],
),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
text: 'modals.yes'.tr(),
)
],
), ),
actionButtonTitle: 'modals.yes'.tr(),
actionButtonOnPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
); );
}, },
title: Text( title: Text(

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';

View File

@ -11,7 +11,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart'; import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart'; import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/users/users.dart'; import 'package:selfprivacy/ui/pages/users/users.dart';

View File

@ -1,5 +1,5 @@
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
String bottomTitle( String bottomTitle(
@ -11,7 +11,7 @@ String bottomTitle(
final day = DateFormat('MMMd'); final day = DateFormat('MMMd');
String res; String res;
if (value <= 0) { if (value <= 0 || value >= data.length) {
return ''; return '';
} }

View File

@ -3,11 +3,11 @@ part of '../server_details_screen.dart';
class _Chart extends StatelessWidget { class _Chart extends StatelessWidget {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final HetznerMetricsCubit cubit = context.watch<HetznerMetricsCubit>(); final MetricsCubit cubit = context.watch<MetricsCubit>();
final Period period = cubit.state.period; final Period period = cubit.state.period;
final HetznerMetricsState state = cubit.state; final MetricsState state = cubit.state;
List<Widget> charts; List<Widget> charts;
if (state is HetznerMetricsLoaded || state is HetznerMetricsLoading) { if (state is MetricsLoaded || state is MetricsLoading) {
charts = [ charts = [
FilledCard( FilledCard(
clipped: false, clipped: false,
@ -26,10 +26,10 @@ class _Chart extends StatelessWidget {
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
if (state is HetznerMetricsLoaded) getCpuChart(state), if (state is MetricsLoaded) getCpuChart(state),
AnimatedOpacity( AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
opacity: state is HetznerMetricsLoading ? 1 : 0, opacity: state is MetricsLoading ? 1 : 0,
child: const _GraphLoadingCardContent(), child: const _GraphLoadingCardContent(),
), ),
], ],
@ -72,10 +72,10 @@ class _Chart extends StatelessWidget {
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
if (state is HetznerMetricsLoaded) getBandwidthChart(state), if (state is MetricsLoaded) getBandwidthChart(state),
AnimatedOpacity( AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
opacity: state is HetznerMetricsLoading ? 1 : 0, opacity: state is MetricsLoading ? 1 : 0,
child: const _GraphLoadingCardContent(), child: const _GraphLoadingCardContent(),
), ),
], ],
@ -122,29 +122,29 @@ class _Chart extends StatelessWidget {
); );
} }
Widget getCpuChart(final HetznerMetricsLoaded state) { Widget getCpuChart(final MetricsLoaded state) {
final data = state.cpu; final data = state.metrics.cpu;
return SizedBox( return SizedBox(
height: 200, height: 200,
child: CpuChart( child: CpuChart(
data: data, data: data,
period: state.period, period: state.period,
start: state.start, start: state.metrics.start,
), ),
); );
} }
Widget getBandwidthChart(final HetznerMetricsLoaded state) { Widget getBandwidthChart(final MetricsLoaded state) {
final ppsIn = state.bandwidthIn; final ppsIn = state.metrics.bandwidthIn;
final ppsOut = state.bandwidthOut; final ppsOut = state.metrics.bandwidthOut;
return SizedBox( return SizedBox(
height: 200, height: 200,
child: NetworkChart( child: NetworkChart(
listData: [ppsIn, ppsOut], listData: [ppsIn, ppsOut],
period: state.period, period: state.period,
start: state.start, start: state.metrics.start,
), ),
); );
} }

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart'; import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';
@ -83,7 +83,7 @@ class CpuChart extends StatelessWidget {
], ],
minY: 0, minY: 0,
maxY: 100, maxY: 100,
minX: data.length - 200, minX: 0,
titlesData: FlTitlesData( titlesData: FlTitlesData(
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles( bottomTitles: AxisTitles(

View File

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart'; import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';
class NetworkChart extends StatelessWidget { class NetworkChart extends StatelessWidget {
@ -116,7 +116,7 @@ class NetworkChart extends StatelessWidget {
...listData[1].map((final e) => e.value) ...listData[1].map((final e) => e.value)
].reduce(max) * ].reduce(max) *
1.2, 1.2,
minX: listData[0].length - 200, minX: 0,
titlesData: FlTitlesData( titlesData: FlTitlesData(
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles( bottomTitles: AxisTitles(

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
@ -21,7 +21,6 @@ import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart';
import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart'; import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart';
import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart'; import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart';
import 'package:selfprivacy/utils/extensions/duration.dart'; import 'package:selfprivacy/utils/extensions/duration.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:selfprivacy/utils/named_font_weight.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:timezone/timezone.dart'; import 'package:timezone/timezone.dart';
@ -92,7 +91,7 @@ class _ServerDetailsScreenState extends State<ServerDetailsScreen>
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
BlocProvider( BlocProvider(
create: (final context) => HetznerMetricsCubit()..restart(), create: (final context) => MetricsCubit()..restart(),
child: _Chart(), child: _Chart(),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

View File

@ -10,7 +10,6 @@ class _TextDetails extends StatelessWidget {
} else if (details is ServerDetailsNotReady) { } else if (details is ServerDetailsNotReady) {
return _TempMessage(message: 'basis.no_data'.tr()); return _TempMessage(message: 'basis.no_data'.tr());
} else if (details is Loaded) { } else if (details is Loaded) {
final data = details.serverInfo;
return FilledCard( return FilledCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -24,37 +23,15 @@ class _TextDetails extends StatelessWidget {
), ),
), ),
), ),
ListTileOnSurfaceVariant( ...details.metadata
leadingIcon: Icons.numbers_outlined, .map(
title: data.id.toString(), (final metadata) => ListTileOnSurfaceVariant(
subtitle: 'server.server_id'.tr(), leadingIcon: metadata.type.icon,
), title: metadata.name,
ListTileOnSurfaceVariant( subtitle: metadata.value,
leadingIcon: Icons.mode_standby_outlined, ),
title: data.status.toString().split('.')[1].capitalize(), )
subtitle: 'server.status'.tr(), .toList(),
),
ListTileOnSurfaceVariant(
leadingIcon: Icons.memory_outlined,
title: 'server.core_count'.plural(data.serverType.cores),
subtitle: 'server.cpu'.tr(),
),
ListTileOnSurfaceVariant(
leadingIcon: Icons.memory_outlined,
title: '${data.serverType.memory.toString()} GB',
subtitle: 'server.ram'.tr(),
),
ListTileOnSurfaceVariant(
leadingIcon: Icons.euro_outlined,
title: data.serverType.prices[1].monthly.toStringAsFixed(2),
subtitle: 'server.monthly_cost'.tr(),
),
// Server location
ListTileOnSurfaceVariant(
leadingIcon: Icons.location_on_outlined,
title: '${data.location.city}, ${data.location.country}',
subtitle: 'server.location'.tr(),
),
], ],
), ),
); );

View File

@ -4,6 +4,7 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/logic/models/disk_status.dart'; import 'package:selfprivacy/logic/models/disk_status.dart';
@ -74,7 +75,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
], ],
); );
} }
_euroPerGb = snapshot.data as double; _euroPerGb = (snapshot.data as Price).value;
_sizeController.text = _currentSliderGbValue.truncate().toString(); _sizeController.text = _currentSliderGbValue.truncate().toString();
_priceController.text = _priceController.text =
(_euroPerGb * double.parse(_sizeController.text)) (_euroPerGb * double.parse(_sizeController.text))
@ -152,7 +153,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
: () { : () {
context.read<ApiProviderVolumeCubit>().resizeVolume( context.read<ApiProviderVolumeCubit>().resizeVolume(
widget.diskVolumeToResize, widget.diskVolumeToResize,
_currentSliderGbValue.round(), DiskSize.fromGibibyte(_currentSliderGbValue),
context.read<ApiServerVolumeCubit>().reload, context.read<ApiServerVolumeCubit>().reload,
); );
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(

View File

@ -2,12 +2,12 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/provider_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
@ -17,6 +17,8 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -33,7 +35,8 @@ class InitializingPage extends StatelessWidget {
Widget? actualInitializingPage; Widget? actualInitializingPage;
if (cubit.state is! ServerInstallationFinished) { if (cubit.state is! ServerInstallationFinished) {
actualInitializingPage = [ actualInitializingPage = [
() => _stepHetzner(cubit), () => _stepServerProviderToken(cubit),
() => _stepServerType(cubit),
() => _stepCloudflare(cubit), () => _stepCloudflare(cubit),
() => _stepBackblaze(cubit), () => _stepBackblaze(cubit),
() => _stepDomain(cubit), () => _stepDomain(cubit),
@ -67,7 +70,8 @@ class InitializingPage extends StatelessWidget {
) )
: ProgressBar( : ProgressBar(
steps: const [ steps: const [
'Hetzner', 'Hosting',
'Server Type',
'CloudFlare', 'CloudFlare',
'Backblaze', 'Backblaze',
'Domain', 'Domain',
@ -78,6 +82,11 @@ class InitializingPage extends StatelessWidget {
activeIndex: cubit.state.porgressBar, activeIndex: cubit.state.porgressBar,
), ),
), ),
if (cubit.state.porgressBar ==
ServerSetupProgress.serverProviderFilled.index)
BrandText.h2(
'initializing.choose_location_type'.tr(),
),
_addCard( _addCard(
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@ -136,55 +145,34 @@ class InitializingPage extends StatelessWidget {
} }
} }
Widget _stepHetzner(final ServerInstallationCubit serverInstallationCubit) => Widget _stepServerProviderToken(
final ServerInstallationCubit serverInstallationCubit,
) =>
BlocProvider( BlocProvider(
create: (final context) => ProviderFormCubit( create: (final context) => ProviderFormCubit(serverInstallationCubit),
serverInstallationCubit,
),
child: Builder( child: Builder(
builder: (final context) { builder: (final context) {
final formCubitState = context.watch<ProviderFormCubit>().state; final providerCubit = context.watch<ProviderFormCubit>();
return Column( return ServerProviderPicker(
crossAxisAlignment: CrossAxisAlignment.start, formCubit: providerCubit,
children: [ serverInstallationCubit: serverInstallationCubit,
Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
const SizedBox(height: 10),
BrandText.h2('initializing.connect_to_server'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.place_where_data'.tr()),
const Spacer(),
CubitFormTextField(
formFieldCubit: context.read<ProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Hetzner API Token',
),
),
const Spacer(),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<ProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(fileName: 'how_hetzner'),
),
title: 'initializing.how'.tr(),
),
],
); );
}, },
), ),
); );
Widget _stepServerType(
final ServerInstallationCubit serverInstallationCubit,
) =>
BlocProvider(
create: (final context) => ProviderFormCubit(serverInstallationCubit),
child: Builder(
builder: (final context) => ServerTypePicker(
serverInstallationCubit: serverInstallationCubit,
),
),
);
void _showModal(final BuildContext context, final Widget widget) { void _showModal(final BuildContext context, final Widget widget) {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
@ -198,49 +186,44 @@ class InitializingPage extends StatelessWidget {
BlocProvider( BlocProvider(
create: (final context) => DnsProviderFormCubit(initializingCubit), create: (final context) => DnsProviderFormCubit(initializingCubit),
child: Builder( child: Builder(
builder: (final context) { builder: (final context) => Column(
final formCubitState = context.watch<DnsProviderFormCubit>().state; crossAxisAlignment: CrossAxisAlignment.start,
children: [
return Column( Image.asset(
crossAxisAlignment: CrossAxisAlignment.start, 'assets/images/logos/cloudflare.png',
children: [ width: 150,
Image.asset( ),
'assets/images/logos/cloudflare.png', const SizedBox(height: 10),
width: 150, BrandText.h2('initializing.connect_cloudflare'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.manage_domain_dns'.tr()),
const Spacer(),
CubitFormTextField(
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'initializing.cloudflare_api_token'.tr(),
), ),
const SizedBox(height: 10), ),
BrandText.h2('initializing.connect_cloudflare'.tr()), const Spacer(),
const SizedBox(height: 10), BrandButton.rised(
BrandText.body2('initializing.manage_domain_dns'.tr()), onPressed: () =>
const Spacer(), context.read<DnsProviderFormCubit>().trySubmit(),
CubitFormTextField( text: 'basis.connect'.tr(),
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey, ),
textAlign: TextAlign.center, const SizedBox(height: 10),
scrollPadding: const EdgeInsets.only(bottom: 70), BrandButton.text(
decoration: InputDecoration( onPressed: () => _showModal(
hintText: 'initializing.cloudflare_api_token'.tr(), context,
const _HowTo(
fileName: 'how_cloudflare',
), ),
), ),
const Spacer(), title: 'initializing.how'.tr(),
BrandButton.rised( ),
onPressed: formCubitState.isSubmitting ],
? null ),
: () => context.read<DnsProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(
fileName: 'how_cloudflare',
),
),
title: 'initializing.how'.tr(),
),
],
);
},
), ),
); );

View File

@ -0,0 +1,204 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/provider_form_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart';
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class ServerProviderPicker extends StatefulWidget {
const ServerProviderPicker({
required this.formCubit,
required this.serverInstallationCubit,
super.key,
});
final ProviderFormCubit formCubit;
final ServerInstallationCubit serverInstallationCubit;
@override
State<ServerProviderPicker> createState() => _ServerProviderPickerState();
}
class _ServerProviderPickerState extends State<ServerProviderPicker> {
ServerProvider selectedProvider = ServerProvider.unknown;
void setProvider(final ServerProvider provider) {
setState(() {
selectedProvider = provider;
});
}
@override
Widget build(final BuildContext context) {
switch (selectedProvider) {
case ServerProvider.unknown:
return ProviderSelectionPage(
serverInstallationCubit: widget.serverInstallationCubit,
callback: setProvider,
);
case ServerProvider.hetzner:
return ProviderInputDataPage(
providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo(
providerType: ServerProvider.hetzner,
pathToHow: 'hetzner_how',
image: Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
),
);
case ServerProvider.digitalOcean:
return ProviderInputDataPage(
providerCubit: widget.formCubit,
providerInfo: ProviderPageInfo(
providerType: ServerProvider.digitalOcean,
pathToHow: 'hetzner_how',
image: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
),
),
);
}
}
}
class ProviderPageInfo {
const ProviderPageInfo({
required this.providerType,
required this.pathToHow,
required this.image,
});
final String pathToHow;
final Image image;
final ServerProvider providerType;
}
class ProviderInputDataPage extends StatelessWidget {
const ProviderInputDataPage({
required this.providerInfo,
required this.providerCubit,
super.key,
});
final ProviderPageInfo providerInfo;
final ProviderFormCubit providerCubit;
@override
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
providerInfo.image,
const SizedBox(height: 10),
Text(
'initializing.connect_to_server'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
CubitFormTextField(
formFieldCubit: providerCubit.apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: const InputDecoration(
hintText: 'Provider API Token',
),
),
const Spacer(),
FilledButton(
title: 'basis.connect'.tr(),
onPressed: () => providerCubit.trySubmit(),
),
const SizedBox(height: 10),
OutlinedButton(
child: Text('initializing.how'.tr()),
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (final BuildContext context) => BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16),
children: [
BrandMarkdown(
fileName: providerInfo.pathToHow,
),
],
),
),
),
),
),
],
);
}
class ProviderSelectionPage extends StatelessWidget {
const ProviderSelectionPage({
required this.callback,
required this.serverInstallationCubit,
super.key,
});
final Function callback;
final ServerInstallationCubit serverInstallationCubit;
@override
Widget build(final BuildContext context) => Column(
children: [
Text(
'initializing.select_provider'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 10),
Text(
'initializing.place_where_data'.tr(),
),
const SizedBox(height: 10),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 320,
),
child: Row(
children: [
InkWell(
onTap: () {
serverInstallationCubit
.setServerProviderType(ServerProvider.hetzner);
callback(ServerProvider.hetzner);
},
child: Image.asset(
'assets/images/logos/hetzner.png',
width: 150,
),
),
const SizedBox(
width: 20,
),
InkWell(
onTap: () {
serverInstallationCubit
.setServerProviderType(ServerProvider.digitalOcean);
callback(ServerProvider.digitalOcean);
},
child: Image.asset(
'assets/images/logos/digital_ocean.png',
width: 150,
),
),
],
),
),
],
);
}

View File

@ -0,0 +1,195 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
class ServerTypePicker extends StatefulWidget {
const ServerTypePicker({
required this.serverInstallationCubit,
super.key,
});
final ServerInstallationCubit serverInstallationCubit;
@override
State<ServerTypePicker> createState() => _ServerTypePickerState();
}
class _ServerTypePickerState extends State<ServerTypePicker> {
ServerProviderLocation? serverProviderLocation;
ServerType? serverType;
void setServerProviderLocation(final ServerProviderLocation? location) {
setState(() {
serverProviderLocation = location;
});
}
@override
Widget build(final BuildContext context) {
if (serverProviderLocation == null) {
return SelectLocationPage(
serverInstallationCubit: widget.serverInstallationCubit,
callback: setServerProviderLocation,
);
}
return SelectTypePage(
location: serverProviderLocation!,
serverInstallationCubit: widget.serverInstallationCubit,
backToLocationPickingCallback: () {
setServerProviderLocation(null);
},
);
}
}
class SelectLocationPage extends StatelessWidget {
const SelectLocationPage({
required this.serverInstallationCubit,
required this.callback,
super.key,
});
final Function callback;
final ServerInstallationCubit serverInstallationCubit;
@override
Widget build(final BuildContext context) => FutureBuilder(
future: serverInstallationCubit.fetchAvailableLocations(),
builder: (
final BuildContext context,
final AsyncSnapshot<Object?> snapshot,
) {
if (snapshot.hasData) {
if ((snapshot.data as List<ServerProviderLocation>).isEmpty) {
return Text('initializing.no_locations_found'.tr());
}
return ListView(
padding: paddingH15V0,
children: [
...(snapshot.data! as List<ServerProviderLocation>).map(
(final location) => InkWell(
onTap: () {
callback(location);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (location.flag != null) Text(location.flag!),
const SizedBox(height: 8),
Text(location.title),
const SizedBox(height: 8),
if (location.description != null)
Text(location.description!),
],
),
),
),
),
),
const SizedBox(height: 24),
],
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}
class SelectTypePage extends StatelessWidget {
const SelectTypePage({
required this.backToLocationPickingCallback,
required this.location,
required this.serverInstallationCubit,
super.key,
});
final ServerProviderLocation location;
final ServerInstallationCubit serverInstallationCubit;
final Function backToLocationPickingCallback;
@override
Widget build(final BuildContext context) => FutureBuilder(
future: serverInstallationCubit.fetchAvailableTypesByLocation(location),
builder: (
final BuildContext context,
final AsyncSnapshot<Object?> snapshot,
) {
if (snapshot.hasData) {
if ((snapshot.data as List<ServerType>).isEmpty) {
return Column(
children: [
Text(
'initializing.no_server_types_found'.tr(),
),
const SizedBox(height: 10),
BrandButton.rised(
onPressed: () {
backToLocationPickingCallback();
},
text: 'initializing.back_to_locations'.tr(),
),
],
);
}
return ListView(
padding: paddingH15V0,
children: [
...(snapshot.data! as List<ServerType>).map(
(final type) => InkWell(
onTap: () {
serverInstallationCubit.setServerType(type);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
type.title,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'cores: ${type.cores.toString()}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Text(
'ram: ${type.ram.toString()}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Text(
'disk: ${type.disk.gibibyte.toString()}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
Text(
'price: ${type.price.value.toString()} ${type.price.currency}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
),
),
const SizedBox(height: 24),
],
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}

View File

@ -45,9 +45,8 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
hasFlashButton: false, hasFlashButton: false,
children: [ children: [
FutureBuilder<List<ServerBasicInfoWithValidators>>( FutureBuilder<List<ServerBasicInfoWithValidators>>(
future: context future:
.read<ServerInstallationCubit>() context.read<ServerInstallationCubit>().getAvailableServers(),
.getServersOnHetznerAccount(),
builder: (final context, final snapshot) { builder: (final context, final snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final servers = snapshot.data; final servers = snapshot.data;

View File

@ -13,7 +13,7 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_server_provider_connected.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
@ -47,8 +47,8 @@ class RecoveryRouting extends StatelessWidget {
case RecoveryStep.oldToken: case RecoveryStep.oldToken:
currentPage = const RecoverByOldToken(); currentPage = const RecoverByOldToken();
break; break;
case RecoveryStep.hetznerToken: case RecoveryStep.serverProviderToken:
currentPage = const RecoveryHetznerConnected(); currentPage = const RecoveryServerProviderConnected();
break; break;
case RecoveryStep.serverSelection: case RecoveryStep.serverSelection:
currentPage = const RecoveryConfirmServer(); currentPage = const RecoveryConfirmServer();

View File

@ -10,8 +10,8 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart';
class RecoveryHetznerConnected extends StatelessWidget { class RecoveryServerProviderConnected extends StatelessWidget {
const RecoveryHetznerConnected({super.key}); const RecoveryServerProviderConnected({super.key});
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
@ -26,8 +26,8 @@ class RecoveryHetznerConnected extends StatelessWidget {
context.watch<ProviderFormCubit>().state; context.watch<ProviderFormCubit>().state;
return BrandHeroScreen( return BrandHeroScreen(
heroTitle: 'recovering.hetzner_connected'.tr(), heroTitle: 'recovering.server_provider_connected'.tr(),
heroSubtitle: 'recovering.hetzner_connected_description'.tr( heroSubtitle: 'recovering.server_provider_connected_description'.tr(
args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], args: [appConfig.state.serverDomain?.domainName ?? 'your domain'],
), ),
hasBackButton: true, hasBackButton: true,
@ -40,7 +40,8 @@ class RecoveryHetznerConnected extends StatelessWidget {
formFieldCubit: context.read<ProviderFormCubit>().apiKey, formFieldCubit: context.read<ProviderFormCubit>().apiKey,
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: 'recovering.hetzner_connected_placeholder'.tr(), labelText:
'recovering.server_provider_connected_placeholder'.tr(),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@ -101,10 +101,10 @@ class StringGenerators {
hasSymbols: true, hasSymbols: true,
); );
static StringGeneratorFunction dbStorageName = () => getRandomString( static StringGeneratorFunction storageName = () => getRandomString(
6, 6,
hasLowercaseLetters: true, hasLowercaseLetters: true,
hasUppercaseLetters: true, hasUppercaseLetters: false,
hasNumbers: true, hasNumbers: true,
); );

View File

@ -35,7 +35,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.9.0"
auto_size_text: auto_size_text:
dependency: "direct main" dependency: "direct main"
description: description:
@ -126,7 +126,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -147,7 +147,7 @@ packages:
name: clock name: clock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
code_builder: code_builder:
dependency: transitive dependency: transitive
description: description:
@ -350,7 +350,7 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -631,12 +631,12 @@ packages:
source: hosted source: hosted
version: "1.1.3" version: "1.1.3"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.13.4" version: "0.13.5"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -762,21 +762,21 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.12"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.4" version: "0.1.5"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.8.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -846,7 +846,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
path_drawing: path_drawing:
dependency: transitive dependency: transitive
description: description:
@ -1159,7 +1159,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.9.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -1187,7 +1187,7 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
system_theme: system_theme:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1208,28 +1208,28 @@ packages:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
test: test:
dependency: transitive dependency: transitive
description: description:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.21.1" version: "1.21.4"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.9" version: "0.4.12"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.13" version: "0.4.16"
timezone: timezone:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -33,6 +33,7 @@ dependencies:
gtk_theme_fl: ^0.0.1 gtk_theme_fl: ^0.0.1
hive: ^2.2.3 hive: ^2.2.3
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
http: ^0.13.5
intl: ^0.17.0 intl: ^0.17.0
ionicons: ^0.1.2 ionicons: ^0.1.2
json_annotation: ^4.6.0 json_annotation: ^4.6.0