feat(hosting): Implement Digital Ocean hosting support #140

Merged
NaiJi merged 60 commits from digital-ocean into master 2022-11-23 15:28:33 +02:00
96 changed files with 3086 additions and 812 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -31,7 +31,8 @@
"remove": "Remove",
"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",

View File

@ -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": "Домен",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/services.graphql.
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/users.graphql.dart';
import 'package:selfprivacy/logic/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
Review

Use final and declare the type. Implication for some reason ignores our GraphQL model and we cannot use properly parsedData without type declaration.

final QueryResult<Query$SystemServerProvider> response = await client.query$SystemServerProvider();
Use final and declare the type. Implication for some reason ignores our GraphQL model and we cannot use properly parsedData without type declaration. ```dart final QueryResult<Query$SystemServerProvider> response = await client.query$SystemServerProvider(); ```
if (response.hasException) {
Review

let's do if (response.hasException || response.parsedData == null) { just in case

let's do `if (response.hasException || response.parsedData == null) {` just in case

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.

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;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/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');
}

View File

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

View File

@ -9,8 +9,8 @@ import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/message.dart';
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;

View File

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

View File

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

View File

@ -1,17 +1,8 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/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,
});
}

View File

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

View File

@ -0,0 +1,780 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
DigitalOceanApi({
required this.region,
this.hasLogger = false,
this.isWithToken = true,
});
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
@override
BaseOptions get options {
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
if (isWithToken) {
final String? token = getIt<ApiConfigModel>().serverProviderKey;
assert(token != null);
options.headers = {'Authorization': 'Bearer $token'};
}
if (validateStatus != null) {
options.validateStatus = validateStatus!;
}
return options;
}
@override
String get rootAddress => 'https://api.digitalocean.com/v2';
@override
String get infectProviderName => 'digitalocean';
@override
String get displayProviderName => 'Digital Ocean';
@override
Future<bool> isApiTokenValid(final String token) async {
bool isValid = false;
Response? response;
final Dio client = await getClient();
try {
response = await client.get(
'/account',
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
} catch (e) {
print(e);
isValid = false;
} finally {
close(client);
}
if (response != null) {
if (response.statusCode == HttpStatus.ok) {
isValid = true;
} else if (response.statusCode == HttpStatus.unauthorized) {
isValid = false;
} else {
throw Exception('code: ${response.statusCode}');
}
}
return isValid;
}
/// Hardcoded on their documentation and there is no pricing API at all
/// Probably we should scrap the doc page manually
@override
Future<Price?> getPricePerGb() async => Price(
value: 0.10,
currency: 'USD',
Review

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

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
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 = [
Review

.tr() should be called by UI layer.

.tr() should be called by UI layer.

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

View File

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

View File

@ -2,28 +2,43 @@ import 'dart:convert';
import 'dart:io';
import '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,

View File

@ -1,25 +1,33 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/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,
);

View File

@ -2,10 +2,27 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_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;
}

View File

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

View File

@ -1,15 +1,15 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/provider_api_settings.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_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,
});
}

View File

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

View File

@ -0,0 +1,8 @@
/// Controls staging environment for network, is used during manual
/// integration testing and such
class StagingOptions {
/// Whether we request for staging temprorary certificates.
/// Hardcode to 'true' in the middle of testing to not
/// get your domain banned by constant certificate renewal
static bool get stagingAcme => false;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,16 +3,16 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package: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),

View File

@ -0,0 +1,52 @@
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
import 'package:selfprivacy/logic/models/metrics.dart';
class MetricsLoadException implements Exception {
MetricsLoadException(this.message);
final String message;
}
class MetricsRepository {
Future<MetricsLoaded> getMetrics(final Period period) async {
final providerApiFactory = ApiController.currentServerProviderApiFactory;
if (providerApiFactory == null) {
throw MetricsLoadException('Server Provider data is null');
}
final DateTime end = DateTime.now();
DateTime start;
switch (period) {
case Period.hour:
start = end.subtract(const Duration(hours: 1));
break;
case Period.day:
start = end.subtract(const Duration(days: 1));
break;
case Period.month:
start = end.subtract(const Duration(days: 15));
break;
}
final serverId = getIt<ApiConfigModel>().serverDetails!.id;
final ServerMetrics? metrics =
await providerApiFactory.getServerProvider().getMetrics(
serverId,
start,
end,
);
if (metrics == null) {
throw MetricsLoadException('Metrics data is null');
}
return MetricsLoaded(
period: period,
metrics: metrics,
);
}
}

View File

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

View File

@ -1,12 +1,13 @@
import 'package:easy_localization/easy_localization.dart';
import 'package: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();
}

View File

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

View File

@ -2,7 +2,7 @@ import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/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(),

View File

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

View File

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

View File

@ -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!
.getDnsProvider()
.getApiTokenValidation();
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!
.getDnsProvider(
settings: const DnsProviderApiSettings(isWithToken: false),
)
.isApiTokenValid(providerToken);
) 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,
),
);
}
void setHetznerKey(final String hetznerKey) async {
await repository.saveHetznerKey(hetznerKey);
return ApiController.currentDnsProviderApiFactory!
.getDnsProvider(
settings: const DnsProviderApiSettings(isWithToken: false),
)
.isApiTokenValid(providerToken);
}
Future<List<ServerProviderLocation>> fetchAvailableLocations() async {
if (ApiController.currentServerProviderApiFactory == null) {
return [];
}
return ApiController.currentServerProviderApiFactory!
.getServerProvider()
.getAvailableLocations();
}
Future<List<ServerType>> fetchAvailableTypesByLocation(
final ServerProviderLocation location,
) async {
if (ApiController.currentServerProviderApiFactory == null) {
return [];
}
return ApiController.currentServerProviderApiFactory!
.getServerProvider()
.getServerTypesByLocation(location: location);
}
void setServerProviderKey(final String serverProviderKey) async {
await repository.saveServerProviderKey(serverProviderKey);
if (state is ServerInstallationRecovery) {
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());
}

View File

@ -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,82 +263,62 @@ 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 {
await api.deleteServer(
domainName: domainName,
);
showPopUpAlert(
alertTitle: 'modals.already_exists'.tr(),
description: 'modals.destroy_server'.tr(),
actionButtonTitle: 'modals.yes'.tr(),
actionButtonOnPressed: () async {
await api.deleteServer(
domainName: domainName,
);
ServerHostingDetails? serverDetails;
try {
serverDetails = await api.createServer(
dnsApiToken: cloudFlareKey,
rootUser: rootUser,
domainName: domainName,
);
} catch (e) {
print(e);
}
ServerHostingDetails? serverDetails;
try {
serverDetails = await api.createServer(
dnsApiToken: cloudFlareKey,
rootUser: rootUser,
domainName: domainName,
serverType: getIt<ApiConfigModel>().serverType!,
);
} catch (e) {
print(e);
}
if (serverDetails == null) {
print('Server is not initialized!');
return;
}
await saveServerDetails(serverDetails);
onSuccess(serverDetails);
},
),
ActionButton(
text: 'basis.cancel'.tr(),
onPressed: onCancel,
),
],
),
if (serverDetails == null) {
print('Server is not initialized!');
return;
}
await saveServerDetails(serverDetails);
onSuccess(serverDetails);
},
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 {
ServerHostingDetails? serverDetails;
try {
serverDetails = await api.createServer(
dnsApiToken: cloudFlareKey,
rootUser: rootUser,
domainName: domainName,
);
} catch (e) {
print(e);
}
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);
}
if (serverDetails == null) {
print('Server is not initialized!');
return;
}
await saveServerDetails(serverDetails);
onSuccess(serverDetails);
},
),
ActionButton(
text: 'basis.cancel'.tr(),
onPressed: onCancel,
),
],
),
if (serverDetails == null) {
print('Server is not initialized!');
return;
}
await saveServerDetails(serverDetails);
onSuccess(serverDetails);
},
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
? '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 {
await serverApi.deleteServer(
domainName: domain.domainName,
);
onCancel();
},
),
ActionButton(
text: 'basis.cancel'.tr(),
onPressed: onCancel,
),
],
),
showPopUpAlert(
alertTitle: e.response!.data['errors'][0]['code'] == 1038
? 'modals.you_cant_use_this_api'.tr()
: 'domain.error'.tr(),
description: 'modals.delete_server_volume'.tr(),
cancelButtonOnPressed: onCancel,
actionButtonTitle: 'basis.delete'.tr(),
actionButtonOnPressed: () async {
await serverApi.deleteServer(
domainName: domain.domainName,
);
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,13 +726,11 @@ class ServerInstallationRepository {
}
Future<void> deleteServer(final ServerDomain serverDomain) async {
final ServerProviderApi api = serverProviderApiFactory!.getServerProvider();
final DnsProviderApi dnsProviderApi =
dnsProviderApiFactory!.getDnsProvider();
await api.deleteServer(
domainName: serverDomain.domainName,
);
await ApiController.currentServerProviderApiFactory!
.getServerProvider()
.deleteServer(
domainName: serverDomain.domainName,
);
await box.put(BNames.hasFinalChecked, false);
await box.put(BNames.isServerStarted, false);
@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'package:hive/hive.dart';
import 'package: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;
}
}
}

View File

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

View File

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

View File

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

Currency should be a smart enum.

Currency should be a smart enum.

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.
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/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';

View File

@ -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(
title: 'jobs.reboot_server'.tr(),
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: () =>
{context.read<JobsCubit>().rebootServer()},
text: 'modals.reboot'.tr(),
)
],
),
showPopUpAlert(
alertTitle: 'jobs.reboot_server'.tr(),
description: 'modals.are_you_sure'.tr(),
actionButtonTitle: 'modals.reboot'.tr(),
actionButtonOnPressed: () =>
{context.read<JobsCubit>().rebootServer()},
);
},
title: 'jobs.reboot_server'.tr(),
),
];
}

View File

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

View File

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

View File

@ -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(
args: [backup.time.toString()],
),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
text: 'modals.yes'.tr(),
)
],
showPopUpAlert(
alertTitle: 'backup.restoring'.tr(),
description: 'backup.restore_alert'.tr(
args: [backup.time.toString()],
),
actionButtonTitle: 'modals.yes'.tr(),
actionButtonOnPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
);
},
title: Text(

View File

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

View File

@ -11,7 +11,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_depe
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/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(

View File

@ -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,49 +186,44 @@ class InitializingPage extends StatelessWidget {
BlocProvider(
create: (final context) => DnsProviderFormCubit(initializingCubit),
child: Builder(
builder: (final context) {
final formCubitState = context.watch<DnsProviderFormCubit>().state;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/logos/cloudflare.png',
width: 150,
builder: (final context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/logos/cloudflare.png',
width: 150,
),
const SizedBox(height: 10),
BrandText.h2('initializing.connect_cloudflare'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.manage_domain_dns'.tr()),
const Spacer(),
CubitFormTextField(
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'initializing.cloudflare_api_token'.tr(),
),
const SizedBox(height: 10),
BrandText.h2('initializing.connect_cloudflare'.tr()),
const SizedBox(height: 10),
BrandText.body2('initializing.manage_domain_dns'.tr()),
const Spacer(),
CubitFormTextField(
formFieldCubit: context.read<DnsProviderFormCubit>().apiKey,
textAlign: TextAlign.center,
scrollPadding: const EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
hintText: 'initializing.cloudflare_api_token'.tr(),
),
const Spacer(),
BrandButton.rised(
onPressed: () =>
context.read<DnsProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(
fileName: 'how_cloudflare',
),
),
const Spacer(),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<DnsProviderFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 10),
BrandButton.text(
onPressed: () => _showModal(
context,
const _HowTo(
fileName: 'how_cloudflare',
),
),
title: 'initializing.how'.tr(),
),
],
);
},
title: 'initializing.how'.tr(),
),
],
),
),
);

View File

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

View File

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

View File

@ -45,9 +45,8 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
hasFlashButton: false,
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;

View File

@ -13,7 +13,7 @@ import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_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();

View File

@ -10,8 +10,8 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/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),

View File

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

View File

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

View File

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