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
pull/149/head
NaiJi ✨ 2022-11-28 22:51:37 +04:00
parent 58ce0f0f8b
commit bd33b8d679
12 changed files with 142 additions and 90 deletions

View File

@ -273,6 +273,7 @@
"place_where_data": "A place where your data and SelfPrivacy services will reside:", "place_where_data": "A place where your data and SelfPrivacy services will reside:",
"how": "How to obtain API token", "how": "How to obtain API token",
"provider_bad_key_error": "Provider API key is invalid", "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:", "choose_location_type": "Choose your server location and type:",
"back_to_locations": "Go back to available locations!", "back_to_locations": "Go back to available locations!",
"no_locations_found": "No available locations found. Make sure your account is accessible.", "no_locations_found": "No available locations found. Make sure your account is accessible.",

View File

@ -272,6 +272,7 @@
"place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:", "place_where_data": "Здесь будут жить ваши данные и SelfPrivacy-сервисы:",
"how": "Как получить API Token", "how": "Как получить API Token",
"provider_bad_key_error": "API ключ провайдера неверен", "provider_bad_key_error": "API ключ провайдера неверен",
"could_not_connect": "Не удалось соединиться с провайдером. Пожалуйста, проверьте подключение.",
"choose_location_type": "Выберите локацию и тип вашего сервера:", "choose_location_type": "Выберите локацию и тип вашего сервера:",
"back_to_locations": "Назад к доступным локациям!", "back_to_locations": "Назад к доступным локациям!",
"no_locations_found": "Не найдено локаций. Убедитесь, что ваш аккаунт доступен.", "no_locations_found": "Не найдено локаций. Убедитесь, что ваш аккаунт доступен.",

View File

@ -0,0 +1,13 @@
class APIGenericResult<T> {
APIGenericResult({
required this.success,
required this.data,
this.message,
});
/// Whether was a response successfully received,
/// doesn't represent success of the request if `data<T>` is `bool`
final bool success;
final String? message;
final T data;
}

View File

@ -1,5 +1,6 @@
import 'package:graphql/client.dart'; import 'package:graphql/client.dart';
import 'package:selfprivacy/config/get_it_config.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/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/disk_volumes.graphql.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.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/ssh_settings.dart';
import 'package:selfprivacy/logic/models/system_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 'jobs_api.dart';
part 'server_actions_api.dart'; part 'server_actions_api.dart';
part 'services_api.dart'; part 'services_api.dart';
part 'users_api.dart'; part 'users_api.dart';
part 'volume_api.dart'; part 'volume_api.dart';
class GenericResult<T> { class GenericMutationResult<T> extends APIGenericResult<T> {
GenericResult({
required this.success,
required this.data,
this.message,
});
/// Whether was a response successfully received,
/// doesn't represent success of the request if `data<T>` is `bool`
final bool success;
final String? message;
final T data;
}
class GenericMutationResult<T> extends GenericResult<T> {
GenericMutationResult({ GenericMutationResult({
required super.success, required super.success,
required this.code, required this.code,
@ -206,7 +195,7 @@ class ServerApi extends ApiMap
return settings; return settings;
} }
Future<GenericResult<RecoveryKeyStatus?>> getRecoveryTokenStatus() async { Future<APIGenericResult<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
RecoveryKeyStatus? key; RecoveryKeyStatus? key;
QueryResult<Query$RecoveryKey> response; QueryResult<Query$RecoveryKey> response;
String? error; String? error;
@ -223,18 +212,18 @@ class ServerApi extends ApiMap
print(e); print(e);
} }
return GenericResult<RecoveryKeyStatus?>( return APIGenericResult<RecoveryKeyStatus?>(
success: error == null, success: error == null,
data: key, data: key,
message: error, message: error,
); );
} }
Future<GenericResult<String>> generateRecoveryToken( Future<APIGenericResult<String>> generateRecoveryToken(
final DateTime? expirationDate, final DateTime? expirationDate,
final int? numberOfUses, final int? numberOfUses,
) async { ) async {
GenericResult<String> key; APIGenericResult<String> key;
QueryResult<Mutation$GetNewRecoveryApiKey> response; QueryResult<Mutation$GetNewRecoveryApiKey> response;
try { try {
@ -255,19 +244,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
key = GenericResult<String>( key = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
key = GenericResult<String>( key = APIGenericResult<String>(
success: true, success: true,
data: response.parsedData!.getNewRecoveryApiKey.key!, data: response.parsedData!.getNewRecoveryApiKey.key!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
key = GenericResult<String>( key = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),
@ -300,8 +289,8 @@ class ServerApi extends ApiMap
return records; return records;
} }
Future<GenericResult<List<ApiToken>>> getApiTokens() async { Future<APIGenericResult<List<ApiToken>>> getApiTokens() async {
GenericResult<List<ApiToken>> tokens; APIGenericResult<List<ApiToken>> tokens;
QueryResult<Query$GetApiTokens> response; QueryResult<Query$GetApiTokens> response;
try { try {
@ -310,7 +299,7 @@ class ServerApi extends ApiMap
if (response.hasException) { if (response.hasException) {
final message = response.exception.toString(); final message = response.exception.toString();
print(message); print(message);
tokens = GenericResult<List<ApiToken>>( tokens = APIGenericResult<List<ApiToken>>(
success: false, success: false,
data: [], data: [],
message: message, message: message,
@ -324,13 +313,13 @@ class ServerApi extends ApiMap
ApiToken.fromGraphQL(device), ApiToken.fromGraphQL(device),
) )
.toList(); .toList();
tokens = GenericResult<List<ApiToken>>( tokens = APIGenericResult<List<ApiToken>>(
success: true, success: true,
data: parsed, data: parsed,
); );
} catch (e) { } catch (e) {
print(e); print(e);
tokens = GenericResult<List<ApiToken>>( tokens = APIGenericResult<List<ApiToken>>(
success: false, success: false,
data: [], data: [],
message: e.toString(), message: e.toString(),
@ -340,8 +329,8 @@ class ServerApi extends ApiMap
return tokens; return tokens;
} }
Future<GenericResult<void>> deleteApiToken(final String name) async { Future<APIGenericResult<void>> deleteApiToken(final String name) async {
GenericResult<void> returnable; APIGenericResult<void> returnable;
QueryResult<Mutation$DeleteDeviceApiToken> response; QueryResult<Mutation$DeleteDeviceApiToken> response;
try { try {
@ -358,19 +347,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
returnable = GenericResult<void>( returnable = APIGenericResult<void>(
success: false, success: false,
data: null, data: null,
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
returnable = GenericResult<void>( returnable = APIGenericResult<void>(
success: true, success: true,
data: null, data: null,
); );
} catch (e) { } catch (e) {
print(e); print(e);
returnable = GenericResult<void>( returnable = APIGenericResult<void>(
success: false, success: false,
data: null, data: null,
message: e.toString(), message: e.toString(),
@ -380,8 +369,8 @@ class ServerApi extends ApiMap
return returnable; return returnable;
} }
Future<GenericResult<String>> createDeviceToken() async { Future<APIGenericResult<String>> createDeviceToken() async {
GenericResult<String> token; APIGenericResult<String> token;
QueryResult<Mutation$GetNewDeviceApiKey> response; QueryResult<Mutation$GetNewDeviceApiKey> response;
try { try {
@ -393,19 +382,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
token = GenericResult<String>( token = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
token = GenericResult<String>( token = APIGenericResult<String>(
success: true, success: true,
data: response.parsedData!.getNewDeviceApiKey.key!, data: response.parsedData!.getNewDeviceApiKey.key!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
token = GenericResult<String>( token = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),
@ -417,10 +406,10 @@ class ServerApi extends ApiMap
Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null; Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null;
Future<GenericResult<String>> authorizeDevice( Future<APIGenericResult<String>> authorizeDevice(
final DeviceToken deviceToken, final DeviceToken deviceToken,
) async { ) async {
GenericResult<String> token; APIGenericResult<String> token;
QueryResult<Mutation$AuthorizeWithNewDeviceApiKey> response; QueryResult<Mutation$AuthorizeWithNewDeviceApiKey> response;
try { try {
@ -442,19 +431,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
token = GenericResult<String>( token = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
token = GenericResult<String>( token = APIGenericResult<String>(
success: true, success: true,
data: response.parsedData!.authorizeWithNewDeviceApiKey.token!, data: response.parsedData!.authorizeWithNewDeviceApiKey.token!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
token = GenericResult<String>( token = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),
@ -464,10 +453,10 @@ class ServerApi extends ApiMap
return token; return token;
} }
Future<GenericResult<String>> useRecoveryToken( Future<APIGenericResult<String>> useRecoveryToken(
final DeviceToken deviceToken, final DeviceToken deviceToken,
) async { ) async {
GenericResult<String> token; APIGenericResult<String> token;
QueryResult<Mutation$UseRecoveryApiKey> response; QueryResult<Mutation$UseRecoveryApiKey> response;
try { try {
@ -489,19 +478,19 @@ class ServerApi extends ApiMap
); );
if (response.hasException) { if (response.hasException) {
print(response.exception.toString()); print(response.exception.toString());
token = GenericResult<String>( token = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: response.exception.toString(), message: response.exception.toString(),
); );
} }
token = GenericResult<String>( token = APIGenericResult<String>(
success: true, success: true,
data: response.parsedData!.useRecoveryApiKey.token!, data: response.parsedData!.useRecoveryApiKey.token!,
); );
} catch (e) { } catch (e) {
print(e); print(e);
token = GenericResult<String>( token = APIGenericResult<String>(
success: false, success: false,
data: '', data: '',
message: e.toString(), message: e.toString(),

View File

@ -59,35 +59,50 @@ class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
String get displayProviderName => 'Digital Ocean'; String get displayProviderName => 'Digital Ocean';
@override @override
Future<bool> isApiTokenValid(final String token) async { Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false; bool isValid = false;
Response? response; Response? response;
String message = '';
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
response = await client.get( response = await client.get(
'/account', '/account',
options: Options( options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'}, headers: {'Authorization': 'Bearer $token'},
), ),
); );
} catch (e) { } catch (e) {
print(e); print(e);
isValid = false; isValid = false;
message = e.toString();
} finally { } finally {
close(client); close(client);
} }
if (response != null) { if (response == null) {
if (response.statusCode == HttpStatus.ok) { return APIGenericResult(
isValid = true; data: isValid,
} else if (response.statusCode == HttpStatus.unauthorized) { success: false,
isValid = false; message: message,
} else { );
throw Exception('code: ${response.statusCode}');
}
} }
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 /// Hardcoded on their documentation and there is no pricing API at all

View File

@ -60,35 +60,50 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
String get displayProviderName => 'Hetzner'; String get displayProviderName => 'Hetzner';
@override @override
Future<bool> isApiTokenValid(final String token) async { Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
bool isValid = false; bool isValid = false;
Response? response; Response? response;
String message = '';
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
response = await client.get( response = await client.get(
'/servers', '/servers',
options: Options( options: Options(
followRedirects: false,
validateStatus: (final status) =>
status != null && (status >= 200 || status == 401),
headers: {'Authorization': 'Bearer $token'}, headers: {'Authorization': 'Bearer $token'},
), ),
); );
} catch (e) { } catch (e) {
print(e); print(e);
isValid = false; isValid = false;
message = e.toString();
} finally { } finally {
close(client); close(client);
} }
if (response != null) { if (response == null) {
if (response.statusCode == HttpStatus.ok) { return APIGenericResult(
isValid = true; data: isValid,
} else if (response.statusCode == HttpStatus.unauthorized) { success: false,
isValid = false; message: message,
} else { );
throw Exception('code: ${response.statusCode}');
}
} }
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 @override

View File

@ -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/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
@ -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_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/models/server_type.dart';
export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
class ProviderApiTokenValidation { class ProviderApiTokenValidation {
ProviderApiTokenValidation({ ProviderApiTokenValidation({
required this.length, required this.length,
@ -39,7 +42,7 @@ abstract class ServerProviderApi extends ApiMap {
required final ServerDomain domain, required final ServerDomain domain,
}); });
Future<bool> isApiTokenValid(final String token); Future<APIGenericResult<bool>> isApiTokenValid(final String token);
ProviderApiTokenValidation getApiTokenValidation(); ProviderApiTokenValidation getApiTokenValidation();
Future<List<ServerMetadataEntity>> getMetadata(final int serverId); Future<List<ServerMetadataEntity>> getMetadata(final int serverId);
Future<ServerMetrics?> getMetrics( Future<ServerMetrics?> getMetrics(

View File

@ -35,7 +35,7 @@ class ApiDevicesCubit
} }
Future<List<ApiToken>?> _getApiTokens() async { Future<List<ApiToken>?> _getApiTokens() async {
final GenericResult<List<ApiToken>> response = await api.getApiTokens(); final APIGenericResult<List<ApiToken>> response = await api.getApiTokens();
if (response.success) { if (response.success) {
return response.data; return response.data;
} else { } else {
@ -44,7 +44,8 @@ class ApiDevicesCubit
} }
Future<void> deleteDevice(final ApiToken device) async { Future<void> deleteDevice(final ApiToken device) async {
final GenericResult<void> response = await api.deleteApiToken(device.name); final APIGenericResult<void> response =
await api.deleteApiToken(device.name);
if (response.success) { if (response.success) {
emit( emit(
ApiDevicesState( ApiDevicesState(
@ -59,7 +60,7 @@ class ApiDevicesCubit
} }
Future<String?> getNewDeviceKey() async { Future<String?> getNewDeviceKey() async {
final GenericResult<String> response = await api.createDeviceToken(); final APIGenericResult<String> response = await api.createDeviceToken();
if (response.success) { if (response.success) {
return response.data; return response.data;
} else { } else {

View File

@ -29,21 +29,24 @@ class ProviderFormCubit extends FormCubit {
@override @override
FutureOr<bool> asyncValidation() async { FutureOr<bool> asyncValidation() async {
late bool isKeyValid; bool? isKeyValid;
try { try {
isKeyValid = await serverInstallationCubit isKeyValid = await serverInstallationCubit
.isServerProviderApiTokenValid(apiKey.state.value); .isServerProviderApiTokenValid(apiKey.state.value);
} catch (e) { } catch (e) {
addError(e); addError(e);
isKeyValid = false; }
if (isKeyValid == null) {
apiKey.setError('');
return false;
} }
if (!isKeyValid) { if (!isKeyValid) {
apiKey.setError('initializing.provider_bad_key_error'.tr()); apiKey.setError('initializing.provider_bad_key_error'.tr());
return false;
} }
return true; return isKeyValid;
} }
} }

View File

@ -32,7 +32,7 @@ class RecoveryKeyCubit
} }
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async { Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
final GenericResult<RecoveryKeyStatus?> response = final APIGenericResult<RecoveryKeyStatus?> response =
await api.getRecoveryTokenStatus(); await api.getRecoveryTokenStatus();
if (response.success) { if (response.success) {
return response.data; return response.data;
@ -57,7 +57,7 @@ class RecoveryKeyCubit
final DateTime? expirationDate, final DateTime? expirationDate,
final int? numberOfUses, final int? numberOfUses,
}) async { }) async {
final GenericResult<String> response = final APIGenericResult<String> response =
await api.generateRecoveryToken(expirationDate, numberOfUses); await api.generateRecoveryToken(expirationDate, numberOfUses);
if (response.success) { if (response.success) {
refresh(); refresh();

View File

@ -76,16 +76,27 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
.getDnsProvider() .getDnsProvider()
.getApiTokenValidation(); .getApiTokenValidation();
Future<bool> isServerProviderApiTokenValid( Future<bool?> isServerProviderApiTokenValid(
final String providerToken, final String providerToken,
) async => ) async {
ApiController.currentServerProviderApiFactory! final APIGenericResult<bool> apiResponse =
.getServerProvider( await ApiController.currentServerProviderApiFactory!
settings: const ServerProviderApiSettings( .getServerProvider(
isWithToken: false, settings: const ServerProviderApiSettings(
), isWithToken: false,
) ),
.isApiTokenValid(providerToken); )
.isApiTokenValid(providerToken);
if (!apiResponse.success) {
getIt<NavigationService>().showSnackBar(
'initializing.could_not_connect'.tr(),
);
return null;
}
return apiResponse.data;
}
Future<bool> isDnsProviderApiTokenValid( Future<bool> isDnsProviderApiTokenValid(
final String providerToken, final String providerToken,

View File

@ -479,7 +479,7 @@ class ServerInstallationRepository {
overrideDomain: serverDomain.domainName, overrideDomain: serverDomain.domainName,
); );
final String serverIp = await getServerIpFromDomain(serverDomain); final String serverIp = await getServerIpFromDomain(serverDomain);
final GenericResult<String> result = await serverApi.authorizeDevice( final APIGenericResult<String> result = await serverApi.authorizeDevice(
DeviceToken(device: await getDeviceName(), token: newDeviceKey), DeviceToken(device: await getDeviceName(), token: newDeviceKey),
); );
@ -516,7 +516,7 @@ class ServerInstallationRepository {
overrideDomain: serverDomain.domainName, overrideDomain: serverDomain.domainName,
); );
final String serverIp = await getServerIpFromDomain(serverDomain); final String serverIp = await getServerIpFromDomain(serverDomain);
final GenericResult<String> result = await serverApi.useRecoveryToken( final APIGenericResult<String> result = await serverApi.useRecoveryToken(
DeviceToken(device: await getDeviceName(), token: recoveryKey), DeviceToken(device: await getDeviceName(), token: recoveryKey),
); );
@ -577,9 +577,9 @@ class ServerInstallationRepository {
); );
} }
} }
final GenericResult<String> deviceAuthKey = final APIGenericResult<String> deviceAuthKey =
await serverApi.createDeviceToken(); await serverApi.createDeviceToken();
final GenericResult<String> result = await serverApi.authorizeDevice( final APIGenericResult<String> result = await serverApi.authorizeDevice(
DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data),
); );