feat(hosting): Implement Digital Ocean hosting support #140
After Width: | Height: | Size: 36 KiB |
|
@ -31,7 +31,8 @@
|
|||
"remove": "Remove",
|
||||
"apply": "Apply",
|
||||
"done": "Done",
|
||||
"continue": "Continue"
|
||||
"continue": "Continue",
|
||||
"alert": "Alert"
|
||||
},
|
||||
"more_page": {
|
||||
"configuration_wizard": "Setup wizard",
|
||||
|
@ -109,6 +110,7 @@
|
|||
"disk": "Disk local",
|
||||
"monthly_cost": "Monthly cost",
|
||||
"location": "Location",
|
||||
"provider": "Provider",
|
||||
"core_count": {
|
||||
"one": "{} core",
|
||||
"two": "{} cores",
|
||||
|
@ -267,9 +269,14 @@
|
|||
},
|
||||
"initializing": {
|
||||
"connect_to_server": "Connect a server",
|
||||
"select_provider": "Select your provider",
|
||||
"place_where_data": "A place where your data and SelfPrivacy services will reside:",
|
||||
"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",
|
||||
"backblaze_bad_key_error": "Backblaze storage information is invalid",
|
||||
"connect_cloudflare": "Connect CloudFlare",
|
||||
|
@ -295,6 +302,7 @@
|
|||
"checks": "Checks have been completed \n{} out of {}"
|
||||
},
|
||||
"recovering": {
|
||||
"generic_error": "Operation failed, please try again.",
|
||||
"recovery_main_header": "Connect to an existing server",
|
||||
"domain_recovery_description": "Enter a server domain you want to get access for:",
|
||||
"domain_recover_placeholder": "Your domain",
|
||||
|
@ -314,9 +322,9 @@
|
|||
"fallback_select_provider_console": "Access to the server console of my prodiver.",
|
||||
"authorization_failed": "Couldn't log in with this key",
|
||||
"fallback_select_provider_console_hint": "For example: Hetzner.",
|
||||
"hetzner_connected": "Connect to Hetzner",
|
||||
"hetzner_connected_description": "Communication established. Enter Hetzner token with access to {}:",
|
||||
"hetzner_connected_placeholder": "Hetzner token",
|
||||
"server_provider_connected": "Connect to your Server Provider",
|
||||
"server_provider_connected_description": "Communication established. Enter you token with access to {}:",
|
||||
"server_provider_connected_placeholder": "Server Provider token",
|
||||
"confirm_server": "Confirm server",
|
||||
"confirm_server_description": "Found your server! Confirm it is the right one:",
|
||||
"confirm_server_accept": "Yes! That's it",
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
"remove": "Удалить",
|
||||
"apply": "Применить",
|
||||
"done": "Готово",
|
||||
"continue": "Продолжить"
|
||||
"continue": "Продолжить",
|
||||
"alert": "Уведомление"
|
||||
},
|
||||
"more_page": {
|
||||
"configuration_wizard": "Мастер настройки",
|
||||
|
@ -109,6 +110,7 @@
|
|||
"disk": "Диск",
|
||||
"monthly_cost": "Ежемесячная стоимость",
|
||||
"location": "Размещение",
|
||||
"provider": "Провайдер",
|
||||
"core_count": {
|
||||
"one": "{} ядро",
|
||||
"two": "{} ядра",
|
||||
|
@ -269,7 +271,11 @@
|
|||
"connect_to_server": "Подключите сервер",
|
||||
"place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:",
|
||||
"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 ключ неверен",
|
||||
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",
|
||||
"connect_cloudflare": "Подключите CloudFlare",
|
||||
|
@ -295,6 +301,7 @@
|
|||
"checks": "Проверок выполнено: \n{} / {}"
|
||||
},
|
||||
"recovering": {
|
||||
"generic_error": "Ошибка проведения операции, попробуйте ещё раз.",
|
||||
"recovery_main_header": "Подключиться к существующему серверу",
|
||||
"domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:",
|
||||
"domain_recover_placeholder": "Домен",
|
||||
|
|
|
@ -87,9 +87,18 @@ class BNames {
|
|||
/// A String field of [serverInstallationBox] box.
|
||||
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.
|
||||
static String cloudFlareKey = 'cloudFlareKey';
|
||||
|
||||
/// A String field of [serverTypeIdentifier] box.
|
||||
static String serverTypeIdentifier = 'serverTypeIdentifier';
|
||||
|
||||
/// A [User] field of [serverInstallationBox] box.
|
||||
static String rootUser = 'rootUser';
|
||||
|
||||
|
|
|
@ -1,10 +1,27 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
||||
|
||||
abstract class ApiMap {
|
||||
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(
|
||||
'https://api.$rootAddress/graphql',
|
||||
httpClient: ioClient,
|
||||
);
|
||||
|
||||
final String token = _getApiToken();
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:graphql/client.dart' as graphql;
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:selfprivacy/utils/scalars.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
import 'server_api.graphql.dart';
|
||||
part 'disk_volumes.graphql.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
|
|
|
@ -173,6 +173,7 @@ input RecoveryKeyLimitsInput {
|
|||
|
||||
enum ServerProvider {
|
||||
HETZNER
|
||||
DIGITALOCEAN
|
||||
}
|
||||
|
||||
type Service {
|
||||
|
|
|
@ -693,6 +693,8 @@ enum Enum$DnsProvider {
|
|||
enum Enum$ServerProvider {
|
||||
@JsonValue('HETZNER')
|
||||
HETZNER,
|
||||
@JsonValue('DIGITALOCEAN')
|
||||
DIGITALOCEAN,
|
||||
$unknown
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,14 @@ mutation RebootSystem {
|
|||
}
|
||||
}
|
||||
|
||||
query SystemServerProvider {
|
||||
system {
|
||||
provider {
|
||||
provider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query GetApiTokens {
|
||||
api {
|
||||
devices {
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:graphql/client.dart' as graphql;
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:selfprivacy/utils/scalars.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
part 'server_api.graphql.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
|
@ -3178,6 +3177,425 @@ class _CopyWithStubImpl$Mutation$RebootSystem$rebootSystem<TRes>
|
|||
_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)
|
||||
class Query$GetApiTokens {
|
||||
Query$GetApiTokens({required this.api, required this.$__typename});
|
||||
|
|
|
@ -330,6 +330,58 @@ Map<String, dynamic> _$Mutation$RebootSystem$rebootSystemToJson(
|
|||
'__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(
|
||||
api: Query$GetApiTokens$api.fromJson(json['api'] as Map<String, dynamic>),
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:gql/ast.dart';
|
|||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
import 'server_api.graphql.dart';
|
||||
part 'server_settings.graphql.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:graphql/client.dart' as graphql;
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:selfprivacy/utils/scalars.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'server_api.graphql.dart';
|
||||
part 'services.graphql.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:gql/ast.dart';
|
|||
import 'package:graphql/client.dart' as graphql;
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'schema.graphql.dart';
|
||||
import 'services.graphql.dart';
|
||||
import 'server_api.graphql.dart';
|
||||
part 'users.graphql.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
part of 'server.dart';
|
||||
part of 'server_api.dart';
|
||||
|
||||
mixin JobsApi on ApiMap {
|
||||
Future<List<ServerJob>> getServerJobs() async {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
part of 'server.dart';
|
||||
part of 'server_api.dart';
|
||||
|
||||
mixin ServerActionsApi on ApiMap {
|
||||
Future<bool> _commonBoolRequest(final Function graphQLMethod) async {
|
||||
|
|
|
@ -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/models/auto_upgrade_settings.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/json/api_token.dart';
|
||||
import 'package:selfprivacy/logic/models/json/backup.dart';
|
||||
|
@ -88,6 +89,25 @@ class ServerApi extends ApiMap
|
|||
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();
|
||||
NaiJi marked this conversation as resolved
|
||||
if (response.hasException) {
|
||||
inex
commented
Review
let's do let's do `if (response.hasException || response.parsedData == null) {` just in case
NaiJi
commented
Review
Condition If Condition `response.hasException` checks for a graphql exception content to `print(response.exception.toString())` it.
If `hasException` returns false, then `response.exception.toString()` won't do anything, it has nothing to do with `parsedData`.
|
||||
print(response.exception.toString());
|
||||
}
|
||||
providerType = ServerProvider.fromGraphQL(
|
||||
response.parsedData!.system.provider.provider,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
return providerType;
|
||||
}
|
||||
|
||||
Future<bool> isUsingBinds() async {
|
||||
QueryResult response;
|
||||
bool usesBinds = false;
|
|
@ -1,4 +1,4 @@
|
|||
part of 'server.dart';
|
||||
part of 'server_api.dart';
|
||||
|
||||
mixin ServicesApi on ApiMap {
|
||||
Future<List<Service>> getAllServices() async {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
part of 'server.dart';
|
||||
part of 'server_api.dart';
|
||||
|
||||
mixin UsersApi on ApiMap {
|
||||
Future<List<User>> getAllUsers() async {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
part of 'server.dart';
|
||||
part of 'server_api.dart';
|
||||
|
||||
mixin VolumeApi on ApiMap {
|
||||
Future<List<ServerDiskVolume>> getServerDiskVolumes() async {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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/dns_provider_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
|
@ -12,20 +14,22 @@ class UnknownApiProviderException implements Exception {
|
|||
|
||||
class ApiFactoryCreator {
|
||||
static ServerProviderApiFactory createServerProviderApiFactory(
|
||||
final ServerProvider provider,
|
||||
final ServerProviderApiFactorySettings settings,
|
||||
) {
|
||||
switch (provider) {
|
||||
switch (settings.provider) {
|
||||
case ServerProvider.hetzner:
|
||||
return HetznerApiFactory();
|
||||
return HetznerApiFactory(region: settings.location);
|
||||
case ServerProvider.digitalOcean:
|
||||
return DigitalOceanApiFactory(region: settings.location);
|
||||
case ServerProvider.unknown:
|
||||
throw UnknownApiProviderException('Unknown server provider');
|
||||
}
|
||||
}
|
||||
|
||||
static DnsProviderApiFactory createDnsProviderApiFactory(
|
||||
final DnsProvider provider,
|
||||
final DnsProviderApiFactorySettings settings,
|
||||
) {
|
||||
switch (provider) {
|
||||
switch (settings.provider) {
|
||||
case DnsProvider.cloudflare:
|
||||
return CloudflareApiFactory();
|
||||
case DnsProvider.unknown:
|
||||
|
@ -36,11 +40,13 @@ class ApiFactoryCreator {
|
|||
|
||||
class VolumeApiFactoryCreator {
|
||||
static VolumeProviderApiFactory createVolumeProviderApiFactory(
|
||||
final ServerProvider provider,
|
||||
final ServerProviderApiFactorySettings settings,
|
||||
) {
|
||||
switch (provider) {
|
||||
switch (settings.provider) {
|
||||
case ServerProvider.hetzner:
|
||||
return HetznerApiFactory();
|
||||
case ServerProvider.digitalOcean:
|
||||
return DigitalOceanApiFactory();
|
||||
case ServerProvider.unknown:
|
||||
throw UnknownApiProviderException('Unknown volume provider');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -9,8 +9,8 @@ import 'package:selfprivacy/config/get_it_config.dart';
|
|||
import 'package:selfprivacy/logic/models/message.dart';
|
||||
|
||||
abstract class ApiMap {
|
||||
Future<Dio> getClient() async {
|
||||
final Dio dio = Dio(await options);
|
||||
Future<Dio> getClient({final BaseOptions? customOptions}) async {
|
||||
final Dio dio = Dio(customOptions ?? (await options));
|
||||
if (hasLogger) {
|
||||
dio.interceptors.add(PrettyDioLogger());
|
||||
}
|
||||
|
@ -41,7 +41,8 @@ abstract class ApiMap {
|
|||
|
||||
FutureOr<BaseOptions> get options;
|
||||
|
||||
abstract final String rootAddress;
|
||||
String get rootAddress;
|
||||
|
||||
abstract final bool hasLogger;
|
||||
abstract final bool isWithToken;
|
||||
|
||||
|
|
|
@ -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/dns_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
|
||||
|
||||
class CloudflareApiFactory extends DnsProviderApiFactory {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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/provider_api_settings.dart';
|
||||
|
||||
class DnsProviderApiSettings extends ProviderApiSettings {
|
||||
const DnsProviderApiSettings({
|
||||
super.hasLogger = false,
|
||||
super.isWithToken = true,
|
||||
this.customToken,
|
||||
});
|
||||
final String? customToken;
|
||||
}
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
|
||||
|
||||
abstract class DnsProviderApiFactory {
|
||||
DnsProviderApi getDnsProvider({
|
||||
final DnsProviderApiSettings settings = const DnsProviderApiSettings(),
|
||||
final DnsProviderApiSettings settings,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
class ProviderApiSettings {
|
||||
const ProviderApiSettings({this.hasLogger = false, this.isWithToken = true});
|
||||
const ProviderApiSettings({
|
||||
this.hasLogger = false,
|
||||
this.isWithToken = true,
|
||||
});
|
||||
final bool hasLogger;
|
||||
final bool isWithToken;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
inex
commented
Review
As we have a Price class, why don't make a Currency class, to ensure type safety and get cool getters like
As we have a Price class, why don't make a Currency class, to ensure type safety and get cool getters like
- getShortcode
- getFormat
- getIcon
etc
NaiJi
commented
Review
It's beyond the scope of Digital Ocean implementation, though it indeed is better to be done asap, it will be planned. It's beyond the scope of Digital Ocean implementation, though it indeed is better to be done asap, it will be planned.
|
||||
);
|
||||
|
||||
@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;
|
||||
|
||||
NaiJi marked this conversation as resolved
inex
commented
Review
commented try block? commented try block?
|
||||
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 = [
|
||||
inex
commented
Review
.tr() should be called by UI layer. .tr() should be called by UI layer.
NaiJi
commented
Review
Will be a part of coming refactoring for business logic layer. Consider it a stub Will be a part of coming refactoring for business logic layer. Consider it a stub
|
||||
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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -2,28 +2,43 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/metrics.dart';
|
||||
import 'package:selfprivacy/logic/models/price.dart';
|
||||
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||
import 'package:selfprivacy/logic/models/server_metadata.dart';
|
||||
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
|
||||
import 'package:selfprivacy/utils/password_generator.dart';
|
||||
|
||||
class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
||||
HetznerApi({this.hasLogger = false, this.isWithToken = true});
|
||||
HetznerApi({
|
||||
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>().hetznerKey;
|
||||
final String? token = getIt<ApiConfigModel>().serverProviderKey;
|
||||
assert(token != null);
|
||||
options.headers = {'Authorization': 'Bearer $token'};
|
||||
}
|
||||
|
@ -36,7 +51,13 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
}
|
||||
|
||||
@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
|
||||
Future<bool> isApiTokenValid(final String token) async {
|
||||
|
@ -71,19 +92,22 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
}
|
||||
|
||||
@override
|
||||
RegExp getApiTokenValidation() =>
|
||||
RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]');
|
||||
ProviderApiTokenValidation getApiTokenValidation() =>
|
||||
ProviderApiTokenValidation(
|
||||
regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'),
|
||||
length: 64,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<double?> getPricePerGb() async {
|
||||
Future<Price?> getPricePerGb() async {
|
||||
double? price;
|
||||
|
||||
final Response dbGetResponse;
|
||||
final Response pricingResponse;
|
||||
final Dio client = await getClient();
|
||||
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'];
|
||||
price = double.parse(volumePrice);
|
||||
} catch (e) {
|
||||
|
@ -92,38 +116,43 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
client.close();
|
||||
}
|
||||
|
||||
return price;
|
||||
return price == null
|
||||
? null
|
||||
: Price(
|
||||
value: price,
|
||||
currency: 'EUR',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ServerVolume?> createVolume() async {
|
||||
ServerVolume? volume;
|
||||
|
||||
final Response dbCreateResponse;
|
||||
final Response createVolumeResponse;
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
dbCreateResponse = await client.post(
|
||||
createVolumeResponse = await client.post(
|
||||
'/volumes',
|
||||
data: {
|
||||
'size': 10,
|
||||
'name': StringGenerators.dbStorageName(),
|
||||
'name': StringGenerators.storageName(),
|
||||
'labels': {'labelkey': 'value'},
|
||||
'location': 'fsn1',
|
||||
'location': region,
|
||||
'automount': false,
|
||||
'format': 'ext4'
|
||||
},
|
||||
);
|
||||
final dbId = dbCreateResponse.data['volume']['id'];
|
||||
final dbSize = dbCreateResponse.data['volume']['size'];
|
||||
final dbServer = dbCreateResponse.data['volume']['server'];
|
||||
final dbName = dbCreateResponse.data['volume']['name'];
|
||||
final dbDevice = dbCreateResponse.data['volume']['linux_device'];
|
||||
final volumeId = createVolumeResponse.data['volume']['id'];
|
||||
final volumeSize = createVolumeResponse.data['volume']['size'];
|
||||
final volumeServer = createVolumeResponse.data['volume']['server'];
|
||||
final volumeName = createVolumeResponse.data['volume']['name'];
|
||||
final volumeDevice = createVolumeResponse.data['volume']['linux_device'];
|
||||
volume = ServerVolume(
|
||||
id: dbId,
|
||||
name: dbName,
|
||||
sizeByte: dbSize,
|
||||
serverId: dbServer,
|
||||
linuxDevice: dbDevice,
|
||||
id: volumeId,
|
||||
name: volumeName,
|
||||
sizeByte: volumeSize,
|
||||
serverId: volumeServer,
|
||||
linuxDevice: volumeDevice,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
@ -138,28 +167,28 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
Future<List<ServerVolume>> getVolumes({final String? status}) async {
|
||||
final List<ServerVolume> volumes = [];
|
||||
|
||||
final Response dbGetResponse;
|
||||
final Response getVolumesResonse;
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
dbGetResponse = await client.get(
|
||||
getVolumesResonse = await client.get(
|
||||
'/volumes',
|
||||
queryParameters: {
|
||||
'status': status,
|
||||
},
|
||||
);
|
||||
final List<dynamic> rawVolumes = dbGetResponse.data['volumes'];
|
||||
final List<dynamic> rawVolumes = getVolumesResonse.data['volumes'];
|
||||
for (final rawVolume in rawVolumes) {
|
||||
final int dbId = rawVolume['id'];
|
||||
final int dbSize = rawVolume['size'] * 1024 * 1024 * 1024;
|
||||
final dbServer = rawVolume['server'];
|
||||
final String dbName = rawVolume['name'];
|
||||
final dbDevice = rawVolume['linux_device'];
|
||||
final int volumeId = rawVolume['id'];
|
||||
final int volumeSize = rawVolume['size'] * 1024 * 1024 * 1024;
|
||||
final volumeServer = rawVolume['server'];
|
||||
final String volumeName = rawVolume['name'];
|
||||
final volumeDevice = rawVolume['linux_device'];
|
||||
final volume = ServerVolume(
|
||||
id: dbId,
|
||||
name: dbName,
|
||||
sizeByte: dbSize,
|
||||
serverId: dbServer,
|
||||
linuxDevice: dbDevice,
|
||||
id: volumeId,
|
||||
name: volumeName,
|
||||
sizeByte: volumeSize,
|
||||
serverId: volumeServer,
|
||||
linuxDevice: volumeDevice,
|
||||
);
|
||||
volumes.add(volume);
|
||||
}
|
||||
|
@ -172,25 +201,26 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
return volumes;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ServerVolume?> getVolume(final int id) async {
|
||||
Future<ServerVolume?> getVolume(
|
||||
final String volumeId,
|
||||
) async {
|
||||
ServerVolume? volume;
|
||||
|
||||
final Response dbGetResponse;
|
||||
final Response getVolumeResponse;
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
dbGetResponse = await client.get('/volumes/$id');
|
||||
final int dbId = dbGetResponse.data['volume']['id'];
|
||||
final int dbSize = dbGetResponse.data['volume']['size'];
|
||||
final int dbServer = dbGetResponse.data['volume']['server'];
|
||||
final String dbName = dbGetResponse.data['volume']['name'];
|
||||
final dbDevice = dbGetResponse.data['volume']['linux_device'];
|
||||
getVolumeResponse = await client.get('/volumes/$volumeId');
|
||||
final int responseVolumeId = getVolumeResponse.data['volume']['id'];
|
||||
final int volumeSize = getVolumeResponse.data['volume']['size'];
|
||||
final int volumeServer = getVolumeResponse.data['volume']['server'];
|
||||
final String volumeName = getVolumeResponse.data['volume']['name'];
|
||||
final volumeDevice = getVolumeResponse.data['volume']['linux_device'];
|
||||
volume = ServerVolume(
|
||||
id: dbId,
|
||||
name: dbName,
|
||||
sizeByte: dbSize,
|
||||
serverId: dbServer,
|
||||
linuxDevice: dbDevice,
|
||||
id: responseVolumeId,
|
||||
name: volumeName,
|
||||
sizeByte: volumeSize,
|
||||
serverId: volumeServer,
|
||||
linuxDevice: volumeDevice,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
@ -202,10 +232,10 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteVolume(final int id) async {
|
||||
Future<void> deleteVolume(final ServerVolume volume) async {
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
await client.delete('/volumes/$id');
|
||||
await client.delete('/volumes/${volume.id}');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
|
@ -214,20 +244,24 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<bool> attachVolume(final int volumeId, final int serverId) async {
|
||||
Future<bool> attachVolume(
|
||||
final ServerVolume volume,
|
||||
final int serverId,
|
||||
) async {
|
||||
bool success = false;
|
||||
|
||||
final Response dbPostResponse;
|
||||
final Response attachVolumeResponse;
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
dbPostResponse = await client.post(
|
||||
'/volumes/$volumeId/actions/attach',
|
||||
attachVolumeResponse = await client.post(
|
||||
'/volumes/${volume.id}/actions/attach',
|
||||
data: {
|
||||
'automount': true,
|
||||
'server': serverId,
|
||||
},
|
||||
);
|
||||
success = dbPostResponse.data['action']['status'].toString() != 'error';
|
||||
success =
|
||||
attachVolumeResponse.data['action']['status'].toString() != 'error';
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
|
@ -238,14 +272,17 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<bool> detachVolume(final int volumeId) async {
|
||||
Future<bool> detachVolume(final ServerVolume volume) async {
|
||||
bool success = false;
|
||||
|
||||
final Response dbPostResponse;
|
||||
final Response detachVolumeResponse;
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
dbPostResponse = await client.post('/volumes/$volumeId/actions/detach');
|
||||
success = dbPostResponse.data['action']['status'].toString() != 'error';
|
||||
detachVolumeResponse = await client.post(
|
||||
'/volumes/${volume.id}/actions/detach',
|
||||
);
|
||||
success =
|
||||
detachVolumeResponse.data['action']['status'].toString() != 'error';
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
|
@ -256,19 +293,23 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<bool> resizeVolume(final int volumeId, final int sizeGb) async {
|
||||
Future<bool> resizeVolume(
|
||||
final ServerVolume volume,
|
||||
final DiskSize size,
|
||||
) async {
|
||||
bool success = false;
|
||||
|
||||
final Response dbPostResponse;
|
||||
final Response resizeVolumeResponse;
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
dbPostResponse = await client.post(
|
||||
'/volumes/$volumeId/actions/resize',
|
||||
resizeVolumeResponse = await client.post(
|
||||
'/volumes/${volume.id}/actions/resize',
|
||||
data: {
|
||||
'size': sizeGb,
|
||||
'size': size.gibibyte,
|
||||
},
|
||||
);
|
||||
success = dbPostResponse.data['action']['status'].toString() != 'error';
|
||||
success =
|
||||
resizeVolumeResponse.data['action']['status'].toString() != 'error';
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
|
@ -283,6 +324,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
required final String dnsApiToken,
|
||||
required final User rootUser,
|
||||
required final String domainName,
|
||||
required final String serverType,
|
||||
}) async {
|
||||
ServerHostingDetails? details;
|
||||
|
||||
|
@ -295,7 +337,8 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
dnsApiToken: dnsApiToken,
|
||||
rootUser: rootUser,
|
||||
domainName: domainName,
|
||||
dataBase: newVolume,
|
||||
volume: newVolume,
|
||||
serverType: serverType,
|
||||
);
|
||||
|
||||
return details;
|
||||
|
@ -305,48 +348,43 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
required final String dnsApiToken,
|
||||
required final User rootUser,
|
||||
required final String domainName,
|
||||
required final ServerVolume dataBase,
|
||||
required final ServerVolume volume,
|
||||
required final String serverType,
|
||||
}) async {
|
||||
final Dio client = await getClient();
|
||||
|
||||
final String dbPassword = StringGenerators.dbPassword();
|
||||
final int dbId = dataBase.id;
|
||||
final int volumeId = volume.id;
|
||||
|
||||
final String apiToken = StringGenerators.apiToken();
|
||||
|
||||
final String hostname = getHostnameFromDomain(domainName);
|
||||
|
||||
const String infectBranch = 'providers/hetzner';
|
||||
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
|
||||
final String base64Password =
|
||||
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
|
||||
|
||||
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 =
|
||||
"#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";
|
||||
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');
|
||||
"#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";
|
||||
|
||||
ServerHostingDetails? serverDetails;
|
||||
DioError? hetznerError;
|
||||
bool success = false;
|
||||
|
||||
try {
|
||||
final Map<String, Object> data = {
|
||||
'name': hostname,
|
||||
'server_type': serverType,
|
||||
'start_after_create': false,
|
||||
'image': 'ubuntu-20.04',
|
||||
'volumes': [volumeId],
|
||||
'networks': [],
|
||||
'user_data': userdataString,
|
||||
'labels': {},
|
||||
'automount': true,
|
||||
'location': region!,
|
||||
};
|
||||
print('Decoded data: $data');
|
||||
|
||||
final Response serverCreateResponse = await client.post(
|
||||
'/servers',
|
||||
data: data,
|
||||
|
@ -356,7 +394,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
id: serverCreateResponse.data['server']['id'],
|
||||
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
|
||||
createTime: DateTime.now(),
|
||||
volume: dataBase,
|
||||
volume: volume,
|
||||
apiToken: apiToken,
|
||||
provider: ServerProvider.hetzner,
|
||||
);
|
||||
|
@ -372,7 +410,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
|
||||
if (!success) {
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
await deleteVolume(dbId);
|
||||
await deleteVolume(volume);
|
||||
}
|
||||
|
||||
if (hetznerError != null) {
|
||||
|
@ -459,14 +497,12 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
return server.copyWith(startTime: DateTime.now());
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getMetrics(
|
||||
Future<Map<String, dynamic>> requestRawMetrics(
|
||||
final int serverId,
|
||||
final DateTime start,
|
||||
final DateTime end,
|
||||
final String type,
|
||||
) async {
|
||||
final ServerHostingDetails? hetznerServer =
|
||||
getIt<ApiConfigModel>().serverDetails;
|
||||
|
||||
Map<String, dynamic> metrics = {};
|
||||
final Dio client = await getClient();
|
||||
try {
|
||||
|
@ -476,10 +512,10 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
'type': type
|
||||
};
|
||||
final Response res = await client.get(
|
||||
'/servers/${hetznerServer!.id}/metrics',
|
||||
'/servers/$serverId/metrics',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
metrics = res.data;
|
||||
metrics = res.data['metrics'];
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
|
@ -489,14 +525,115 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
return metrics;
|
||||
}
|
||||
|
||||
Future<HetznerServerInfo> getInfo() async {
|
||||
final ServerHostingDetails? hetznerServer =
|
||||
getIt<ApiConfigModel>().serverDetails;
|
||||
final Dio client = await getClient();
|
||||
final Response response = await client.get('/servers/${hetznerServer!.id}');
|
||||
close(client);
|
||||
List<TimeSeriesData> serializeTimeSeries(
|
||||
final Map<String, dynamic> json,
|
||||
final String type,
|
||||
) {
|
||||
final List list = json['time_series'][type]['values'];
|
||||
return list
|
||||
.map((final el) => TimeSeriesData(el[0], double.parse(el[1])))
|
||||
.toList();
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -521,7 +658,6 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
ip: server.publicNet.ipv4.ip,
|
||||
reverseDns: server.publicNet.ipv4.reverseDns,
|
||||
created: server.created,
|
||||
volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
@ -535,6 +671,96 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
|
|||
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
|
||||
Future<void> createReverseDns({
|
||||
required final ServerHostingDetails serverDetails,
|
||||
|
|
|
@ -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/server_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
|
||||
|
||||
class HetznerApiFactory extends ServerProviderApiFactory
|
||||
with VolumeProviderApiFactory {
|
||||
HetznerApiFactory({this.region});
|
||||
|
||||
final String? region;
|
||||
|
||||
@override
|
||||
ServerProviderApi getServerProvider({
|
||||
final ProviderApiSettings settings = const ProviderApiSettings(),
|
||||
final ServerProviderApiSettings settings =
|
||||
const ServerProviderApiSettings(),
|
||||
}) =>
|
||||
HetznerApi(
|
||||
region: settings.region ?? region,
|
||||
hasLogger: settings.hasLogger,
|
||||
isWithToken: settings.isWithToken,
|
||||
);
|
||||
|
||||
@override
|
||||
VolumeProviderApi getVolumeProvider({
|
||||
final ProviderApiSettings settings = const ProviderApiSettings(),
|
||||
final ServerProviderApiSettings settings =
|
||||
const ServerProviderApiSettings(),
|
||||
}) =>
|
||||
HetznerApi(
|
||||
region: settings.region ?? region,
|
||||
hasLogger: settings.hasLogger,
|
||||
isWithToken: settings.isWithToken,
|
||||
);
|
||||
|
|
|
@ -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_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/metrics.dart';
|
||||
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||
import 'package:selfprivacy/logic/models/server_metadata.dart';
|
||||
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||
|
||||
class ProviderApiTokenValidation {
|
||||
ProviderApiTokenValidation({
|
||||
required this.length,
|
||||
required this.regexp,
|
||||
});
|
||||
final int length;
|
||||
final RegExp regexp;
|
||||
}
|
||||
|
||||
abstract class ServerProviderApi extends ApiMap {
|
||||
Future<List<ServerBasicInfo>> getServers();
|
||||
Future<List<ServerProviderLocation>> getAvailableLocations();
|
||||
Future<List<ServerType>> getServerTypesByLocation({
|
||||
required final ServerProviderLocation location,
|
||||
});
|
||||
|
||||
Future<ServerHostingDetails> restart();
|
||||
Future<ServerHostingDetails> powerOn();
|
||||
|
@ -15,6 +32,7 @@ abstract class ServerProviderApi extends ApiMap {
|
|||
required final String dnsApiToken,
|
||||
required final User rootUser,
|
||||
required final String domainName,
|
||||
required final String serverType,
|
||||
});
|
||||
Future<void> createReverseDns({
|
||||
required final ServerHostingDetails serverDetails,
|
||||
|
@ -22,5 +40,19 @@ abstract class ServerProviderApi extends ApiMap {
|
|||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
|
||||
|
||||
abstract class ServerProviderApiFactory {
|
||||
ServerProviderApi getServerProvider({
|
||||
final ProviderApiSettings settings = const ProviderApiSettings(),
|
||||
final ServerProviderApiSettings settings,
|
||||
});
|
||||
}
|
||||
|
||||
mixin VolumeProviderApiFactory {
|
||||
VolumeProviderApi getVolumeProvider({
|
||||
final ProviderApiSettings settings = const ProviderApiSettings(),
|
||||
final ServerProviderApiSettings settings,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/price.dart';
|
||||
|
||||
mixin VolumeProviderApi on ApiMap {
|
||||
Future<ServerVolume?> createVolume();
|
||||
Future<List<ServerVolume>> getVolumes({final String? status});
|
||||
Future<ServerVolume?> getVolume(final int id);
|
||||
Future<bool> attachVolume(final int volumeId, final int serverId);
|
||||
Future<bool> detachVolume(final int volumeId);
|
||||
Future<bool> resizeVolume(final int volumeId, final int sizeGb);
|
||||
Future<void> deleteVolume(final int id);
|
||||
Future<double?> getPricePerGb();
|
||||
Future<bool> attachVolume(final ServerVolume volume, final int serverId);
|
||||
Future<bool> detachVolume(final ServerVolume volume);
|
||||
Future<bool> resizeVolume(final ServerVolume volume, final DiskSize size);
|
||||
Future<void> deleteVolume(final ServerVolume volume);
|
||||
Future<Price?> getPricePerGb();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -5,17 +5,6 @@ enum LoadingStatus {
|
|||
error,
|
||||
}
|
||||
|
||||
enum InitializingSteps {
|
||||
setHetznerKey,
|
||||
setCloudFlareKey,
|
||||
setDomainName,
|
||||
setRootUser,
|
||||
createServer,
|
||||
checkCloudFlareDns,
|
||||
startServer,
|
||||
checkSystemDnsAndDkimSet,
|
||||
}
|
||||
|
||||
enum Period {
|
||||
hour,
|
||||
day,
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:async';
|
|||
import 'package:easy_localization/easy_localization.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/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/models/hive/backblaze_bucket.dart';
|
||||
import 'package:selfprivacy/logic/models/json/backup.dart';
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/users/users_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/json/api_token.dart';
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
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_factory.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
||||
import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
||||
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/utils/network_utils.dart';
|
||||
|
||||
part 'dns_records_state.dart';
|
||||
|
@ -19,11 +18,6 @@ class DnsRecordsCubit
|
|||
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();
|
||||
|
||||
@override
|
||||
|
@ -44,7 +38,8 @@ class DnsRecordsCubit
|
|||
final String? ipAddress =
|
||||
serverInstallationCubit.state.serverDetails?.ip4;
|
||||
if (domain != null && ipAddress != null) {
|
||||
final List<DnsRecord> records = await dnsProviderApiFactory!
|
||||
final List<DnsRecord> records = await ApiController
|
||||
.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.getDnsRecords(domain: domain);
|
||||
final String? dkimPublicKey =
|
||||
|
@ -124,7 +119,7 @@ class DnsRecordsCubit
|
|||
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
|
||||
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
dnsProviderApiFactory!.getDnsProvider();
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider();
|
||||
await dnsProviderApi.removeSimilarRecords(domain: domain!);
|
||||
await dnsProviderApi.createMultipleDnsRecords(
|
||||
domain: domain,
|
||||
|
|
|
@ -7,15 +7,10 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
|||
|
||||
class DnsProviderFormCubit extends FormCubit {
|
||||
DnsProviderFormCubit(this.initializingCubit) {
|
||||
final RegExp regExp = initializingCubit.getDnsProviderApiTokenValidation();
|
||||
apiKey = FieldCubit(
|
||||
initalValue: '',
|
||||
validations: [
|
||||
RequiredStringValidation('validations.required'.tr()),
|
||||
ValidationModel<String>(
|
||||
regExp.hasMatch,
|
||||
'validations.invalid_format'.tr(),
|
||||
),
|
||||
LengthStringNotEqualValidation(40)
|
||||
],
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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/models/hive/server_domain.dart';
|
||||
|
||||
|
@ -9,8 +10,7 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
|
|||
|
||||
Future<void> load() async {
|
||||
emit(Loading(LoadingTypes.loadingDomain));
|
||||
final List<String> list = await serverInstallationCubit
|
||||
.repository.dnsProviderApiFactory!
|
||||
final List<String> list = await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.domainList();
|
||||
if (list.isEmpty) {
|
||||
|
@ -31,8 +31,7 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
|
|||
|
||||
emit(Loading(LoadingTypes.saving));
|
||||
|
||||
final String? zoneId = await serverInstallationCubit
|
||||
.repository.dnsProviderApiFactory!
|
||||
final String? zoneId = await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.getZoneId(domainName);
|
||||
|
||||
|
|
|
@ -3,21 +3,16 @@ import 'dart:async';
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
|
||||
|
||||
class ProviderFormCubit extends FormCubit {
|
||||
ProviderFormCubit(this.serverInstallationCubit) {
|
||||
final RegExp regExp =
|
||||
serverInstallationCubit.getServerProviderApiTokenValidation();
|
||||
//final int tokenLength =
|
||||
// serverInstallationCubit.serverProviderApiTokenValidation().length;
|
||||
apiKey = FieldCubit(
|
||||
initalValue: '',
|
||||
validations: [
|
||||
RequiredStringValidation('validations.required'.tr()),
|
||||
ValidationModel<String>(
|
||||
regExp.hasMatch,
|
||||
'validations.invalid_format'.tr(),
|
||||
),
|
||||
LengthStringNotEqualValidation(64)
|
||||
//LengthStringNotEqualValidation(tokenLength),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -26,7 +21,7 @@ class ProviderFormCubit extends FormCubit {
|
|||
|
||||
@override
|
||||
FutureOr<void> onSubmit() async {
|
||||
serverInstallationCubit.setHetznerKey(apiKey.state.value);
|
||||
serverInstallationCubit.setServerProviderKey(apiKey.state.value);
|
||||
}
|
||||
|
||||
final ServerInstallationCubit serverInstallationCubit;
|
||||
|
@ -45,7 +40,7 @@ class ProviderFormCubit extends FormCubit {
|
|||
}
|
||||
|
||||
if (!isKeyValid) {
|
||||
apiKey.setError('initializing.hetzner_bad_key_error'.tr());
|
||||
apiKey.setError('initializing.provider_bad_key_error'.tr());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:cubit_form/cubit_form.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/forms/factories/field_cubit_factory.dart';
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -3,16 +3,16 @@ import 'dart:async';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.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> {
|
||||
HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day));
|
||||
class MetricsCubit extends Cubit<MetricsState> {
|
||||
MetricsCubit() : super(const MetricsLoading(Period.day));
|
||||
|
||||
final HetznerMetricsRepository repository = HetznerMetricsRepository();
|
||||
final MetricsRepository repository = MetricsRepository();
|
||||
|
||||
Timer? timer;
|
||||
|
||||
|
@ -30,7 +30,7 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
|
|||
|
||||
void changePeriod(final Period period) async {
|
||||
closeTimer();
|
||||
emit(HetznerMetricsLoading(period));
|
||||
emit(MetricsLoading(period));
|
||||
load(period);
|
||||
}
|
||||
|
||||
|
@ -40,14 +40,14 @@ class HetznerMetricsCubit extends Cubit<HetznerMetricsState> {
|
|||
|
||||
void load(final Period period) async {
|
||||
try {
|
||||
final HetznerMetricsLoaded newState = await repository.getMetrics(period);
|
||||
final MetricsLoaded newState = await repository.getMetrics(period);
|
||||
timer = Timer(
|
||||
Duration(seconds: newState.stepInSeconds.toInt()),
|
||||
Duration(seconds: newState.metrics.stepsInSecond.toInt()),
|
||||
() => load(newState.period),
|
||||
);
|
||||
emit(newState);
|
||||
} on StateError {
|
||||
print('Tried to emit Hetzner metrics when cubit is closed');
|
||||
print('Tried to emit metrics when cubit is closed');
|
||||
} on MetricsLoadException {
|
||||
timer = Timer(
|
||||
Duration(seconds: state.period.stepPeriodInSeconds),
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_status.dart';
|
||||
import 'package:selfprivacy/logic/models/price.dart';
|
||||
|
||||
part 'provider_volume_state.dart';
|
||||
|
||||
|
@ -14,26 +15,19 @@ class ApiProviderVolumeCubit
|
|||
extends ServerInstallationDependendCubit<ApiProviderVolumeState> {
|
||||
ApiProviderVolumeCubit(final ServerInstallationCubit serverInstallationCubit)
|
||||
: super(serverInstallationCubit, const ApiProviderVolumeState.initial());
|
||||
|
||||
VolumeProviderApiFactory? providerApi;
|
||||
|
||||
final ServerApi serverApi = ServerApi();
|
||||
|
||||
@override
|
||||
Future<void> load() async {
|
||||
if (serverInstallationCubit.state is ServerInstallationFinished) {
|
||||
final serverDetails = getIt<ApiConfigModel>().serverDetails;
|
||||
providerApi = serverDetails == null
|
||||
? null
|
||||
: VolumeApiFactoryCreator.createVolumeProviderApiFactory(
|
||||
getIt<ApiConfigModel>().serverDetails!.provider,
|
||||
);
|
||||
_refetch();
|
||||
}
|
||||
}
|
||||
|
||||
Future<double?> getPricePerGb() async =>
|
||||
providerApi!.getVolumeProvider().getPricePerGb();
|
||||
Future<Price?> getPricePerGb() async =>
|
||||
ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.getPricePerGb();
|
||||
|
||||
Future<void> refresh() async {
|
||||
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
|
||||
|
@ -41,12 +35,14 @@ class ApiProviderVolumeCubit
|
|||
}
|
||||
|
||||
Future<void> _refetch() async {
|
||||
if (providerApi == null) {
|
||||
if (ApiController.currentVolumeProviderApiFactory == null) {
|
||||
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
|
||||
}
|
||||
|
||||
final List<ServerVolume> volumes =
|
||||
await providerApi!.getVolumeProvider().getVolumes();
|
||||
final List<ServerVolume> volumes = await ApiController
|
||||
.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.getVolumes();
|
||||
|
||||
if (volumes.isEmpty) {
|
||||
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
|
||||
|
@ -57,31 +53,33 @@ class ApiProviderVolumeCubit
|
|||
|
||||
Future<void> attachVolume(final DiskVolume volume) async {
|
||||
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
|
||||
await providerApi!
|
||||
await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.attachVolume(volume.providerVolume!.id, server.id);
|
||||
.attachVolume(volume.providerVolume!, server.id);
|
||||
refresh();
|
||||
}
|
||||
|
||||
Future<void> detachVolume(final DiskVolume volume) async {
|
||||
await providerApi!
|
||||
await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.detachVolume(volume.providerVolume!.id);
|
||||
.detachVolume(volume.providerVolume!);
|
||||
refresh();
|
||||
}
|
||||
|
||||
Future<bool> resizeVolume(
|
||||
final DiskVolume volume,
|
||||
final int newSizeGb,
|
||||
final DiskSize newSize,
|
||||
final Function() callback,
|
||||
) async {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'Starting resize',
|
||||
);
|
||||
emit(state.copyWith(isResizing: true));
|
||||
final bool resized = await providerApi!.getVolumeProvider().resizeVolume(
|
||||
volume.providerVolume!.id,
|
||||
newSizeGb,
|
||||
final bool resized = await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.resizeVolume(
|
||||
volume.providerVolume!,
|
||||
newSize,
|
||||
);
|
||||
|
||||
if (!resized) {
|
||||
|
@ -93,13 +91,13 @@ class ApiProviderVolumeCubit
|
|||
}
|
||||
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'Hetzner resized, waiting 10 seconds',
|
||||
'Provider volume resized, waiting 10 seconds',
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
|
||||
await ServerApi().resizeVolume(volume.name);
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'Server api resized, waiting 20 seconds',
|
||||
'Server volume resized, waiting 20 seconds',
|
||||
);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 20));
|
||||
|
@ -115,8 +113,10 @@ class ApiProviderVolumeCubit
|
|||
}
|
||||
|
||||
Future<void> createVolume() async {
|
||||
final ServerVolume? volume =
|
||||
await providerApi!.getVolumeProvider().createVolume();
|
||||
final ServerVolume? volume = await ApiController
|
||||
.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.createVolume();
|
||||
|
||||
final diskVolume = DiskVolume(providerVolume: volume);
|
||||
await attachVolume(diskVolume);
|
||||
|
@ -128,9 +128,9 @@ class ApiProviderVolumeCubit
|
|||
}
|
||||
|
||||
Future<void> deleteVolume(final DiskVolume volume) async {
|
||||
await providerApi!
|
||||
await ApiController.currentVolumeProviderApiFactory!
|
||||
.getVolumeProvider()
|
||||
.deleteVolume(volume.providerVolume!.id);
|
||||
.deleteVolume(volume.providerVolume!);
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
|
|
@ -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/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
|
||||
|
|
|
@ -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/server_detailed_info/server_detailed_info_repository.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';
|
||||
|
||||
part 'server_detailed_info_state.dart';
|
||||
|
@ -22,7 +22,7 @@ class ServerDetailsCubit
|
|||
final ServerDetailsRepositoryDto data = await repository.load();
|
||||
emit(
|
||||
Loaded(
|
||||
serverInfo: data.hetznerServerInfo,
|
||||
metadata: data.metadata,
|
||||
autoUpgradeSettings: data.autoUpgradeSettings,
|
||||
serverTimezone: data.serverTimezone,
|
||||
checkTime: DateTime.now(),
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/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';
|
||||
|
||||
class ServerDetailsRepository {
|
||||
HetznerApi hetzner = HetznerApi();
|
||||
ServerApi server = ServerApi();
|
||||
|
||||
Future<ServerDetailsRepositoryDto> load() async {
|
||||
final serverProviderApi = ApiController.currentServerProviderApiFactory;
|
||||
final settings = await server.getSystemSettings();
|
||||
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
|
||||
final metadata =
|
||||
await serverProviderApi!.getServerProvider().getMetadata(serverId);
|
||||
|
||||
return ServerDetailsRepositoryDto(
|
||||
autoUpgradeSettings: settings.autoUpgradeSettings,
|
||||
hetznerServerInfo: await hetzner.getInfo(),
|
||||
metadata: metadata,
|
||||
serverTimezone: TimeZoneSettings.fromString(
|
||||
settings.timezone,
|
||||
),
|
||||
|
@ -36,13 +41,11 @@ class ServerDetailsRepository {
|
|||
|
||||
class ServerDetailsRepositoryDto {
|
||||
ServerDetailsRepositoryDto({
|
||||
required this.hetznerServerInfo,
|
||||
required this.metadata,
|
||||
required this.serverTimezone,
|
||||
required this.autoUpgradeSettings,
|
||||
});
|
||||
final HetznerServerInfo hetznerServerInfo;
|
||||
|
||||
final List<ServerMetadataEntity> metadata;
|
||||
final TimeZoneSettings serverTimezone;
|
||||
|
||||
final AutoUpgradeSettings autoUpgradeSettings;
|
||||
}
|
||||
|
|
|
@ -17,21 +17,19 @@ class Loading extends ServerDetailsState {}
|
|||
|
||||
class Loaded extends ServerDetailsState {
|
||||
const Loaded({
|
||||
required this.serverInfo,
|
||||
required this.metadata,
|
||||
required this.serverTimezone,
|
||||
required this.autoUpgradeSettings,
|
||||
required this.checkTime,
|
||||
});
|
||||
final HetznerServerInfo serverInfo;
|
||||
|
||||
final List<ServerMetadataEntity> metadata;
|
||||
final TimeZoneSettings serverTimezone;
|
||||
|
||||
final AutoUpgradeSettings autoUpgradeSettings;
|
||||
final DateTime checkTime;
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
serverInfo,
|
||||
metadata,
|
||||
serverTimezone,
|
||||
autoUpgradeSettings,
|
||||
checkTime,
|
||||
|
|
|
@ -4,8 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/server_details.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/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';
|
||||
|
||||
|
@ -51,40 +57,85 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
}
|
||||
}
|
||||
|
||||
RegExp getServerProviderApiTokenValidation() =>
|
||||
repository.serverProviderApiFactory!
|
||||
void setServerProviderType(final ServerProvider providerType) async {
|
||||
await repository.saveServerProviderType(providerType);
|
||||
ApiController.initServerProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
provider: providerType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ProviderApiTokenValidation serverProviderApiTokenValidation() =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.getApiTokenValidation();
|
||||
|
||||
RegExp getDnsProviderApiTokenValidation() => repository.dnsProviderApiFactory!
|
||||
RegExp getDnsProviderApiTokenValidation() =>
|
||||
ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.getApiTokenValidation();
|
||||
|
||||
Future<bool> isServerProviderApiTokenValid(
|
||||
final String providerToken,
|
||||
) async =>
|
||||
repository.serverProviderApiFactory!
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider(
|
||||
settings: const ProviderApiSettings(isWithToken: false),
|
||||
settings: const ServerProviderApiSettings(
|
||||
isWithToken: false,
|
||||
),
|
||||
)
|
||||
.isApiTokenValid(providerToken);
|
||||
|
||||
Future<bool> isDnsProviderApiTokenValid(
|
||||
final String providerToken,
|
||||
) async =>
|
||||
repository.dnsProviderApiFactory!
|
||||
) async {
|
||||
if (ApiController.currentDnsProviderApiFactory == null) {
|
||||
// No other DNS provider is supported for now,
|
||||
// so it's safe to hardcode Cloudflare
|
||||
ApiController.initDnsProviderApiFactory(
|
||||
DnsProviderApiFactorySettings(
|
||||
provider: DnsProvider.cloudflare,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider(
|
||||
settings: const DnsProviderApiSettings(isWithToken: false),
|
||||
)
|
||||
.isApiTokenValid(providerToken);
|
||||
}
|
||||
|
||||
void setHetznerKey(final String hetznerKey) async {
|
||||
await repository.saveHetznerKey(hetznerKey);
|
||||
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) {
|
||||
emit(
|
||||
(state as ServerInstallationRecovery).copyWith(
|
||||
providerApiToken: hetznerKey,
|
||||
providerApiToken: serverProviderKey,
|
||||
currentStep: RecoveryStep.serverSelection,
|
||||
),
|
||||
);
|
||||
|
@ -93,7 +144,33 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
|
||||
emit(
|
||||
(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;
|
||||
}
|
||||
await repository.saveCloudFlareKey(cloudFlareKey);
|
||||
|
||||
emit(
|
||||
(state as ServerInstallationNotFinished)
|
||||
.copyWith(cloudFlareKey: cloudFlareKey),
|
||||
|
@ -248,14 +326,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
),
|
||||
);
|
||||
timer = Timer(pauseDuration, () async {
|
||||
final ServerHostingDetails hetznerServerDetails =
|
||||
await repository.restart();
|
||||
final ServerHostingDetails serverDetails = await repository.restart();
|
||||
await repository.saveIsServerResetedFirstTime(true);
|
||||
await repository.saveServerDetails(hetznerServerDetails);
|
||||
await repository.saveServerDetails(serverDetails);
|
||||
|
||||
final ServerInstallationNotFinished newState = dataState.copyWith(
|
||||
isServerResetedFirstTime: true,
|
||||
serverDetails: hetznerServerDetails,
|
||||
serverDetails: serverDetails,
|
||||
isLoading: false,
|
||||
);
|
||||
|
||||
|
@ -290,14 +367,13 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
),
|
||||
);
|
||||
timer = Timer(pauseDuration, () async {
|
||||
final ServerHostingDetails hetznerServerDetails =
|
||||
await repository.restart();
|
||||
final ServerHostingDetails serverDetails = await repository.restart();
|
||||
await repository.saveIsServerResetedSecondTime(true);
|
||||
await repository.saveServerDetails(hetznerServerDetails);
|
||||
await repository.saveServerDetails(serverDetails);
|
||||
|
||||
final ServerInstallationNotFinished newState = dataState.copyWith(
|
||||
isServerResetedSecondTime: true,
|
||||
serverDetails: hetznerServerDetails,
|
||||
serverDetails: serverDetails,
|
||||
isLoading: false,
|
||||
);
|
||||
|
||||
|
@ -423,11 +499,21 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
token,
|
||||
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);
|
||||
setServerProviderType(provider);
|
||||
emit(
|
||||
dataState.copyWith(
|
||||
serverDetails: serverDetails,
|
||||
currentStep: RecoveryStep.hetznerToken,
|
||||
currentStep: RecoveryStep.serverProviderToken,
|
||||
),
|
||||
);
|
||||
} on ServerAuthorizationException {
|
||||
|
@ -503,8 +589,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<ServerBasicInfoWithValidators>>
|
||||
getServersOnHetznerAccount() async {
|
||||
Future<List<ServerBasicInfoWithValidators>> getAvailableServers() async {
|
||||
final ServerInstallationRecovery dataState =
|
||||
state as ServerInstallationRecovery;
|
||||
final List<ServerBasicInfo> servers =
|
||||
|
@ -515,7 +600,9 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
serverBasicInfo: server,
|
||||
isIpValid: server.ip == dataState.serverDetails?.ip4,
|
||||
isReverseDnsValid:
|
||||
server.reverseDns == dataState.serverDomain?.domainName,
|
||||
server.reverseDns == dataState.serverDomain?.domainName ||
|
||||
server.reverseDns ==
|
||||
dataState.serverDomain?.domainName.split('.')[0],
|
||||
),
|
||||
);
|
||||
return validated.toList();
|
||||
|
@ -533,7 +620,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
id: server.id,
|
||||
createTime: server.created,
|
||||
volume: ServerVolume(
|
||||
id: server.volumeId,
|
||||
id: 0,
|
||||
name: 'recovered_volume',
|
||||
sizeByte: 0,
|
||||
serverId: server.id,
|
||||
|
@ -612,7 +699,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
|
||||
void clearAppConfig() {
|
||||
closeTimer();
|
||||
|
||||
ApiController.clearProviderApiFactories();
|
||||
repository.clearAppConfig();
|
||||
emit(const ServerInstallationEmpty());
|
||||
}
|
||||
|
|
|
@ -9,12 +9,12 @@ import 'package:hive/hive.dart';
|
|||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/config/hive_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/api_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_factory.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/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/models/hive/backblaze_credential.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/message.dart';
|
||||
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
||||
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
|
||||
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
|
||||
import 'package:selfprivacy/logic/models/server_type.dart';
|
||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||
import 'package:selfprivacy/utils/network_utils.dart';
|
||||
|
||||
class IpNotFoundException implements Exception {
|
||||
|
@ -41,41 +41,52 @@ class ServerAuthorizationException implements Exception {
|
|||
class ServerInstallationRepository {
|
||||
Box box = Hive.box(BNames.serverInstallationBox);
|
||||
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 {
|
||||
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? serverTypeIdentificator = getIt<ApiConfigModel>().serverType;
|
||||
final ServerDomain? serverDomain = getIt<ApiConfigModel>().serverDomain;
|
||||
final ServerProvider? serverProvider =
|
||||
getIt<ApiConfigModel>().serverProvider;
|
||||
final BackblazeCredential? backblazeCredential =
|
||||
getIt<ApiConfigModel>().backblazeCredential;
|
||||
final ServerHostingDetails? serverDetails =
|
||||
getIt<ApiConfigModel>().serverDetails;
|
||||
|
||||
if (serverDetails != null &&
|
||||
serverDetails.provider != ServerProvider.unknown) {
|
||||
serverProviderApiFactory =
|
||||
ApiFactoryCreator.createServerProviderApiFactory(
|
||||
serverDetails.provider,
|
||||
if (serverProvider != null ||
|
||||
(serverDetails != null &&
|
||||
serverDetails.provider != ServerProvider.unknown)) {
|
||||
ApiController.initServerProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
provider: serverProvider ?? serverDetails!.provider,
|
||||
location: location,
|
||||
),
|
||||
);
|
||||
|
||||
// All current providers support volumes
|
||||
// so it's safe to hardcode for now
|
||||
ApiController.initVolumeProviderApiFactory(
|
||||
ServerProviderApiFactorySettings(
|
||||
provider: serverProvider ?? serverDetails!.provider,
|
||||
location: location,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (serverDomain != null && serverDomain.provider != DnsProvider.unknown) {
|
||||
dnsProviderApiFactory = ApiFactoryCreator.createDnsProviderApiFactory(
|
||||
serverDomain.provider,
|
||||
ApiController.initDnsProviderApiFactory(
|
||||
DnsProviderApiFactorySettings(
|
||||
provider: serverDomain.provider,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
|
||||
return ServerInstallationFinished(
|
||||
providerApiToken: providerApiToken!,
|
||||
serverTypeIdentificator: serverTypeIdentificator ?? '',
|
||||
cloudFlareKey: cloudflareToken!,
|
||||
serverDomain: serverDomain!,
|
||||
backblazeCredential: backblazeCredential!,
|
||||
|
@ -126,13 +137,13 @@ class ServerInstallationRepository {
|
|||
}
|
||||
|
||||
RecoveryStep _getCurrentRecoveryStep(
|
||||
final String? hetznerToken,
|
||||
final String? serverProviderToken,
|
||||
final String? cloudflareToken,
|
||||
final ServerDomain serverDomain,
|
||||
final ServerHostingDetails? serverDetails,
|
||||
) {
|
||||
if (serverDetails != null) {
|
||||
if (hetznerToken != null) {
|
||||
if (serverProviderToken != null) {
|
||||
if (serverDetails.provider != ServerProvider.unknown) {
|
||||
if (serverDomain.provider != DnsProvider.unknown) {
|
||||
return RecoveryStep.backblazeToken;
|
||||
|
@ -141,7 +152,7 @@ class ServerInstallationRepository {
|
|||
}
|
||||
return RecoveryStep.serverSelection;
|
||||
}
|
||||
return RecoveryStep.hetznerToken;
|
||||
return RecoveryStep.serverProviderToken;
|
||||
}
|
||||
return RecoveryStep.selecting;
|
||||
}
|
||||
|
@ -152,18 +163,20 @@ class ServerInstallationRepository {
|
|||
}
|
||||
|
||||
Future<ServerHostingDetails> startServer(
|
||||
final ServerHostingDetails hetznerServer,
|
||||
final ServerHostingDetails server,
|
||||
) async {
|
||||
ServerHostingDetails serverDetails;
|
||||
|
||||
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
|
||||
serverDetails = await api.powerOn();
|
||||
serverDetails = await ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.powerOn();
|
||||
|
||||
return serverDetails;
|
||||
}
|
||||
|
||||
Future<String?> getDomainId(final String token, final String domain) async {
|
||||
final DnsProviderApi dnsProviderApi = dnsProviderApiFactory!.getDnsProvider(
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider(
|
||||
settings: DnsProviderApiSettings(
|
||||
isWithToken: false,
|
||||
customToken: token,
|
||||
|
@ -232,12 +245,14 @@ class ServerInstallationRepository {
|
|||
required final Future<void> Function(ServerHostingDetails serverDetails)
|
||||
onSuccess,
|
||||
}) async {
|
||||
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
|
||||
final ServerProviderApi api =
|
||||
ApiController.currentServerProviderApiFactory!.getServerProvider();
|
||||
try {
|
||||
final ServerHostingDetails? serverDetails = await api.createServer(
|
||||
dnsApiToken: cloudFlareKey,
|
||||
rootUser: rootUser,
|
||||
domainName: domainName,
|
||||
serverType: getIt<ApiConfigModel>().serverType!,
|
||||
);
|
||||
|
||||
if (serverDetails == null) {
|
||||
|
@ -248,16 +263,11 @@ class ServerInstallationRepository {
|
|||
onSuccess(serverDetails);
|
||||
} on DioError catch (e) {
|
||||
if (e.response!.data['error']['code'] == 'uniqueness_error') {
|
||||
final NavigationService nav = getIt.get<NavigationService>();
|
||||
nav.showPopUpDialog(
|
||||
BrandAlert(
|
||||
title: 'modals.already_exists'.tr(),
|
||||
contentText: 'modals.destroy_server'.tr(),
|
||||
actions: [
|
||||
ActionButton(
|
||||
text: 'basis.delete'.tr(),
|
||||
isRed: true,
|
||||
onPressed: () async {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'modals.already_exists'.tr(),
|
||||
description: 'modals.destroy_server'.tr(),
|
||||
actionButtonTitle: 'modals.yes'.tr(),
|
||||
actionButtonOnPressed: () async {
|
||||
await api.deleteServer(
|
||||
domainName: domainName,
|
||||
);
|
||||
|
@ -268,6 +278,7 @@ class ServerInstallationRepository {
|
|||
dnsApiToken: cloudFlareKey,
|
||||
rootUser: rootUser,
|
||||
domainName: domainName,
|
||||
serverType: getIt<ApiConfigModel>().serverType!,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
@ -280,31 +291,21 @@ class ServerInstallationRepository {
|
|||
await saveServerDetails(serverDetails);
|
||||
onSuccess(serverDetails);
|
||||
},
|
||||
),
|
||||
ActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
onPressed: onCancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
cancelButtonOnPressed: onCancel,
|
||||
);
|
||||
} else {
|
||||
final NavigationService nav = getIt.get<NavigationService>();
|
||||
nav.showPopUpDialog(
|
||||
BrandAlert(
|
||||
title: 'modals.unexpected_error'.tr(),
|
||||
contentText: 'modals.try_again'.tr(),
|
||||
actions: [
|
||||
ActionButton(
|
||||
text: 'modals.yes'.tr(),
|
||||
isRed: true,
|
||||
onPressed: () async {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'modals.unexpected_error'.tr(),
|
||||
description: 'modals.try_again'.tr(),
|
||||
actionButtonTitle: 'modals.yes'.tr(),
|
||||
actionButtonOnPressed: () async {
|
||||
ServerHostingDetails? serverDetails;
|
||||
try {
|
||||
serverDetails = await api.createServer(
|
||||
dnsApiToken: cloudFlareKey,
|
||||
rootUser: rootUser,
|
||||
domainName: domainName,
|
||||
serverType: getIt<ApiConfigModel>().serverType!,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
@ -317,13 +318,7 @@ class ServerInstallationRepository {
|
|||
await saveServerDetails(serverDetails);
|
||||
onSuccess(serverDetails);
|
||||
},
|
||||
),
|
||||
ActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
onPressed: onCancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
cancelButtonOnPressed: onCancel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -335,9 +330,9 @@ class ServerInstallationRepository {
|
|||
required final void Function() onCancel,
|
||||
}) async {
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
dnsProviderApiFactory!.getDnsProvider();
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider();
|
||||
final ServerProviderApi serverApi =
|
||||
serverProviderApiFactory!.getServerProvider();
|
||||
ApiController.currentServerProviderApiFactory!.getServerProvider();
|
||||
|
||||
await dnsProviderApi.removeSimilarRecords(
|
||||
ip4: serverDetails.ip4,
|
||||
|
@ -350,31 +345,19 @@ class ServerInstallationRepository {
|
|||
domain: domain,
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
final NavigationService nav = getIt.get<NavigationService>();
|
||||
nav.showPopUpDialog(
|
||||
BrandAlert(
|
||||
title: e.response!.data['errors'][0]['code'] == 1038
|
||||
showPopUpAlert(
|
||||
alertTitle: e.response!.data['errors'][0]['code'] == 1038
|
||||
? 'modals.you_cant_use_this_api'.tr()
|
||||
: 'domain.error'.tr(),
|
||||
contentText: 'modals.delete_server_volume'.tr(),
|
||||
actions: [
|
||||
ActionButton(
|
||||
text: 'basis.delete'.tr(),
|
||||
isRed: true,
|
||||
onPressed: () async {
|
||||
description: 'modals.delete_server_volume'.tr(),
|
||||
cancelButtonOnPressed: onCancel,
|
||||
actionButtonTitle: 'basis.delete'.tr(),
|
||||
actionButtonOnPressed: () async {
|
||||
await serverApi.deleteServer(
|
||||
domainName: domain.domainName,
|
||||
);
|
||||
|
||||
onCancel();
|
||||
},
|
||||
),
|
||||
ActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
onPressed: onCancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
@ -389,7 +372,7 @@ class ServerInstallationRepository {
|
|||
|
||||
Future<void> createDkimRecord(final ServerDomain cloudFlareDomain) async {
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
dnsProviderApiFactory!.getDnsProvider();
|
||||
ApiController.currentDnsProviderApiFactory!.getDnsProvider();
|
||||
final ServerApi api = ServerApi();
|
||||
|
||||
late DnsRecord record;
|
||||
|
@ -408,15 +391,15 @@ class ServerInstallationRepository {
|
|||
return api.isHttpServerWorking();
|
||||
}
|
||||
|
||||
Future<ServerHostingDetails> restart() async {
|
||||
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
|
||||
return api.restart();
|
||||
}
|
||||
Future<ServerHostingDetails> restart() async =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.restart();
|
||||
|
||||
Future<ServerHostingDetails> powerOn() async {
|
||||
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
|
||||
return api.powerOn();
|
||||
}
|
||||
Future<ServerHostingDetails> powerOn() async =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.powerOn();
|
||||
|
||||
Future<ServerRecoveryCapabilities> getRecoveryCapabilities(
|
||||
final ServerDomain serverDomain,
|
||||
|
@ -651,10 +634,10 @@ class ServerInstallationRepository {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<ServerBasicInfo>> getServersOnProviderAccount() async {
|
||||
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
|
||||
return api.getServers();
|
||||
}
|
||||
Future<List<ServerBasicInfo>> getServersOnProviderAccount() async =>
|
||||
ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.getServers();
|
||||
|
||||
Future<void> saveServerDetails(
|
||||
final ServerHostingDetails serverDetails,
|
||||
|
@ -667,12 +650,24 @@ class ServerInstallationRepository {
|
|||
getIt<ApiConfigModel>().init();
|
||||
}
|
||||
|
||||
Future<void> saveHetznerKey(final String key) async {
|
||||
print('saved');
|
||||
await getIt<ApiConfigModel>().storeHetznerKey(key);
|
||||
Future<void> saveServerProviderType(final ServerProvider type) async {
|
||||
await getIt<ApiConfigModel>().storeServerProviderType(type);
|
||||
}
|
||||
|
||||
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);
|
||||
getIt<ApiConfigModel>().init();
|
||||
}
|
||||
|
@ -731,11 +726,9 @@ class ServerInstallationRepository {
|
|||
}
|
||||
|
||||
Future<void> deleteServer(final ServerDomain serverDomain) async {
|
||||
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
|
||||
final DnsProviderApi dnsProviderApi =
|
||||
dnsProviderApiFactory!.getDnsProvider();
|
||||
|
||||
await api.deleteServer(
|
||||
await ApiController.currentServerProviderApiFactory!
|
||||
.getServerProvider()
|
||||
.deleteServer(
|
||||
domainName: serverDomain.domainName,
|
||||
);
|
||||
|
||||
|
@ -746,7 +739,9 @@ class ServerInstallationRepository {
|
|||
await box.put(BNames.isLoading, false);
|
||||
await box.put(BNames.serverDetails, null);
|
||||
|
||||
await dnsProviderApi.removeSimilarRecords(domain: serverDomain);
|
||||
await ApiController.currentDnsProviderApiFactory!
|
||||
.getDnsProvider()
|
||||
.removeSimilarRecords(domain: serverDomain);
|
||||
}
|
||||
|
||||
Future<void> deleteServerRelatedRecords() async {
|
||||
|
|
|
@ -3,6 +3,7 @@ part of '../server_installation/server_installation_cubit.dart';
|
|||
abstract class ServerInstallationState extends Equatable {
|
||||
const ServerInstallationState({
|
||||
required this.providerApiToken,
|
||||
required this.serverTypeIdentificator,
|
||||
required this.cloudFlareKey,
|
||||
required this.backblazeCredential,
|
||||
required this.serverDomain,
|
||||
|
@ -16,6 +17,7 @@ abstract class ServerInstallationState extends Equatable {
|
|||
@override
|
||||
List<Object?> get props => [
|
||||
providerApiToken,
|
||||
serverTypeIdentificator,
|
||||
cloudFlareKey,
|
||||
backblazeCredential,
|
||||
serverDomain,
|
||||
|
@ -27,6 +29,7 @@ abstract class ServerInstallationState extends Equatable {
|
|||
|
||||
final String? providerApiToken;
|
||||
final String? cloudFlareKey;
|
||||
final String? serverTypeIdentificator;
|
||||
final BackblazeCredential? backblazeCredential;
|
||||
final ServerDomain? serverDomain;
|
||||
final User? rootUser;
|
||||
|
@ -35,7 +38,8 @@ abstract class ServerInstallationState extends Equatable {
|
|||
final bool isServerResetedFirstTime;
|
||||
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 isBackupsProviderFilled => backblazeCredential != null;
|
||||
bool get isDomainSelected => serverDomain != null;
|
||||
|
@ -58,7 +62,8 @@ abstract class ServerInstallationState extends Equatable {
|
|||
|
||||
List<bool?> get _fulfilementList {
|
||||
final List<bool> res = [
|
||||
isServerProviderFilled,
|
||||
isServerProviderApiKeyFilled,
|
||||
isServerTypeFilled,
|
||||
isDnsProviderFilled,
|
||||
isBackupsProviderFilled,
|
||||
isDomainSelected,
|
||||
|
@ -81,6 +86,7 @@ class TimerState extends ServerInstallationNotFinished {
|
|||
this.duration,
|
||||
}) : super(
|
||||
providerApiToken: dataState.providerApiToken,
|
||||
serverTypeIdentificator: dataState.serverTypeIdentificator,
|
||||
cloudFlareKey: dataState.cloudFlareKey,
|
||||
backblazeCredential: dataState.backblazeCredential,
|
||||
serverDomain: dataState.serverDomain,
|
||||
|
@ -106,7 +112,8 @@ class TimerState extends ServerInstallationNotFinished {
|
|||
|
||||
enum ServerSetupProgress {
|
||||
nothingYet,
|
||||
hetznerFilled,
|
||||
serverProviderFilled,
|
||||
servertTypeFilled,
|
||||
cloudFlareFilled,
|
||||
backblazeFilled,
|
||||
domainFilled,
|
||||
|
@ -125,6 +132,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
required this.isLoading,
|
||||
required this.dnsMatches,
|
||||
super.providerApiToken,
|
||||
super.serverTypeIdentificator,
|
||||
super.cloudFlareKey,
|
||||
super.backblazeCredential,
|
||||
super.serverDomain,
|
||||
|
@ -137,6 +145,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
@override
|
||||
List<Object?> get props => [
|
||||
providerApiToken,
|
||||
serverTypeIdentificator,
|
||||
cloudFlareKey,
|
||||
backblazeCredential,
|
||||
serverDomain,
|
||||
|
@ -150,6 +159,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
|
||||
ServerInstallationNotFinished copyWith({
|
||||
final String? providerApiToken,
|
||||
final String? serverTypeIdentificator,
|
||||
final String? cloudFlareKey,
|
||||
final BackblazeCredential? backblazeCredential,
|
||||
final ServerDomain? serverDomain,
|
||||
|
@ -163,6 +173,8 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
}) =>
|
||||
ServerInstallationNotFinished(
|
||||
providerApiToken: providerApiToken ?? this.providerApiToken,
|
||||
serverTypeIdentificator:
|
||||
serverTypeIdentificator ?? this.serverTypeIdentificator,
|
||||
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
|
||||
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
|
||||
serverDomain: serverDomain ?? this.serverDomain,
|
||||
|
@ -179,6 +191,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
|
|||
|
||||
ServerInstallationFinished finish() => ServerInstallationFinished(
|
||||
providerApiToken: providerApiToken!,
|
||||
serverTypeIdentificator: serverTypeIdentificator ?? '',
|
||||
cloudFlareKey: cloudFlareKey!,
|
||||
backblazeCredential: backblazeCredential!,
|
||||
serverDomain: serverDomain!,
|
||||
|
@ -194,6 +207,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
|
|||
const ServerInstallationEmpty()
|
||||
: super(
|
||||
providerApiToken: null,
|
||||
serverTypeIdentificator: null,
|
||||
cloudFlareKey: null,
|
||||
backblazeCredential: null,
|
||||
serverDomain: null,
|
||||
|
@ -210,6 +224,7 @@ class ServerInstallationEmpty extends ServerInstallationNotFinished {
|
|||
class ServerInstallationFinished extends ServerInstallationState {
|
||||
const ServerInstallationFinished({
|
||||
required String super.providerApiToken,
|
||||
required String super.serverTypeIdentificator,
|
||||
required String super.cloudFlareKey,
|
||||
required BackblazeCredential super.backblazeCredential,
|
||||
required ServerDomain super.serverDomain,
|
||||
|
@ -223,6 +238,7 @@ class ServerInstallationFinished extends ServerInstallationState {
|
|||
@override
|
||||
List<Object?> get props => [
|
||||
providerApiToken,
|
||||
serverTypeIdentificator,
|
||||
cloudFlareKey,
|
||||
backblazeCredential,
|
||||
serverDomain,
|
||||
|
@ -238,7 +254,7 @@ enum RecoveryStep {
|
|||
recoveryKey,
|
||||
newDeviceKey,
|
||||
oldToken,
|
||||
hetznerToken,
|
||||
serverProviderToken,
|
||||
serverSelection,
|
||||
cloudflareToken,
|
||||
backblazeToken,
|
||||
|
@ -261,6 +277,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
required this.currentStep,
|
||||
required this.recoveryCapabilities,
|
||||
super.providerApiToken,
|
||||
super.serverTypeIdentificator,
|
||||
super.cloudFlareKey,
|
||||
super.backblazeCredential,
|
||||
super.serverDomain,
|
||||
|
@ -277,6 +294,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
@override
|
||||
List<Object?> get props => [
|
||||
providerApiToken,
|
||||
serverTypeIdentificator,
|
||||
cloudFlareKey,
|
||||
backblazeCredential,
|
||||
serverDomain,
|
||||
|
@ -289,6 +307,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
|
||||
ServerInstallationRecovery copyWith({
|
||||
final String? providerApiToken,
|
||||
final String? serverTypeIdentificator,
|
||||
final String? cloudFlareKey,
|
||||
final BackblazeCredential? backblazeCredential,
|
||||
final ServerDomain? serverDomain,
|
||||
|
@ -299,6 +318,8 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
}) =>
|
||||
ServerInstallationRecovery(
|
||||
providerApiToken: providerApiToken ?? this.providerApiToken,
|
||||
serverTypeIdentificator:
|
||||
serverTypeIdentificator ?? this.serverTypeIdentificator,
|
||||
cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey,
|
||||
backblazeCredential: backblazeCredential ?? this.backblazeCredential,
|
||||
serverDomain: serverDomain ?? this.serverDomain,
|
||||
|
@ -310,6 +331,7 @@ class ServerInstallationRecovery extends ServerInstallationState {
|
|||
|
||||
ServerInstallationFinished finish() => ServerInstallationFinished(
|
||||
providerApiToken: providerApiToken!,
|
||||
serverTypeIdentificator: serverTypeIdentificator ?? '',
|
||||
cloudFlareKey: cloudFlareKey!,
|
||||
backblazeCredential: backblazeCredential!,
|
||||
serverDomain: serverDomain!,
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.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/models/json/server_job.dart';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.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/models/service.dart';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:selfprivacy/config/get_it_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/models/hive/user.dart';
|
||||
|
||||
|
|
|
@ -9,22 +9,33 @@ class ApiConfigModel {
|
|||
final Box _box = Hive.box(BNames.serverInstallationBox);
|
||||
|
||||
ServerHostingDetails? get serverDetails => _serverDetails;
|
||||
String? get hetznerKey => _hetznerKey;
|
||||
String? get serverProviderKey => _serverProviderKey;
|
||||
String? get serverLocation => _serverLocation;
|
||||
String? get serverType => _serverType;
|
||||
String? get cloudFlareKey => _cloudFlareKey;
|
||||
ServerProvider? get serverProvider => _serverProvider;
|
||||
BackblazeCredential? get backblazeCredential => _backblazeCredential;
|
||||
ServerDomain? get serverDomain => _serverDomain;
|
||||
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
||||
|
||||
String? _hetznerKey;
|
||||
String? _serverProviderKey;
|
||||
String? _serverLocation;
|
||||
String? _cloudFlareKey;
|
||||
String? _serverType;
|
||||
ServerProvider? _serverProvider;
|
||||
ServerHostingDetails? _serverDetails;
|
||||
BackblazeCredential? _backblazeCredential;
|
||||
ServerDomain? _serverDomain;
|
||||
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);
|
||||
_hetznerKey = value;
|
||||
_serverProviderKey = value;
|
||||
}
|
||||
|
||||
Future<void> storeCloudFlareKey(final String value) async {
|
||||
|
@ -32,6 +43,16 @@ class ApiConfigModel {
|
|||
_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 {
|
||||
await _box.put(BNames.backblazeCredential, value);
|
||||
_backblazeCredential = value;
|
||||
|
@ -53,20 +74,26 @@ class ApiConfigModel {
|
|||
}
|
||||
|
||||
void clear() {
|
||||
_hetznerKey = null;
|
||||
_serverProviderKey = null;
|
||||
_serverLocation = null;
|
||||
_cloudFlareKey = null;
|
||||
_backblazeCredential = null;
|
||||
_serverDomain = null;
|
||||
_serverDetails = null;
|
||||
_backblazeBucket = null;
|
||||
_serverType = null;
|
||||
_serverProvider = null;
|
||||
}
|
||||
|
||||
void init() {
|
||||
_hetznerKey = _box.get(BNames.hetznerKey);
|
||||
_serverProviderKey = _box.get(BNames.hetznerKey);
|
||||
_serverLocation = _box.get(BNames.serverLocation);
|
||||
_cloudFlareKey = _box.get(BNames.cloudFlareKey);
|
||||
_backblazeCredential = _box.get(BNames.backblazeCredential);
|
||||
_serverDomain = _box.get(BNames.serverDomain);
|
||||
_serverDetails = _box.get(BNames.serverDetails);
|
||||
_backblazeBucket = _box.get(BNames.backblazeBucket);
|
||||
_serverType = _box.get(BNames.serverTypeIdentifier);
|
||||
_serverProvider = _box.get(BNames.serverProvider);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class TimeSeriesData {
|
||||
TimeSeriesData(
|
||||
this.secondsSinceEpoch,
|
||||
this.value,
|
||||
);
|
||||
|
||||
final int secondsSinceEpoch;
|
||||
DateTime get time =>
|
||||
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
|
||||
final double value;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:hive/hive.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart';
|
||||
|
||||
part 'server_details.g.dart';
|
||||
|
||||
|
@ -58,6 +59,7 @@ class ServerVolume {
|
|||
required this.sizeByte,
|
||||
required this.serverId,
|
||||
required this.linuxDevice,
|
||||
this.uuid,
|
||||
});
|
||||
|
||||
@HiveField(1)
|
||||
|
@ -70,6 +72,8 @@ class ServerVolume {
|
|||
int? serverId;
|
||||
@HiveField(5, defaultValue: null)
|
||||
String? linuxDevice;
|
||||
@HiveField(6, defaultValue: null)
|
||||
String? uuid;
|
||||
}
|
||||
|
||||
@HiveType(typeId: 101)
|
||||
|
@ -78,4 +82,17 @@ enum ServerProvider {
|
|||
unknown,
|
||||
@HiveField(1)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,13 +76,14 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
|
|||
sizeByte: fields[3] == null ? 10737418240 : fields[3] as int,
|
||||
serverId: fields[4] as int?,
|
||||
linuxDevice: fields[5] as String?,
|
||||
uuid: fields[6] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerVolume obj) {
|
||||
writer
|
||||
..writeByte(5)
|
||||
..writeByte(6)
|
||||
..writeByte(1)
|
||||
..write(obj.id)
|
||||
..writeByte(2)
|
||||
|
@ -92,7 +93,9 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
|
|||
..writeByte(4)
|
||||
..write(obj.serverId)
|
||||
..writeByte(5)
|
||||
..write(obj.linuxDevice);
|
||||
..write(obj.linuxDevice)
|
||||
..writeByte(6)
|
||||
..write(obj.uuid);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -117,6 +120,8 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
|
|||
return ServerProvider.unknown;
|
||||
case 1:
|
||||
return ServerProvider.hetzner;
|
||||
case 2:
|
||||
return ServerProvider.digitalOcean;
|
||||
default:
|
||||
return ServerProvider.unknown;
|
||||
}
|
||||
|
@ -131,6 +136,9 @@ class ServerProviderAdapter extends TypeAdapter<ServerProvider> {
|
|||
case ServerProvider.hetzner:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case ServerProvider.digitalOcean:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
class Price {
|
||||
Price({
|
||||
required this.value,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
double value;
|
||||
String currency;
|
||||
inex
commented
Review
Currency should be a smart enum. Currency should be a smart enum.
NaiJi
commented
Review
It's beyond the scope of Digital Ocean implementation, though it indeed is better to be done asap, it will be planned. It's beyond the scope of Digital Ocean implementation, though it indeed is better to be done asap, it will be planned.
|
||||
}
|
|
@ -5,14 +5,12 @@ class ServerBasicInfo {
|
|||
required this.reverseDns,
|
||||
required this.ip,
|
||||
required this.created,
|
||||
required this.volumeId,
|
||||
});
|
||||
final int id;
|
||||
final String name;
|
||||
final String reverseDns;
|
||||
final String ip;
|
||||
final DateTime created;
|
||||
final int volumeId;
|
||||
}
|
||||
|
||||
class ServerBasicInfoWithValidators extends ServerBasicInfo {
|
||||
|
@ -26,7 +24,6 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo {
|
|||
reverseDns: serverBasicInfo.reverseDns,
|
||||
ip: serverBasicInfo.ip,
|
||||
created: serverBasicInfo.created,
|
||||
volumeId: serverBasicInfo.volumeId,
|
||||
isIpValid: isIpValid,
|
||||
isReverseDnsValid: isReverseDnsValid,
|
||||
);
|
||||
|
@ -37,7 +34,6 @@ class ServerBasicInfoWithValidators extends ServerBasicInfo {
|
|||
required super.reverseDns,
|
||||
required super.ip,
|
||||
required super.created,
|
||||
required super.volumeId,
|
||||
required this.isIpValid,
|
||||
required this.isReverseDnsValid,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import 'package:selfprivacy/config/brand_colors.dart';
|
||||
import 'package:selfprivacy/config/hive_config.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/root_route.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
|
|
@ -2,18 +2,16 @@ import 'package:flutter/material.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/server_installation/server_installation_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/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_cards/brand_cards.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/jobs_content/server_job_card.dart';
|
||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||
|
||||
class JobsContent extends StatelessWidget {
|
||||
const JobsContent({super.key});
|
||||
|
@ -47,26 +45,16 @@ class JobsContent extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 10),
|
||||
BrandButton.text(
|
||||
onPressed: () {
|
||||
final NavigationService nav = getIt<NavigationService>();
|
||||
nav.showPopUpDialog(
|
||||
BrandAlert(
|
||||
title: 'jobs.reboot_server'.tr(),
|
||||
contentText: 'modals.are_you_sure'.tr(),
|
||||
actions: [
|
||||
ActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
),
|
||||
ActionButton(
|
||||
onPressed: () =>
|
||||
onPressed: () {
|
||||
showPopUpAlert(
|
||||
alertTitle: 'jobs.reboot_server'.tr(),
|
||||
description: 'modals.are_you_sure'.tr(),
|
||||
actionButtonTitle: 'modals.reboot'.tr(),
|
||||
actionButtonOnPressed: () =>
|
||||
{context.read<JobsCubit>().rebootServer()},
|
||||
text: 'modals.reboot'.tr(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
title: 'jobs.reboot_server'.tr(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/brand_colors.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:easy_localization/easy_localization.dart';
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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>({
|
||||
required final BuildContext context,
|
||||
|
@ -12,3 +16,30 @@ Future<T?> showBrandBottomSheet<T>({
|
|||
shadow: const BoxShadow(color: 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import 'package:easy_localization/easy_localization.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/backups/backups_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/json/backup.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_cards/outlined_card.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_text/brand_text.dart';
|
||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||
|
||||
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
@ -157,28 +155,17 @@ class _BackupDetailsState extends State<BackupDetails>
|
|||
onTap: preventActions
|
||||
? null
|
||||
: () {
|
||||
final NavigationService nav =
|
||||
getIt<NavigationService>();
|
||||
nav.showPopUpDialog(
|
||||
BrandAlert(
|
||||
title: 'backup.restoring'.tr(),
|
||||
contentText: 'backup.restore_alert'.tr(
|
||||
showPopUpAlert(
|
||||
alertTitle: 'backup.restoring'.tr(),
|
||||
description: 'backup.restore_alert'.tr(
|
||||
args: [backup.time.toString()],
|
||||
),
|
||||
actions: [
|
||||
ActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
),
|
||||
ActionButton(
|
||||
onPressed: () => {
|
||||
actionButtonTitle: 'modals.yes'.tr(),
|
||||
actionButtonOnPressed: () => {
|
||||
context
|
||||
.read<BackupsCubit>()
|
||||
.restoreBackup(backup.id)
|
||||
},
|
||||
text: 'modals.yes'.tr(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.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_text/brand_text.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
|
|
|
@ -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/recovery_key/recovery_key.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/root_route.dart';
|
||||
import 'package:selfprivacy/ui/pages/users/users.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
String bottomTitle(
|
||||
|
@ -11,7 +11,7 @@ String bottomTitle(
|
|||
final day = DateFormat('MMMd');
|
||||
String res;
|
||||
|
||||
if (value <= 0) {
|
||||
if (value <= 0 || value >= data.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ part of '../server_details_screen.dart';
|
|||
class _Chart extends StatelessWidget {
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final HetznerMetricsCubit cubit = context.watch<HetznerMetricsCubit>();
|
||||
final MetricsCubit cubit = context.watch<MetricsCubit>();
|
||||
final Period period = cubit.state.period;
|
||||
final HetznerMetricsState state = cubit.state;
|
||||
final MetricsState state = cubit.state;
|
||||
List<Widget> charts;
|
||||
if (state is HetznerMetricsLoaded || state is HetznerMetricsLoading) {
|
||||
if (state is MetricsLoaded || state is MetricsLoading) {
|
||||
charts = [
|
||||
FilledCard(
|
||||
clipped: false,
|
||||
|
@ -26,10 +26,10 @@ class _Chart extends StatelessWidget {
|
|||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (state is HetznerMetricsLoaded) getCpuChart(state),
|
||||
if (state is MetricsLoaded) getCpuChart(state),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: state is HetznerMetricsLoading ? 1 : 0,
|
||||
opacity: state is MetricsLoading ? 1 : 0,
|
||||
child: const _GraphLoadingCardContent(),
|
||||
),
|
||||
],
|
||||
|
@ -72,10 +72,10 @@ class _Chart extends StatelessWidget {
|
|||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (state is HetznerMetricsLoaded) getBandwidthChart(state),
|
||||
if (state is MetricsLoaded) getBandwidthChart(state),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: state is HetznerMetricsLoading ? 1 : 0,
|
||||
opacity: state is MetricsLoading ? 1 : 0,
|
||||
child: const _GraphLoadingCardContent(),
|
||||
),
|
||||
],
|
||||
|
@ -122,29 +122,29 @@ class _Chart extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget getCpuChart(final HetznerMetricsLoaded state) {
|
||||
final data = state.cpu;
|
||||
Widget getCpuChart(final MetricsLoaded state) {
|
||||
final data = state.metrics.cpu;
|
||||
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: CpuChart(
|
||||
data: data,
|
||||
period: state.period,
|
||||
start: state.start,
|
||||
start: state.metrics.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getBandwidthChart(final HetznerMetricsLoaded state) {
|
||||
final ppsIn = state.bandwidthIn;
|
||||
final ppsOut = state.bandwidthOut;
|
||||
Widget getBandwidthChart(final MetricsLoaded state) {
|
||||
final ppsIn = state.metrics.bandwidthIn;
|
||||
final ppsOut = state.metrics.bandwidthOut;
|
||||
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: NetworkChart(
|
||||
listData: [ppsIn, ppsOut],
|
||||
period: state.period,
|
||||
start: state.start,
|
||||
start: state.metrics.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.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:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';
|
||||
|
||||
|
@ -83,7 +83,7 @@ class CpuChart extends StatelessWidget {
|
|||
],
|
||||
minY: 0,
|
||||
maxY: 100,
|
||||
minX: data.length - 200,
|
||||
minX: 0,
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
bottomTitles: AxisTitles(
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:selfprivacy/logic/common_enum/common_enum.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';
|
||||
|
||||
class NetworkChart extends StatelessWidget {
|
||||
|
@ -116,7 +116,7 @@ class NetworkChart extends StatelessWidget {
|
|||
...listData[1].map((final e) => e.value)
|
||||
].reduce(max) *
|
||||
1.2,
|
||||
minX: listData[0].length - 200,
|
||||
minX: 0,
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
bottomTitles: AxisTitles(
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:selfprivacy/config/brand_colors.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/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_installation/server_installation_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_storage/storage_card.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/route_transitions/basic.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
@ -92,7 +91,7 @@ class _ServerDetailsScreenState extends State<ServerDetailsScreen>
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
BlocProvider(
|
||||
create: (final context) => HetznerMetricsCubit()..restart(),
|
||||
create: (final context) => MetricsCubit()..restart(),
|
||||
child: _Chart(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
|
@ -10,7 +10,6 @@ class _TextDetails extends StatelessWidget {
|
|||
} else if (details is ServerDetailsNotReady) {
|
||||
return _TempMessage(message: 'basis.no_data'.tr());
|
||||
} else if (details is Loaded) {
|
||||
final data = details.serverInfo;
|
||||
return FilledCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -24,37 +23,15 @@ class _TextDetails extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
ListTileOnSurfaceVariant(
|
||||
leadingIcon: Icons.numbers_outlined,
|
||||
title: data.id.toString(),
|
||||
subtitle: 'server.server_id'.tr(),
|
||||
),
|
||||
ListTileOnSurfaceVariant(
|
||||
leadingIcon: Icons.mode_standby_outlined,
|
||||
title: data.status.toString().split('.')[1].capitalize(),
|
||||
subtitle: 'server.status'.tr(),
|
||||
),
|
||||
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(),
|
||||
...details.metadata
|
||||
.map(
|
||||
(final metadata) => ListTileOnSurfaceVariant(
|
||||
leadingIcon: metadata.type.icon,
|
||||
title: metadata.name,
|
||||
subtitle: metadata.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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/server_volumes/server_volume_cubit.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_hero_screen/brand_hero_screen.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();
|
||||
_priceController.text =
|
||||
(_euroPerGb * double.parse(_sizeController.text))
|
||||
|
@ -152,7 +153,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
|
|||
: () {
|
||||
context.read<ApiProviderVolumeCubit>().resizeVolume(
|
||||
widget.diskVolumeToResize,
|
||||
_currentSliderGbValue.round(),
|
||||
DiskSize.fromGibibyte(_currentSliderGbValue),
|
||||
context.read<ApiServerVolumeCubit>().reload,
|
||||
);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
|
|
|
@ -2,12 +2,12 @@ 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/forms/setup/initializing/provider_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
|
||||
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/provider_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_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/progress_bar/progress_bar.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/utils/route_transitions/basic.dart';
|
||||
|
||||
|
@ -33,7 +35,8 @@ class InitializingPage extends StatelessWidget {
|
|||
Widget? actualInitializingPage;
|
||||
if (cubit.state is! ServerInstallationFinished) {
|
||||
actualInitializingPage = [
|
||||
() => _stepHetzner(cubit),
|
||||
() => _stepServerProviderToken(cubit),
|
||||
() => _stepServerType(cubit),
|
||||
() => _stepCloudflare(cubit),
|
||||
() => _stepBackblaze(cubit),
|
||||
() => _stepDomain(cubit),
|
||||
|
@ -67,7 +70,8 @@ class InitializingPage extends StatelessWidget {
|
|||
)
|
||||
: ProgressBar(
|
||||
steps: const [
|
||||
'Hetzner',
|
||||
'Hosting',
|
||||
'Server Type',
|
||||
'CloudFlare',
|
||||
'Backblaze',
|
||||
'Domain',
|
||||
|
@ -78,6 +82,11 @@ class InitializingPage extends StatelessWidget {
|
|||
activeIndex: cubit.state.porgressBar,
|
||||
),
|
||||
),
|
||||
if (cubit.state.porgressBar ==
|
||||
ServerSetupProgress.serverProviderFilled.index)
|
||||
BrandText.h2(
|
||||
'initializing.choose_location_type'.tr(),
|
||||
),
|
||||
_addCard(
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
@ -136,55 +145,34 @@ class InitializingPage extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _stepHetzner(final ServerInstallationCubit serverInstallationCubit) =>
|
||||
Widget _stepServerProviderToken(
|
||||
final ServerInstallationCubit serverInstallationCubit,
|
||||
) =>
|
||||
BlocProvider(
|
||||
create: (final context) => ProviderFormCubit(
|
||||
serverInstallationCubit,
|
||||
),
|
||||
create: (final context) => ProviderFormCubit(serverInstallationCubit),
|
||||
child: Builder(
|
||||
builder: (final context) {
|
||||
final formCubitState = context.watch<ProviderFormCubit>().state;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(),
|
||||
),
|
||||
],
|
||||
final providerCubit = context.watch<ProviderFormCubit>();
|
||||
return ServerProviderPicker(
|
||||
formCubit: providerCubit,
|
||||
serverInstallationCubit: serverInstallationCubit,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
|
@ -198,10 +186,7 @@ class InitializingPage extends StatelessWidget {
|
|||
BlocProvider(
|
||||
create: (final context) => DnsProviderFormCubit(initializingCubit),
|
||||
child: Builder(
|
||||
builder: (final context) {
|
||||
final formCubitState = context.watch<DnsProviderFormCubit>().state;
|
||||
|
||||
return Column(
|
||||
builder: (final context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Image.asset(
|
||||
|
@ -223,9 +208,8 @@ class InitializingPage extends StatelessWidget {
|
|||
),
|
||||
const Spacer(),
|
||||
BrandButton.rised(
|
||||
onPressed: formCubitState.isSubmitting
|
||||
? null
|
||||
: () => context.read<DnsProviderFormCubit>().trySubmit(),
|
||||
onPressed: () =>
|
||||
context.read<DnsProviderFormCubit>().trySubmit(),
|
||||
text: 'basis.connect'.tr(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
@ -239,8 +223,7 @@ class InitializingPage extends StatelessWidget {
|
|||
title: 'initializing.how'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
|
@ -45,9 +45,8 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
|
|||
hasFlashButton: false,
|
||||
children: [
|
||||
FutureBuilder<List<ServerBasicInfoWithValidators>>(
|
||||
future: context
|
||||
.read<ServerInstallationCubit>()
|
||||
.getServersOnHetznerAccount(),
|
||||
future:
|
||||
context.read<ServerInstallationCubit>().getAvailableServers(),
|
||||
builder: (final context, final snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final servers = snapshot.data;
|
||||
|
|
|
@ -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_cloudflare.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/utils/route_transitions/basic.dart';
|
||||
|
||||
|
@ -47,8 +47,8 @@ class RecoveryRouting extends StatelessWidget {
|
|||
case RecoveryStep.oldToken:
|
||||
currentPage = const RecoverByOldToken();
|
||||
break;
|
||||
case RecoveryStep.hetznerToken:
|
||||
currentPage = const RecoveryHetznerConnected();
|
||||
case RecoveryStep.serverProviderToken:
|
||||
currentPage = const RecoveryServerProviderConnected();
|
||||
break;
|
||||
case RecoveryStep.serverSelection:
|
||||
currentPage = const RecoveryConfirmServer();
|
||||
|
|
|
@ -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/ui/components/brand_md/brand_md.dart';
|
||||
|
||||
class RecoveryHetznerConnected extends StatelessWidget {
|
||||
const RecoveryHetznerConnected({super.key});
|
||||
class RecoveryServerProviderConnected extends StatelessWidget {
|
||||
const RecoveryServerProviderConnected({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
|
@ -26,8 +26,8 @@ class RecoveryHetznerConnected extends StatelessWidget {
|
|||
context.watch<ProviderFormCubit>().state;
|
||||
|
||||
return BrandHeroScreen(
|
||||
heroTitle: 'recovering.hetzner_connected'.tr(),
|
||||
heroSubtitle: 'recovering.hetzner_connected_description'.tr(
|
||||
heroTitle: 'recovering.server_provider_connected'.tr(),
|
||||
heroSubtitle: 'recovering.server_provider_connected_description'.tr(
|
||||
args: [appConfig.state.serverDomain?.domainName ?? 'your domain'],
|
||||
),
|
||||
hasBackButton: true,
|
||||
|
@ -40,7 +40,8 @@ class RecoveryHetznerConnected extends StatelessWidget {
|
|||
formFieldCubit: context.read<ProviderFormCubit>().apiKey,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'recovering.hetzner_connected_placeholder'.tr(),
|
||||
labelText:
|
||||
'recovering.server_provider_connected_placeholder'.tr(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
|
@ -101,10 +101,10 @@ class StringGenerators {
|
|||
hasSymbols: true,
|
||||
);
|
||||
|
||||
static StringGeneratorFunction dbStorageName = () => getRandomString(
|
||||
static StringGeneratorFunction storageName = () => getRandomString(
|
||||
6,
|
||||
hasLowercaseLetters: true,
|
||||
hasUppercaseLetters: true,
|
||||
hasUppercaseLetters: false,
|
||||
hasNumbers: true,
|
||||
);
|
||||
|
||||
|
|
32
pubspec.lock
|
@ -35,7 +35,7 @@ packages:
|
|||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
version: "2.9.0"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -126,7 +126,7 @@ packages:
|
|||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -147,7 +147,7 @@ packages:
|
|||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -350,7 +350,7 @@ packages:
|
|||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -631,12 +631,12 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.3"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.4"
|
||||
version: "0.13.5"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -762,21 +762,21 @@ packages:
|
|||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
version: "0.12.12"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
version: "0.1.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -846,7 +846,7 @@ packages:
|
|||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
version: "1.8.2"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1159,7 +1159,7 @@ packages:
|
|||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
version: "1.9.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1187,7 +1187,7 @@ packages:
|
|||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
system_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1208,28 +1208,28 @@ packages:
|
|||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.21.1"
|
||||
version: "1.21.4"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.9"
|
||||
version: "0.4.12"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.13"
|
||||
version: "0.4.16"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -33,6 +33,7 @@ dependencies:
|
|||
gtk_theme_fl: ^0.0.1
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
http: ^0.13.5
|
||||
intl: ^0.17.0
|
||||
ionicons: ^0.1.2
|
||||
json_annotation: ^4.6.0
|
||||
|
|
Use final and declare the type. Implication for some reason ignores our GraphQL model and we cannot use properly parsedData without type declaration.