From bd33b8d679dea439d211105b9ce5225db46fc701 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Mon, 28 Nov 2022 22:51:37 +0400 Subject: [PATCH] feat: Implement distinction for connection errors on initialing page Now it's 'false' when api token is invalid and null response if couldn't connect at all, to show different kinds of errors to the user --- assets/translations/en.json | 1 + assets/translations/ru.json | 1 + lib/logic/api_maps/api_generic_result.dart | 13 +++ .../graphql_maps/server_api/server_api.dart | 83 ++++++++----------- .../digital_ocean/digital_ocean.dart | 35 +++++--- .../server_providers/hetzner/hetzner.dart | 35 +++++--- .../server_providers/server_provider.dart | 5 +- lib/logic/cubit/devices/devices_cubit.dart | 7 +- .../initializing/provider_form_cubit.dart | 11 ++- .../recovery_key/recovery_key_cubit.dart | 4 +- .../server_installation_cubit.dart | 29 +++++-- .../server_installation_repository.dart | 8 +- 12 files changed, 142 insertions(+), 90 deletions(-) create mode 100644 lib/logic/api_maps/api_generic_result.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 18e70c5b..de30d6f0 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -273,6 +273,7 @@ "place_where_data": "A place where your data and SelfPrivacy services will reside:", "how": "How to obtain API token", "provider_bad_key_error": "Provider API key is invalid", + "could_not_connect": "Counldn't connect to the provider, please check your connection.", "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.", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 3dd4e590..b1228fa1 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -272,6 +272,7 @@ "place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:", "how": "Как получить API Token", "provider_bad_key_error": "API ключ провайдера неверен", + "could_not_connect": "Не удалось соединиться с провайдером. Пожалуйста, проверьте подключение.", "choose_location_type": "Выберите локацию и тип вашего сервера:", "back_to_locations": "Назад к доступным локациям!", "no_locations_found": "Не найдено локаций. Убедитесь, что ваш аккаунт доступен.", diff --git a/lib/logic/api_maps/api_generic_result.dart b/lib/logic/api_maps/api_generic_result.dart new file mode 100644 index 00000000..b5bacfb6 --- /dev/null +++ b/lib/logic/api_maps/api_generic_result.dart @@ -0,0 +1,13 @@ +class APIGenericResult { + APIGenericResult({ + required this.success, + required this.data, + this.message, + }); + + /// Whether was a response successfully received, + /// doesn't represent success of the request if `data` is `bool` + final bool success; + final String? message; + final T data; +} diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart index 4370131f..5216ffa8 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart @@ -1,5 +1,6 @@ import 'package:graphql/client.dart'; import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/api_map.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart'; @@ -22,27 +23,15 @@ import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/ssh_settings.dart'; import 'package:selfprivacy/logic/models/system_settings.dart'; +export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; + part 'jobs_api.dart'; part 'server_actions_api.dart'; part 'services_api.dart'; part 'users_api.dart'; part 'volume_api.dart'; -class GenericResult { - GenericResult({ - required this.success, - required this.data, - this.message, - }); - - /// Whether was a response successfully received, - /// doesn't represent success of the request if `data` is `bool` - final bool success; - final String? message; - final T data; -} - -class GenericMutationResult extends GenericResult { +class GenericMutationResult extends APIGenericResult { GenericMutationResult({ required super.success, required this.code, @@ -206,7 +195,7 @@ class ServerApi extends ApiMap return settings; } - Future> getRecoveryTokenStatus() async { + Future> getRecoveryTokenStatus() async { RecoveryKeyStatus? key; QueryResult response; String? error; @@ -223,18 +212,18 @@ class ServerApi extends ApiMap print(e); } - return GenericResult( + return APIGenericResult( success: error == null, data: key, message: error, ); } - Future> generateRecoveryToken( + Future> generateRecoveryToken( final DateTime? expirationDate, final int? numberOfUses, ) async { - GenericResult key; + APIGenericResult key; QueryResult response; try { @@ -255,19 +244,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - key = GenericResult( + key = APIGenericResult( success: false, data: '', message: response.exception.toString(), ); } - key = GenericResult( + key = APIGenericResult( success: true, data: response.parsedData!.getNewRecoveryApiKey.key!, ); } catch (e) { print(e); - key = GenericResult( + key = APIGenericResult( success: false, data: '', message: e.toString(), @@ -300,8 +289,8 @@ class ServerApi extends ApiMap return records; } - Future>> getApiTokens() async { - GenericResult> tokens; + Future>> getApiTokens() async { + APIGenericResult> tokens; QueryResult response; try { @@ -310,7 +299,7 @@ class ServerApi extends ApiMap if (response.hasException) { final message = response.exception.toString(); print(message); - tokens = GenericResult>( + tokens = APIGenericResult>( success: false, data: [], message: message, @@ -324,13 +313,13 @@ class ServerApi extends ApiMap ApiToken.fromGraphQL(device), ) .toList(); - tokens = GenericResult>( + tokens = APIGenericResult>( success: true, data: parsed, ); } catch (e) { print(e); - tokens = GenericResult>( + tokens = APIGenericResult>( success: false, data: [], message: e.toString(), @@ -340,8 +329,8 @@ class ServerApi extends ApiMap return tokens; } - Future> deleteApiToken(final String name) async { - GenericResult returnable; + Future> deleteApiToken(final String name) async { + APIGenericResult returnable; QueryResult response; try { @@ -358,19 +347,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - returnable = GenericResult( + returnable = APIGenericResult( success: false, data: null, message: response.exception.toString(), ); } - returnable = GenericResult( + returnable = APIGenericResult( success: true, data: null, ); } catch (e) { print(e); - returnable = GenericResult( + returnable = APIGenericResult( success: false, data: null, message: e.toString(), @@ -380,8 +369,8 @@ class ServerApi extends ApiMap return returnable; } - Future> createDeviceToken() async { - GenericResult token; + Future> createDeviceToken() async { + APIGenericResult token; QueryResult response; try { @@ -393,19 +382,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - token = GenericResult( + token = APIGenericResult( success: false, data: '', message: response.exception.toString(), ); } - token = GenericResult( + token = APIGenericResult( success: true, data: response.parsedData!.getNewDeviceApiKey.key!, ); } catch (e) { print(e); - token = GenericResult( + token = APIGenericResult( success: false, data: '', message: e.toString(), @@ -417,10 +406,10 @@ class ServerApi extends ApiMap Future isHttpServerWorking() async => (await getApiVersion()) != null; - Future> authorizeDevice( + Future> authorizeDevice( final DeviceToken deviceToken, ) async { - GenericResult token; + APIGenericResult token; QueryResult response; try { @@ -442,19 +431,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - token = GenericResult( + token = APIGenericResult( success: false, data: '', message: response.exception.toString(), ); } - token = GenericResult( + token = APIGenericResult( success: true, data: response.parsedData!.authorizeWithNewDeviceApiKey.token!, ); } catch (e) { print(e); - token = GenericResult( + token = APIGenericResult( success: false, data: '', message: e.toString(), @@ -464,10 +453,10 @@ class ServerApi extends ApiMap return token; } - Future> useRecoveryToken( + Future> useRecoveryToken( final DeviceToken deviceToken, ) async { - GenericResult token; + APIGenericResult token; QueryResult response; try { @@ -489,19 +478,19 @@ class ServerApi extends ApiMap ); if (response.hasException) { print(response.exception.toString()); - token = GenericResult( + token = APIGenericResult( success: false, data: '', message: response.exception.toString(), ); } - token = GenericResult( + token = APIGenericResult( success: true, data: response.parsedData!.useRecoveryApiKey.token!, ); } catch (e) { print(e); - token = GenericResult( + token = APIGenericResult( success: false, data: '', message: e.toString(), 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 index ba3f0d63..b97018e2 100644 --- 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 @@ -59,35 +59,50 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi { String get displayProviderName => 'Digital Ocean'; @override - Future isApiTokenValid(final String token) async { + Future> isApiTokenValid(final String token) async { bool isValid = false; Response? response; + String message = ''; final Dio client = await getClient(); try { response = await client.get( '/account', options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), headers: {'Authorization': 'Bearer $token'}, ), ); } catch (e) { print(e); isValid = false; + message = e.toString(); } finally { close(client); } - if (response != null) { - if (response.statusCode == HttpStatus.ok) { - isValid = true; - } else if (response.statusCode == HttpStatus.unauthorized) { - isValid = false; - } else { - throw Exception('code: ${response.statusCode}'); - } + if (response == null) { + return APIGenericResult( + data: isValid, + success: false, + message: message, + ); } - return isValid; + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return APIGenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); } /// Hardcoded on their documentation and there is no pricing API at all 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 57df7837..87331d85 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 @@ -60,35 +60,50 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi { String get displayProviderName => 'Hetzner'; @override - Future isApiTokenValid(final String token) async { + Future> isApiTokenValid(final String token) async { bool isValid = false; Response? response; + String message = ''; final Dio client = await getClient(); try { response = await client.get( '/servers', options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), headers: {'Authorization': 'Bearer $token'}, ), ); } catch (e) { print(e); isValid = false; + message = e.toString(); } finally { close(client); } - if (response != null) { - if (response.statusCode == HttpStatus.ok) { - isValid = true; - } else if (response.statusCode == HttpStatus.unauthorized) { - isValid = false; - } else { - throw Exception('code: ${response.statusCode}'); - } + if (response == null) { + return APIGenericResult( + data: isValid, + success: false, + message: message, + ); } - return isValid; + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return APIGenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); } @override 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 a2eb71f3..45e2bd2e 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 @@ -1,3 +1,4 @@ +import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; @@ -8,6 +9,8 @@ import 'package:selfprivacy/logic/models/server_metadata.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; +export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; + class ProviderApiTokenValidation { ProviderApiTokenValidation({ required this.length, @@ -39,7 +42,7 @@ abstract class ServerProviderApi extends ApiMap { required final ServerDomain domain, }); - Future isApiTokenValid(final String token); + Future> isApiTokenValid(final String token); ProviderApiTokenValidation getApiTokenValidation(); Future> getMetadata(final int serverId); Future getMetrics( diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart index 10ad943d..5e5c145c 100644 --- a/lib/logic/cubit/devices/devices_cubit.dart +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -35,7 +35,7 @@ class ApiDevicesCubit } Future?> _getApiTokens() async { - final GenericResult> response = await api.getApiTokens(); + final APIGenericResult> response = await api.getApiTokens(); if (response.success) { return response.data; } else { @@ -44,7 +44,8 @@ class ApiDevicesCubit } Future deleteDevice(final ApiToken device) async { - final GenericResult response = await api.deleteApiToken(device.name); + final APIGenericResult response = + await api.deleteApiToken(device.name); if (response.success) { emit( ApiDevicesState( @@ -59,7 +60,7 @@ class ApiDevicesCubit } Future getNewDeviceKey() async { - final GenericResult response = await api.createDeviceToken(); + final APIGenericResult response = await api.createDeviceToken(); if (response.success) { return response.data; } else { 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 d3307762..ebabb5e7 100644 --- a/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/provider_form_cubit.dart @@ -29,21 +29,24 @@ class ProviderFormCubit extends FormCubit { @override FutureOr asyncValidation() async { - late bool isKeyValid; + bool? isKeyValid; try { isKeyValid = await serverInstallationCubit .isServerProviderApiTokenValid(apiKey.state.value); } catch (e) { addError(e); - isKeyValid = false; + } + + if (isKeyValid == null) { + apiKey.setError(''); + return false; } if (!isKeyValid) { apiKey.setError('initializing.provider_bad_key_error'.tr()); - return false; } - return true; + return isKeyValid; } } diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 76a572d1..56800be3 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -32,7 +32,7 @@ class RecoveryKeyCubit } Future _getRecoveryKeyStatus() async { - final GenericResult response = + final APIGenericResult response = await api.getRecoveryTokenStatus(); if (response.success) { return response.data; @@ -57,7 +57,7 @@ class RecoveryKeyCubit final DateTime? expirationDate, final int? numberOfUses, }) async { - final GenericResult response = + final APIGenericResult response = await api.generateRecoveryToken(expirationDate, numberOfUses); if (response.success) { refresh(); diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 08852825..dd9dadbd 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -76,16 +76,27 @@ class ServerInstallationCubit extends Cubit { .getDnsProvider() .getApiTokenValidation(); - Future isServerProviderApiTokenValid( + Future isServerProviderApiTokenValid( final String providerToken, - ) async => - ApiController.currentServerProviderApiFactory! - .getServerProvider( - settings: const ServerProviderApiSettings( - isWithToken: false, - ), - ) - .isApiTokenValid(providerToken); + ) async { + final APIGenericResult apiResponse = + await ApiController.currentServerProviderApiFactory! + .getServerProvider( + settings: const ServerProviderApiSettings( + isWithToken: false, + ), + ) + .isApiTokenValid(providerToken); + + if (!apiResponse.success) { + getIt().showSnackBar( + 'initializing.could_not_connect'.tr(), + ); + return null; + } + + return apiResponse.data; + } Future isDnsProviderApiTokenValid( final String providerToken, diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index cc3860e3..8365529a 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -479,7 +479,7 @@ class ServerInstallationRepository { overrideDomain: serverDomain.domainName, ); final String serverIp = await getServerIpFromDomain(serverDomain); - final GenericResult result = await serverApi.authorizeDevice( + final APIGenericResult result = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: newDeviceKey), ); @@ -516,7 +516,7 @@ class ServerInstallationRepository { overrideDomain: serverDomain.domainName, ); final String serverIp = await getServerIpFromDomain(serverDomain); - final GenericResult result = await serverApi.useRecoveryToken( + final APIGenericResult result = await serverApi.useRecoveryToken( DeviceToken(device: await getDeviceName(), token: recoveryKey), ); @@ -577,9 +577,9 @@ class ServerInstallationRepository { ); } } - final GenericResult deviceAuthKey = + final APIGenericResult deviceAuthKey = await serverApi.createDeviceToken(); - final GenericResult result = await serverApi.authorizeDevice( + final APIGenericResult result = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), );