mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-02-04 07:20:39 +00:00
Implement server selection pages
Co-authored-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
parent
eaa1ba143c
commit
eddeac57d6
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -52,7 +52,7 @@ class FieldCubitFactory {
|
|||
);
|
||||
}
|
||||
|
||||
FieldCubit<String> createServerDomainField() {
|
||||
FieldCubit<String> createRequiredStringField() {
|
||||
return FieldCubit(
|
||||
initalValue: '',
|
||||
validations: [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
55
lib/logic/models/server_basic_info.dart
Normal file
55
lib/logic/models/server_basic_info.dart
Normal 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,
|
||||
);
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue