diff --git a/assets/images/logos/digital_ocean.png b/assets/images/logos/digital_ocean.png new file mode 100644 index 00000000..04f5efd5 Binary files /dev/null and b/assets/images/logos/digital_ocean.png differ diff --git a/assets/translations/en.json b/assets/translations/en.json index ddb5c541..18e70c5b 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index d2e298c2..3dd4e590 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -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": "Домен", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 29ab0519..93bce6ee 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -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'; diff --git a/lib/logic/api_maps/graphql_maps/api_map.dart b/lib/logic/api_maps/graphql_maps/api_map.dart index c01f1837..185a5e54 100644 --- a/lib/logic/api_maps/graphql_maps/api_map.dart +++ b/lib/logic/api_maps/graphql_maps/api_map.dart @@ -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 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(); diff --git a/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart index 2464c561..359a7a27 100644 --- a/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart @@ -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) diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql b/lib/logic/api_maps/graphql_maps/schema/schema.graphql index 5da67b2c..ed167742 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql @@ -173,6 +173,7 @@ input RecoveryKeyLimitsInput { enum ServerProvider { HETZNER + DIGITALOCEAN } type Service { diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart index 7187e0e2..11d49a43 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart @@ -693,6 +693,8 @@ enum Enum$DnsProvider { enum Enum$ServerProvider { @JsonValue('HETZNER') HETZNER, + @JsonValue('DIGITALOCEAN') + DIGITALOCEAN, $unknown } diff --git a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql index 96374fad..d4339094 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql @@ -64,6 +64,14 @@ mutation RebootSystem { } } +query SystemServerProvider { + system { + provider { + provider + } + } +} + query GetApiTokens { api { devices { diff --git a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart index c31a3487..325ee89d 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.dart @@ -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 _res; } +@JsonSerializable(explicitToJson: true) +class Query$SystemServerProvider { + Query$SystemServerProvider({required this.system, required this.$__typename}); + + @override + factory Query$SystemServerProvider.fromJson(Map json) => + _$Query$SystemServerProviderFromJson(json); + + final Query$SystemServerProvider$system system; + + @JsonKey(name: '__typename') + final String $__typename; + + Map 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 + get copyWith => CopyWith$Query$SystemServerProvider(this, (i) => i); +} + +abstract class CopyWith$Query$SystemServerProvider { + 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 get system; +} + +class _CopyWithImpl$Query$SystemServerProvider + implements CopyWith$Query$SystemServerProvider { + _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 get system { + final local$system = _instance.system; + return CopyWith$Query$SystemServerProvider$system( + local$system, (e) => call(system: e)); + } +} + +class _CopyWithStubImpl$Query$SystemServerProvider + implements CopyWith$Query$SystemServerProvider { + _CopyWithStubImpl$Query$SystemServerProvider(this._res); + + TRes _res; + + call({Query$SystemServerProvider$system? system, String? $__typename}) => + _res; + CopyWith$Query$SystemServerProvider$system 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 data) => + Query$SystemServerProvider.fromJson(data); + +class Options$Query$SystemServerProvider + extends graphql.QueryOptions { + 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 { + 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> + query$SystemServerProvider( + [Options$Query$SystemServerProvider? options]) async => + await this.query(options ?? Options$Query$SystemServerProvider()); + graphql.ObservableQuery + 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 json) => + _$Query$SystemServerProvider$systemFromJson(json); + + final Query$SystemServerProvider$system$provider provider; + + @JsonKey(name: '__typename') + final String $__typename; + + Map 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 + get copyWith => + CopyWith$Query$SystemServerProvider$system(this, (i) => i); +} + +abstract class CopyWith$Query$SystemServerProvider$system { + 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 get provider; +} + +class _CopyWithImpl$Query$SystemServerProvider$system + implements CopyWith$Query$SystemServerProvider$system { + _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 get provider { + final local$provider = _instance.provider; + return CopyWith$Query$SystemServerProvider$system$provider( + local$provider, (e) => call(provider: e)); + } +} + +class _CopyWithStubImpl$Query$SystemServerProvider$system + implements CopyWith$Query$SystemServerProvider$system { + _CopyWithStubImpl$Query$SystemServerProvider$system(this._res); + + TRes _res; + + call( + {Query$SystemServerProvider$system$provider? provider, + String? $__typename}) => + _res; + CopyWith$Query$SystemServerProvider$system$provider 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 json) => + _$Query$SystemServerProvider$system$providerFromJson(json); + + @JsonKey(unknownEnumValue: Enum$ServerProvider.$unknown) + final Enum$ServerProvider provider; + + @JsonKey(name: '__typename') + final String $__typename; + + Map 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 { + 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 + implements CopyWith$Query$SystemServerProvider$system$provider { + _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 + implements CopyWith$Query$SystemServerProvider$system$provider { + _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}); diff --git a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.g.dart b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.g.dart index 525f8d64..f0ec390c 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.g.dart +++ b/lib/logic/api_maps/graphql_maps/schema/server_api.graphql.g.dart @@ -330,6 +330,58 @@ Map _$Mutation$RebootSystem$rebootSystemToJson( '__typename': instance.$__typename, }; +Query$SystemServerProvider _$Query$SystemServerProviderFromJson( + Map json) => + Query$SystemServerProvider( + system: Query$SystemServerProvider$system.fromJson( + json['system'] as Map), + $__typename: json['__typename'] as String, + ); + +Map _$Query$SystemServerProviderToJson( + Query$SystemServerProvider instance) => + { + 'system': instance.system.toJson(), + '__typename': instance.$__typename, + }; + +Query$SystemServerProvider$system _$Query$SystemServerProvider$systemFromJson( + Map json) => + Query$SystemServerProvider$system( + provider: Query$SystemServerProvider$system$provider.fromJson( + json['provider'] as Map), + $__typename: json['__typename'] as String, + ); + +Map _$Query$SystemServerProvider$systemToJson( + Query$SystemServerProvider$system instance) => + { + 'provider': instance.provider.toJson(), + '__typename': instance.$__typename, + }; + +Query$SystemServerProvider$system$provider + _$Query$SystemServerProvider$system$providerFromJson( + Map json) => + Query$SystemServerProvider$system$provider( + provider: $enumDecode(_$Enum$ServerProviderEnumMap, json['provider'], + unknownValue: Enum$ServerProvider.$unknown), + $__typename: json['__typename'] as String, + ); + +Map _$Query$SystemServerProvider$system$providerToJson( + Query$SystemServerProvider$system$provider instance) => + { + '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 json) => Query$GetApiTokens( api: Query$GetApiTokens$api.fromJson(json['api'] as Map), diff --git a/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart index 5d036afa..a077cf7d 100644 --- a/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart @@ -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) diff --git a/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart index a31058c4..92138d02 100644 --- a/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/services.graphql.dart @@ -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) diff --git a/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart index 18a15aa9..ce846b30 100644 --- a/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/users.graphql.dart @@ -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) diff --git a/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart b/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart index 67e8bcb3..84acff43 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart @@ -1,4 +1,4 @@ -part of 'server.dart'; +part of 'server_api.dart'; mixin JobsApi on ApiMap { Future> getServerJobs() async { diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart index 780f1d37..65e77b98 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart @@ -1,4 +1,4 @@ -part of 'server.dart'; +part of 'server_api.dart'; mixin ServerActionsApi on ApiMap { Future _commonBoolRequest(final Function graphQLMethod) async { diff --git a/lib/logic/api_maps/graphql_maps/server_api/server.dart b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart similarity index 95% rename from lib/logic/api_maps/graphql_maps/server_api/server.dart rename to lib/logic/api_maps/graphql_maps/server_api/server_api.dart index c2cda13b..4370131f 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart @@ -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 getServerProviderType() async { + QueryResult response; + ServerProvider providerType = ServerProvider.unknown; + + try { + final GraphQLClient client = await getClient(); + response = await client.query$SystemServerProvider(); + if (response.hasException) { + print(response.exception.toString()); + } + providerType = ServerProvider.fromGraphQL( + response.parsedData!.system.provider.provider, + ); + } catch (e) { + print(e); + } + return providerType; + } + Future isUsingBinds() async { QueryResult response; bool usesBinds = false; diff --git a/lib/logic/api_maps/graphql_maps/server_api/services_api.dart b/lib/logic/api_maps/graphql_maps/server_api/services_api.dart index 2ef0e2ca..a2e85914 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/services_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/services_api.dart @@ -1,4 +1,4 @@ -part of 'server.dart'; +part of 'server_api.dart'; mixin ServicesApi on ApiMap { Future> getAllServices() async { diff --git a/lib/logic/api_maps/graphql_maps/server_api/users_api.dart b/lib/logic/api_maps/graphql_maps/server_api/users_api.dart index bb46bfef..c11f6a0e 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/users_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/users_api.dart @@ -1,4 +1,4 @@ -part of 'server.dart'; +part of 'server_api.dart'; mixin UsersApi on ApiMap { Future> getAllUsers() async { diff --git a/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart b/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart index 70119f28..360dd491 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/volume_api.dart @@ -1,4 +1,4 @@ -part of 'server.dart'; +part of 'server_api.dart'; mixin VolumeApi on ApiMap { Future> getServerDiskVolumes() async { diff --git a/lib/logic/api_maps/rest_maps/api_controller.dart b/lib/logic/api_maps/rest_maps/api_controller.dart new file mode 100644 index 00000000..440d25af --- /dev/null +++ b/lib/logic/api_maps/rest_maps/api_controller.dart @@ -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; +} diff --git a/lib/logic/api_maps/rest_maps/api_factory_creator.dart b/lib/logic/api_maps/rest_maps/api_factory_creator.dart index 18b4ea33..25518f3c 100644 --- a/lib/logic/api_maps/rest_maps/api_factory_creator.dart +++ b/lib/logic/api_maps/rest_maps/api_factory_creator.dart @@ -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'); } diff --git a/lib/logic/api_maps/rest_maps/api_factory_settings.dart b/lib/logic/api_maps/rest_maps/api_factory_settings.dart new file mode 100644 index 00000000..438b92d5 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/api_factory_settings.dart @@ -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; +} diff --git a/lib/logic/api_maps/rest_maps/api_map.dart b/lib/logic/api_maps/rest_maps/api_map.dart index 007bfd98..6fd0bdda 100644 --- a/lib/logic/api_maps/rest_maps/api_map.dart +++ b/lib/logic/api_maps/rest_maps/api_map.dart @@ -9,8 +9,8 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/models/message.dart'; abstract class ApiMap { - Future getClient() async { - final Dio dio = Dio(await options); + Future 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 get options; - abstract final String rootAddress; + String get rootAddress; + abstract final bool hasLogger; abstract final bool isWithToken; diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart index 9266471b..ccb58e6a 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart @@ -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 { diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart new file mode 100644 index 00000000..6b737df5 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart @@ -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; +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart index a425ab93..fb573135 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart @@ -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, }); } diff --git a/lib/logic/api_maps/rest_maps/provider_api_settings.dart b/lib/logic/api_maps/rest_maps/provider_api_settings.dart index 4350fbe7..9e601d2a 100644 --- a/lib/logic/api_maps/rest_maps/provider_api_settings.dart +++ b/lib/logic/api_maps/rest_maps/provider_api_settings.dart @@ -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; } diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart new file mode 100644 index 00000000..ba3f0d63 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean.dart @@ -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().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 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 getPricePerGb() async => Price( + value: 0.10, + currency: 'USD', + ); + + @override + Future createVolume() async { + ServerVolume? volume; + + final Response createVolumeResponse; + final Dio client = await getClient(); + try { + final List 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> getVolumes({final String? status}) async { + final List volumes = []; + + final Response getVolumesResponse; + final Dio client = await getClient(); + try { + getVolumesResponse = await client.get( + '/volumes', + queryParameters: { + 'status': status, + }, + ); + final List 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 getVolume(final String volumeUuid) async { + ServerVolume? requestedVolume; + + final List volumes = await getVolumes(); + + for (final volume in volumes) { + if (volume.uuid == volumeUuid) { + requestedVolume = volume; + } + } + + return requestedVolume; + } + + @override + Future 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 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 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 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 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 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 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 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 laterFutures = []; + + 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 restart() async { + final ServerHostingDetails server = getIt().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 powerOn() async { + final ServerHostingDetails server = getIt().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 calculateCpuLoadMetrics(final List rawProcStatMetrics) { + final List 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 getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + ServerMetrics? metrics; + + const int step = 15; + final Dio client = await getClient(); + try { + Response response = await client.get( + '/monitoring/metrics/droplet/bandwidth', + queryParameters: { + 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', + 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', + 'host_id': '$serverId', + 'interface': 'public', + 'direction': 'inbound', + }, + ); + + final List inbound = response.data['data']['result'][0]['values']; + + response = await client.get( + '/monitoring/metrics/droplet/bandwidth', + queryParameters: { + 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', + 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', + 'host_id': '$serverId', + 'interface': 'public', + 'direction': 'outbound', + }, + ); + + final List outbound = response.data['data']['result'][0]['values']; + + response = await client.get( + '/monitoring/metrics/droplet/cpu', + queryParameters: { + 'start': '${(start.microsecondsSinceEpoch / 1000000).round()}', + 'end': '${(end.microsecondsSinceEpoch / 1000000).round()}', + 'host_id': '$serverId', + }, + ); + + metrics = ServerMetrics( + bandwidthIn: inbound + .map( + (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), + ) + .toList(), + bandwidthOut: outbound + .map( + (final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000), + ) + .toList(), + cpu: calculateCpuLoadMetrics(response.data['data']['result']), + start: start, + end: end, + stepsInSecond: step, + ); + } catch (e) { + print(e); + } finally { + close(client); + } + + return metrics; + } + + @override + Future> getMetadata(final int serverId) async { + List metadata = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get('/droplets/$serverId'); + final droplet = response.data!['droplet']; + metadata = [ + ServerMetadataEntity( + type: MetadataType.id, + name: 'server.server_id'.tr(), + value: droplet['id'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.status, + name: 'server.status'.tr(), + value: droplet['status'].toString().capitalize(), + ), + ServerMetadataEntity( + type: MetadataType.cpu, + name: 'server.cpu'.tr(), + value: 'server.core_count'.plural(droplet['vcpus']), + ), + ServerMetadataEntity( + type: MetadataType.ram, + name: 'server.ram'.tr(), + value: "${droplet['memory'].toString()} MB", + ), + ServerMetadataEntity( + type: MetadataType.cost, + name: 'server.monthly_cost'.tr(), + value: droplet['size']['price_monthly'].toString(), + ), + ServerMetadataEntity( + type: MetadataType.location, + name: 'server.location'.tr(), + value: + '${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}', + ), + ServerMetadataEntity( + type: MetadataType.other, + name: 'server.provider'.tr(), + value: displayProviderName, + ), + ]; + } catch (e) { + print(e); + } finally { + close(client); + } + + return metadata; + } + + @override + Future> getServers() async { + List servers = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get('/droplets'); + servers = response.data!['droplets'].map( + (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> getAvailableLocations() async { + List locations = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/regions', + ); + + locations = response.data!['regions'] + .map( + (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> getServerTypesByLocation({ + required final ServerProviderLocation location, + }) async { + final List 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 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, + ); +} diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart new file mode 100644 index 00000000..73a1e647 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart @@ -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, + ); +} diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart index 37ecc0f0..57df7837 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart @@ -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().hetznerKey; + final String? token = getIt().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 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 getPricePerGb() async { + Future 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 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> getVolumes({final String? status}) async { final List 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 rawVolumes = dbGetResponse.data['volumes']; + final List 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 getVolume(final int id) async { + Future 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 deleteVolume(final int id) async { + Future 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 attachVolume(final int volumeId, final int serverId) async { + Future 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 detachVolume(final int volumeId) async { + Future 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 resizeVolume(final int volumeId, final int sizeGb) async { + Future 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 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 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> getMetrics( + Future> requestRawMetrics( + final int serverId, final DateTime start, final DateTime end, final String type, ) async { - final ServerHostingDetails? hetznerServer = - getIt().serverDetails; - Map 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 getInfo() async { - final ServerHostingDetails? hetznerServer = - getIt().serverDetails; - final Dio client = await getClient(); - final Response response = await client.get('/servers/${hetznerServer!.id}'); - close(client); + List serializeTimeSeries( + final Map 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 getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + ServerMetrics? metrics; + + final Map rawCpuMetrics = await requestRawMetrics( + serverId, + start, + end, + 'cpu', + ); + final Map 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> getMetadata(final int serverId) async { + List 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> getAvailableLocations() async { + List locations = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + '/locations', + ); + + locations = response.data!['locations'] + .map( + (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> getServerTypesByLocation({ + required final ServerProviderLocation location, + }) async { + final List 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 createReverseDns({ required final ServerHostingDetails serverDetails, diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart index 60f61d1b..5f8fcab5 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart @@ -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, ); diff --git a/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart b/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart index 010ebd75..a2eb71f3 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/server_provider.dart @@ -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> getServers(); + Future> getAvailableLocations(); + Future> getServerTypesByLocation({ + required final ServerProviderLocation location, + }); Future restart(); Future 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 createReverseDns({ required final ServerHostingDetails serverDetails, @@ -22,5 +40,19 @@ abstract class ServerProviderApi extends ApiMap { }); Future isApiTokenValid(final String token); - RegExp getApiTokenValidation(); + ProviderApiTokenValidation getApiTokenValidation(); + Future> getMetadata(final int serverId); + Future 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; } diff --git a/lib/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart b/lib/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart new file mode 100644 index 00000000..3931b45b --- /dev/null +++ b/lib/logic/api_maps/rest_maps/server_providers/server_provider_api_settings.dart @@ -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; +} diff --git a/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart b/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart index 10f4c40f..dbbb8035 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart @@ -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, }); } diff --git a/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart b/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart index bdff72f2..d3ae6f2a 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/volume_provider.dart @@ -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 createVolume(); Future> getVolumes({final String? status}); - Future getVolume(final int id); - Future attachVolume(final int volumeId, final int serverId); - Future detachVolume(final int volumeId); - Future resizeVolume(final int volumeId, final int sizeGb); - Future deleteVolume(final int id); - Future getPricePerGb(); + Future attachVolume(final ServerVolume volume, final int serverId); + Future detachVolume(final ServerVolume volume); + Future resizeVolume(final ServerVolume volume, final DiskSize size); + Future deleteVolume(final ServerVolume volume); + Future getPricePerGb(); } diff --git a/lib/logic/api_maps/staging_options.dart b/lib/logic/api_maps/staging_options.dart new file mode 100644 index 00000000..7d3084b7 --- /dev/null +++ b/lib/logic/api_maps/staging_options.dart @@ -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; +} diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 69686978..557448b1 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -5,17 +5,6 @@ enum LoadingStatus { error, } -enum InitializingSteps { - setHetznerKey, - setCloudFlareKey, - setDomainName, - setRootUser, - createServer, - checkCloudFlareDns, - startServer, - checkSystemDnsAndDkimSet, -} - enum Period { hour, day, diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 5f72f2e2..b0fbc020 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -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'; diff --git a/lib/logic/cubit/client_jobs/client_jobs_cubit.dart b/lib/logic/cubit/client_jobs/client_jobs_cubit.dart index e9378bed..8b6a66b4 100644 --- a/lib/logic/cubit/client_jobs/client_jobs_cubit.dart +++ b/lib/logic/cubit/client_jobs/client_jobs_cubit.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index 065113f1..10ad943d 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -1,5 +1,5 @@ import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart'; diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 6159b9be..3403dc68 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,12 +1,11 @@ import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/api_controller.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/utils/network_utils.dart'; part 'dns_records_state.dart'; @@ -19,11 +18,6 @@ class DnsRecordsCubit const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing), ); - DnsProviderApiFactory? dnsProviderApiFactory = - ApiFactoryCreator.createDnsProviderApiFactory( - DnsProvider.cloudflare, // TODO: HARDCODE FOR NOW!!! - ); // TODO: Remove when provider selection is implemented. - final ServerApi api = ServerApi(); @override @@ -44,7 +38,8 @@ class DnsRecordsCubit final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; if (domain != null && ipAddress != null) { - final List records = await dnsProviderApiFactory! + final List 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, diff --git a/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart index c2348a69..e50d7db3 100644 --- a/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart @@ -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( - regExp.hasMatch, - 'validations.invalid_format'.tr(), - ), LengthStringNotEqualValidation(40) ], ); diff --git a/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart b/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart index a1c458fb..62fc1050 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart @@ -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 { Future load() async { emit(Loading(LoadingTypes.loadingDomain)); - final List list = await serverInstallationCubit - .repository.dnsProviderApiFactory! + final List list = await ApiController.currentDnsProviderApiFactory! .getDnsProvider() .domainList(); if (list.isEmpty) { @@ -31,8 +31,7 @@ class DomainSetupCubit extends Cubit { emit(Loading(LoadingTypes.saving)); - final String? zoneId = await serverInstallationCubit - .repository.dnsProviderApiFactory! + final String? zoneId = await ApiController.currentDnsProviderApiFactory! .getDnsProvider() .getZoneId(domainName); diff --git a/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart index 44f40b57..d3307762 100644 --- a/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart @@ -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( - regExp.hasMatch, - 'validations.invalid_format'.tr(), - ), - LengthStringNotEqualValidation(64) + //LengthStringNotEqualValidation(tokenLength), ], ); @@ -26,7 +21,7 @@ class ProviderFormCubit extends FormCubit { @override FutureOr 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; } diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart index dbfc7259..7f840d0a 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -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'; diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart deleted file mode 100644 index 134c955e..00000000 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart +++ /dev/null @@ -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 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> 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 timeSeriesSerializer( - final Map 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(); -} diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart deleted file mode 100644 index b6204db9..00000000 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart +++ /dev/null @@ -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 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 cpu; - final List ppsIn; - final List ppsOut; - final List bandwidthIn; - final List bandwidthOut; - - @override - List get props => [period, start, end]; -} diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart b/lib/logic/cubit/metrics/metrics_cubit.dart similarity index 57% rename from lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart rename to lib/logic/cubit/metrics/metrics_cubit.dart index 1cfdc23a..2a2dec28 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart +++ b/lib/logic/cubit/metrics/metrics_cubit.dart @@ -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 { - HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day)); +class MetricsCubit extends Cubit { + MetricsCubit() : super(const MetricsLoading(Period.day)); - final HetznerMetricsRepository repository = HetznerMetricsRepository(); + final MetricsRepository repository = MetricsRepository(); Timer? timer; @@ -30,7 +30,7 @@ class HetznerMetricsCubit extends Cubit { void changePeriod(final Period period) async { closeTimer(); - emit(HetznerMetricsLoading(period)); + emit(MetricsLoading(period)); load(period); } @@ -40,14 +40,14 @@ class HetznerMetricsCubit extends Cubit { 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), diff --git a/lib/logic/cubit/metrics/metrics_repository.dart b/lib/logic/cubit/metrics/metrics_repository.dart new file mode 100644 index 00000000..71c298bf --- /dev/null +++ b/lib/logic/cubit/metrics/metrics_repository.dart @@ -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 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().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, + ); + } +} diff --git a/lib/logic/cubit/metrics/metrics_state.dart b/lib/logic/cubit/metrics/metrics_state.dart new file mode 100644 index 00000000..b27546ce --- /dev/null +++ b/lib/logic/cubit/metrics/metrics_state.dart @@ -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 get props => [period]; +} + +class MetricsLoaded extends MetricsState { + const MetricsLoaded({ + required this.period, + required this.metrics, + }); + + @override + final Period period; + + final ServerMetrics metrics; + + @override + List get props => [period, metrics]; +} diff --git a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart index b0500452..11e180d0 100644 --- a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart +++ b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart @@ -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 { ApiProviderVolumeCubit(final ServerInstallationCubit serverInstallationCubit) : super(serverInstallationCubit, const ApiProviderVolumeState.initial()); - - VolumeProviderApiFactory? providerApi; - final ServerApi serverApi = ServerApi(); @override Future load() async { if (serverInstallationCubit.state is ServerInstallationFinished) { - final serverDetails = getIt().serverDetails; - providerApi = serverDetails == null - ? null - : VolumeApiFactoryCreator.createVolumeProviderApiFactory( - getIt().serverDetails!.provider, - ); _refetch(); } } - Future getPricePerGb() async => - providerApi!.getVolumeProvider().getPricePerGb(); + Future getPricePerGb() async => + ApiController.currentVolumeProviderApiFactory! + .getVolumeProvider() + .getPricePerGb(); Future refresh() async { emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); @@ -41,12 +35,14 @@ class ApiProviderVolumeCubit } Future _refetch() async { - if (providerApi == null) { + if (ApiController.currentVolumeProviderApiFactory == null) { return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); } - final List volumes = - await providerApi!.getVolumeProvider().getVolumes(); + final List volumes = await ApiController + .currentVolumeProviderApiFactory! + .getVolumeProvider() + .getVolumes(); if (volumes.isEmpty) { return emit(const ApiProviderVolumeState([], LoadingStatus.error, false)); @@ -57,31 +53,33 @@ class ApiProviderVolumeCubit Future attachVolume(final DiskVolume volume) async { final ServerHostingDetails server = getIt().serverDetails!; - await providerApi! + await ApiController.currentVolumeProviderApiFactory! .getVolumeProvider() - .attachVolume(volume.providerVolume!.id, server.id); + .attachVolume(volume.providerVolume!, server.id); refresh(); } Future detachVolume(final DiskVolume volume) async { - await providerApi! + await ApiController.currentVolumeProviderApiFactory! .getVolumeProvider() - .detachVolume(volume.providerVolume!.id); + .detachVolume(volume.providerVolume!); refresh(); } Future resizeVolume( final DiskVolume volume, - final int newSizeGb, + final DiskSize newSize, final Function() callback, ) async { getIt().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().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().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 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 deleteVolume(final DiskVolume volume) async { - await providerApi! + await ApiController.currentVolumeProviderApiFactory! .getVolumeProvider() - .deleteVolume(volume.providerVolume!.id); + .deleteVolume(volume.providerVolume!); refresh(); } diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 875d419a..76a572d1 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -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'; diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index a3af96e8..b6a39733 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -2,7 +2,7 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; part 'server_detailed_info_state.dart'; @@ -22,7 +22,7 @@ class ServerDetailsCubit final ServerDetailsRepositoryDto data = await repository.load(); emit( Loaded( - serverInfo: data.hetznerServerInfo, + metadata: data.metadata, autoUpgradeSettings: data.autoUpgradeSettings, serverTimezone: data.serverTimezone, checkTime: DateTime.now(), diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index dada896b..ca6848bc 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -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 load() async { + final serverProviderApi = ApiController.currentServerProviderApiFactory; final settings = await server.getSystemSettings(); + final serverId = getIt().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 metadata; final TimeZoneSettings serverTimezone; - final AutoUpgradeSettings autoUpgradeSettings; } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart index ea5f4864..64f4d91d 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -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 metadata; final TimeZoneSettings serverTimezone; - final AutoUpgradeSettings autoUpgradeSettings; final DateTime checkTime; @override List get props => [ - serverInfo, + metadata, serverTimezone, autoUpgradeSettings, checkTime, diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 9cb68359..08852825 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -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 { } } - 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 isServerProviderApiTokenValid( final String providerToken, ) async => - repository.serverProviderApiFactory! + ApiController.currentServerProviderApiFactory! .getServerProvider( - settings: const ProviderApiSettings(isWithToken: false), + settings: const ServerProviderApiSettings( + isWithToken: false, + ), ) .isApiTokenValid(providerToken); Future 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> fetchAvailableLocations() async { + if (ApiController.currentServerProviderApiFactory == null) { + return []; + } + + return ApiController.currentServerProviderApiFactory! + .getServerProvider() + .getAvailableLocations(); + } + + Future> 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 { emit( (state as ServerInstallationNotFinished).copyWith( - providerApiToken: hetznerKey, + providerApiToken: serverProviderKey, + ), + ); + } + + void setServerType(final ServerType serverType) async { + await repository.saveServerType(serverType); + + ApiController.initServerProviderApiFactory( + ServerProviderApiFactorySettings( + provider: getIt().serverProvider!, + location: serverType.location.identifier, + ), + ); + + // All server providers support volumes for now, + // so it's safe to initialize. + ApiController.initVolumeProviderApiFactory( + ServerProviderApiFactorySettings( + provider: getIt().serverProvider!, + location: serverType.location.identifier, + ), + ); + + emit( + (state as ServerInstallationNotFinished).copyWith( + serverTypeIdentificator: serverType.identifier, ), ); } @@ -104,6 +181,7 @@ class ServerInstallationCubit extends Cubit { return; } await repository.saveCloudFlareKey(cloudFlareKey); + emit( (state as ServerInstallationNotFinished) .copyWith(cloudFlareKey: cloudFlareKey), @@ -248,14 +326,13 @@ class ServerInstallationCubit extends Cubit { ), ); 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 { ), ); 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 { token, dataState.recoveryCapabilities, ); + final ServerProvider provider = await ServerApi( + customToken: serverDetails.apiToken, + isWithToken: true, + ).getServerProviderType(); + if (provider == ServerProvider.unknown) { + getIt() + .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 { } } - Future> - getServersOnHetznerAccount() async { + Future> getAvailableServers() async { final ServerInstallationRecovery dataState = state as ServerInstallationRecovery; final List servers = @@ -515,7 +600,9 @@ class ServerInstallationCubit extends Cubit { 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 { 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 { void clearAppConfig() { closeTimer(); - + ApiController.clearProviderApiFactories(); repository.clearAppConfig(); emit(const ServerInstallationEmpty()); } diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index c4a3e7ff..cc3860e3 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -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 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 load() async { - final String? providerApiToken = getIt().hetznerKey; + final String? providerApiToken = getIt().serverProviderKey; + final String? location = getIt().serverLocation; final String? cloudflareToken = getIt().cloudFlareKey; + final String? serverTypeIdentificator = getIt().serverType; final ServerDomain? serverDomain = getIt().serverDomain; + final ServerProvider? serverProvider = + getIt().serverProvider; final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; final ServerHostingDetails? serverDetails = getIt().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 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 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 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().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(); - 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().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(); - 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().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(); - 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 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 restart() async { - final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); - return api.restart(); - } + Future restart() async => + ApiController.currentServerProviderApiFactory! + .getServerProvider() + .restart(); - Future powerOn() async { - final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); - return api.powerOn(); - } + Future powerOn() async => + ApiController.currentServerProviderApiFactory! + .getServerProvider() + .powerOn(); Future getRecoveryCapabilities( final ServerDomain serverDomain, @@ -651,10 +634,10 @@ class ServerInstallationRepository { } } - Future> getServersOnProviderAccount() async { - final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); - return api.getServers(); - } + Future> getServersOnProviderAccount() async => + ApiController.currentServerProviderApiFactory! + .getServerProvider() + .getServers(); Future saveServerDetails( final ServerHostingDetails serverDetails, @@ -667,12 +650,24 @@ class ServerInstallationRepository { getIt().init(); } - Future saveHetznerKey(final String key) async { - print('saved'); - await getIt().storeHetznerKey(key); + Future saveServerProviderType(final ServerProvider type) async { + await getIt().storeServerProviderType(type); } - Future deleteHetznerKey() async { + Future saveServerProviderKey(final String key) async { + await getIt().storeServerProviderKey(key); + } + + Future saveServerType(final ServerType serverType) async { + await getIt().storeServerTypeIdentifier( + serverType.identifier, + ); + await getIt().storeServerLocation( + serverType.location.identifier, + ); + } + + Future deleteServerProviderKey() async { await box.delete(BNames.hetznerKey); getIt().init(); } @@ -731,13 +726,11 @@ class ServerInstallationRepository { } Future 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 deleteServerRelatedRecords() async { diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index 11c66ffe..331c3e2a 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -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 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 get _fulfilementList { final List 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 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 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 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!, diff --git a/lib/logic/cubit/server_jobs/server_jobs_cubit.dart b/lib/logic/cubit/server_jobs/server_jobs_cubit.dart index fc102115..4cc0cf97 100644 --- a/lib/logic/cubit/server_jobs/server_jobs_cubit.dart +++ b/lib/logic/cubit/server_jobs/server_jobs_cubit.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/json/server_job.dart'; diff --git a/lib/logic/cubit/server_volumes/server_volume_cubit.dart b/lib/logic/cubit/server_volumes/server_volume_cubit.dart index e48a809e..c10bc377 100644 --- a/lib/logic/cubit/server_volumes/server_volume_cubit.dart +++ b/lib/logic/cubit/server_volumes/server_volume_cubit.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart'; diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index f76c15a7..54e22b3d 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/service.dart'; diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index f31580fb..070fce2c 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index ec2feb55..434c9b32 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -9,22 +9,33 @@ class ApiConfigModel { final Box _box = Hive.box(BNames.serverInstallationBox); ServerHostingDetails? get serverDetails => _serverDetails; - String? get hetznerKey => _hetznerKey; + String? get serverProviderKey => _serverProviderKey; + String? get serverLocation => _serverLocation; + String? get serverType => _serverType; String? get cloudFlareKey => _cloudFlareKey; + ServerProvider? get serverProvider => _serverProvider; BackblazeCredential? get backblazeCredential => _backblazeCredential; ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; - String? _hetznerKey; + String? _serverProviderKey; + String? _serverLocation; String? _cloudFlareKey; + String? _serverType; + ServerProvider? _serverProvider; ServerHostingDetails? _serverDetails; BackblazeCredential? _backblazeCredential; ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; - Future storeHetznerKey(final String value) async { + Future storeServerProviderType(final ServerProvider value) async { + await _box.put(BNames.serverProvider, value); + _serverProvider = value; + } + + Future storeServerProviderKey(final String value) async { await _box.put(BNames.hetznerKey, value); - _hetznerKey = value; + _serverProviderKey = value; } Future storeCloudFlareKey(final String value) async { @@ -32,6 +43,16 @@ class ApiConfigModel { _cloudFlareKey = value; } + Future storeServerTypeIdentifier(final String typeIdentifier) async { + await _box.put(BNames.serverTypeIdentifier, typeIdentifier); + _serverType = typeIdentifier; + } + + Future storeServerLocation(final String serverLocation) async { + await _box.put(BNames.serverLocation, serverLocation); + _serverLocation = serverLocation; + } + Future 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); } } diff --git a/lib/logic/models/hetzner_metrics.dart b/lib/logic/models/hetzner_metrics.dart deleted file mode 100644 index 2f41a4b2..00000000 --- a/lib/logic/models/hetzner_metrics.dart +++ /dev/null @@ -1,11 +0,0 @@ -class TimeSeriesData { - TimeSeriesData( - this.secondsSinceEpoch, - this.value, - ); - - final int secondsSinceEpoch; - DateTime get time => - DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000); - final double value; -} diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart index 3791c664..57a54762 100644 --- a/lib/logic/models/hive/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -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; + } + } } diff --git a/lib/logic/models/hive/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart index 29716607..6fad2e92 100644 --- a/lib/logic/models/hive/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -76,13 +76,14 @@ class ServerVolumeAdapter extends TypeAdapter { 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 { ..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 { 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 { case ServerProvider.hetzner: writer.writeByte(1); break; + case ServerProvider.digitalOcean: + writer.writeByte(2); + break; } } diff --git a/lib/logic/models/metrics.dart b/lib/logic/models/metrics.dart new file mode 100644 index 00000000..4f5d3efc --- /dev/null +++ b/lib/logic/models/metrics.dart @@ -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 cpu; + final List bandwidthIn; + final List bandwidthOut; + + final DateTime start; + final DateTime end; +} diff --git a/lib/logic/models/price.dart b/lib/logic/models/price.dart new file mode 100644 index 00000000..1da2677d --- /dev/null +++ b/lib/logic/models/price.dart @@ -0,0 +1,9 @@ +class Price { + Price({ + required this.value, + required this.currency, + }); + + double value; + String currency; +} diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart index c7d44ff8..3037a2d5 100644 --- a/lib/logic/models/server_basic_info.dart +++ b/lib/logic/models/server_basic_info.dart @@ -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, }); diff --git a/lib/logic/models/server_metadata.dart b/lib/logic/models/server_metadata.dart new file mode 100644 index 00000000..0275a2ef --- /dev/null +++ b/lib/logic/models/server_metadata.dart @@ -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; +} diff --git a/lib/logic/models/server_provider_location.dart b/lib/logic/models/server_provider_location.dart new file mode 100644 index 00000000..f76c226f --- /dev/null +++ b/lib/logic/models/server_provider_location.dart @@ -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; +} diff --git a/lib/logic/models/server_type.dart b/lib/logic/models/server_type.dart new file mode 100644 index 00000000..4e7b8d92 --- /dev/null +++ b/lib/logic/models/server_type.dart @@ -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; +} diff --git a/lib/main.dart b/lib/main.dart index ee771474..3bab9f0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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'; diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index 25fb3612..bafe94a7 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -2,18 +2,16 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; import 'package:selfprivacy/logic/models/json/server_job.dart'; -import 'package:selfprivacy/ui/components/action_button/action_button.dart'; -import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart'; +import 'package:selfprivacy/ui/helpers/modals.dart'; class JobsContent extends StatelessWidget { const JobsContent({super.key}); @@ -47,26 +45,16 @@ class JobsContent extends StatelessWidget { ), const SizedBox(height: 10), BrandButton.text( + title: 'jobs.reboot_server'.tr(), onPressed: () { - final NavigationService nav = getIt(); - nav.showPopUpDialog( - BrandAlert( - title: 'jobs.reboot_server'.tr(), - contentText: 'modals.are_you_sure'.tr(), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => - {context.read().rebootServer()}, - text: 'modals.reboot'.tr(), - ) - ], - ), + showPopUpAlert( + alertTitle: 'jobs.reboot_server'.tr(), + description: 'modals.are_you_sure'.tr(), + actionButtonTitle: 'modals.reboot'.tr(), + actionButtonOnPressed: () => + {context.read().rebootServer()}, ); }, - title: 'jobs.reboot_server'.tr(), ), ]; } diff --git a/lib/ui/components/not_ready_card/not_ready_card.dart b/lib/ui/components/not_ready_card/not_ready_card.dart index 1faf586c..e161e8d3 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -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'; diff --git a/lib/ui/helpers/modals.dart b/lib/ui/helpers/modals.dart index 8867885f..1750c2aa 100644 --- a/lib/ui/helpers/modals.dart +++ b/lib/ui/helpers/modals.dart @@ -1,5 +1,9 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/ui/components/action_button/action_button.dart'; +import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; Future showBrandBottomSheet({ required final BuildContext context, @@ -12,3 +16,30 @@ Future showBrandBottomSheet({ 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().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, + ), + ], + ), + ); +} diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index e982a3d4..268b0870 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -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 navigatorKey = GlobalKey(); @@ -157,28 +155,17 @@ class _BackupDetailsState extends State onTap: preventActions ? null : () { - final NavigationService nav = - getIt(); - 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() - .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() + .restoreBackup(backup.id) + }, ); }, title: Text( diff --git a/lib/ui/pages/more/about_application.dart b/lib/ui/pages/more/about_application.dart index 697e7811..ceefd0c7 100644 --- a/lib/ui/pages/more/about_application.dart +++ b/lib/ui/pages/more/about_application.dart @@ -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'; diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 2e75ad26..77b1d34a 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -11,7 +11,7 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/pages/devices/devices.dart'; import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart'; -import 'package:selfprivacy/ui/pages/setup/initializing.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/users/users.dart'; diff --git a/lib/ui/pages/server_details/charts/bottom_title.dart b/lib/ui/pages/server_details/charts/bottom_title.dart index 3db976ad..8d215d7e 100644 --- a/lib/ui/pages/server_details/charts/bottom_title.dart +++ b/lib/ui/pages/server_details/charts/bottom_title.dart @@ -1,5 +1,5 @@ import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; +import 'package:selfprivacy/logic/models/metrics.dart'; import 'package:intl/intl.dart'; String bottomTitle( @@ -11,7 +11,7 @@ String bottomTitle( final day = DateFormat('MMMd'); String res; - if (value <= 0) { + if (value <= 0 || value >= data.length) { return ''; } diff --git a/lib/ui/pages/server_details/charts/chart.dart b/lib/ui/pages/server_details/charts/chart.dart index fb46eb71..774dcf75 100644 --- a/lib/ui/pages/server_details/charts/chart.dart +++ b/lib/ui/pages/server_details/charts/chart.dart @@ -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(); + final MetricsCubit cubit = context.watch(); final Period period = cubit.state.period; - final HetznerMetricsState state = cubit.state; + final MetricsState state = cubit.state; List 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, ), ); } diff --git a/lib/ui/pages/server_details/charts/cpu_chart.dart b/lib/ui/pages/server_details/charts/cpu_chart.dart index 9b437b16..8c3ae9c7 100644 --- a/lib/ui/pages/server_details/charts/cpu_chart.dart +++ b/lib/ui/pages/server_details/charts/cpu_chart.dart @@ -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( diff --git a/lib/ui/pages/server_details/charts/network_charts.dart b/lib/ui/pages/server_details/charts/network_charts.dart index c71ae7b1..946d0247 100644 --- a/lib/ui/pages/server_details/charts/network_charts.dart +++ b/lib/ui/pages/server_details/charts/network_charts.dart @@ -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( diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index e0d82b6d..487e1a25 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -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 ), const SizedBox(height: 8), BlocProvider( - create: (final context) => HetznerMetricsCubit()..restart(), + create: (final context) => MetricsCubit()..restart(), child: _Chart(), ), const SizedBox(height: 8), diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index e9b3c0f7..5f447901 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -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(), ], ), ); diff --git a/lib/ui/pages/server_storage/extending_volume.dart b/lib/ui/pages/server_storage/extending_volume.dart index d9889336..e16544b5 100644 --- a/lib/ui/pages/server_storage/extending_volume.dart +++ b/lib/ui/pages/server_storage/extending_volume.dart @@ -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 { ], ); } - _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 { : () { context.read().resizeVolume( widget.diskVolumeToResize, - _currentSliderGbValue.round(), + DiskSize.fromGibibyte(_currentSliderGbValue), context.read().reload, ); Navigator.of(context).pushAndRemoveUntil( diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart similarity index 85% rename from lib/ui/pages/setup/initializing.dart rename to lib/ui/pages/setup/initializing/initializing.dart index c65d17ae..29fcd6f0 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -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().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().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().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(); + 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( 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().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().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().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().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().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(), + ), + ], + ), ), ); diff --git a/lib/ui/pages/setup/initializing/server_provider_picker.dart b/lib/ui/pages/setup/initializing/server_provider_picker.dart new file mode 100644 index 00000000..4934d1e3 --- /dev/null +++ b/lib/ui/pages/setup/initializing/server_provider_picker.dart @@ -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 createState() => _ServerProviderPickerState(); +} + +class _ServerProviderPickerState extends State { + 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( + 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, + ), + ), + ], + ), + ), + ], + ); +} diff --git a/lib/ui/pages/setup/initializing/server_type_picker.dart b/lib/ui/pages/setup/initializing/server_type_picker.dart new file mode 100644 index 00000000..04b3bd5f --- /dev/null +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -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 createState() => _ServerTypePickerState(); +} + +class _ServerTypePickerState extends State { + 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 snapshot, + ) { + if (snapshot.hasData) { + if ((snapshot.data as List).isEmpty) { + return Text('initializing.no_locations_found'.tr()); + } + return ListView( + padding: paddingH15V0, + children: [ + ...(snapshot.data! as List).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 snapshot, + ) { + if (snapshot.hasData) { + if ((snapshot.data as List).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).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()); + } + }, + ); +} diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 430e0894..969e3d39 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -45,9 +45,8 @@ class _RecoveryConfirmServerState extends State { hasFlashButton: false, children: [ FutureBuilder>( - future: context - .read() - .getServersOnHetznerAccount(), + future: + context.read().getAvailableServers(), builder: (final context, final snapshot) { if (snapshot.hasData) { final servers = snapshot.data; diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index c2b777fc..c2fb1d13 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -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(); diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart similarity index 89% rename from lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart rename to lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart index 76951395..152e4308 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart @@ -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().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().apiKey, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: 'recovering.hetzner_connected_placeholder'.tr(), + labelText: + 'recovering.server_provider_connected_placeholder'.tr(), ), ), const SizedBox(height: 16), diff --git a/lib/utils/password_generator.dart b/lib/utils/password_generator.dart index 5acf3888..a940bb19 100644 --- a/lib/utils/password_generator.dart +++ b/lib/utils/password_generator.dart @@ -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, ); diff --git a/pubspec.lock b/pubspec.lock index 8eae4c89..c5901997 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" auto_size_text: dependency: "direct main" description: @@ -126,7 +126,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -147,7 +147,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -350,7 +350,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -631,12 +631,12 @@ packages: source: hosted version: "1.1.3" http: - dependency: transitive + dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.4" + version: "0.13.5" http_multi_server: dependency: transitive description: @@ -762,21 +762,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -846,7 +846,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: @@ -1159,7 +1159,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -1187,7 +1187,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" system_theme: dependency: "direct main" description: @@ -1208,28 +1208,28 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" timezone: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 372bd4a2..af1521e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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