Implement pages for server confirmation on restoring access

Co-authored-by: Inex Code <inex.code@selfprivacy.org>
pull/90/head
NaiJi ✨ 2022-05-19 20:43:25 +03:00
parent 6fd7f9400d
commit eaa1ba143c
16 changed files with 252 additions and 31 deletions

View File

@ -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",

View File

@ -55,19 +55,6 @@ class HetznerApi extends ApiMap {
}
}
Future<bool> 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<ServerVolume> 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<List<HetznerServerInfo>> 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<void> createReverseDns({
required String ip4,
required String domainName,

View File

@ -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<void> onSubmit() async {
serverSetupCubit.setBackblazeKey(
serverInstallationCubit.setBackblazeKey(
keyId.state.value,
applicationKey.state.value,
);
}
final ServerInstallationCubit serverSetupCubit;
final ServerInstallationCubit serverInstallationCubit;
late final FieldCubit<String> keyId;
late final FieldCubit<String> applicationKey;

View File

@ -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<DomainSetupState> {
DomainSetupCubit(this.serverSetupCubit) : super(Initial());
DomainSetupCubit(this.serverInstallationCubit) : super(Initial());
final ServerInstallationCubit serverSetupCubit;
final ServerInstallationCubit serverInstallationCubit;
Future<void> load() async {
emit(Loading(LoadingTypes.loadingDomain));
@ -42,7 +42,7 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
provider: DnsProvider.Cloudflare,
);
serverSetupCubit.setDomain(domain);
serverInstallationCubit.setDomain(domain);
emit(DomainSet());
}
}

View File

@ -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<void> onSubmit() async {
serverSetupCubit.setHetznerKey(apiKey.state.value);
serverInstallationCubit.setHetznerKey(apiKey.state.value);
}
final ServerInstallationCubit serverSetupCubit;
final ServerInstallationCubit serverInstallationCubit;
late final FieldCubit<String> apiKey;

View File

@ -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<String> userName;
late final FieldCubit<String> password;

View File

@ -47,6 +47,14 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
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<ServerInstallationState> {
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;

View File

@ -261,6 +261,7 @@ enum RecoveryStep {
NewDeviceKey,
OldToken,
HetznerToken,
ServerSelection,
CloudflareToken,
BackblazeToken,
}

View File

@ -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<String, dynamic> 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<String, dynamic> json) =>
_$HetznerIp4FromJson(json);
HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns);
}
enum ServerStatus {
running,
initializing,

View File

@ -15,6 +15,7 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
HetznerServerTypeInfo.fromJson(
json['server_type'] as Map<String, dynamic>),
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
HetznerPublicNetInfo.fromJson(json['public_net'] as Map<String, dynamic>),
);
const _$ServerStatusEnumMap = {
@ -29,6 +30,19 @@ const _$ServerStatusEnumMap = {
ServerStatus.unknown: 'unknown',
};
HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson(
Map<String, dynamic> json) =>
HetznerPublicNetInfo(
HetznerIp4.fromJson(json['ip4'] as Map<String, dynamic>),
);
HetznerIp4 _$HetznerIp4FromJson(Map<String, dynamic> json) => HetznerIp4(
json['id'] as int,
json['ip'] as String,
json['blocked'] as bool,
json['dns_ptr'] as String,
);
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
Map<String, dynamic> json) =>
HetznerServerTypeInfo(

View File

@ -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),

View File

@ -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<HetznerFormCubit>().state;
return Column(

View File

@ -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<ServerInstallationCubit>()
.selectRecoveryMethod(ServerRecoveryMethods.oldToken),
)
],
);

View File

@ -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<ServerInstallationCubit>();
return Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDomainFormCubit>().state;
return BlocListener<ServerInstallationCubit, ServerInstallationState>(
listener: (context, state) {
if (state is ServerInstallationRecovery) {
if (state.currentStep == RecoveryStep.Selecting) {
if (state.recoveryCapabilities ==
ServerRecoveryCapabilities.none) {
context
.read<RecoveryDomainFormCubit>()
.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<RecoveryDomainFormCubit>().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<RecoveryDomainFormCubit>().trySubmit(),
)
],
),
);
},
);
}
}

View File

@ -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<ServerInstallationCubit>();
return BlocProvider(
create: (context) => HetznerFormCubit(appConfig),
child: Builder(
builder: (context) {
var formCubitState = context.watch<HetznerFormCubit>().state;
return BrandHeroScreen(
heroTitle: "recovering.hetzner_connected".tr(),
heroSubtitle: "recovering.hetzner_connected_description".tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
CubitFormTextField(
formFieldCubit: context.read<HetznerFormCubit>().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<HetznerFormCubit>().trySubmit(),
),
SizedBox(height: 16),
BrandButton.text(
title: 'initializing.how'.tr(),
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return BrandBottomSheet(
isExpended: true,
child: Padding(
padding: paddingH15V0,
child: BrandMarkdown(
fileName: 'how_hetzner',
),
),
);
},
),
),
],
);
},
),
);
}
}

View File

@ -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: