diff --git a/assets/translations/en.json b/assets/translations/en.json index 88889d8d..847fb35e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -274,7 +274,6 @@ "15": "Server created. DNS checks and server boot in progress...", "16": "Until the next check: ", "17": "Check", - "18": "How to obtain Hetzner API Token", "19": "1 Go via this link ", "20": "\n", "21": "One more restart to apply your security certificates.", @@ -301,7 +300,15 @@ "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.", - "fallback_select_provider_console_hint": "For example: Hetzner." + "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", + "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" + }, "modals": { "_comment": "messages in modals", diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 2256f9e5..cbcacf80 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -55,19 +55,6 @@ class HetznerApi extends ApiMap { } } - Future isFreeToCreate() async { - var client = await getClient(); - - Response serversReponse = await client.get('/servers'); - List servers = serversReponse.data['servers']; - var server = servers.firstWhere( - (el) => el['name'] == 'selfprivacy-server', - orElse: null, - ); - client.close(); - return server == null; - } - Future createVolume() async { var client = await getClient(); Response dbCreateResponse = await client.post( @@ -237,6 +224,16 @@ class HetznerApi extends ApiMap { return HetznerServerInfo.fromJson(response.data!['server']); } + Future> getServers() async { + var client = await getClient(); + Response response = await client.get('/servers'); + close(client); + + return (response.data!['servers'] as List) + .map((e) => HetznerServerInfo.fromJson(e)) + .toList(); + } + Future createReverseDns({ required String ip4, required String domainName, diff --git a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index f3554ef4..fc9062e7 100644 --- a/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -6,7 +6,7 @@ import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:easy_localization/easy_localization.dart'; class BackblazeFormCubit extends FormCubit { - BackblazeFormCubit(this.serverSetupCubit) { + BackblazeFormCubit(this.serverInstallationCubit) { //var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); keyId = FieldCubit( initalValue: '', @@ -27,13 +27,13 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr onSubmit() async { - serverSetupCubit.setBackblazeKey( + serverInstallationCubit.setBackblazeKey( keyId.state.value, applicationKey.state.value, ); } - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit keyId; late final FieldCubit applicationKey; diff --git a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 07c46c74..fe595927 100644 --- a/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -4,9 +4,9 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class DomainSetupCubit extends Cubit { - DomainSetupCubit(this.serverSetupCubit) : super(Initial()); + DomainSetupCubit(this.serverInstallationCubit) : super(Initial()); - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; Future load() async { emit(Loading(LoadingTypes.loadingDomain)); @@ -42,7 +42,7 @@ class DomainSetupCubit extends Cubit { provider: DnsProvider.Cloudflare, ); - serverSetupCubit.setDomain(domain); + serverInstallationCubit.setDomain(domain); emit(DomainSet()); } } diff --git a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index 6871942e..0ac97e84 100644 --- a/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { - HetznerFormCubit(this.serverSetupCubit) { + HetznerFormCubit(this.serverInstallationCubit) { var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); apiKey = FieldCubit( initalValue: '', @@ -24,10 +24,10 @@ class HetznerFormCubit extends FormCubit { @override FutureOr onSubmit() async { - serverSetupCubit.setHetznerKey(apiKey.state.value); + serverInstallationCubit.setHetznerKey(apiKey.state.value); } - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit apiKey; diff --git a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 81ccd88d..6e3e5c3d 100644 --- a/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -7,7 +7,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( - this.serverSetupCubit, final FieldCubitFactory fieldFactory) { + this.serverInstallationCubit, final FieldCubitFactory fieldFactory) { userName = fieldFactory.createUserLoginField(); password = fieldFactory.createUserPasswordField(); @@ -22,10 +22,10 @@ class RootUserFormCubit extends FormCubit { login: userName.state.value, password: password.state.value, ); - serverSetupCubit.setRootUser(user); + serverInstallationCubit.setRootUser(user); } - final ServerInstallationCubit serverSetupCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit userName; late final FieldCubit password; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 33d360fe..7b99298d 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -47,6 +47,14 @@ class ServerInstallationCubit extends Cubit { void setHetznerKey(String hetznerKey) async { await repository.saveHetznerKey(hetznerKey); + + if (state is ServerInstallationRecovery) { + emit((state as ServerInstallationRecovery).copyWith( + hetznerKey: hetznerKey, + currentStep: RecoveryStep.ServerSelection, + )); + } + emit((state as ServerInstallationNotFinished) .copyWith(hetznerKey: hetznerKey)); } @@ -318,6 +326,11 @@ class ServerInstallationCubit extends Cubit { currentStep: RecoveryStep.Selecting, )); break; + case RecoveryStep.ServerSelection: + emit(dataState.copyWith( + currentStep: RecoveryStep.HetznerToken, + )); + break; // We won't revert steps after client is authorized default: break; diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart index a318dd18..b376914d 100644 --- a/lib/logic/cubit/server_installation/server_installation_state.dart +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -261,6 +261,7 @@ enum RecoveryStep { NewDeviceKey, OldToken, HetznerToken, + ServerSelection, CloudflareToken, BackblazeToken, } diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index 98af1c3e..42763559 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -15,6 +15,9 @@ class HetznerServerInfo { @JsonKey(name: 'datacenter', fromJson: HetznerServerInfo.locationFromJson) final HetznerLocation location; + @JsonKey(name: 'public_net') + final HetznerPublicNetInfo publicNet; + static HetznerLocation locationFromJson(Map json) => HetznerLocation.fromJson(json['location']); @@ -28,9 +31,34 @@ class HetznerServerInfo { this.created, this.serverType, this.location, + this.publicNet, ); } +@JsonSerializable() +class HetznerPublicNetInfo { + final HetznerIp4 ip4; + + static HetznerPublicNetInfo fromJson(Map json) => + _$HetznerPublicNetInfoFromJson(json); + + HetznerPublicNetInfo(this.ip4); +} + +@JsonSerializable() +class HetznerIp4 { + final bool blocked; + @JsonKey(name: 'dns_ptr') + final String reverseDns; + final int id; + final String ip; + + static HetznerIp4 fromJson(Map json) => + _$HetznerIp4FromJson(json); + + HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns); +} + enum ServerStatus { running, initializing, diff --git a/lib/logic/models/json/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart index 73e6be68..a1b3f130 100644 --- a/lib/logic/models/json/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -15,6 +15,7 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => HetznerServerTypeInfo.fromJson( json['server_type'] as Map), HetznerServerInfo.locationFromJson(json['datacenter'] as Map), + HetznerPublicNetInfo.fromJson(json['public_net'] as Map), ); const _$ServerStatusEnumMap = { @@ -29,6 +30,19 @@ const _$ServerStatusEnumMap = { ServerStatus.unknown: 'unknown', }; +HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson( + Map json) => + HetznerPublicNetInfo( + HetznerIp4.fromJson(json['ip4'] as Map), + ); + +HetznerIp4 _$HetznerIp4FromJson(Map json) => HetznerIp4( + json['id'] as int, + json['ip'] as String, + json['blocked'] as bool, + json['dns_ptr'] as String, + ); + HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( Map json) => HetznerServerTypeInfo( diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 67ad09af..777663cf 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -23,6 +23,9 @@ class BrandCards { static Widget outlined({required Widget child}) => _OutlinedCard( child: child, ); + static Widget filled({required Widget child}) => _FilledCard( + child: child, + ); } class _BrandCard extends StatelessWidget { @@ -78,6 +81,27 @@ class _OutlinedCard extends StatelessWidget { } } +class _FilledCard extends StatelessWidget { + const _FilledCard({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + @override + Widget build(BuildContext context) { + return Card( + elevation: 0.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.antiAlias, + child: child, + color: Theme.of(context).colorScheme.surfaceVariant, + ); + } +} + final bigShadow = [ BoxShadow( offset: Offset(0, 4), diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart index 81630b63..fc01e373 100644 --- a/lib/ui/pages/setup/initializing.dart +++ b/lib/ui/pages/setup/initializing.dart @@ -124,9 +124,9 @@ class InitializingPage extends StatelessWidget { ); } - Widget _stepHetzner(ServerInstallationCubit initializingCubit) { + Widget _stepHetzner(ServerInstallationCubit serverInstallationCubit) { return BlocProvider( - create: (context) => HetznerFormCubit(initializingCubit), + create: (context) => HetznerFormCubit(serverInstallationCubit), child: Builder(builder: (context) { var formCubitState = context.watch().state; return Column( 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 522494fd..e8a7e12b 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -27,8 +27,9 @@ class RecoverByOldTokenInstruction extends StatelessWidget { SizedBox(height: 16), FilledButton( title: "recovering.method_device_button".tr(), - onPressed: () => - Navigator.of(context).push(materialRoute(RecoverByOldToken())), + onPressed: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.oldToken), ) ], ); diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart new file mode 100644 index 00000000..262dfa6a --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -0,0 +1,62 @@ +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/ui/components/brand_button/FilledButton.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class RecoveryConfirmServer extends StatelessWidget { + @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()); + } + } + } + }, + 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(), + ) + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart new file mode 100644 index 00000000..bfd8fd76 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -0,0 +1,70 @@ +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/hetzner_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.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_hero_screen/brand_hero_screen.dart'; +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 { + @override + Widget build(BuildContext context) { + var appConfig = context.watch(); + + return BlocProvider( + create: (context) => HetznerFormCubit(appConfig), + child: Builder( + builder: (context) { + var formCubitState = context.watch().state; + + return BrandHeroScreen( + heroTitle: "recovering.hetzner_connected".tr(), + heroSubtitle: "recovering.hetzner_connected_description".tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "recovering.hetzner_connected_placeholder".tr(), + ), + ), + SizedBox(height: 16), + FilledButton( + title: "more.continue".tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ), + SizedBox(height: 16), + BrandButton.text( + title: 'initializing.how'.tr(), + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: BrandMarkdown( + fileName: 'how_hetzner', + ), + ), + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 5db32eef..7237ed97 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -6,6 +6,7 @@ import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart'; 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/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_method_select.dart'; @@ -31,9 +32,12 @@ class RecoveryRouting extends StatelessWidget { currentPage = RecoverByNewDeviceKeyInstruction(); break; case RecoveryStep.OldToken: + currentPage = RecoverByOldToken(); break; case RecoveryStep.HetznerToken: break; + case RecoveryStep.ServerSelection: + break; case RecoveryStep.CloudflareToken: break; case RecoveryStep.BackblazeToken: