Backups and server upgrade

pull/81/head
Inex Code 2021-12-06 18:31:19 +00:00
parent 650e0e7376
commit b40bea63d1
36 changed files with 1012 additions and 40 deletions

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

@ -4,7 +4,6 @@ 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';
import 'package:selfprivacy/logic/models/hetzner_server_info.dart';
import 'package:selfprivacy/logic/models/server_details.dart';
import 'package:selfprivacy/logic/models/user.dart';
@ -94,7 +93,6 @@ class HetznerApi extends ApiMap {
required User rootUser,
required String domainName,
required HetznerDataBase dataBase,
required BackblazeCredential backblazeCredential,
}) async {
var client = await getClient();
@ -122,7 +120,7 @@ class HetznerApi extends ApiMap {
/// 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/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 BACKBLAZE_KEY_ID=${backblazeCredential.keyId} BACKBLAZE_ACCOUNT_KEY=${backblazeCredential.applicationKey} API_TOKEN=$apiToken HOSTNAME=$hostname 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/rolling-testing/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',

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';
@ -23,7 +25,7 @@ class ServerApi extends ApiMap {
var apiToken = getIt<ApiConfigModel>().hetznerServer?.apiToken;
options = BaseOptions(baseUrl: 'https://api.$domainName', headers: {
'Authorization': 'Bearer ${apiToken}',
'Authorization': 'Bearer $apiToken',
});
}
@ -123,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((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

@ -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';
@ -127,7 +125,6 @@ class AppConfigRepository {
rootUser: rootUser,
domainName: domainName,
dataBase: dataBase,
backblazeCredential: backblazeCredential,
);
saveServerDetails(serverDetails);
onSuccess(serverDetails);
@ -138,7 +135,7 @@ class AppConfigRepository {
BrandAlert(
title: 'modals.1'.tr(),
contentText: 'modals.2'.tr(),
acitons: [
actions: [
ActionButton(
text: 'basis.delete'.tr(),
isRed: true,
@ -151,7 +148,6 @@ class AppConfigRepository {
rootUser: rootUser,
domainName: domainName,
dataBase: dataBase,
backblazeCredential: backblazeCredential,
);
await saveServerDetails(serverDetails);

View File

@ -0,0 +1,168 @@
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> 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

@ -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,239 @@
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(
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,70 @@
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;
}
},
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()),
),
),
],
),
),
],
);
}
}
enum _PopupMenuItemType { reuploadKey }

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,7 +96,7 @@ class StringGenerators {
hasUppercaseLetters: true,
hasNumbers: true,
);
static StringGeneratorFunction apiToken = () => getRandomString(
64,
hasLowercaseLetters: 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