Improve server endpoints, add recovery page

- Handle Dio error codes properly to avoid exceptions
- Improve en and ru assets
- Improve dns recordings failure handling
- Add recovery button to initializing page
- Add recovery pages group
pull/90/head
NaiJi ✨ 2022-05-10 23:42:33 +03:00
parent 31be961dd0
commit ce3e046f5a
10 changed files with 347 additions and 44 deletions

View File

@ -21,7 +21,8 @@
"saving": "Saving..",
"nickname": "Nickname",
"loading": "Loading...",
"later": "I will setup it later",
"later": "Skip to setup later",
"connect_to_existing": "Connect to existing server",
"reset": "Reset",
"details": "Details",
"no_data": "No data",

View File

@ -21,7 +21,8 @@
"saving": "Сохранение…",
"nickname": "Никнейм",
"loading": "Загрузка",
"later": "Настрою потом",
"later": "Пропустить и настроить потом",
"connect_to_existing": "Подключиться к существующему серверу",
"reset": "Сбросить",
"details": "Детальная информация",
"no_data": "Нет данных",

View File

@ -58,7 +58,17 @@ class ServerApi extends ApiMap {
var client = await getClient();
try {
response = await client.get('/services/status');
response = await client.get(
'/services/status',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
res = response.statusCode == HttpStatus.ok;
} catch (e) {
res = false;
@ -129,7 +139,17 @@ class ServerApi extends ApiMap {
Response response;
var client = await getClient();
response = await client.get('/users');
response = await client.get(
'/users',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
try {
for (var user in response.data) {
res.add(user.toString());
@ -155,6 +175,14 @@ class ServerApi extends ApiMap {
data: {
'public_key': sshKey,
},
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
@ -174,6 +202,14 @@ class ServerApi extends ApiMap {
response = await client.put(
'/services/ssh/key/send',
data: {"public_key": ssh},
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
@ -191,7 +227,17 @@ class ServerApi extends ApiMap {
Response response;
var client = await getClient();
response = await client.get('/services/ssh/keys/${user.login}');
response = await client.get(
'/services/ssh/keys/${user.login}',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
try {
res = (response.data as List<dynamic>).map((e) => e as String).toList();
} catch (e) {
@ -215,8 +261,18 @@ class ServerApi extends ApiMap {
Response response;
var client = await getClient();
response = await client.delete('/services/ssh/keys/${user.login}',
data: {"public_key": sshKey});
response = await client.delete(
'/services/ssh/keys/${user.login}',
data: {"public_key": sshKey},
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return ApiResponse<void>(
@ -237,8 +293,13 @@ class ServerApi extends ApiMap {
response = await client.delete(
'/users/${user.login}',
options: Options(
contentType: 'application/json',
),
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
res = response.statusCode == HttpStatus.ok ||
response.statusCode == HttpStatus.notFound;
@ -262,6 +323,14 @@ class ServerApi extends ApiMap {
try {
response = await client.get(
'/system/configuration/apply',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
res = response.statusCode == HttpStatus.ok;
@ -276,13 +345,33 @@ class ServerApi extends ApiMap {
Future<void> switchService(ServiceTypes type, bool needToTurnOn) async {
var client = await getClient();
client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}');
client.post(
'/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
Future<Map<ServiceTypes, bool>> servicesPowerCheck() async {
var client = await getClient();
Response response = await client.get('/services/status');
Response response = await client.get(
'/services/status',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return {
@ -303,13 +392,31 @@ class ServerApi extends ApiMap {
'accountKey': bucket.applicationKey,
'bucket': bucket.bucketName,
},
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
Future<void> startBackup() async {
var client = await getClient();
client.put('/services/restic/backup/create');
client.put(
'/services/restic/backup/create',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
@ -320,6 +427,14 @@ class ServerApi extends ApiMap {
try {
response = await client.get(
'/services/restic/backup/list',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
return response.data.map<Backup>((e) => Backup.fromJson(e)).toList();
} catch (e) {
@ -336,6 +451,14 @@ class ServerApi extends ApiMap {
try {
response = await client.get(
'/services/restic/backup/status',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
return BackupStatus.fromJson(response.data);
} catch (e) {
@ -352,40 +475,101 @@ class ServerApi extends ApiMap {
Future<void> forceBackupListReload() async {
var client = await getClient();
client.get('/services/restic/backup/reload');
client.get(
'/services/restic/backup/reload',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
Future<void> restoreBackup(String backupId) async {
var client = await getClient();
client.put('/services/restic/backup/restore', data: {'backupId': backupId});
client.put(
'/services/restic/backup/restore',
data: {'backupId': backupId},
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
Future<bool> pullConfigurationUpdate() async {
var client = await getClient();
Response response = await client.get('/system/configuration/pull');
Response response = await client.get(
'/system/configuration/pull',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return response.statusCode == HttpStatus.ok;
}
Future<bool> reboot() async {
var client = await getClient();
Response response = await client.get('/system/reboot');
Response response = await client.get(
'/system/reboot',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return response.statusCode == HttpStatus.ok;
}
Future<bool> upgrade() async {
var client = await getClient();
Response response = await client.get('/system/configuration/upgrade');
Response response = await client.get(
'/system/configuration/upgrade',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return response.statusCode == HttpStatus.ok;
}
Future<AutoUpgradeSettings> getAutoUpgradeSettings() async {
var client = await getClient();
Response response = await client.get('/system/configuration/autoUpgrade');
Response response = await client.get(
'/system/configuration/autoUpgrade',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return AutoUpgradeSettings.fromJson(response.data);
}
@ -395,13 +579,31 @@ class ServerApi extends ApiMap {
await client.put(
'/system/configuration/autoUpgrade',
data: settings.toJson(),
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
Future<TimeZoneSettings> getServerTimezone() async {
var client = await getClient();
Response response = await client.get('/system/configuration/timezone');
Response response = await client.get(
'/system/configuration/timezone',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
return TimeZoneSettings.fromString(response.data);
@ -412,20 +614,45 @@ class ServerApi extends ApiMap {
await client.put(
'/system/configuration/timezone',
data: settings.toJson(),
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
}
Future<String> getDkim() async {
Future<String?> getDkim() async {
var client = await getClient();
Response response = await client.get('/services/mailserver/dkim');
Response response = await client.get(
'/services/mailserver/dkim',
options: Options(
contentType: 'application/json',
receiveDataWhenStatusError: true,
followRedirects: false,
validateStatus: (status) {
return (status != null) &&
(status < HttpStatus.internalServerError);
}),
);
close(client);
// if got 404 raise exception
if (response.statusCode == HttpStatus.notFound) {
if (response.statusCode == null) {
return null;
}
if (response.statusCode == HttpStatus.notFound || response.data == null) {
throw Exception('No DKIM key found');
}
if (response.statusCode != HttpStatus.ok) {
return "";
}
final base64toString = utf8.fuse(base64);
return base64toString

View File

@ -206,7 +206,7 @@ class AppConfigRepository {
var dkimRecordString = await api.getDkim();
await cloudflareApi.setDkim(dkimRecordString, cloudFlareDomain);
await cloudflareApi.setDkim(dkimRecordString ?? "", cloudFlareDomain);
}
Future<bool> isHttpServerWorking() async {

View File

@ -97,11 +97,11 @@ class DnsRecordsCubit extends AppConfigDependendCubit<DnsRecordsState> {
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain;
final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4;
final dkimPublicKey = await api.getDkim();
final String? dkimPublicKey = await api.getDkim();
await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!);
await cloudflare.createMultipleDnsRecords(
cloudFlareDomain: domain, ip4: ipAddress);
await cloudflare.setDkim(dkimPublicKey, domain);
await cloudflare.setDkim(dkimPublicKey ?? "", domain);
await load();
}

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/ui/pages/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/rootRoute.dart';
import 'package:wakelock/wakelock.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/ui/pages/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -7,7 +7,7 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/pages/initializing/initializing.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/rootRoute.dart';
import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart';

View File

@ -17,13 +17,14 @@ 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_domain.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
class InitializingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var cubit = context.watch<AppConfigCubit>();
var actualPage = [
var actualInitializingPage = [
() => _stepHetzner(cubit),
() => _stepCloudflare(cubit),
() => _stepBackblaze(cubit),
@ -69,7 +70,7 @@ class InitializingPage extends StatelessWidget {
_addCard(
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: actualPage,
child: actualInitializingPage,
),
),
ConstrainedBox(
@ -79,19 +80,38 @@ class InitializingPage extends StatelessWidget {
MediaQuery.of(context).padding.bottom -
566,
),
child: Container(
alignment: Alignment.center,
child: BrandButton.text(
title: cubit.state is AppConfigFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(RootPage()),
(predicate) => false,
);
},
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
alignment: Alignment.center,
child: BrandButton.text(
title: cubit.state is AppConfigFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(RootPage()),
(predicate) => false,
);
},
),
),
(cubit.state is AppConfigFinished)
? Container()
: Container(
alignment: Alignment.center,
child: BrandButton.text(
title: 'basis.connect_to_existing'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(RecoveryDomain()),
(predicate) => false,
);
},
),
)
],
)),
],
),

View File

@ -0,0 +1,54 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/pages/rootRoute.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
class RecoveryDomain extends StatelessWidget {
@override
Widget build(BuildContext context) {
var cubit = context.watch<AppConfigCubit>();
return BlocListener<AppConfigCubit, AppConfigState>(
listener: (context, state) {
if (cubit.state is AppConfigFinished) {
Navigator.of(context).pushReplacement(materialRoute(RootPage()));
}
},
child: SafeArea(
child: Scaffold(
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom -
566,
),
child: Container(
alignment: Alignment.center,
child: BrandButton.text(
title: cubit.state is AppConfigFinished
? 'basis.close'.tr()
: 'basis.later'.tr(),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(RootPage()),
(predicate) => false,
);
},
),
),
),
],
),
),
),
),
);
}
}