Implement server selection pages

Co-authored-by: Inex Code <inex.code@selfprivacy.org>
pull/90/head
NaiJi ✨ 2022-05-21 01:56:50 +03:00
parent eaa1ba143c
commit eddeac57d6
20 changed files with 545 additions and 187 deletions

View File

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

View File

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

View File

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

View File

@ -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<void> removeSimilarRecords({
@ -209,7 +233,7 @@ class CloudflareApi extends ApiMap {
}
Future<List<String>> 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<String>((el) => el['name'] as String)
.toList();
}
@override
final bool hasLogger;
@override
final bool isWithToken;
}

View File

@ -31,7 +31,9 @@ class ApiResponse<D> {
}
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<ApiResponse<String>> createDeviceToken() async {

View File

@ -52,7 +52,7 @@ class FieldCubitFactory {
);
}
FieldCubit<String> createServerDomainField() {
FieldCubit<String> createRequiredStringField() {
return FieldCubit(
initalValue: '',
validations: [

View File

@ -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<void> 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<String> tokenField;
final ServerRecoveryMethods recoveryMethod;
}

View File

@ -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<void> onSubmit() async {
initializingCubit.setDomain(ServerDomain(
domainName: serverDomainField.state.value,
provider: DnsProvider.Unknown,
zoneId: ""));
initializingCubit
.submitDomainForAccessRecovery(serverDomainField.state.value);
}
@override

View File

@ -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<ServerInstallationState> {
hetznerKey: hetznerKey,
currentStep: RecoveryStep.ServerSelection,
));
return;
}
emit((state as ServerInstallationNotFinished)
@ -269,6 +273,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
final recoveryCapabilities =
await repository.getRecoveryCapabilities(serverDomain);
await repository.saveDomain(serverDomain);
emit(ServerInstallationRecovery(
serverDomain: serverDomain,
recoveryCapabilities: recoveryCapabilities,
@ -302,13 +308,18 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
serverDomain,
token,
);
await repository.saveServerDetails(serverDetails);
emit(dataState.copyWith(
serverDetails: serverDetails,
currentStep: RecoveryStep.HetznerToken,
));
} on ServerAuthorizationException {
getIt<NavigationService>()
.showSnackBar('recovering.authorization_failed'.tr());
return;
} on IpNotFoundException {
getIt<NavigationService>()
.showSnackBar('recovering.domain_recover_error'.tr());
return;
}
}
@ -317,6 +328,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
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<ServerInstallationState> {
));
break;
case RecoveryStep.ServerSelection:
repository.deleteHetznerKey();
emit(dataState.copyWith(
currentStep: RecoveryStep.HetznerToken,
));
@ -358,6 +371,72 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
}
}
Future<List<ServerBasicInfoWithValidators>>
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<void> 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<void> 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<ServerInstallationState> 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();

View File

@ -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<String?> 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<Map<String, bool>> isDnsAddressesMatch(String? domainName, String? ip4,
Map<String, bool>? skippedMatches) async {
var addresses = <String>[
@ -467,6 +485,21 @@ class ServerInstallationRepository {
);
}
Future<List<ServerBasicInfo>> 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<void> saveServerDetails(ServerHostingDetails serverDetails) async {
await getIt<ApiConfigModel>().storeServerDetails(serverDetails);
}
@ -476,6 +509,11 @@ class ServerInstallationRepository {
await getIt<ApiConfigModel>().storeHetznerKey(key);
}
Future<void> deleteHetznerKey() async {
await box.delete(BNames.hetznerKey);
getIt<ApiConfigModel>().init();
}
Future<void> saveBackblazeKey(BackblazeCredential backblazeCredential) async {
await getIt<ApiConfigModel>().storeBackblazeCredential(backblazeCredential);
}
@ -488,6 +526,11 @@ class ServerInstallationRepository {
await getIt<ApiConfigModel>().storeServerDomain(serverDomain);
}
Future<void> deleteDomain() async {
await box.delete(BNames.serverDomain);
getIt<ApiConfigModel>().init();
}
Future<void> saveIsServerStarted(bool value) async {
await box.put(BNames.isServerStarted, value);
}

View File

@ -8,6 +8,7 @@ class HetznerServerInfo {
final String name;
final ServerStatus status;
final DateTime created;
final List<int> 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<String, dynamic> json) =>
_$HetznerPublicNetInfoFromJson(json);
HetznerPublicNetInfo(this.ip4);
HetznerPublicNetInfo(this.ipv4);
}
@JsonSerializable()

View File

@ -16,6 +16,7 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) =>
json['server_type'] as Map<String, dynamic>),
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
HetznerPublicNetInfo.fromJson(json['public_net'] as Map<String, dynamic>),
(json['volumes'] as List<dynamic>).map((e) => e as int).toList(),
);
const _$ServerStatusEnumMap = {
@ -33,7 +34,7 @@ const _$ServerStatusEnumMap = {
HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson(
Map<String, dynamic> json) =>
HetznerPublicNetInfo(
HetznerIp4.fromJson(json['ip4'] as Map<String, dynamic>),
HetznerIp4.fromJson(json['ipv4'] as Map<String, dynamic>),
);
HetznerIp4 _$HetznerIp4FromJson(Map<String, dynamic> json) => HetznerIp4(

View File

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

View File

@ -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<ServerInstallationCubit>();
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<ServerInstallationCubit, ServerInstallationState>(
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<ServerInstallationCubit, ServerInstallationState>(
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) {

View File

@ -35,8 +35,11 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget {
var appConfig = context.watch<ServerInstallationCubit>();
return BlocProvider(
create: (context) =>
RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)),
create: (context) => RecoveryDeviceFormCubit(
appConfig,
FieldCubitFactory(context),
ServerRecoveryMethods.newDeviceKey,
),
child: Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state;

View File

@ -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<ServerInstallationCubit>();
return BlocProvider(
create: (context) =>
RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)),
create: (context) => RecoveryDeviceFormCubit(
appConfig,
FieldCubitFactory(context),
ServerRecoveryMethods.oldToken,
),
child: Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state;

View File

@ -13,8 +13,11 @@ class RecoverByRecoveryKey extends StatelessWidget {
var appConfig = context.watch<ServerInstallationCubit>();
return BlocProvider(
create: (context) =>
RecoveryDeviceFormCubit(appConfig, FieldCubitFactory(context)),
create: (context) => RecoveryDeviceFormCubit(
appConfig,
FieldCubitFactory(context),
ServerRecoveryMethods.recoveryKey,
),
child: Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state;

View File

@ -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<RecoveryConfirmServer> {
bool _isExtended = false;
bool _isServerFound(List<ServerBasicInfoWithValidators> servers) {
return servers
.where((server) => server.isIpValid && server.isReverseDnsValid)
.length ==
1;
}
ServerBasicInfoWithValidators _firstValidServer(
List<ServerBasicInfoWithValidators> servers) {
return servers
.where((server) => server.isIpValid && server.isReverseDnsValid)
.first;
}
@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());
}
}
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<List<ServerBasicInfoWithValidators>>(
future: context
.read<ServerInstallationCubit>()
.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<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(),
)
],
),
);
},
)
],
);
}
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<ServerBasicInfoWithValidators> 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: <Widget>[
Text("WOW DIALOGUE TEXT WOW :)"),
],
),
),
actions: <Widget>[
TextButton(
child: Text('basis.cancel'.tr()),
onPressed: () {
Navigator.of(context)..pop();
},
),
],
);
},
);
}

View File

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

View File

@ -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<ServerInstallationCubit>();
var serverInstallation = context.watch<ServerInstallationCubit>().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;