Hot bug fixing of recovery flow

Co-authored-by: Inex Code <inex.code@selfprivacy.org>
pull/90/head
NaiJi ✨ 2022-05-24 20:45:13 +03:00
parent a096e7e732
commit edce25ec55
19 changed files with 220 additions and 75 deletions

View File

@ -23,7 +23,7 @@ linter:
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -313,11 +313,25 @@
"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.",
"domain_not_available_on_token": "Selected domain is not available on this token.",
"modal_confirmation_title": "Is it really your server?",
"modal_confirmation_description": "If you connect to a wrong server you may lose all your data.",
"modal_confirmation_dns_valid": "Reverse DNS is valid",
"modal_confirmation_dns_invalid": "Reverse DNS points to another domain",
"modal_confirmation_ip_valid": "IP is the same as in DNS record",
"modal_confirmation_ip_invalid": "IP is not the same as in DNS record",
"confirm_cloudflare": "Connect to CloudFlare",
"confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:",
"confirm_backblze": "Connect to Backblaze",
"confirm_backblaze": "Connect to Backblaze",
"confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:"
},
"recovery_key": {
"key_main_header": "Recovery key",
"key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.",
"key_amount_toggle": "Limit by number of uses",
"key_amount_field_title": "Max number of uses",
"key_duedate_toggle": "Limit by time",
"key_duedate_field_title": "Due date of expiration",
"key_receive_button": "Receive key"
},
"modals": {
"_comment": "messages in modals",
@ -331,7 +345,8 @@
"8": "Remove task",
"9": "Reboot",
"10": "You cannot use this API for domains with such TLD.",
"yes": "Yes"
"yes": "Yes",
"no": "No"
},
"timer": {
"sec": "{} sec"

View File

@ -315,11 +315,22 @@
"choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.",
"no_servers": "На вашем аккаунте нет доступных серверов.",
"domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.",
"modal_confirmation_title": "Это действительно ваш сервер?",
"modal_confirmation_description": "Подключение к неправильному серверу может привести к деструктивным последствиям.",
"confirm_cloudflare": "Подключение к Cloudflare",
"confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:",
"confirm_backblze": "Подключение к Backblaze",
"confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:"
},
"recovery_key": {
"key_main_header": "Ключ восстановления",
"key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.",
"key_amount_toggle": "Ограничить использования",
"key_amount_field_title": "Макс. кол-во использований",
"key_duedate_toggle": "Ограничить срок использования",
"key_duedate_field_title": "Дата окончания срока",
"key_receive_button": "Получить ключ"
},
"modals": {
"_comment": "messages in modals",
"1": "Сервер с таким именем уже существует",
@ -332,7 +343,8 @@
"8": "Удалить задачу",
"9": "Перезагрузить",
"10": "API не поддерживает домены с таким TLD.",
"yes": "Да"
"yes": "Да",
"no": "Нет"
},
"timer": {
"sec": "{} сек"

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
@ -22,6 +23,7 @@ class BlocAndProviderConfig extends StatelessWidget {
var servicesCubit = ServicesCubit(serverInstallationCubit);
var backupsCubit = BackupsCubit(serverInstallationCubit);
var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
return MultiProvider(
providers: [
BlocProvider(
@ -36,6 +38,7 @@ class BlocAndProviderConfig extends StatelessWidget {
BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
BlocProvider(create: (_) => dnsRecordsCubit..load()),
BlocProvider(create: (_) => recoveryKeyCubit..load()),
BlocProvider(
create: (_) =>
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),

View File

@ -628,7 +628,7 @@ class ServerApi extends ApiMap {
.replaceAll('"', '');
}
Future<ApiResponse<RecoveryKeyStatus>> getRecoveryTokenStatus() async {
Future<ApiResponse<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
Response response;
var client = await getClient();
@ -649,7 +649,7 @@ class ServerApi extends ApiMap {
return ApiResponse(
statusCode: code,
data: response.data != null
? response.data.fromJson(response.data)
? RecoveryKeyStatus.fromJson(response.data)
: null);
}

View File

@ -51,6 +51,7 @@ class BackblazeFormCubit extends FormCubit {
isKeyValid = await apiClient.isValid(encodedApiKey);
} catch (e) {
addError(e);
isKeyValid = false;
}
if (!isKeyValid) {

View File

@ -26,7 +26,7 @@ class RecoveryKeyCubit
}
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
final ApiResponse<RecoveryKeyStatus> response =
final ApiResponse<RecoveryKeyStatus?> response =
await api.getRecoveryTokenStatus();
if (response.isSuccess) {
return response.data;

View File

@ -64,6 +64,10 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
}
void setCloudflareKey(String cloudFlareKey) async {
if (state is ServerInstallationRecovery) {
setAndValidateCloudflareToken(cloudFlareKey);
return;
}
await repository.saveCloudFlareKey(cloudFlareKey);
emit((state as ServerInstallationNotFinished)
.copyWith(cloudFlareKey: cloudFlareKey));
@ -431,12 +435,19 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
.showSnackBar('recovering.domain_not_available_on_token'.tr());
return;
}
await repository.saveDomain(ServerDomain(
domainName: serverDomain.domainName,
zoneId: zoneId,
provider: DnsProvider.Cloudflare,
));
await repository.saveCloudFlareKey(token);
emit(dataState.copyWith(
serverDomain: ServerDomain(
domainName: serverDomain.domainName,
zoneId: zoneId,
provider: DnsProvider.Cloudflare,
),
cloudFlareKey: token,
currentStep: RecoveryStep.BackblazeToken,
));
}

View File

@ -88,8 +88,9 @@ class MyApp extends StatelessWidget {
: RootPage(),
builder: (BuildContext context, Widget? widget) {
Widget error = Text('...rendering error...');
if (widget is Scaffold || widget is Navigator)
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder =
(FlutterErrorDetails errorDetails) => error;
return widget!;

View File

@ -0,0 +1,14 @@
/*import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
class RecoveryKey extends StatefulWidget {
const RecoveryKey({Key? key}) : super(key: key);
@override
State<RecoveryKey> createState() => _RecoveryKeyState();
}
class _RecoveryKeyState extends State<RecoveryKey> {
@override
Widget build(BuildContext context) {}
}*/

View File

@ -0,0 +1 @@

View File

@ -40,34 +40,44 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget {
FieldCubitFactory(context),
ServerRecoveryMethods.newDeviceKey,
),
child: Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state;
return BrandHeroScreen(
heroTitle: "recovering.recovery_main_header".tr(),
heroSubtitle: "recovering.method_device_input_description".tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
CubitFormTextField(
formFieldCubit:
context.read<RecoveryDeviceFormCubit>().tokenField,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "recovering.method_device_input_placeholder".tr(),
),
),
SizedBox(height: 16),
FilledButton(
title: "more.continue".tr(),
onPressed: formCubitState.isSubmitting
? null
: () => context.read<RecoveryDeviceFormCubit>().trySubmit(),
)
],
);
child: BlocListener<ServerInstallationCubit, ServerInstallationState>(
listener: (context, state) {
if (state is ServerInstallationRecovery &&
state.currentStep != RecoveryStep.NewDeviceKey) {
Navigator.of(context).pop();
}
},
child: Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state;
return BrandHeroScreen(
heroTitle: "recovering.recovery_main_header".tr(),
heroSubtitle: "recovering.method_device_input_description".tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
CubitFormTextField(
formFieldCubit:
context.read<RecoveryDeviceFormCubit>().tokenField,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText:
"recovering.method_device_input_placeholder".tr(),
),
),
SizedBox(height: 16),
FilledButton(
title: "more.continue".tr(),
onPressed: formCubitState.isSubmitting
? null
: () =>
context.read<RecoveryDeviceFormCubit>().trySubmit(),
)
],
);
},
),
),
);
}

View File

@ -13,24 +13,33 @@ class RecoverByOldTokenInstruction extends StatelessWidget {
RecoverByOldTokenInstruction({required this.instructionFilename});
Widget build(BuildContext context) {
return BrandHeroScreen(
heroTitle: "recovering.recovery_main_header".tr(),
hasBackButton: true,
hasFlashButton: false,
onBackButtonPressed: () =>
context.read<ServerInstallationCubit>().revertRecoveryStep(),
children: [
BrandMarkdown(
fileName: instructionFilename,
),
SizedBox(height: 16),
FilledButton(
title: "recovering.method_device_button".tr(),
onPressed: () => context
.read<ServerInstallationCubit>()
.selectRecoveryMethod(ServerRecoveryMethods.oldToken),
)
],
return BlocListener<ServerInstallationCubit, ServerInstallationState>(
listener: (context, state) {
if (state is ServerInstallationRecovery &&
state.currentStep != RecoveryStep.Selecting) {
Navigator.of(context).pop();
Navigator.of(context).pop();
}
},
child: BrandHeroScreen(
heroTitle: "recovering.recovery_main_header".tr(),
hasBackButton: true,
hasFlashButton: false,
onBackButtonPressed: () =>
context.read<ServerInstallationCubit>().revertRecoveryStep(),
children: [
BrandMarkdown(
fileName: instructionFilename,
),
SizedBox(height: 18),
FilledButton(
title: "recovering.method_device_button".tr(),
onPressed: () => context
.read<ServerInstallationCubit>()
.selectRecoveryMethod(ServerRecoveryMethods.oldToken),
)
],
),
);
}
@ -66,7 +75,7 @@ class RecoverByOldToken extends StatelessWidget {
labelText: "recovering.method_device_input_placeholder".tr(),
),
),
SizedBox(height: 16),
SizedBox(height: 18),
FilledButton(
title: "more.continue".tr(),
onPressed: formCubitState.isSubmitting

View File

@ -30,26 +30,28 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
textAlign: TextAlign.center,
scrollPadding: EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'KeyID',
),
),
Spacer(),
const SizedBox(height: 18),
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().applicationKey,
textAlign: TextAlign.center,
scrollPadding: EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'Master Application Key',
),
),
Spacer(),
const SizedBox(height: 18),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<BackblazeFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
SizedBox(height: 10),
const SizedBox(height: 18),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,

View File

@ -32,17 +32,18 @@ class RecoveryConfirmCloudflare extends StatelessWidget {
textAlign: TextAlign.center,
scrollPadding: EdgeInsets.only(bottom: 70),
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'initializing.5'.tr(),
),
),
Spacer(),
const SizedBox(height: 18),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<CloudFlareFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
SizedBox(height: 10),
const SizedBox(height: 18),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,

View File

@ -36,11 +36,11 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
Widget build(BuildContext context) {
return BrandHeroScreen(
heroTitle: _isExtended
? "recovering.choose_server".tr()
: "recovering.confirm_server".tr(),
? 'recovering.choose_server'.tr()
: 'recovering.confirm_server'.tr(),
heroSubtitle: _isExtended
? "recovering.choose_server_description".tr()
: "recovering.confirm_server_description".tr(),
? 'recovering.choose_server_description'.tr()
: 'recovering.confirm_server_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
@ -68,7 +68,7 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
if (servers?.isEmpty ?? true)
Center(
child: Text(
"recovering.no_servers".tr(),
'recovering.no_servers'.tr(),
style: Theme.of(context).textTheme.headline6,
),
),
@ -99,7 +99,7 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
),
SizedBox(height: 16),
FilledButton(
title: "recovering.confirm_server_accept".tr(),
title: 'recovering.confirm_server_accept'.tr(),
onPressed: () => _showConfirmationDialog(context, server),
),
SizedBox(height: 16),
@ -166,19 +166,65 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('ssh.delete'.tr()),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text("WOW DIALOGUE TEXT WOW :)"),
],
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.warning_amber_outlined),
const SizedBox(height: 8),
Text(
'recovering.modal_confirmation_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('recovering.modal_confirmation_description'.tr(),
style: Theme.of(context).textTheme.bodyMedium),
const Divider(),
Text(
server.name,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.start,
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(server.isReverseDnsValid ? Icons.check : Icons.close),
const SizedBox(width: 8),
Text(server.isReverseDnsValid
? 'recovering.modal_confirmation_dns_valid'.tr()
: 'recovering.modal_confirmation_dns_invalid'.tr()),
],
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(server.isIpValid ? Icons.check : Icons.close),
const SizedBox(width: 8),
Text(server.isIpValid
? 'recovering.modal_confirmation_ip_valid'.tr()
: 'recovering.modal_confirmation_ip_invalid'.tr()),
],
),
],
),
actions: <Widget>[
TextButton(
child: Text('basis.cancel'.tr()),
child: Text('modals.no'.tr()),
onPressed: () {
Navigator.of(context)..pop();
Navigator.of(context).pop();
},
),
TextButton(
child: Text('modals.yes'.tr()),
onPressed: () {
context.read<ServerInstallationCubit>().setServerId(server);
Navigator.of(context).pop();
},
),
],

View File

@ -9,6 +9,8 @@ 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_backblaze.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.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';
@ -43,8 +45,10 @@ class RecoveryRouting extends StatelessWidget {
currentPage = RecoveryConfirmServer();
break;
case RecoveryStep.CloudflareToken:
currentPage = RecoveryConfirmCloudflare();
break;
case RecoveryStep.BackblazeToken:
currentPage = RecoveryConfirmBackblaze();
break;
}
}

View File

@ -363,6 +363,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.2"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
@ -560,6 +567,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
local_auth:
dependency: "direct main"
description:

View File

@ -52,6 +52,7 @@ dev_dependencies:
flutter_launcher_icons: ^0.9.2
hive_generator: ^1.0.0
json_serializable: ^6.1.4
flutter_lints: ^2.0.1
flutter_icons:
android: "launcher_icon"