Compare commits

...

19 Commits

Author SHA1 Message Date
Illia Chub 9594c538c7 Merge pull request 'Fix branch of nixos-infect' (#82) from inex/selfprivacy.org.app:backups into master
Reviewed-on: kherel/selfprivacy.org.app#82
2021-12-16 19:37:04 +02:00
Inex Code 1c15712596 Bump version to 0.4.1 2021-12-16 11:27:29 +00:00
Inex Code 1b42d3a382 Fix nixos-infect link to use master, not rolling 2021-12-16 14:26:40 +03:00
Illia Chub 6653408dfb Add 'fastlane/metadata/android/en-US/changelogs/0.4.0.txt' 2021-12-10 08:20:43 +02:00
Illia Chub dfed4a113e Updated changelogs 2021-12-10 08:19:36 +02:00
Illia Chub af80823678 Updated changelogs 2021-12-10 08:15:37 +02:00
Illia Chub fe41ae2cb8 Update 'fastlane/metadata/android/en-US/full_description.txt' 2021-12-10 08:14:29 +02:00
Illia Chub 71d0cafdec Merge pull request 'Version 0.4.0' (#81) from inex/selfprivacy.org.app:backups into master
Reviewed-on: kherel/selfprivacy.org.app#81
2021-12-09 06:56:29 +02:00
Inex Code f633fecd57 Fix backup list parsing 2021-12-09 03:44:05 +00:00
Inex Code 2b8c009ef1 Add button to force refetch backups list 2021-12-09 03:35:15 +00:00
Inex Code 35c1eea7f0 Fix error when error is null 2021-12-09 06:23:27 +03:00
Inex Code 49efd16d37 Merge branch 'master' into backups 2021-12-06 20:42:35 +02:00
Inex Code d21b9df734 Version bump 2021-12-06 18:33:17 +00:00
Inex Code b40bea63d1 Backups and server upgrade 2021-12-06 18:31:19 +00:00
Inex Code 650e0e7376 Add translation strings for backups 2021-12-06 18:30:30 +00:00
kherel 1b490c7bc9 Merge pull request 'Hotfix SPCVE-0001' (#80) from inex/selfprivacy.org.app:fix-typos into master
Reviewed-on: kherel/selfprivacy.org.app#80
2021-11-19 07:59:10 +02:00
Inex Code 79f672acbc Bump version 2021-11-18 19:11:49 +00:00
Inex Code 6011d6fdce Hotfix SPCVE-0001 2021-11-18 19:10:40 +00:00
Illia Chub b0a2c3312b Merge pull request 'Add a license' (#79) from inex/selfprivacy.org.app:master into master
Reviewed-on: kherel/selfprivacy.org.app#79
2021-11-18 17:08:56 +02:00
46 changed files with 1136 additions and 55 deletions

View File

@ -99,7 +99,20 @@
"bottom_sheet": {
"1": "Will save your day in case of incident: hackers attack, server deletion, etc.",
"2": "3Gb/10Gb, last backup was yesterday {}"
}
},
"reuploadKey": "Force reupload key",
"reuploadedKey": "Key reuploaded",
"initialize": "Initialize",
"waitingForRebuild": "You will be able to create your first backup in a few minutes.",
"restore": "Restore from backup",
"no_backups": "There are no backups yet",
"create_new": "Create a new backup",
"creating": "Creating a new backup: {}%",
"restoring": "Restoring from backup",
"error_pending": "Server returned error, check it below",
"restore_alert": "You are about to restore from backup created on {}. All current data will be lost. Are you sure?",
"refetchBackups": "Refetch backup list",
"refetchingList": "In a few minutes list will be updated"
}
},
"not_ready_card": {
@ -223,7 +236,9 @@
"5": "Yes, purge all my tokens",
"6": "Delete the server and volume?",
"7": "Yes",
"8": "Remove task"
"8": "Remove task",
"9": "Reboot",
"yes": "Yes"
},
"timer": {
"sec": "{} sec"
@ -237,7 +252,14 @@
"serviceTurnOff": "Turn off",
"serviceTurnOn": "Turn on",
"jobAdded": "Job added",
"runJobs": "Run jobs"
"runJobs": "Run jobs",
"rebootSuccess": "Server is rebooting",
"rebootFailed": "Couldn't reboot the server. Check the app logs.",
"configPullFailed": "Failed to pull configuration upgrade. Started software upgrade anyways.",
"upgradeSuccess": "Server upgrade started",
"upgradeFailed": "Failed to upgrade server",
"upgradeServer": "Upgrade server",
"rebootServer": "Reboot server"
},
"validations": {
"required": "Required",

View File

@ -99,7 +99,21 @@
"bottom_sheet": {
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и т.д.",
"2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}."
}
},
"reuploadKey": "Принудительно обновить ключ",
"reuploadedKey": "Ключ на сервере обновлён",
"initialize": "Настроить",
"waitingForRebuild": "Через несколько минут можно будет создать первую копию.",
"restore": "Восстановить из копии",
"no_backups": "Резервных копий пока нет",
"create_new": "Создать новую копию",
"creating": "Создание копии: {}%",
"restoring": "Восстановление из копии",
"error_pending": "Сервер вернул ошибку: проверьте её ниже.",
"restore_alert": "Вы собираетесь восстановить из копии созданной {}. Все текущие данные будут потеряны. Вы уверены?",
"refetchBackups": "Обновить список копий",
"refetchingList": "Через несколько минут список будет обновлён"
}
},
"not_ready_card": {
@ -223,7 +237,9 @@
"5": "Да, сбросить",
"6": "Удалить сервер и диск?",
"7": "Да, удалить",
"8": "Удалить задачу"
"8": "Удалить задачу",
"9": "Перезагрузить",
"yes": "Да"
},
"timer": {
"sec": "{} сек"
@ -237,7 +253,14 @@
"serviceTurnOff": "Остановить",
"serviceTurnOn": "Запустить",
"jobAdded": "Задача добавленна",
"runJobs": "Запустите задачи"
"runJobs": "Запустите задачи",
"rebootSuccess": "Сервер перезагружается",
"rebootFailed": "Не удалось перезагрузить сервер, проверьте логи",
"configPullFailed": "Не удалось обновить конфигурацию сервера. Обновление ПО запущено.",
"upgradeSuccess": "Запущено обновление сервера",
"upgradeFailed": "Обновить сервер не вышло",
"upgradeServer": "Обновить сервер",
"rebootServer": "Перезагрузить сервер"
},
"validations": {
"required": "Обязательное поле.",

View File

@ -0,0 +1,6 @@
- Added service management
- Fixed e-mail and VPN login issues for newly created users
- Fixed connection issues from Cisco AnyConnect VPN client
- Fixed Nextcloud connection issues for Desktop and mobile clients
- Updated Nextcloud to Nextcloud 22
- Added Ad-Hoc SSH support

View File

@ -0,0 +1,2 @@
- Addressed SPCVE-0001 security issue
- Majorly improved SelfPrivacy REST API stability

View File

@ -0,0 +1,2 @@
- Added backup creation, restoration and deletion possibility
- Implemented backend updates without server redeployment

View File

@ -1,5 +1,5 @@
SelfPrivacy - is a platform on your cloud hosting, that allows to deploy your own private services and control them using mobile application.
To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://hugo.selfprivacy.org/posts/getting_started
To use this application, you'll be required to create accounts of different service providers. Please reffer to this manual: https://selfprivacy.org/en/second.html
Application will do the following things for you:
1. Create your personal server
2. Setup NixOS

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
@ -18,6 +19,7 @@ class BlocAndProviderConfig extends StatelessWidget {
var usersCubit = UsersCubit();
var appConfigCubit = AppConfigCubit()..load();
var servicesCubit = ServicesCubit(appConfigCubit);
var backupsCubit = BackupsCubit(appConfigCubit);
return MultiProvider(
providers: [
BlocProvider(
@ -26,10 +28,11 @@ class BlocAndProviderConfig extends StatelessWidget {
isOnbordingShowing: true,
)..load(),
),
BlocProvider(lazy: false, create: (_) => appConfigCubit),
BlocProvider(create: (_) => appConfigCubit, lazy: false),
BlocProvider(create: (_) => ProvidersCubit()),
BlocProvider(create: (_) => usersCubit..load(), lazy: false),
BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
BlocProvider(
create: (_) =>
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:selfprivacy/logic/models/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
import 'package:selfprivacy/logic/models/server_details.dart';
@ -16,6 +17,7 @@ class HiveConfig {
Hive.registerAdapter(HetznerServerDetailsAdapter());
Hive.registerAdapter(CloudFlareDomainAdapter());
Hive.registerAdapter(BackblazeCredentialAdapter());
Hive.registerAdapter(BackblazeBucketAdapter());
Hive.registerAdapter(HetznerDataBaseAdapter());
await Hive.openBox(BNames.appSettings);
@ -62,6 +64,7 @@ class BNames {
static String hasFinalChecked = 'hasFinalChecked';
static String isServerStarted = 'isServerStarted';
static String backblazeKey = 'backblazeKey';
static String backblazeBucket = 'backblazeBucket';
static String isLoading = 'isLoading';
static String isServerResetedFirstTime = 'isServerResetedFirstTime';
static String isServerResetedSecondTime = 'isServerResetedSecondTime';

View File

@ -0,0 +1 @@

View File

@ -2,6 +2,22 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/api_map.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
class BackblazeApiAuth {
BackblazeApiAuth({required this.authorizationToken, required this.apiUrl});
final String authorizationToken;
final String apiUrl;
}
class BackblazeApplicationKey {
BackblazeApplicationKey(
{required this.applicationKeyId, required this.applicationKey});
final String applicationKeyId;
final String applicationKey;
}
class BackblazeApi extends ApiMap {
BackblazeApi({this.hasLoger = false, this.isWithToken = true});
@ -24,6 +40,29 @@ class BackblazeApi extends ApiMap {
@override
String rootAddress = 'https://api.backblazeb2.com/b2api/v2/';
String apiPrefix = '/b2api/v2';
Future<BackblazeApiAuth> getAuthorizationToken() async {
var client = await getClient();
var backblazeCredential = getIt<ApiConfigModel>().backblazeCredential;
if (backblazeCredential == null) {
throw Exception('Backblaze credential is null');
}
final String encodedApiKey = encodedBackblazeKey(
backblazeCredential.keyId, backblazeCredential.applicationKey);
var response = await client.get(
'b2_authorize_account',
options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}),
);
if (response.statusCode != 200) {
throw Exception('code: ${response.statusCode}');
}
return BackblazeApiAuth(
authorizationToken: response.data['authorizationToken'],
apiUrl: response.data['apiUrl'],
);
}
Future<bool> isValid(String encodedApiKey) async {
var client = await getClient();
Response response = await client.get(
@ -32,7 +71,10 @@ class BackblazeApi extends ApiMap {
);
close(client);
if (response.statusCode == HttpStatus.ok) {
return true;
if (response.data['allowed']['capabilities'].contains('listBuckets')) {
return true;
}
return false;
} else if (response.statusCode == HttpStatus.unauthorized) {
return false;
} else {
@ -40,6 +82,65 @@ class BackblazeApi extends ApiMap {
}
}
// Create bucket
Future<String> createBucket(String bucketName) async {
final auth = await getAuthorizationToken();
var backblazeCredential = getIt<ApiConfigModel>().backblazeCredential;
var client = await getClient();
client.options.baseUrl = auth.apiUrl;
var response = await client.post(
'$apiPrefix/b2_create_bucket',
data: {
'accountId': backblazeCredential!.keyId,
'bucketName': bucketName,
'bucketType': 'allPrivate',
'lifecycleRules': [
{
"daysFromHidingToDeleting": 30,
"daysFromUploadingToHiding": null,
"fileNamePrefix": ""
}
],
},
options: Options(
headers: {'Authorization': auth.authorizationToken},
),
);
close(client);
if (response.statusCode == HttpStatus.ok) {
return response.data['bucketId'];
} else {
throw Exception('code: ${response.statusCode}');
}
}
// Create a limited capability key with access to the given bucket
Future<BackblazeApplicationKey> createKey(String bucketId) async {
final auth = await getAuthorizationToken();
var client = await getClient();
client.options.baseUrl = auth.apiUrl;
var response = await client.post(
'$apiPrefix/b2_create_key',
data: {
'accountId': getIt<ApiConfigModel>().backblazeCredential!.keyId,
'bucketId': bucketId,
'capabilities': ['listBuckets', 'listFiles', 'readFiles', 'writeFiles'],
'keyName': 'selfprivacy-restricted-server-key',
},
options: Options(
headers: {'Authorization': auth.authorizationToken},
),
);
close(client);
if (response.statusCode == HttpStatus.ok) {
return BackblazeApplicationKey(
applicationKeyId: response.data['applicationKeyId'],
applicationKey: response.data['applicationKey']);
} else {
throw Exception('code: ${response.statusCode}');
}
}
@override
bool hasLoger;

View File

@ -112,11 +112,15 @@ class HetznerApi extends ApiMap {
// var dbId = dbCreateResponse.data['volume']['id'];
var dbId = dataBase.id;
final apiToken = StringGenerators.apiToken();
final hostname = domainName.split('.')[0];
/// add ssh key when you need it: e.g. "ssh_keys":["kherel"]
/// check the branch name, it could be "development" or "master".
var data = jsonDecode(
'''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId], "networks":[], "user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/development/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''');
'''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId], "networks":[], "user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''');
Response serverCreateResponse = await client.post(
'/servers',
@ -129,6 +133,7 @@ class HetznerApi extends ApiMap {
ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'],
createTime: DateTime.now(),
dataBase: dataBase,
apiToken: apiToken,
);
}

View File

@ -4,6 +4,8 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/models/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'api_map.dart';
@ -20,8 +22,11 @@ class ServerApi extends ApiMap {
if (isWithToken) {
var cloudFlareDomain = getIt<ApiConfigModel>().cloudFlareDomain;
var domainName = cloudFlareDomain!.domainName;
var apiToken = getIt<ApiConfigModel>().hetznerServer?.apiToken;
options = BaseOptions(baseUrl: 'https://api.$domainName');
options = BaseOptions(baseUrl: 'https://api.$domainName', headers: {
'Authorization': 'Bearer $apiToken',
});
}
return options;
@ -47,18 +52,19 @@ class ServerApi extends ApiMap {
Response response;
var client = await getClient();
// POST request with JSON body containing username and password
try {
response = await client.post(
'/users/create',
'/users',
data: {
'username': user.login,
'password': user.password,
},
options: Options(
headers: {
"X-User": user.login,
"X-Password": user.password,
"X-Domain": getIt<ApiConfigModel>().cloudFlareDomain!.domainName
},
contentType: 'application/json',
),
);
res = response.statusCode == HttpStatus.ok;
res = response.statusCode == HttpStatus.created;
} catch (e) {
print(e);
res = false;
@ -99,8 +105,8 @@ class ServerApi extends ApiMap {
Future<void> sendSsh(String ssh) async {
var client = await getClient();
client.post(
'/services/ssh/enable',
client.put(
'/services/ssh/key/send',
data: {"public_key": ssh},
);
client.close();
@ -119,6 +125,95 @@ class ServerApi extends ApiMap {
ServiceTypes.socialNetwork: response.data['pleroma'] == 0,
};
}
Future<void> uploadBackblazeConfig(BackblazeBucket bucket) async {
var client = await getClient();
client.put(
'/services/restic/backblaze/config',
data: {
'accountId': bucket.applicationKeyId,
'accountKey': bucket.applicationKey,
'bucket': bucket.bucketName,
},
);
client.close();
}
Future<void> startBackup() async {
var client = await getClient();
client.put('/services/restic/backup/create');
client.close();
}
Future<List<Backup>> getBackups() async {
Response response;
var client = await getClient();
try {
response = await client.get(
'/services/restic/backup/list',
);
return response.data.map<Backup>((e) => Backup.fromJson(e)).toList();
} catch (e) {
print(e);
}
close(client);
return <Backup>[];
}
Future<BackupStatus> getBackupStatus() async {
Response response;
var client = await getClient();
try {
response = await client.get(
'/services/restic/backup/status',
);
return BackupStatus.fromJson(response.data);
} catch (e) {
print(e);
}
close(client);
return BackupStatus(
status: BackupStatusEnum.error,
errorMessage: 'Network error',
progress: 0,
);
}
Future<void> forceBackupListReload() async {
var client = await getClient();
client.get('/services/restic/backup/reload');
client.close();
}
Future<void> restoreBackup(String backupId) async {
var client = await getClient();
client.put('/services/restic/backup/restore', data: {'backupId': backupId});
client.close();
}
Future<bool> pullConfigurationUpdate() async {
var client = await getClient();
Response response = await client.get('/system/configuration/pull');
close(client);
return response.statusCode == HttpStatus.ok;
}
Future<bool> reboot() async {
var client = await getClient();
Response response = await client.get('/system/reboot');
client.close();
return response.statusCode == HttpStatus.ok;
}
Future<bool> upgrade() async {
var client = await getClient();
Response response = await client.get('/system/configuration/upgrade');
client.close();
return response.statusCode == HttpStatus.ok;
}
}
extension UrlServerExt on ServiceTypes {

View File

@ -347,6 +347,7 @@ class AppConfigCubit extends Cubit<AppConfigState> {
state.rootUser!,
state.cloudFlareDomain!.domainName,
state.cloudFlareKey!,
state.backblazeCredential!,
onCancel: onCancel,
onSuccess: onSuccess,
);

View File

@ -4,13 +4,11 @@ import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
import 'package:selfprivacy/logic/api_maps/hetzner.dart';
import 'package:selfprivacy/logic/api_maps/server.dart';
import 'package:selfprivacy/logic/get_it/api_config.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
import 'package:selfprivacy/logic/models/server_details.dart';
import 'package:selfprivacy/logic/models/user.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/get_it/console.dart';
import 'package:selfprivacy/logic/models/message.dart';
import 'package:basic_utils/basic_utils.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
@ -110,7 +108,8 @@ class AppConfigRepository {
Future<void> createServer(
User rootUser,
String domainName,
String cloudFlareKey, {
String cloudFlareKey,
BackblazeCredential backblazeCredential, {
required void Function() onCancel,
required Future<void> Function(HetznerServerDetails serverDetails)
onSuccess,
@ -136,7 +135,7 @@ class AppConfigRepository {
BrandAlert(
title: 'modals.1'.tr(),
contentText: 'modals.2'.tr(),
acitons: [
actions: [
ActionButton(
text: 'basis.delete'.tr(),
isRed: true,

View File

@ -0,0 +1,175 @@
import 'dart:async';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/api_maps/server.dart';
import 'package:selfprivacy/logic/api_maps/backblaze.dart';
part 'backups_state.dart';
class BackupsCubit extends AppConfigDependendCubit<BackupsState> {
BackupsCubit(AppConfigCubit appConfigCubit)
: super(appConfigCubit, BackupsState(preventActions: true));
final api = ServerApi();
final backblaze = BackblazeApi();
Future<void> load() async {
if (appConfigCubit.state is AppConfigFinished) {
final bucket = getIt<ApiConfigModel>().backblazeBucket;
if (bucket == null) {
emit(BackupsState(
isInitialized: false, preventActions: false, refreshing: false));
} else {
final status = await api.getBackupStatus();
switch (status.status) {
case BackupStatusEnum.noKey:
case BackupStatusEnum.notInitialized:
emit(BackupsState(
backups: [],
isInitialized: true,
preventActions: false,
progress: 0,
status: status.status,
refreshing: false,
));
break;
case BackupStatusEnum.initializing:
emit(BackupsState(
backups: [],
isInitialized: true,
preventActions: false,
progress: 0,
status: status.status,
refreshTimer: Duration(seconds: 10),
refreshing: false,
));
break;
case BackupStatusEnum.initialized:
case BackupStatusEnum.error:
final backups = await api.getBackups();
emit(BackupsState(
backups: backups,
isInitialized: true,
preventActions: false,
progress: status.progress,
status: status.status,
error: status.errorMessage ?? '',
refreshing: false,
));
break;
case BackupStatusEnum.backingUp:
case BackupStatusEnum.restoring:
final backups = await api.getBackups();
emit(BackupsState(
backups: backups,
isInitialized: true,
preventActions: true,
progress: status.progress,
status: status.status,
error: status.errorMessage ?? '',
refreshTimer: Duration(seconds: 5),
refreshing: false,
));
break;
default:
emit(BackupsState());
}
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
}
}
}
Future<void> createBucket() async {
emit(state.copyWith(preventActions: true));
final domain =
appConfigCubit.state.cloudFlareDomain!.domainName.replaceAll('.', '-');
final serverId = appConfigCubit.state.hetznerServer!.id;
var bucketName = 'selfprivacy-$domain-$serverId';
// If bucket name is too long, shorten it
if (bucketName.length > 49) {
bucketName = bucketName.substring(0, 49);
}
final bucketId = await backblaze.createBucket(bucketName);
final key = await backblaze.createKey(bucketId);
final bucket = BackblazeBucket(
bucketId: bucketId,
bucketName: bucketName,
applicationKey: key.applicationKey,
applicationKeyId: key.applicationKeyId);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
await api.uploadBackblazeConfig(bucket);
await updateBackups();
emit(state.copyWith(isInitialized: true, preventActions: false));
}
Future<void> reuploadKey() async {
emit(state.copyWith(preventActions: true));
final bucket = getIt<ApiConfigModel>().backblazeBucket;
if (bucket == null) {
emit(state.copyWith(isInitialized: false));
} else {
await api.uploadBackblazeConfig(bucket);
emit(state.copyWith(isInitialized: true, preventActions: false));
getIt<NavigationService>().showSnackBar('providers.backup.reuploadedKey');
}
}
Duration refreshTimeFromState(BackupStatusEnum status) {
switch (status) {
case BackupStatusEnum.backingUp:
case BackupStatusEnum.restoring:
return Duration(seconds: 5);
case BackupStatusEnum.initializing:
return Duration(seconds: 10);
default:
return Duration(seconds: 60);
}
}
Future<void> updateBackups({bool useTimer = false}) async {
emit(state.copyWith(refreshing: true));
final backups = await api.getBackups();
final status = await api.getBackupStatus();
emit(state.copyWith(
backups: backups,
progress: status.progress,
status: status.status,
error: status.errorMessage,
refreshTimer: refreshTimeFromState(status.status),
refreshing: false,
));
if (useTimer)
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
}
Future<void> forceUpdateBackups() async {
emit(state.copyWith(preventActions: true));
await api.forceBackupListReload();
getIt<NavigationService>().showSnackBar('providers.backup.refetchingList');
emit(state.copyWith(preventActions: false));
}
Future<void> createBackup() async {
emit(state.copyWith(preventActions: true));
await api.startBackup();
await updateBackups();
emit(state.copyWith(preventActions: false));
}
Future<void> restoreBackup(String backupId) async {
emit(state.copyWith(preventActions: true));
await api.restoreBackup(backupId);
emit(state.copyWith(preventActions: false));
}
@override
void clear() async {
emit(BackupsState());
}
}

View File

@ -0,0 +1,56 @@
part of 'backups_cubit.dart';
class BackupsState extends AppConfigDependendState {
const BackupsState({
this.isInitialized = false,
this.backups = const [],
this.progress = 0.0,
this.status = BackupStatusEnum.noKey,
this.preventActions = true,
this.error = "",
this.refreshTimer = const Duration(seconds: 60),
this.refreshing = true,
});
final bool isInitialized;
final List<Backup> backups;
final double progress;
final BackupStatusEnum status;
final bool preventActions;
final String error;
final Duration refreshTimer;
final bool refreshing;
@override
List<Object> get props => [
isInitialized,
backups,
progress,
preventActions,
status,
error,
refreshTimer,
refreshing
];
BackupsState copyWith({
bool? isInitialized,
List<Backup>? backups,
double? progress,
BackupStatusEnum? status,
bool? preventActions,
String? error,
Duration? refreshTimer,
bool? refreshing,
}) =>
BackupsState(
isInitialized: isInitialized ?? this.isInitialized,
backups: backups ?? this.backups,
progress: progress ?? this.progress,
status: status ?? this.status,
preventActions: preventActions ?? this.preventActions,
error: error ?? this.error,
refreshTimer: refreshTimer ?? this.refreshTimer,
refreshing: refreshing ?? this.refreshing,
);
}

View File

@ -15,7 +15,8 @@ class HetznerFormCubit extends FormCubit {
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LegnthStringValidationWithLenghShowing(64, 'validations.length'.tr(args: ["64"]))
LegnthStringValidationWithLenghShowing(
64, 'validations.length'.tr(args: ["64"]))
],
);

View File

@ -25,8 +25,8 @@ class RootUserFormCubit extends FormCubit {
initalValue: '',
validations: [
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => passwordRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
ValidationModel<String>((s) => passwordRegExp.hasMatch(s),
'validations.invalid_format'.tr()),
],
);

View File

@ -68,6 +68,29 @@ class JobsCubit extends Cubit<JobsState> {
}
}
Future<void> rebootServer() async {
final isSuccessful = await api.reboot();
if (isSuccessful) {
getIt<NavigationService>().showSnackBar('jobs.rebootSuccess'.tr());
} else {
getIt<NavigationService>().showSnackBar('jobs.rebootFailed'.tr());
}
}
Future<void> upgradeServer() async {
final isPullSuccessful = await api.pullConfigurationUpdate();
final isSuccessful = await api.upgrade();
if (isSuccessful) {
if (!isPullSuccessful) {
getIt<NavigationService>().showSnackBar('jobs.configPullFailed'.tr());
} else {
getIt<NavigationService>().showSnackBar('jobs.upgradeSuccess'.tr());
}
} else {
getIt<NavigationService>().showSnackBar('jobs.upgradeFailed'.tr());
}
}
Future<void> applyAll() async {
if (state is JobsStateWithJobs) {
var jobs = (state as JobsStateWithJobs).jobList;
@ -89,6 +112,7 @@ class JobsCubit extends Cubit<JobsState> {
}
usersCubit.addUsers(newUsers);
await api.pullConfigurationUpdate();
await api.apply();
if (hasServiceJobs) {
await servicesCubit.load();

View File

@ -16,5 +16,3 @@ class ProvidersCubit extends Cubit<ProvidersState> {
emit(newState);
}
}

View File

@ -13,7 +13,7 @@ class UsersCubit extends Cubit<UsersState> {
void load() async {
var loadedUsers = box.values.toList();
if (loadedUsers.isNotEmpty) {
emit(UsersState(loadedUsers));
}

View File

@ -1,5 +1,6 @@
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/models/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/backblaze_credential.dart';
import 'package:selfprivacy/logic/models/cloudflare_domain.dart';
import 'package:selfprivacy/logic/models/server_details.dart';
@ -12,12 +13,14 @@ class ApiConfigModel {
String? get cloudFlareKey => _cloudFlareKey;
BackblazeCredential? get backblazeCredential => _backblazeCredential;
CloudFlareDomain? get cloudFlareDomain => _cloudFlareDomain;
BackblazeBucket? get backblazeBucket => _backblazeBucket;
String? _hetznerKey;
String? _cloudFlareKey;
HetznerServerDetails? _hetznerServer;
BackblazeCredential? _backblazeCredential;
CloudFlareDomain? _cloudFlareDomain;
BackblazeBucket? _backblazeBucket;
Future<void> storeHetznerKey(String value) async {
await _box.put(BNames.hetznerKey, value);
@ -45,12 +48,18 @@ class ApiConfigModel {
_hetznerServer = value;
}
Future<void> storeBackblazeBucket(BackblazeBucket value) async {
await _box.put(BNames.backblazeBucket, value);
_backblazeBucket = value;
}
clear() {
_hetznerKey = null;
_cloudFlareKey = null;
_backblazeCredential = null;
_cloudFlareDomain = null;
_hetznerServer = null;
_backblazeBucket = null;
}
void init() {
@ -60,5 +69,6 @@ class ApiConfigModel {
_backblazeCredential = _box.get(BNames.backblazeKey);
_cloudFlareDomain = _box.get(BNames.cloudFlareDomain);
_hetznerServer = _box.get(BNames.hetznerServer);
_backblazeBucket = _box.get(BNames.backblazeBucket);
}
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/text_themes.dart';

View File

@ -0,0 +1,29 @@
import 'package:hive/hive.dart';
part 'backblaze_bucket.g.dart';
@HiveType(typeId: 6)
class BackblazeBucket {
BackblazeBucket(
{required this.bucketId,
required this.bucketName,
required this.applicationKeyId,
required this.applicationKey});
@HiveField(0)
final String bucketId;
@HiveField(1)
final String applicationKeyId;
@HiveField(2)
final String applicationKey;
@HiveField(3)
final String bucketName;
@override
String toString() {
return '$bucketName';
}
}

View File

@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backblaze_bucket.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class BackblazeBucketAdapter extends TypeAdapter<BackblazeBucket> {
@override
final int typeId = 6;
@override
BackblazeBucket read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return BackblazeBucket(
bucketId: fields[0] as String,
bucketName: fields[3] as String,
applicationKeyId: fields[1] as String,
applicationKey: fields[2] as String,
);
}
@override
void write(BinaryWriter writer, BackblazeBucket obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.bucketId)
..writeByte(1)
..write(obj.applicationKeyId)
..writeByte(2)
..write(obj.applicationKey)
..writeByte(3)
..write(obj.bucketName);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackblazeBucketAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -0,0 +1,48 @@
import 'package:json_annotation/json_annotation.dart';
part 'backup.g.dart';
@JsonSerializable()
class Backup {
Backup({required this.time, required this.id});
// Time of the backup
final DateTime time;
@JsonKey(name: 'short_id')
final String id;
factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);
}
enum BackupStatusEnum {
@JsonValue('NO_KEY')
noKey,
@JsonValue('NOT_INITIALIZED')
notInitialized,
@JsonValue('INITIALIZED')
initialized,
@JsonValue('BACKING_UP')
backingUp,
@JsonValue('RESTORING')
restoring,
@JsonValue('ERROR')
error,
@JsonValue('INITIALIZING')
initializing,
}
@JsonSerializable()
class BackupStatus {
BackupStatus(
{required this.status,
required this.progress,
required this.errorMessage});
final BackupStatusEnum status;
final double progress;
@JsonKey(name: 'error_message')
final String? errorMessage;
factory BackupStatus.fromJson(Map<String, dynamic> json) =>
_$BackupStatusFromJson(json);
}

View File

@ -0,0 +1,58 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Backup _$BackupFromJson(Map<String, dynamic> json) {
return Backup(
time: DateTime.parse(json['time'] as String),
id: json['short_id'] as String,
);
}
BackupStatus _$BackupStatusFromJson(Map<String, dynamic> json) {
return BackupStatus(
status: _$enumDecode(_$BackupStatusEnumEnumMap, json['status']),
progress: (json['progress'] as num).toDouble(),
errorMessage: json['error_message'] as String?,
);
}
K _$enumDecode<K, V>(
Map<K, V> enumValues,
Object? source, {
K? unknownValue,
}) {
if (source == null) {
throw ArgumentError(
'A value must be provided. Supported values: '
'${enumValues.values.join(', ')}',
);
}
return enumValues.entries.singleWhere(
(e) => e.value == source,
orElse: () {
if (unknownValue == null) {
throw ArgumentError(
'`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}',
);
}
return MapEntry(unknownValue, enumValues.values.first);
},
).key;
}
const _$BackupStatusEnumEnumMap = {
BackupStatusEnum.noKey: 'NO_KEY',
BackupStatusEnum.notInitialized: 'NOT_INITIALIZED',
BackupStatusEnum.initialized: 'INITIALIZED',
BackupStatusEnum.backingUp: 'BACKING_UP',
BackupStatusEnum.restoring: 'RESTORING',
BackupStatusEnum.error: 'ERROR',
BackupStatusEnum.initializing: 'INITIALIZING',
};

View File

@ -9,6 +9,7 @@ class HetznerServerDetails {
required this.id,
required this.createTime,
required this.dataBase,
required this.apiToken,
this.startTime,
});
@ -27,6 +28,9 @@ class HetznerServerDetails {
@HiveField(4)
final HetznerDataBase dataBase;
@HiveField(5)
final String apiToken;
HetznerServerDetails copyWith({DateTime? startTime}) {
return HetznerServerDetails(
startTime: startTime ?? this.startTime,
@ -34,6 +38,7 @@ class HetznerServerDetails {
id: id,
ip4: ip4,
dataBase: dataBase,
apiToken: apiToken,
);
}

View File

@ -21,6 +21,7 @@ class HetznerServerDetailsAdapter extends TypeAdapter<HetznerServerDetails> {
id: fields[1] as int,
createTime: fields[3] as DateTime?,
dataBase: fields[4] as HetznerDataBase,
apiToken: fields[5] as String,
startTime: fields[2] as DateTime?,
);
}
@ -28,7 +29,7 @@ class HetznerServerDetailsAdapter extends TypeAdapter<HetznerServerDetails> {
@override
void write(BinaryWriter writer, HetznerServerDetails obj) {
writer
..writeByte(5)
..writeByte(6)
..writeByte(0)
..write(obj.ip4)
..writeByte(1)
@ -38,7 +39,9 @@ class HetznerServerDetailsAdapter extends TypeAdapter<HetznerServerDetails> {
..writeByte(2)
..write(obj.startTime)
..writeByte(4)
..write(obj.dataBase);
..write(obj.dataBase)
..writeByte(5)
..write(obj.apiToken);
}
@override

View File

@ -5,11 +5,11 @@ class BrandAlert extends AlertDialog {
Key? key,
String? title,
String? contentText,
List<Widget>? acitons,
List<Widget>? actions,
}) : super(
key: key,
title: title != null ? Text(title) : null,
content: title != null ? Text(contentText!) : null,
actions: acitons,
actions: actions,
);
}

View File

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';

View File

@ -13,4 +13,3 @@ class BrandDivider extends StatelessWidget {
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class BrandError extends StatelessWidget {
const BrandError({Key? key, this.error, this.stackTrace}) : super(key: key);

View File

@ -3,8 +3,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.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_loader/brand_loader.dart';
@ -22,6 +25,32 @@ class JobsContent extends StatelessWidget {
widgets = [
SizedBox(height: 80),
Center(child: BrandText.body1('jobs.empty'.tr())),
SizedBox(height: 80),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().upgradeServer(),
text: 'jobs.upgradeServer'.tr(),
),
SizedBox(height: 10),
BrandButton.text(
onPressed: () {
var nav = getIt<NavigationService>();
nav.showPopUpDialog(BrandAlert(
title: 'jobs.rebootServer'.tr(),
contentText: 'modals.3'.tr(),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () =>
{context.read<JobsCubit>().rebootServer()},
text: 'modals.9'.tr(),
)
],
));
},
title: 'jobs.rebootServer'.tr(),
),
];
} else if (state is JobsStateLoading) {
widgets = [

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/config/brand_colors.dart';

View File

@ -0,0 +1,246 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.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/components/icon_status_mask/icon_status_mask.dart';
import 'package:easy_localization/easy_localization.dart';
part 'header.dart';
var navigatorKey = GlobalKey<NavigatorState>();
class BackupDetails extends StatefulWidget {
const BackupDetails({Key? key}) : super(key: key);
@override
_BackupDetailsState createState() => _BackupDetailsState();
}
class _BackupDetailsState extends State<BackupDetails>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
var isBackupInitialized = context.watch<BackupsCubit>().state.isInitialized;
var backupStatus = context.watch<BackupsCubit>().state.status;
var providerState = isReady && isBackupInitialized
? (backupStatus == BackupStatusEnum.error
? StateType.warning
: StateType.stable)
: StateType.uninitialized;
var preventActions = context.watch<BackupsCubit>().state.preventActions;
var backupProgress = context.watch<BackupsCubit>().state.progress;
var backupError = context.watch<BackupsCubit>().state.error;
var backups = context.watch<BackupsCubit>().state.backups;
var refreshing = context.watch<BackupsCubit>().state.refreshing;
return Scaffold(
appBar: PreferredSize(
child: Column(
children: [
Container(
height: 51,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15),
child: BrandText.h4('basis.details'.tr()),
),
BrandDivider(),
],
),
preferredSize: Size.fromHeight(52),
),
body: SingleChildScrollView(
physics: ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: paddingH15V0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(
providerState: providerState,
refreshing: refreshing,
),
SizedBox(height: 10),
BrandText.h2('providers.backup.card_title'.tr()),
SizedBox(height: 10),
BrandText.body1('providers.backup.bottom_sheet.1'.tr()),
SizedBox(height: 20),
if (isReady && !isBackupInitialized)
BrandButton.rised(
onPressed: preventActions
? null
: () async {
await context.read<BackupsCubit>().createBucket();
},
text: 'providers.backup.initialize'.tr(),
),
if (backupStatus == BackupStatusEnum.initializing)
BrandText.body1('providers.backup.waitingForRebuild'.tr()),
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
elevation: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (backupStatus == BackupStatusEnum.initialized)
ListTile(
onTap: preventActions
? null
: () async {
await context
.read<BackupsCubit>()
.createBackup();
},
leading: Icon(
Icons.add_circle_outline_rounded,
color: BrandColors.textColor1,
),
title: BrandText.h5(
'providers.backup.create_new'.tr()),
),
if (backupStatus == BackupStatusEnum.backingUp)
ListTile(
title: BrandText.h5('providers.backup.creating'
.tr(args: [
(backupProgress * 100).round().toString()
])),
subtitle: LinearProgressIndicator(
value: backupProgress,
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.restoring)
ListTile(
title: BrandText.h5('providers.backup.restoring'
.tr(args: [
(backupProgress * 100).round().toString()
])),
subtitle: LinearProgressIndicator(
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.error)
ListTile(
leading: Icon(
Icons.error_outline,
color: BrandColors.red1,
),
title: BrandText.h5(
'providers.backup.error_pending'.tr()),
),
],
),
),
SizedBox(height: 16),
// Card with a list of existing backups
// Each list item has a date
// When clicked, starts the restore action
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
elevation: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Icon(
Icons.refresh,
color: BrandColors.textColor1,
),
title:
BrandText.h5('providers.backup.restore'.tr()),
),
Divider(
height: 1.0,
),
if (backups.isEmpty)
ListTile(
leading: Icon(
Icons.error_outline,
),
title: Text('providers.backup.no_backups'.tr()),
),
if (backups.isNotEmpty)
Column(
children: backups.map((backup) {
return ListTile(
onTap: preventActions
? null
: () {
var nav = getIt<NavigationService>();
nav.showPopUpDialog(BrandAlert(
title: 'providers.backup.restoring'
.tr(),
contentText:
'providers.backup.restore_alert'
.tr(args: [
backup.time.toString()
]),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
text: 'modals.yes'.tr(),
)
],
));
},
title: Text(
MaterialLocalizations.of(context)
.formatShortDate(backup.time) +
' ' +
TimeOfDay.fromDateTime(backup.time)
.format(context),
),
);
}).toList(),
),
],
),
),
if (backupStatus == BackupStatusEnum.error)
BrandText.body1(backupError.toString()),
],
),
),
SizedBox(height: 10),
],
),
),
);
}
}

View File

@ -0,0 +1,80 @@
part of 'backup_details.dart';
class _Header extends StatelessWidget {
const _Header(
{Key? key, required this.providerState, required this.refreshing})
: super(key: key);
final StateType providerState;
final bool refreshing;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconStatusMask(
status: providerState,
child: Icon(
BrandIcons.save,
size: 40,
color: Colors.white,
),
),
Spacer(),
Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 2,
),
child: IconButton(
onPressed: refreshing
? null
: () => {context.read<BackupsCubit>().updateBackups()},
icon: const Icon(Icons.refresh_rounded),
),
),
Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 2,
),
child: PopupMenuButton<_PopupMenuItemType>(
enabled: providerState != StateType.uninitialized,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
onSelected: (_PopupMenuItemType result) {
switch (result) {
case _PopupMenuItemType.reuploadKey:
context.read<BackupsCubit>().reuploadKey();
break;
case _PopupMenuItemType.refetchBackups:
context.read<BackupsCubit>().forceUpdateBackups();
break;
}
},
icon: Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => [
PopupMenuItem<_PopupMenuItemType>(
value: _PopupMenuItemType.reuploadKey,
child: Container(
padding: EdgeInsets.only(left: 5),
child: Text('providers.backup.reuploadKey'.tr()),
),
),
PopupMenuItem<_PopupMenuItemType>(
value: _PopupMenuItemType.refetchBackups,
child: Container(
padding: EdgeInsets.only(left: 5),
child: Text('providers.backup.refetchBackups'.tr()),
),
),
],
),
),
],
);
}
}
enum _PopupMenuItemType { reuploadKey, refetchBackups }

View File

@ -97,7 +97,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
return BrandAlert(
title: 'modals.3'.tr(),
contentText: 'modals.4'.tr(),
acitons: [
actions: [
ActionButton(
text: 'modals.5'.tr(),
isRed: true,
@ -167,7 +167,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
return BrandAlert(
title: 'modals.3'.tr(),
contentText: 'modals.6'.tr(),
acitons: [
actions: [
ActionButton(
text: 'modals.7'.tr(),
isRed: true,

View File

@ -105,7 +105,7 @@ class MorePage extends StatelessWidget {
title: 'modals.3'.tr(),
contentText:
'more.delete_ssh_text'.tr(),
acitons: [
actions: [
ActionButton(
text: 'more.yes_delete'.tr(),
isRed: true,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/models/provider.dart';
@ -12,6 +13,7 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart';
import 'package:selfprivacy/ui/pages/server_details/server_details.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
@ -29,6 +31,7 @@ class _ProvidersPageState extends State<ProvidersPage> {
@override
Widget build(BuildContext context) {
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
var isBackupInitialized = context.watch<BackupsCubit>().state.isInitialized;
final cards = ProviderType.values
.map(
@ -36,7 +39,11 @@ class _ProvidersPageState extends State<ProvidersPage> {
padding: EdgeInsets.only(bottom: 30),
child: _Card(
provider: ProviderModel(
state: isReady ? StateType.stable : StateType.uninitialized,
state: isReady
? (type == ProviderType.backup && !isBackupInitialized
? StateType.uninitialized
: StateType.stable)
: StateType.uninitialized,
type: type,
),
),
@ -113,14 +120,12 @@ class _Card extends StatelessWidget {
title = 'providers.backup.card_title'.tr();
stableText = 'providers.backup.status'.tr();
onTap = () => showModalBottomSheet<void>(
onTap = () => showBrandBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return _ProviderDetails(
provider: provider,
statusText: stableText,
return BrandBottomSheet(
isExpended: true,
child: BackupDetails(),
);
},
);

View File

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
extension ElevationExtension on BoxDecoration {
BoxDecoration copyWith({
Color? color,
DecorationImage? image,

View File

@ -7,4 +7,4 @@ class NamedFontWeight {
static const FontWeight demiBold = FontWeight.w600;
static const FontWeight bold = FontWeight.bold;
static const FontWeight extraBold = FontWeight.w800;
}
}

View File

@ -96,4 +96,11 @@ class StringGenerators {
hasUppercaseLetters: true,
hasNumbers: true,
);
static StringGeneratorFunction apiToken = () => getRandomString(
64,
hasLowercaseLetters: true,
hasUppercaseLetters: true,
hasNumbers: true,
);
}

View File

@ -37,7 +37,8 @@ class SlideRightRoute extends PageRouteBuilder {
SlideRightRoute(this.widget)
: super(
pageBuilder: pageBuilder(widget),
transitionsBuilder: transitionsBuilder as Widget Function(BuildContext, Animation<double>, Animation<double>, Widget),
transitionsBuilder: transitionsBuilder as Widget Function(
BuildContext, Animation<double>, Animation<double>, Widget),
);
final Widget widget;

View File

@ -1,6 +1,6 @@
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
/// it's ui helpers use only for ui components, don't use for logic components.
/// it's ui helpers use only for ui components, don't use for logic components.
class UiHelpers {
static String getDomainName(AppConfigState config) => config.isDomainFilled

View File

@ -1,7 +1,7 @@
name: selfprivacy
description: selfprivacy.org
publish_to: 'none'
version: 0.2.4+6
version: 0.4.1+9
environment:
sdk: '>=2.13.4 <3.0.0'