Merge master into platform-path

pull/240/head
NaiJi ✨ 2023-07-26 20:20:21 -03:00
commit 603946ba73
14 changed files with 445 additions and 56 deletions

View File

@ -34,7 +34,8 @@
"apply": "Apply",
"done": "Done",
"continue": "Continue",
"alert": "Alert"
"alert": "Alert",
"copied_to_clipboard": "Copied to clipboard!"
},
"more_page": {
"configuration_wizard": "Setup wizard",
@ -196,6 +197,11 @@
"autobackup_custom_hint": "Enter custom period in minutes",
"autobackup_set_period": "Set period",
"autobackup_period_set": "Period set",
"backups_encryption_key": "Encryption key",
"backups_encryption_key_subtitle": "Keep it in a safe place.",
"backups_encryption_key_copy": "Copy the encryption key",
"backups_encryption_key_show": "Show the encryption key",
"backups_encryption_key_description": "This key is used to encrypt your backups. If you lose it, you will not be able to restore your backups. Keep it in a safe place, as it will be useful if you ever need to restore from backups manually.",
"pending_jobs": "Currently running backup jobs",
"snapshots_title": "Snapshot list"
},

View File

@ -34,6 +34,7 @@
"done": "Готово",
"continue": "Продолжить",
"alert": "Уведомление",
"copied_to_clipboard": "Скопировано в буфер обмена!",
"app_name": "SelfPrivacy"
},
"more_page": {
@ -197,6 +198,7 @@
"autobackup_custom_hint": "Введите период в минутах",
"autobackup_set_period": "Установить период",
"autobackup_period_set": "Период установлен",
"backups_encryption_key": "Ключ шифрования",
"snapshots_title": "Список снимков"
},
"storage": {
@ -536,4 +538,4 @@
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
"ignore_tls": "Не проверять сертификаты TLS"
}
}
}

View File

@ -71,15 +71,14 @@ abstract class GraphQLApiMap {
'https://api.$rootAddress/graphql',
httpClient: ioClient,
parser: ResponseLoggingParser(),
defaultHeaders: {'Accept-Language': _locale},
);
final String token = _getApiToken();
final Link graphQLLink = RequestLoggingLink().concat(
isWithToken
? AuthLink(
getToken: () async =>
customToken == '' ? 'Bearer $token' : customToken,
customToken == '' ? 'Bearer $_token' : customToken,
).concat(httpLink)
: httpLink,
);
@ -95,13 +94,16 @@ abstract class GraphQLApiMap {
}
Future<GraphQLClient> getSubscriptionClient() async {
final String token = _getApiToken();
final WebSocketLink webSocketLink = WebSocketLink(
'ws://api.$rootAddress/graphql',
config: SocketClientConfig(
autoReconnect: true,
headers: token.isEmpty ? null : {'Authorization': 'Bearer $token'},
headers: _token.isEmpty
? null
: {
'Authorization': 'Bearer $_token',
'Accept-Language': _locale,
},
),
);
@ -111,7 +113,9 @@ abstract class GraphQLApiMap {
);
}
String _getApiToken() {
String get _locale => getIt.get<ApiConfigModel>().localeCode ?? 'en';
String get _token {
String token = '';
final serverDetails = getIt<ApiConfigModel>().serverDetails;
if (serverDetails != null) {

View File

@ -721,12 +721,18 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
await repository.saveIsServerResetedSecondTime(true);
await repository.saveHasFinalChecked(true);
await repository.saveIsRecoveringServer(false);
final serverType = await ProvidersController.currentServerProvider!
.getServerType(state.serverDetails!.id);
await repository.saveServerType(serverType.data!);
await ProvidersController.currentServerProvider!
.trySetServerLocation(serverType.data!.location.identifier);
final User mainUser = await repository.getMainUser();
await repository.saveRootUser(mainUser);
final ServerInstallationRecovery updatedState =
(state as ServerInstallationRecovery).copyWith(
backblazeCredential: backblazeCredential,
rootUser: mainUser,
serverTypeIdentificator: serverType.data!.identifier,
);
emit(updatedState.finish());
}

View File

@ -75,21 +75,44 @@ class ServerInstallationRepository {
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
TlsOptions.verifyCertificate = true;
return ServerInstallationFinished(
installationDialoguePopUp: null,
providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator!,
dnsApiToken: dnsApiToken!,
serverDomain: serverDomain!,
backblazeCredential: backblazeCredential!,
serverDetails: serverDetails!,
rootUser: box.get(BNames.rootUser),
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
isServerResetedFirstTime:
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
isServerResetedSecondTime:
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
);
if (serverTypeIdentificator == null && serverDetails != null) {
final finalServerType = await ProvidersController.currentServerProvider!
.getServerType(serverDetails.id);
await saveServerType(finalServerType.data!);
await ProvidersController.currentServerProvider!
.trySetServerLocation(finalServerType.data!.location.identifier);
return ServerInstallationFinished(
installationDialoguePopUp: null,
providerApiToken: providerApiToken!,
serverTypeIdentificator: finalServerType.data!.identifier,
dnsApiToken: dnsApiToken!,
serverDomain: serverDomain!,
backblazeCredential: backblazeCredential!,
serverDetails: serverDetails,
rootUser: box.get(BNames.rootUser),
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
isServerResetedFirstTime:
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
isServerResetedSecondTime:
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
);
} else {
return ServerInstallationFinished(
installationDialoguePopUp: null,
providerApiToken: providerApiToken!,
serverTypeIdentificator: serverTypeIdentificator!,
dnsApiToken: dnsApiToken!,
serverDomain: serverDomain!,
backblazeCredential: backblazeCredential!,
serverDetails: serverDetails!,
rootUser: box.get(BNames.rootUser),
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
isServerResetedFirstTime:
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
isServerResetedSecondTime:
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
);
}
}
if (box.get(BNames.isRecoveringServer, defaultValue: false) &&

View File

@ -9,6 +9,7 @@ class ApiConfigModel {
final Box _box = Hive.box(BNames.serverInstallationBox);
ServerHostingDetails? get serverDetails => _serverDetails;
String? get localeCode => _localeCode;
String? get serverProviderKey => _serverProviderKey;
String? get serverLocation => _serverLocation;
String? get serverType => _serverType;
@ -20,6 +21,7 @@ class ApiConfigModel {
ServerDomain? get serverDomain => _serverDomain;
BackblazeBucket? get backblazeBucket => _backblazeBucket;
String? _localeCode;
String? _serverProviderKey;
String? _serverLocation;
String? _dnsProviderKey;
@ -31,6 +33,10 @@ class ApiConfigModel {
ServerDomain? _serverDomain;
BackblazeBucket? _backblazeBucket;
Future<void> setLocaleCode(final String value) async {
_localeCode = value;
}
Future<void> storeServerProviderType(final ServerProviderType value) async {
await _box.put(BNames.serverProvider, value);
_serverProvider = value;
@ -82,6 +88,7 @@ class ApiConfigModel {
}
void clear() {
_localeCode = null;
_serverProviderKey = null;
_dnsProvider = null;
_serverLocation = null;
@ -95,6 +102,7 @@ class ApiConfigModel {
}
void init() {
_localeCode = 'en';
_serverProviderKey = _box.get(BNames.hetznerKey);
_serverLocation = _box.get(BNames.serverLocation);
_dnsProviderKey = _box.get(BNames.cloudFlareKey);

View File

@ -88,6 +88,93 @@ class DigitalOceanServerProvider extends ServerProvider {
return GenericResult(success: true, data: servers);
}
@override
Future<GenericResult<ServerType?>> getServerType(final int serverId) async {
ServerType? serverType;
dynamic server;
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: serverType,
code: result.code,
message: result.message,
);
}
final List rawServers = result.data;
for (final rawServer in rawServers) {
if (rawServer['networks']['v4'].isNotEmpty) {
for (final v4 in rawServer['networks']['v4']) {
if (v4['type'].toString() == 'public') {
server = rawServer;
}
}
}
}
if (server == null) {
const String msg = 'getServerType: no server!';
print(msg);
return GenericResult(
success: false,
data: serverType,
message: msg,
);
}
final rawLocationsResult = await getAvailableLocations();
if (rawLocationsResult.data.isEmpty || !rawLocationsResult.success) {
return GenericResult(
success: rawLocationsResult.success,
data: serverType,
code: rawLocationsResult.code,
message: rawLocationsResult.message,
);
}
ServerProviderLocation? location;
for (final rawLocation in rawLocationsResult.data) {
if (rawLocation.identifier == server['region']['slug']) {
location = rawLocation;
}
}
if (location == null) {
const String msg = 'getServerType: no location!';
print(msg);
return GenericResult(
success: false,
data: serverType,
message: msg,
);
}
ServerType? type;
final rawSize = DigitalOceanServerType.fromJson(server['size']);
for (final rawRegion in rawSize.regions) {
if (rawRegion == server['region']['slug']) {
type = ServerType(
title: rawSize.description,
identifier: rawSize.slug,
ram: rawSize.memory / 1024,
cores: rawSize.vcpus,
disk: DiskSize(byte: rawSize.disk * 1024 * 1024 * 1024),
price: Price(
value: rawSize.priceMonthly,
currency: currency,
),
location: location,
);
}
}
return GenericResult(
success: true,
data: type,
);
}
@override
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,

View File

@ -88,6 +88,78 @@ class HetznerServerProvider extends ServerProvider {
return GenericResult(success: true, data: servers);
}
@override
Future<GenericResult<ServerType?>> getServerType(final int serverId) async {
ServerType? serverType;
HetznerServerInfo? server;
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: serverType,
code: result.code,
message: result.message,
);
}
final List<HetznerServerInfo> hetznerServers = result.data;
for (final hetznerServer in hetznerServers) {
if (hetznerServer.publicNet.ipv4 != null ||
hetznerServer.id == serverId) {
server = hetznerServer;
break;
}
}
if (server == null) {
const String msg = 'getServerType: no server!';
print(msg);
return GenericResult(
success: false,
data: serverType,
message: msg,
);
}
double? priceValue;
for (final price in server.serverType.prices) {
if (price.location == server.location.name) {
priceValue = price.monthly;
}
}
if (priceValue == null) {
const String msg = 'getServerType: no price!';
print(msg);
return GenericResult(
success: false,
data: serverType,
message: msg,
);
}
return GenericResult(
success: true,
data: ServerType(
title: server.serverType.description,
identifier: server.serverType.name,
ram: server.serverType.memory.toDouble(),
cores: server.serverType.cores,
disk: DiskSize(byte: server.serverType.disk * 1024 * 1024 * 1024),
price: Price(
value: priceValue,
currency: currency,
),
location: ServerProviderLocation(
title: server.location.city,
description: server.location.description,
flag: server.location.flag,
identifier: server.location.name,
),
),
);
}
@override
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,

View File

@ -24,6 +24,11 @@ abstract class ServerProvider {
/// Only with public IPv4 addresses.
Future<GenericResult<List<ServerBasicInfo>>> getServers();
/// Returns actual [ServerType] of the
/// requested server entry assigned
/// to the authorized user.
Future<GenericResult<ServerType?>> getServerType(final int serverId);
/// Tries to launch installation of SelfPrivacy on
/// the requested server entry for the authorized account.
/// Depending on a server provider, the algorithm

View File

@ -71,34 +71,38 @@ class SelfprivacyApp extends StatelessWidget {
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) =>
MaterialApp.router(
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: lightThemeData,
darkTheme: darkThemeData,
themeMode: appSettings.isAutoDarkModeOn
? ThemeMode.system
: appSettings.isDarkModeOn
? ThemeMode.dark
: ThemeMode.light,
builder: (final BuildContext context, final Widget? widget) {
Widget error = const Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error;
return widget!;
},
),
) {
getIt.get<ApiConfigModel>().setLocaleCode(
context.locale.languageCode,
);
return MaterialApp.router(
routeInformationParser: _appRouter.defaultRouteParser(),
routerDelegate: _appRouter.delegate(),
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
title: 'SelfPrivacy',
theme: lightThemeData,
darkTheme: darkThemeData,
themeMode: appSettings.isAutoDarkModeOn
? ThemeMode.system
: appSettings.isDarkModeOn
? ThemeMode.dark
: ThemeMode.light,
builder: (final BuildContext context, final Widget? widget) {
Widget error = const Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error;
return widget!;
},
);
},
),
),
);

View File

@ -16,6 +16,7 @@ import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
import 'package:selfprivacy/ui/pages/backups/copy_encryption_key_modal.dart';
import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/extensions/duration.dart';
@ -144,6 +145,37 @@ class BackupDetailsPage extends StatelessWidget {
: 'backup.autobackup_period_never'.tr(),
),
),
ListTile(
onTap: preventActions
? null
: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (final BuildContext context) =>
DraggableScrollableSheet(
expand: false,
maxChildSize: 0.9,
minChildSize: 0.5,
initialChildSize: 0.7,
builder: (final context, final scrollController) =>
CopyEncryptionKeyModal(
scrollController: scrollController,
),
),
);
},
leading: const Icon(
Icons.key_outlined,
),
title: Text(
'backup.backups_encryption_key'.tr(),
),
subtitle: Text(
'backup.backups_encryption_key_subtitle'.tr(),
),
),
const SizedBox(height: 16),
if (backupJobs.isNotEmpty)
Column(

View File

@ -20,9 +20,6 @@ class ChangeAutobackupsPeriodModal extends StatefulWidget {
class _ChangeAutobackupsPeriodModalState
extends State<ChangeAutobackupsPeriodModal> {
// This is a modal with radio buttons to select the autobackup period
// Period might be none, selected from predefined list or custom
// Store in state the selected period
Duration? selectedPeriod;
static const List<Duration> autobackupPeriods = [

View File

@ -0,0 +1,141 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
class CopyEncryptionKeyModal extends StatefulWidget {
const CopyEncryptionKeyModal({
required this.scrollController,
super.key,
});
final ScrollController scrollController;
@override
State<CopyEncryptionKeyModal> createState() => _CopyEncryptionKeyModalState();
}
class _CopyEncryptionKeyModalState extends State<CopyEncryptionKeyModal> {
bool isKeyVisible = false;
bool copiedToClipboard = false;
Timer? copyToClipboardTimer;
@override
void dispose() {
copyToClipboardTimer?.cancel();
super.dispose();
}
@override
Widget build(final BuildContext context) {
final String encryptionKey =
context.watch<BackupsCubit>().state.backblazeBucket!.encryptionKey;
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
Text(
'backup.backups_encryption_key'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'backup.backups_encryption_key_description'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.surfaceVariant,
),
padding: const EdgeInsets.all(16),
child: Stack(
children: [
SelectableText(
encryptionKey,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Positioned.fill(
child: InkWell(
onTap: () {
setState(
() {
isKeyVisible = !isKeyVisible;
},
);
},
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isKeyVisible ? 0 : 1,
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.visibility_outlined),
const SizedBox(width: 8),
Text(
'backup.backups_encryption_key_show'.tr(),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
],
)),
),
),
),
],
)),
const SizedBox(height: 8),
FilledButton.icon(
onPressed: () {
setState(
() {
copiedToClipboard = true;
},
);
setState(() {
copyToClipboardTimer?.cancel();
copyToClipboardTimer = Timer(
const Duration(seconds: 5),
() {
setState(() {
copiedToClipboard = false;
});
},
);
});
Clipboard.setData(
ClipboardData(
text: encryptionKey,
),
);
},
icon: const Icon(Icons.copy_all_outlined),
label: Text(
copiedToClipboard
? 'basis.copied_to_clipboard'.tr()
: 'backup.backups_encryption_key_copy'.tr(),
),
),
],
);
}
}

View File

@ -139,7 +139,9 @@ class _ServicePageState extends State<ServicePage> {
),
style: Theme.of(context).textTheme.bodyMedium,
),
enabled: !serviceDisabled && !serviceLocked,
enabled: !serviceDisabled &&
!serviceLocked &&
service.storageUsage.volume != null,
),
if (service.canBeBackedUp)
ListTile(