From eddeac57d676ff944de5a4ebfef9a8dd41619c45 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sat, 21 May 2022 01:56:50 +0300 Subject: [PATCH] Implement server selection pages Co-authored-by: Inex Code --- assets/translations/en.json | 6 +- assets/translations/ru.json | 13 +- lib/config/hive_config.dart | 3 + lib/logic/api_maps/cloudflare.dart | 36 ++- lib/logic/api_maps/server.dart | 5 +- .../forms/factories/field_cubit_factory.dart | 2 +- .../recovery_device_form_cubit.dart | 14 +- .../recovery_domain_form_cubit.dart | 9 +- .../server_installation_cubit.dart | 79 ++++++ .../server_installation_repository.dart | 49 +++- .../models/json/hetzner_server_info.dart | 6 +- .../models/json/hetzner_server_info.g.dart | 3 +- lib/logic/models/server_basic_info.dart | 55 +++++ lib/ui/pages/setup/initializing.dart | 186 +++++++------- .../recovering/recover_by_new_device_key.dart | 7 +- .../recovering/recover_by_old_token.dart | 8 +- .../recovering/recover_by_recovery_key.dart | 7 +- .../recovering/recovery_confirm_server.dart | 226 ++++++++++++++---- .../recovery_hentzner_connected.dart | 4 +- .../setup/recovering/recovery_routing.dart | 14 +- 20 files changed, 545 insertions(+), 187 deletions(-) create mode 100644 lib/logic/models/server_basic_info.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 847fb35e..58b844a8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -300,6 +300,7 @@ "fallback_select_token_copy": "Copy of auth token from other version of the application.", "fallback_select_root_ssh": "Root SSH access to the server.", "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 {}:", @@ -307,7 +308,10 @@ "confirm_server": "Confirm server", "confirm_server_description": "Found your server! Confirm it is correct.", "confirm_server_accept": "Yes! That's it", - "confirm_server_decline": "Choose a different server" + "confirm_server_decline": "Choose a different server", + "choose_server": "Choose your server", + "choose_server_description": "We couldn't figure out which server your are trying to connect to.", + "no_servers": "There is no available servers on your account." }, "modals": { diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 6a898516..c2fc34bc 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -302,7 +302,18 @@ "fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.", "fallback_select_root_ssh": "Root доступ к серверу по SSH.", "fallback_select_provider_console": "Доступ к консоли хостинга.", - "fallback_select_provider_console_hint": "Например, Hetzner." + "authorization_failed": "Не удалось войти с этим ключом", + "fallback_select_provider_console_hint": "Например, Hetzner.", + "hetzner_connected": "Подключение к Hetzner", + "hetzner_connected_description": "Связь с сервером установлена. Введите токен Hetzner с доступом к {}:", + "hetzner_connected_placeholder": "Hetzner токен", + "confirm_server": "Подтвердите сервер", + "confirm_server_description": "Нашли сервер! Подтвердите, что это он:", + "confirm_server_accept": "Да, это он", + "confirm_server_decline": "Выбрать другой сервер", + "choose_server": "Выберите сервер", + "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", + "no_servers": "На вашем аккаунте нет доступных серверов." }, "modals": { "_comment": "messages in modals", diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 094c83c8..360c5e55 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -19,6 +19,9 @@ class HiveConfig { Hive.registerAdapter(BackblazeBucketAdapter()); Hive.registerAdapter(ServerVolumeAdapter()); + Hive.registerAdapter(DnsProviderAdapter()); + Hive.registerAdapter(ServerProviderAdapter()); + await Hive.openBox(BNames.appSettingsBox); var cipher = HiveAesCipher( diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index c0d7334b..9b950546 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -6,8 +6,24 @@ import 'package:selfprivacy/logic/api_maps/api_map.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; +class DomainNotFoundException implements Exception { + final String message; + DomainNotFoundException(this.message); +} + class CloudflareApi extends ApiMap { - CloudflareApi({this.hasLogger = false, this.isWithToken = true}); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + CloudflareApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); @@ -17,6 +33,10 @@ class CloudflareApi extends ApiMap { options.headers = {'Authorization': 'Bearer $token'}; } + if (customToken != null) { + options.headers = {'Authorization': 'Bearer $customToken'}; + } + if (validateStatus != null) { options.validateStatus = validateStatus!; } @@ -58,7 +78,11 @@ class CloudflareApi extends ApiMap { close(client); - return response.data['result'][0]['id']; + if (response.data['result'].isEmpty) { + throw DomainNotFoundException('No domains found'); + } else { + return response.data['result'][0]['id']; + } } Future removeSimilarRecords({ @@ -209,7 +233,7 @@ class CloudflareApi extends ApiMap { } Future> domainList() async { - var url = '$rootAddress/zones?per_page=50'; + var url = '$rootAddress/zones'; var client = await getClient(); var response = await client.get( @@ -222,10 +246,4 @@ class CloudflareApi extends ApiMap { .map((el) => el['name'] as String) .toList(); } - - @override - final bool hasLogger; - - @override - final bool isWithToken; } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 21bceee9..92bd6932 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -31,7 +31,9 @@ class ApiResponse { } class ServerApi extends ApiMap { + @override bool hasLogger; + @override bool isWithToken; String? overrideDomain; String? customToken; @@ -734,7 +736,8 @@ class ServerApi extends ApiMap { final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: code, data: response.data != null ? response.data : ''); + statusCode: code, + data: response.data["token"] != null ? response.data["token"] : ''); } Future> createDeviceToken() async { diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index ac75c2c9..a86f6445 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -52,7 +52,7 @@ class FieldCubitFactory { ); } - FieldCubit createServerDomainField() { + FieldCubit createRequiredStringField() { return FieldCubit( initalValue: '', validations: [ diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart index 4486bf14..ddc35426 100644 --- a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -5,21 +5,19 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; class RecoveryDeviceFormCubit extends FormCubit { - RecoveryDeviceFormCubit( - this.initializingCubit, final FieldCubitFactory fieldFactory) { - tokenField = fieldFactory.createServerDomainField(); + RecoveryDeviceFormCubit(this.installationCubit, + final FieldCubitFactory fieldFactory, this.recoveryMethod) { + tokenField = fieldFactory.createRequiredStringField(); super.addFields([tokenField]); } @override FutureOr onSubmit() async { - // initializingCubit.setDomain(ServerDomain( - // domainName: serverDomainField.state.value, - // provider: DnsProvider.Unknown, - // zoneId: "")); + installationCubit.tryToRecover(tokenField.state.value, recoveryMethod); } - final ServerInstallationCubit initializingCubit; + final ServerInstallationCubit installationCubit; late final FieldCubit tokenField; + final ServerRecoveryMethods recoveryMethod; } 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 921cf996..c67f3bee 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 @@ -5,22 +5,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/server.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/models/hive/server_domain.dart'; class RecoveryDomainFormCubit extends FormCubit { RecoveryDomainFormCubit( this.initializingCubit, final FieldCubitFactory fieldFactory) { - serverDomainField = fieldFactory.createServerDomainField(); + serverDomainField = fieldFactory.createRequiredStringField(); super.addFields([serverDomainField]); } @override FutureOr onSubmit() async { - initializingCubit.setDomain(ServerDomain( - domainName: serverDomainField.state.value, - provider: DnsProvider.Unknown, - zoneId: "")); + initializingCubit + .submitDomainForAccessRecovery(serverDomainField.state.value); } @override diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 7b99298d..c2849903 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -1,11 +1,14 @@ import 'dart:async'; import 'package:bloc/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/models/hive/backblaze_credential.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/server_basic_info.dart'; import '../server_installation/server_installation_repository.dart'; @@ -53,6 +56,7 @@ class ServerInstallationCubit extends Cubit { hetznerKey: hetznerKey, currentStep: RecoveryStep.ServerSelection, )); + return; } emit((state as ServerInstallationNotFinished) @@ -269,6 +273,8 @@ class ServerInstallationCubit extends Cubit { final recoveryCapabilities = await repository.getRecoveryCapabilities(serverDomain); + await repository.saveDomain(serverDomain); + emit(ServerInstallationRecovery( serverDomain: serverDomain, recoveryCapabilities: recoveryCapabilities, @@ -302,13 +308,18 @@ class ServerInstallationCubit extends Cubit { serverDomain, token, ); + await repository.saveServerDetails(serverDetails); emit(dataState.copyWith( serverDetails: serverDetails, currentStep: RecoveryStep.HetznerToken, )); } on ServerAuthorizationException { + getIt() + .showSnackBar('recovering.authorization_failed'.tr()); return; } on IpNotFoundException { + getIt() + .showSnackBar('recovering.domain_recover_error'.tr()); return; } } @@ -317,6 +328,7 @@ class ServerInstallationCubit extends Cubit { final dataState = this.state as ServerInstallationRecovery; switch (dataState.currentStep) { case RecoveryStep.Selecting: + repository.deleteDomain(); emit(ServerInstallationEmpty()); break; case RecoveryStep.RecoveryKey: @@ -327,6 +339,7 @@ class ServerInstallationCubit extends Cubit { )); break; case RecoveryStep.ServerSelection: + repository.deleteHetznerKey(); emit(dataState.copyWith( currentStep: RecoveryStep.HetznerToken, )); @@ -358,6 +371,72 @@ class ServerInstallationCubit extends Cubit { } } + Future> + getServersOnHetznerAccount() async { + final dataState = this.state as ServerInstallationRecovery; + final servers = await repository.getServersOnHetznerAccount(); + final validated = servers + .map((server) => ServerBasicInfoWithValidators.fromServerBasicInfo( + serverBasicInfo: server, + isIpValid: server.ip == dataState.serverDetails?.ip4, + isReverseDnsValid: + server.reverseDns == dataState.serverDomain?.domainName, + )); + return validated.toList(); + } + + Future setServerId(ServerBasicInfo server) async { + final dataState = this.state as ServerInstallationRecovery; + final serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + final serverDetails = ServerHostingDetails( + ip4: server.ip, + id: server.id, + createTime: server.created, + volume: ServerVolume( + id: server.volumeId, + name: "recovered_volume", + ), + apiToken: dataState.serverDetails!.apiToken, + provider: ServerProvider.Hetzner, + ); + await repository.saveDomain(serverDomain); + await repository.saveServerDetails(serverDetails); + emit(dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.CloudflareToken, + )); + } + + // Future setAndValidateCloudflareToken(String token) async { + // final dataState = this.state as ServerInstallationRecovery; + // final serverDomain = dataState.serverDomain; + // if (serverDomain == null) { + // return; + // } + // final domainId = await repository.getDomainId(serverDomain.domainName); + // } + + @override + void onChange(Change change) { + super.onChange(change); + print('================================'); + print('ServerInstallationState changed!'); + print('Current type: ${change.nextState.runtimeType}'); + print('Hetzner key: ${change.nextState.hetznerKey}'); + print('Cloudflare key: ${change.nextState.cloudFlareKey}'); + print('Domain: ${change.nextState.serverDomain}'); + print('BackblazeCredential: ${change.nextState.backblazeCredential}'); + if (change.nextState is ServerInstallationRecovery) { + print( + 'Recovery Step: ${(change.nextState as ServerInstallationRecovery).currentStep}'); + print( + 'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}'); + } + } + void clearAppConfig() { closeTimer(); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 9a0a826c..af006c6b 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -18,6 +18,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/device_token.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'; @@ -100,10 +101,13 @@ class ServerInstallationRepository { ) { if (serverDetails != null) { if (hetznerToken != null) { - if (cloudflareToken != null) { - return RecoveryStep.BackblazeToken; + if (serverDetails.provider != ServerProvider.Unknown) { + if (serverDomain.provider != DnsProvider.Unknown) { + return RecoveryStep.BackblazeToken; + } + return RecoveryStep.CloudflareToken; } - return RecoveryStep.CloudflareToken; + return RecoveryStep.ServerSelection; } return RecoveryStep.HetznerToken; } @@ -123,6 +127,20 @@ class ServerInstallationRepository { return serverDetails; } + Future getDomainId(String token, String domain) async { + var cloudflareApi = CloudflareApi( + isWithToken: false, + customToken: token, + ); + + try { + final domainId = await cloudflareApi.getZoneId(domain); + return domainId; + } on DomainNotFoundException { + return null; + } + } + Future> isDnsAddressesMatch(String? domainName, String? ip4, Map? skippedMatches) async { var addresses = [ @@ -467,6 +485,21 @@ class ServerInstallationRepository { ); } + Future> getServersOnHetznerAccount() async { + var hetznerApi = HetznerApi(); + final servers = await hetznerApi.getServers(); + return servers + .map((server) => ServerBasicInfo( + id: server.id, + name: server.name, + ip: server.publicNet.ipv4.ip, + reverseDns: server.publicNet.ipv4.reverseDns, + created: server.created, + volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0, + )) + .toList(); + } + Future saveServerDetails(ServerHostingDetails serverDetails) async { await getIt().storeServerDetails(serverDetails); } @@ -476,6 +509,11 @@ class ServerInstallationRepository { await getIt().storeHetznerKey(key); } + Future deleteHetznerKey() async { + await box.delete(BNames.hetznerKey); + getIt().init(); + } + Future saveBackblazeKey(BackblazeCredential backblazeCredential) async { await getIt().storeBackblazeCredential(backblazeCredential); } @@ -488,6 +526,11 @@ class ServerInstallationRepository { await getIt().storeServerDomain(serverDomain); } + Future deleteDomain() async { + await box.delete(BNames.serverDomain); + getIt().init(); + } + Future saveIsServerStarted(bool value) async { await box.put(BNames.isServerStarted, value); } diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 42763559..6e173181 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -8,6 +8,7 @@ class HetznerServerInfo { final String name; final ServerStatus status; final DateTime created; + final List volumes; @JsonKey(name: 'server_type') final HetznerServerTypeInfo serverType; @@ -32,17 +33,18 @@ class HetznerServerInfo { this.serverType, this.location, this.publicNet, + this.volumes, ); } @JsonSerializable() class HetznerPublicNetInfo { - final HetznerIp4 ip4; + final HetznerIp4 ipv4; static HetznerPublicNetInfo fromJson(Map json) => _$HetznerPublicNetInfoFromJson(json); - HetznerPublicNetInfo(this.ip4); + HetznerPublicNetInfo(this.ipv4); } @JsonSerializable() diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index a1b3f130..e8c21917 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -16,6 +16,7 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => json['server_type'] as Map), HetznerServerInfo.locationFromJson(json['datacenter'] as Map), HetznerPublicNetInfo.fromJson(json['public_net'] as Map), + (json['volumes'] as List).map((e) => e as int).toList(), ); const _$ServerStatusEnumMap = { @@ -33,7 +34,7 @@ const _$ServerStatusEnumMap = { HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson( Map json) => HetznerPublicNetInfo( - HetznerIp4.fromJson(json['ip4'] as Map), + HetznerIp4.fromJson(json['ipv4'] as Map), ); HetznerIp4 _$HetznerIp4FromJson(Map json) => HetznerIp4( diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart new file mode 100644 index 00000000..fcd37ed4 --- /dev/null +++ b/lib/logic/models/server_basic_info.dart @@ -0,0 +1,55 @@ +class ServerBasicInfo { + final int id; + final String name; + final String reverseDns; + final String ip; + final DateTime created; + final int volumeId; + + ServerBasicInfo({ + required this.id, + required this.name, + required this.reverseDns, + required this.ip, + required this.created, + required this.volumeId, + }); +} + +class ServerBasicInfoWithValidators extends ServerBasicInfo { + final bool isIpValid; + final bool isReverseDnsValid; + + ServerBasicInfoWithValidators({ + required int id, + required String name, + required String reverseDns, + required String ip, + required DateTime created, + required int volumeId, + required this.isIpValid, + required this.isReverseDnsValid, + }) : super( + id: id, + name: name, + reverseDns: reverseDns, + ip: ip, + created: created, + volumeId: volumeId, + ); + + ServerBasicInfoWithValidators.fromServerBasicInfo({ + required ServerBasicInfo serverBasicInfo, + required isIpValid, + required isReverseDnsValid, + }) : this( + id: serverBasicInfo.id, + name: serverBasicInfo.name, + reverseDns: serverBasicInfo.reverseDns, + ip: serverBasicInfo.ip, + created: serverBasicInfo.created, + volumeId: serverBasicInfo.volumeId, + isIpValid: isIpValid, + isReverseDnsValid: isReverseDnsValid, + ); +} diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index fc01e373..e0971ae1 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.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/rootRoute.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_routing.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -25,103 +27,105 @@ class InitializingPage extends StatelessWidget { @override Widget build(BuildContext context) { var cubit = context.watch(); - var actualInitializingPage = [ - () => _stepHetzner(cubit), - () => _stepCloudflare(cubit), - () => _stepBackblaze(cubit), - () => _stepDomain(cubit), - () => _stepUser(cubit), - () => _stepServer(cubit), - () => _stepCheck(cubit), - () => _stepCheck(cubit), - () => _stepCheck(cubit), - () => Container(child: Center(child: Text('initializing.finish'.tr()))) - ][cubit.state.progress.index](); - if (cubit is ServerInstallationRecovery) { + if (cubit.state is ServerInstallationRecovery) { return RecoveryRouting(); - } - return BlocListener( - listener: (context, state) { - if (cubit.state is ServerInstallationFinished) { - Navigator.of(context).pushReplacement(materialRoute(RootPage())); - } - }, - child: SafeArea( - child: Scaffold( - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: paddingH15V0.copyWith(top: 10, bottom: 10), - child: cubit.state.isFullyInitilized - ? SizedBox( - height: 80, - ) - : ProgressBar( - steps: [ - 'Hetzner', - 'CloudFlare', - 'Backblaze', - 'Domain', - 'User', - 'Server', - '✅ Check', - ], - activeIndex: cubit.state.porgressBar, - ), - ), - _addCard( - AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: actualInitializingPage, - ), - ), - ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - - MediaQuery.of(context).padding.top - - MediaQuery.of(context).padding.bottom - - 566, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - alignment: Alignment.center, - child: BrandButton.text( - title: cubit.state is ServerInstallationFinished - ? 'basis.close'.tr() - : 'basis.later'.tr(), - onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(RootPage()), - (predicate) => false, - ); - }, + } else { + var actualInitializingPage = [ + () => _stepHetzner(cubit), + () => _stepCloudflare(cubit), + () => _stepBackblaze(cubit), + () => _stepDomain(cubit), + () => _stepUser(cubit), + () => _stepServer(cubit), + () => _stepCheck(cubit), + () => _stepCheck(cubit), + () => _stepCheck(cubit), + () => Container(child: Center(child: Text('initializing.finish'.tr()))) + ][cubit.state.progress.index](); + + return BlocListener( + listener: (context, state) { + if (cubit.state is ServerInstallationFinished) { + Navigator.of(context).pushReplacement(materialRoute(RootPage())); + } + }, + child: SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: paddingH15V0.copyWith(top: 10, bottom: 10), + child: cubit.state.isFullyInitilized + ? SizedBox( + height: 80, + ) + : ProgressBar( + steps: [ + 'Hetzner', + 'CloudFlare', + 'Backblaze', + 'Domain', + 'User', + 'Server', + '✅ Check', + ], + activeIndex: cubit.state.porgressBar, ), - ), - (cubit.state is ServerInstallationFinished) - ? Container() - : Container( - alignment: Alignment.center, - child: BrandButton.text( - title: 'basis.connect_to_existing'.tr(), - onPressed: () { - Navigator.of(context).push( - materialRoute(RecoveryMethodSelect())); - }, - ), - ) - ], - )), - ], + ), + _addCard( + AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: actualInitializingPage, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - + MediaQuery.of(context).padding.top - + MediaQuery.of(context).padding.bottom - + 566, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: BrandButton.text( + title: cubit.state is ServerInstallationFinished + ? 'basis.close'.tr() + : 'basis.later'.tr(), + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(RootPage()), + (predicate) => false, + ); + }, + ), + ), + (cubit.state is ServerInstallationFinished) + ? Container() + : Container( + alignment: Alignment.center, + child: BrandButton.text( + title: 'basis.connect_to_existing'.tr(), + onPressed: () { + Navigator.of(context).push( + materialRoute(RecoveryRouting())); + }, + ), + ) + ], + )), + ], + ), ), ), ), - ), - ); + ); + } } Widget _stepHetzner(ServerInstallationCubit serverInstallationCubit) { diff --git a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart index 63f7e127..e93b4c4f 100644 --- a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart @@ -35,8 +35,11 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { var appConfig = context.watch(); return BlocProvider( - create: (context) => - RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.newDeviceKey, + ), child: Builder( builder: (context) { var formCubitState = context.watch().state; diff --git a/lib/ui/pages/setup/recovering/recover_by_old_token.dart b/lib/ui/pages/setup/recovering/recover_by_old_token.dart index e8a7e12b..4d36262b 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -4,7 +4,6 @@ import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_f import 'package:selfprivacy/ui/components/brand_button/FilledButton.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; @@ -44,8 +43,11 @@ class RecoverByOldToken extends StatelessWidget { var appConfig = context.watch(); return BlocProvider( - create: (context) => - RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.oldToken, + ), child: Builder( builder: (context) { var formCubitState = context.watch().state; diff --git a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart index 780c1ca4..34969b25 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -13,8 +13,11 @@ class RecoverByRecoveryKey extends StatelessWidget { var appConfig = context.watch(); return BlocProvider( - create: (context) => - RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)), + create: (context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.recoveryKey, + ), child: Builder( builder: (context) { var formCubitState = context.watch().state; diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart index 262dfa6a..b83f00f7 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -1,62 +1,188 @@ -import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/ui/components/brand_button/FilledButton.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_hero_screen/brand_hero_screen.dart'; -class RecoveryConfirmServer extends StatelessWidget { +class RecoveryConfirmServer extends StatefulWidget { + const RecoveryConfirmServer({Key? key}) : super(key: key); + + @override + _RecoveryConfirmServerState createState() => _RecoveryConfirmServerState(); +} + +class _RecoveryConfirmServerState extends State { + bool _isExtended = false; + + bool _isServerFound(List servers) { + return servers + .where((server) => server.isIpValid && server.isReverseDnsValid) + .length == + 1; + } + + ServerBasicInfoWithValidators _firstValidServer( + List servers) { + return servers + .where((server) => server.isIpValid && server.isReverseDnsValid) + .first; + } + @override Widget build(BuildContext context) { - var serverInstallation = context.watch(); - - return Builder( - builder: (context) { - var formCubitState = context.watch().state; - - return BlocListener( - listener: (context, state) { - if (state is ServerInstallationRecovery) { - if (state.currentStep == RecoveryStep.Selecting) { - if (state.recoveryCapabilities == - ServerRecoveryCapabilities.none) { - context - .read() - .setCustomError("recovering.domain_recover_error".tr()); - } - } + return BrandHeroScreen( + heroTitle: _isExtended + ? "recovering.choose_server".tr() + : "recovering.confirm_server".tr(), + heroSubtitle: _isExtended + ? "recovering.choose_server_description".tr() + : "recovering.confirm_server_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder>( + future: context + .read() + .getServersOnHetznerAccount(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final servers = snapshot.data; + return Column( + children: [ + if (servers != null && servers.isNotEmpty) + Column( + children: [ + if (servers.length == 1 || + (!_isExtended && _isServerFound(servers))) + _ConfirmServer(context, _firstValidServer(servers), + servers.length > 1), + if (servers.length > 1 && + (_isExtended || !_isServerFound(servers))) + _ChooseServer(context, servers), + ], + ), + if (servers?.isEmpty ?? true) + Center( + child: Text( + "recovering.no_servers".tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ], + ); + } else { + return Center( + child: CircularProgressIndicator(), + ); } }, - child: BrandHeroScreen( - heroTitle: "recovering.recovery_main_header".tr(), - heroSubtitle: "recovering.domain_recovery_description".tr(), - hasBackButton: true, - hasFlashButton: false, - onBackButtonPressed: - serverInstallation is ServerInstallationRecovery - ? () => serverInstallation.clearAppConfig() - : null, - children: [ - CubitFormTextField( - formFieldCubit: - context.read().serverDomainField, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: "recovering.domain_recover_placeholder".tr(), - ), - ), - SizedBox(height: 16), - FilledButton( - title: "more.continue".tr(), - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - ) - ], - ), - ); - }, + ) + ], ); } + + Widget _ConfirmServer( + BuildContext context, + ServerBasicInfoWithValidators server, + bool showMoreServersButton, + ) { + return Container( + child: Column( + children: [ + _ServerCard( + context: context, + server: server, + ), + SizedBox(height: 16), + FilledButton( + title: "recovering.confirm_server_accept".tr(), + onPressed: () => _showConfirmationDialog(context, server), + ), + SizedBox(height: 16), + if (showMoreServersButton) + BrandButton.text( + title: 'recovering.confirm_server_decline'.tr(), + onPressed: () => setState(() => _isExtended = true), + ), + ], + ), + ); + } + + Widget _ChooseServer( + BuildContext context, List servers) { + return Column( + children: [ + for (final server in servers) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: _ServerCard( + context: context, + server: server, + onTap: () => _showConfirmationDialog(context, server), + ), + ), + ], + ); + } + + Widget _ServerCard( + {required BuildContext context, + required ServerBasicInfoWithValidators server, + VoidCallback? onTap}) { + return BrandCards.filled( + child: ListTile( + onTap: onTap, + title: Text(server.name), + leading: Icon(Icons.dns), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(server.isReverseDnsValid ? Icons.check : Icons.close), + Text('rDNS: ${server.reverseDns}'), + ], + ), + Row( + children: [ + Icon(server.isIpValid ? Icons.check : Icons.close), + Text('IP: ${server.ip}'), + ], + ), + ], + ), + ), + ); + } + + _showConfirmationDialog( + BuildContext context, ServerBasicInfoWithValidators server) => + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('ssh.delete'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text("WOW DIALOGUE TEXT WOW :)"), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context)..pop(); + }, + ), + ], + ); + }, + ); } diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart index bfd8fd76..f49eb982 100644 --- a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -23,7 +23,9 @@ class RecoveryHetznerConnected extends StatelessWidget { return BrandHeroScreen( heroTitle: "recovering.hetzner_connected".tr(), - heroSubtitle: "recovering.hetzner_connected_description".tr(), + heroSubtitle: "recovering.hetzner_connected_description".tr(args: [ + appConfig.state.serverDomain?.domainName ?? "your domain" + ]), hasBackButton: true, hasFlashButton: false, children: [ diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 7237ed97..d3a8a0b9 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -9,20 +9,22 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.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_method_select.dart'; class RecoveryRouting extends StatelessWidget { @override Widget build(BuildContext context) { - var serverInstallation = context.watch(); + var serverInstallation = context.watch().state; - StatelessWidget currentPage = SelectDomainToRecover(); + Widget currentPage = SelectDomainToRecover(); if (serverInstallation is ServerInstallationRecovery) { - final state = (serverInstallation as ServerInstallationRecovery); - switch (state.currentStep) { + switch (serverInstallation.currentStep) { case RecoveryStep.Selecting: - if (state.recoveryCapabilities != ServerRecoveryCapabilities.none) + if (serverInstallation.recoveryCapabilities != + ServerRecoveryCapabilities.none) currentPage = RecoveryMethodSelect(); break; case RecoveryStep.RecoveryKey: @@ -35,8 +37,10 @@ class RecoveryRouting extends StatelessWidget { currentPage = RecoverByOldToken(); break; case RecoveryStep.HetznerToken: + currentPage = RecoveryHetznerConnected(); break; case RecoveryStep.ServerSelection: + currentPage = RecoveryConfirmServer(); break; case RecoveryStep.CloudflareToken: break;