feat: BackupsProvider and TokensBloc

This commit is contained in:
Inex Code 2024-07-30 01:18:54 +03:00
parent 1c7724347f
commit 74eb1135df
36 changed files with 1058 additions and 96 deletions

View file

@ -668,5 +668,19 @@
"australia": "Australia",
"united_states": "United States",
"finland": "Finland"
},
"tokens": {
"title": "Provider tokens",
"description": "These tokens are stored on this device and are used to connect SelfPrivacy to your server provider, DNS provider, and backup storage provider.",
"server_provider_tokens": "Server provider tokens",
"dns_provider_tokens": "DNS provider tokens",
"backup_provider_tokens": "Backup storage provider tokens",
"valid": "Valid",
"invalid": "Invalid",
"no_access": "Has no access to associated resources",
"loading": "Loading token status",
"used_by": "Used for {servers}.",
"check_again": "Check again",
"no_tokens": "No tokens"
}
}

View file

@ -8,6 +8,7 @@ import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_logs/server_logs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/tokens/tokens_bloc.dart';
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
@ -40,6 +41,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
late final VolumesBloc volumesBloc;
late final ServerLogsBloc serverLogsBloc;
late final OutdatedServerCheckerBloc outdatedServerCheckerBloc;
late final TokensBloc tokensBloc;
@override
void initState() {
@ -58,6 +60,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
volumesBloc = VolumesBloc();
serverLogsBloc = ServerLogsBloc();
outdatedServerCheckerBloc = OutdatedServerCheckerBloc();
tokensBloc = TokensBloc();
}
@override
@ -106,6 +109,9 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
BlocProvider(
create: (final _) => outdatedServerCheckerBloc,
),
BlocProvider(
create: (final _) => tokensBloc,
),
],
child: widget.child,
);

View file

@ -29,7 +29,20 @@ class BackblazeApplicationKey {
}
class BackblazeApi extends RestApiMap {
BackblazeApi({this.hasLogger = false, this.isWithToken = true});
BackblazeApi({
this.token = '',
this.tokenId = '',
this.hasLogger = false,
this.isWithToken = true,
}) : assert(isWithToken ? token.isNotEmpty && tokenId.isNotEmpty : true);
@override
bool hasLogger;
@override
bool isWithToken;
final String token;
final String tokenId;
@override
BaseOptions get options {
@ -39,10 +52,8 @@ class BackblazeApi extends RestApiMap {
responseType: ResponseType.json,
);
if (isWithToken) {
final BackupsCredential? backblazeCredential =
getIt<ResourcesModel>().backblazeCredential;
final String token = backblazeCredential!.applicationKey;
options.headers = {'Authorization': 'Basic $token'};
final encodedApiKey = encodedBackblazeKey(tokenId, token);
options.headers = {'Authorization': 'Basic $encodedApiKey'};
}
if (validateStatus != null) {
@ -59,14 +70,12 @@ class BackblazeApi extends RestApiMap {
Future<BackblazeApiAuth> getAuthorizationToken() async {
final Dio client = await getClient();
final BackupsCredential? backblazeCredential =
getIt<ResourcesModel>().backblazeCredential;
if (backblazeCredential == null) {
if (token.isEmpty || tokenId.isEmpty) {
throw Exception('Backblaze credential is null');
}
final String encodedApiKey = encodedBackblazeKey(
backblazeCredential.keyId,
backblazeCredential.applicationKey,
tokenId,
token,
);
final Response response = await client.get(
'b2_authorize_account',
@ -122,16 +131,14 @@ class BackblazeApi extends RestApiMap {
}
// Create bucket
Future<String> createBucket(final String bucketName) async {
Future<GenericResult<String>> createBucket(final String bucketName) async {
final BackblazeApiAuth auth = await getAuthorizationToken();
final BackupsCredential? backblazeCredential =
getIt<ResourcesModel>().backblazeCredential;
final Dio client = await getClient();
client.options.baseUrl = auth.apiUrl;
final Response response = await client.post(
'$apiPrefix/b2_create_bucket',
data: {
'accountId': backblazeCredential!.keyId,
'accountId': tokenId,
'bucketName': bucketName,
'bucketType': 'allPrivate',
'lifecycleRules': [
@ -148,14 +155,23 @@ class BackblazeApi extends RestApiMap {
);
close(client);
if (response.statusCode == HttpStatus.ok) {
return response.data['bucketId'];
return GenericResult(
data: response.data['bucketId'],
success: true,
);
} else {
throw Exception('code: ${response.statusCode}');
return GenericResult(
data: '',
success: false,
message: 'code: ${response.statusCode}, ${response.data}',
);
}
}
// Create a limited capability key with access to the given bucket
Future<BackblazeApplicationKey> createKey(final String bucketId) async {
Future<GenericResult<BackblazeApplicationKey>> createKey(
final String bucketId,
) async {
final BackblazeApiAuth auth = await getAuthorizationToken();
final Dio client = await getClient();
client.options.baseUrl = auth.apiUrl;
@ -173,16 +189,26 @@ class BackblazeApi extends RestApiMap {
);
close(client);
if (response.statusCode == HttpStatus.ok) {
return BackblazeApplicationKey(
applicationKeyId: response.data['applicationKeyId'],
applicationKey: response.data['applicationKey'],
return GenericResult(
success: true,
data: BackblazeApplicationKey(
applicationKeyId: response.data['applicationKeyId'],
applicationKey: response.data['applicationKey'],
),
);
} else {
throw Exception('code: ${response.statusCode}');
return GenericResult(
success: false,
data: BackblazeApplicationKey(
applicationKeyId: '',
applicationKey: '',
),
message: 'code: ${response.statusCode}, ${response.data}',
);
}
}
Future<BackblazeBucket?> fetchBucket(
Future<GenericResult<BackblazeBucket?>> fetchBucket(
final BackupsCredential credentials,
final BackupConfiguration configuration,
) async {
@ -212,15 +238,16 @@ class BackblazeApi extends RestApiMap {
);
}
}
return bucket;
return GenericResult(
success: bucket != null,
data: bucket,
);
} else {
throw Exception('code: ${response.statusCode}');
return GenericResult(
success: false,
data: null,
message: 'code: ${response.statusCode}, ${response.data}',
);
}
}
@override
bool hasLogger;
@override
bool isWithToken;
}

View file

@ -1,23 +1,24 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/cloudflare_dns_info.dart';
class CloudflareApi extends RestApiMap {
CloudflareApi({
this.token = '',
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
}) : assert(isWithToken ? token.isNotEmpty : true);
@override
final bool hasLogger;
@override
final bool isWithToken;
final String token;
final String? customToken;
@override
@ -28,8 +29,7 @@ class CloudflareApi extends RestApiMap {
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ResourcesModel>().dnsProviderKey;
assert(token != null);
assert(token.isNotEmpty);
options.headers = {'Authorization': 'Bearer $token'};
}

View file

@ -1,23 +1,24 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/desec_dns_info.dart';
class DesecApi extends RestApiMap {
DesecApi({
this.token = '',
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
}) : assert(isWithToken ? token.isNotEmpty : true);
@override
final bool hasLogger;
@override
final bool isWithToken;
final String token;
final String? customToken;
@override
@ -28,8 +29,7 @@ class DesecApi extends RestApiMap {
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ResourcesModel>().dnsProviderKey;
assert(token != null);
assert(token.isNotEmpty);
options.headers = {'Authorization': 'Token $token'};
}

View file

@ -1,23 +1,24 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/digital_ocean_dns_info.dart';
class DigitalOceanDnsApi extends RestApiMap {
DigitalOceanDnsApi({
this.token = '',
this.hasLogger = false,
this.isWithToken = true,
this.customToken,
});
}) : assert(isWithToken ? token.isNotEmpty : true);
@override
final bool hasLogger;
@override
final bool isWithToken;
final String token;
final String? customToken;
@override
@ -28,8 +29,7 @@ class DigitalOceanDnsApi extends RestApiMap {
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ResourcesModel>().dnsProviderKey;
assert(token != null);
assert(token.isNotEmpty);
options.headers = {'Authorization': 'Bearer $token'};
}

View file

@ -1,11 +1,9 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/digital_ocean_server_info.dart';
import 'package:selfprivacy/utils/password_generator.dart';
@ -13,15 +11,18 @@ import 'package:selfprivacy/utils/password_generator.dart';
class DigitalOceanApi extends RestApiMap {
DigitalOceanApi({
required this.region,
this.token = '',
this.hasLogger = true,
this.isWithToken = true,
});
}) : assert(isWithToken ? token.isNotEmpty : true);
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
final String token;
@override
BaseOptions get options {
@ -31,8 +32,7 @@ class DigitalOceanApi extends RestApiMap {
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ResourcesModel>().serverProviderKey;
assert(token != null);
assert(token.isNotEmpty);
options.headers = {'Authorization': 'Bearer $token'};
}

View file

@ -1,11 +1,9 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/rest_api_map.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart';
@ -14,15 +12,18 @@ import 'package:selfprivacy/utils/password_generator.dart';
class HetznerApi extends RestApiMap {
HetznerApi({
this.region,
this.token = '',
this.hasLogger = true,
this.isWithToken = true,
});
}) : assert(isWithToken ? token.isNotEmpty : true);
@override
bool hasLogger;
@override
bool isWithToken;
final String? region;
final String token;
@override
BaseOptions get options {
@ -32,8 +33,7 @@ class HetznerApi extends RestApiMap {
responseType: ResponseType.json,
);
if (isWithToken) {
final String? token = getIt<ResourcesModel>().serverProviderKey;
assert(token != null);
assert(token.isNotEmpty);
options.headers = {'Authorization': 'Bearer $token'};
}

View file

@ -5,7 +5,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
@ -13,6 +12,9 @@ import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/initialize_repository_input.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider_factory.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
part 'backups_event.dart';
part 'backups_state.dart';
@ -103,8 +105,6 @@ class BackupsBloc extends Bloc<BackupsEvent, BackupsState> {
}
}
final BackblazeApi backblaze = BackblazeApi();
Future<void> _loadState(
final BackupsServerLoaded event,
final Emitter<BackupsState> emit,
@ -166,6 +166,14 @@ class BackupsBloc extends Bloc<BackupsEvent, BackupsState> {
final BackblazeBucket bucket;
if (state.backblazeBucket == null) {
final settings = BackupsProviderSettings(
provider: BackupsProviderType.backblaze,
tokenId: event.credential.keyId,
token: event.credential.applicationKey,
isAuthorized: true,
);
final provider =
BackupsProviderFactory.createBackupsProviderInterface(settings);
final String domain = getIt<ApiConnectionRepository>()
.serverDomain!
.domainName
@ -176,9 +184,30 @@ class BackupsBloc extends Bloc<BackupsEvent, BackupsState> {
if (bucketName.length > 49) {
bucketName = bucketName.substring(0, 49);
}
final String bucketId = await backblaze.createBucket(bucketName);
final BackblazeApplicationKey key = await backblaze.createKey(bucketId);
final createStorageResult = await provider.createStorage(bucketName);
if (createStorageResult.success == false ||
createStorageResult.data.isEmpty) {
getIt<NavigationService>().showSnackBar(
createStorageResult.message ??
"Couldn't create storage on your server.",
);
emit(BackupsUnititialized());
return;
}
final String bucketId = createStorageResult.data;
final BackupsApplicationKey? key =
(await provider.createApplicationKey(bucketId)).data;
if (key == null) {
getIt<NavigationService>().showSnackBar(
"Couldn't create application key on your server.",
);
emit(BackupsUnititialized());
return;
}
bucket = BackblazeBucket(
bucketId: bucketId,
bucketName: bucketName,

View file

@ -19,7 +19,11 @@ class BackupsServerReset extends BackupsEvent {
}
class InitializeBackupsRepository extends BackupsEvent {
const InitializeBackupsRepository();
const InitializeBackupsRepository(
this.credential,
);
final BackupsCredential credential;
@override
List<Object?> get props => [];

View file

@ -0,0 +1,164 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/hive/dns_provider_credential.dart';
import 'package:selfprivacy/logic/models/hive/server.dart';
import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider_factory.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider_factory.dart';
part 'tokens_event.dart';
part 'tokens_state.dart';
class TokensBloc extends Bloc<TokensEvent, TokensState> {
TokensBloc() : super(const TokensInitial()) {
on<RevalidateTokens>(
validateTokens,
transformer: droppable(),
);
add(const RevalidateTokens());
_resourcesModelSubscription =
getIt<ResourcesModel>().statusStream.listen((final _) {
add(const RevalidateTokens());
});
}
Future<void> validateTokens(
final RevalidateTokens event,
final Emitter<TokensState> emit,
) async {
emit(const TokensInitial());
final List<ServerProviderCredential> serverProviderCredentials =
getIt<ResourcesModel>().serverProviderCredentials;
final List<DnsProviderCredential> dnsProviderCredentials =
getIt<ResourcesModel>().dnsProviderCredentials;
final List<BackupsCredential> backupsCredentials =
getIt<ResourcesModel>().backupsCredentials;
final List<TokenStatusWrapper<ServerProviderCredential>>
validatedServerProviderCredentials = [];
final List<TokenStatusWrapper<DnsProviderCredential>>
validatedDnsProviderCredentials = [];
final List<TokenStatusWrapper<BackupsCredential>>
validatedBackupsCredentials = [];
for (final credential in serverProviderCredentials) {
final TokenStatus status = await _validateServerProviderToken(credential);
validatedServerProviderCredentials
.add(TokenStatusWrapper(data: credential, status: status));
}
for (final credential in dnsProviderCredentials) {
final TokenStatus status = await _validateDnsProviderToken(credential);
validatedDnsProviderCredentials
.add(TokenStatusWrapper(data: credential, status: status));
}
for (final credential in backupsCredentials) {
final TokenStatus status = await _validateBackupsToken(credential);
validatedBackupsCredentials
.add(TokenStatusWrapper(data: credential, status: status));
}
emit(
TokensChecked(
serverProviderCredentials: validatedServerProviderCredentials,
dnsProviderCredentials: validatedDnsProviderCredentials,
backupsCredentials: validatedBackupsCredentials,
),
);
}
Future<TokenStatus> _validateServerProviderToken(
final ServerProviderCredential credential,
) async {
final ServerProviderSettings settings = ServerProviderSettings(
provider: credential.provider,
token: credential.token,
isAuthorized: true,
);
final serverProvider =
ServerProviderFactory.createServerProviderInterface(settings);
// First, we check if the token works at all
final basicInitCheckResult =
await serverProvider.tryInitApiByToken(credential.token);
if (!basicInitCheckResult.data) {
return TokenStatus.invalid;
}
// Now, if there are associated servers, check if we have access to them
if (credential.associatedServerIds.isNotEmpty) {
final servers = (await serverProvider.getServers()).data;
for (final serverId in credential.associatedServerIds) {
if (!servers.any((final server) => server.id == serverId)) {
return TokenStatus.noAccess;
}
}
}
return TokenStatus.valid;
}
Future<TokenStatus> _validateDnsProviderToken(
final DnsProviderCredential credential,
) async {
final DnsProviderSettings settings = DnsProviderSettings(
provider: credential.provider,
token: credential.token,
isAuthorized: true,
);
final dnsProvider = DnsProviderFactory.createDnsProviderInterface(settings);
final basicInitCheckResult =
await dnsProvider.tryInitApiByToken(credential.token);
if (!basicInitCheckResult.data) {
return TokenStatus.invalid;
}
if (credential.associatedDomainNames.isNotEmpty) {
final domains = (await dnsProvider.domainList()).data;
for (final domainName in credential.associatedDomainNames) {
if (!domains.any((final domain) => domain.domainName == domainName)) {
return TokenStatus.noAccess;
}
}
}
return TokenStatus.valid;
}
Future<TokenStatus> _validateBackupsToken(
final BackupsCredential credential,
) async {
final BackupsProviderSettings settings = BackupsProviderSettings(
provider: credential.provider,
token: credential.applicationKey,
tokenId: credential.keyId,
isAuthorized: true,
);
final backupsProvider =
BackupsProviderFactory.createBackupsProviderInterface(settings);
final basicInitCheckResult = await backupsProvider.tryInitApiByToken(
encodedBackblazeKey(
credential.keyId,
credential.applicationKey,
),
);
if (!basicInitCheckResult.data) {
return TokenStatus.invalid;
}
return TokenStatus.valid;
}
@override
Future<void> close() {
_resourcesModelSubscription.cancel();
return super.close();
}
late StreamSubscription _resourcesModelSubscription;
}

View file

@ -0,0 +1,12 @@
part of 'tokens_bloc.dart';
sealed class TokensEvent extends Equatable {
const TokensEvent();
}
class RevalidateTokens extends TokensEvent {
const RevalidateTokens();
@override
List<Object> get props => [];
}

View file

@ -0,0 +1,110 @@
part of 'tokens_bloc.dart';
enum TokenStatus {
loading,
valid,
invalid,
noAccess,
}
class TokenStatusWrapper<T> {
TokenStatusWrapper({
required this.data,
required this.status,
});
final T data;
final TokenStatus status;
}
sealed class TokensState extends Equatable {
const TokensState();
List<TokenStatusWrapper<ServerProviderCredential>>
get serverProviderCredentials;
List<TokenStatusWrapper<DnsProviderCredential>> get dnsProviderCredentials;
List<TokenStatusWrapper<BackupsCredential>> get backupsCredentials;
List<Server> get servers => _servers;
Server getServerById(final int serverId) => servers.firstWhere(
(final Server server) => server.hostingDetails.id == serverId,
);
List<ServerProviderCredential> get _serverProviderCredentials =>
getIt<ResourcesModel>().serverProviderCredentials;
List<DnsProviderCredential> get _dnsProviderCredentials =>
getIt<ResourcesModel>().dnsProviderCredentials;
List<BackupsCredential> get _backupsCredentials =>
getIt<ResourcesModel>().backupsCredentials;
List<Server> get _servers => getIt<ResourcesModel>().servers;
}
final class TokensInitial extends TokensState {
const TokensInitial();
@override
List<TokenStatusWrapper<ServerProviderCredential>>
get serverProviderCredentials => _serverProviderCredentials
.map(
(final ServerProviderCredential serverProviderCredential) =>
TokenStatusWrapper<ServerProviderCredential>(
data: serverProviderCredential,
status: TokenStatus.loading,
),
)
.toList();
@override
List<TokenStatusWrapper<DnsProviderCredential>> get dnsProviderCredentials =>
_dnsProviderCredentials
.map(
(final DnsProviderCredential dnsProviderCredential) =>
TokenStatusWrapper<DnsProviderCredential>(
data: dnsProviderCredential,
status: TokenStatus.loading,
),
)
.toList();
@override
List<TokenStatusWrapper<BackupsCredential>> get backupsCredentials =>
_backupsCredentials
.map(
(final BackupsCredential backupsCredential) =>
TokenStatusWrapper<BackupsCredential>(
data: backupsCredential,
status: TokenStatus.loading,
),
)
.toList();
@override
List<Server> get servers => _servers;
@override
List<Object> get props => [];
}
final class TokensChecked extends TokensState {
const TokensChecked({
required this.serverProviderCredentials,
required this.dnsProviderCredentials,
required this.backupsCredentials,
});
@override
final List<TokenStatusWrapper<ServerProviderCredential>>
serverProviderCredentials;
@override
final List<TokenStatusWrapper<DnsProviderCredential>> dnsProviderCredentials;
@override
final List<TokenStatusWrapper<BackupsCredential>> backupsCredentials;
@override
List<Object> get props => [
serverProviderCredentials,
dnsProviderCredentials,
backupsCredentials,
servers,
];
}

View file

@ -5,7 +5,6 @@ import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
@ -21,6 +20,7 @@ import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider_factory.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
@ -234,8 +234,17 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
).getBackupsConfiguration();
if (configuration != null) {
try {
bucket = await BackblazeApi()
.fetchBucket(backblazeCredential, configuration);
final settings = BackupsProviderSettings(
provider: BackupsProviderType.backblaze,
tokenId: keyId,
token: applicationKey,
isAuthorized: true,
);
final provider =
BackupsProviderFactory.createBackupsProviderInterface(settings);
final result =
await provider.getStorage(backblazeCredential, configuration);
bucket = result.data;
await getIt<ApiConfigModel>().setBackblazeBucket(bucket!);
} catch (e) {
print(e);

View file

@ -66,6 +66,7 @@ class ServerInstallationRepository {
provider: serverProvider ?? serverDetails!.provider,
isAuthorized: providerApiToken != null,
location: location,
token: providerApiToken,
),
);
}
@ -77,6 +78,7 @@ class ServerInstallationRepository {
DnsProviderSettings(
isAuthorized: dnsApiToken != null,
provider: dnsProvider ?? serverDomain!.provider,
token: dnsApiToken,
),
);
}
@ -461,6 +463,14 @@ class ServerInstallationRepository {
associatedServerIds: [],
),
);
ProvidersController.initServerProvider(
ServerProviderSettings(
provider:
getIt<WizardDataModel>().serverInstallation!.serverProviderType!,
token: key,
isAuthorized: true,
),
);
}
Future<void> saveServerType(final ServerType serverType) async {
@ -488,6 +498,13 @@ class ServerInstallationRepository {
associatedDomainNames: [],
),
);
ProvidersController.initDnsProvider(
DnsProviderSettings(
provider: getIt<WizardDataModel>().serverInstallation!.dnsProviderType!,
token: key,
isAuthorized: true,
),
);
}
Future<void> saveDomain(final ServerDomain serverDomain) async {

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
@ -10,9 +12,43 @@ import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/hive/wizards_data/server_installation_wizard_data.dart';
sealed class ResourcesModelEvent {
const ResourcesModelEvent();
}
class ResourcesModelLoaded extends ResourcesModelEvent {
const ResourcesModelLoaded();
}
class ChangedServerProviderCredentials extends ResourcesModelEvent {
const ChangedServerProviderCredentials();
}
class ChangedDnsProviderCredentials extends ResourcesModelEvent {
const ChangedDnsProviderCredentials();
}
class ChangedBackupsCredentials extends ResourcesModelEvent {
const ChangedBackupsCredentials();
}
class ChangedServers extends ResourcesModelEvent {
const ChangedServers();
}
class ClearedModel extends ResourcesModelEvent {
const ClearedModel();
}
class ResourcesModel {
final Box _box = Hive.box(BNames.resourcesBox);
final _statusStreamController =
StreamController<ResourcesModelEvent>.broadcast();
Stream<ResourcesModelEvent> get statusStream =>
_statusStreamController.stream;
List<ServerProviderCredential> get serverProviderCredentials =>
_serverProviderTokens;
List<DnsProviderCredential> get dnsProviderCredentials => _dnsProviderTokens;
@ -55,6 +91,7 @@ class ResourcesModel {
) async {
_serverProviderTokens.add(token);
await _box.put(BNames.serverProviderTokens, _serverProviderTokens);
_statusStreamController.add(const ChangedServerProviderCredentials());
}
Future<void> associateServerWithToken(
@ -68,6 +105,7 @@ class ResourcesModel {
.associatedServerIds
.add(serverId);
await _box.put(BNames.serverProviderTokens, _serverProviderTokens);
_statusStreamController.add(const ChangedServerProviderCredentials());
}
Future<void> removeServerProviderToken(
@ -75,6 +113,7 @@ class ResourcesModel {
) async {
_serverProviderTokens.remove(token);
await _box.put(BNames.serverProviderTokens, _serverProviderTokens);
_statusStreamController.add(const ChangedServerProviderCredentials());
}
Future<void> addDnsProviderToken(final DnsProviderCredential token) async {
@ -85,6 +124,7 @@ class ResourcesModel {
}
_dnsProviderTokens.add(token);
await _box.put(BNames.dnsProviderTokens, _dnsProviderTokens);
_statusStreamController.add(const ChangedDnsProviderCredentials());
}
Future<void> associateDomainWithToken(
@ -98,16 +138,19 @@ class ResourcesModel {
.associatedDomainNames
.add(domain);
await _box.put(BNames.dnsProviderTokens, _dnsProviderTokens);
_statusStreamController.add(const ChangedDnsProviderCredentials());
}
Future<void> removeDnsProviderToken(final DnsProviderCredential token) async {
_dnsProviderTokens.remove(token);
await _box.put(BNames.dnsProviderTokens, _dnsProviderTokens);
_statusStreamController.add(const ChangedDnsProviderCredentials());
}
Future<void> addBackupsCredential(final BackupsCredential credential) async {
_backupsCredentials.add(credential);
await _box.put(BNames.backupsProviderTokens, _backupsCredentials);
_statusStreamController.add(const ChangedBackupsCredentials());
}
Future<void> removeBackupsCredential(
@ -115,16 +158,19 @@ class ResourcesModel {
) async {
_backupsCredentials.remove(credential);
await _box.put(BNames.backupsProviderTokens, _backupsCredentials);
_statusStreamController.add(const ChangedBackupsCredentials());
}
Future<void> addServer(final Server server) async {
_servers.add(server);
await _box.put(BNames.servers, _servers);
_statusStreamController.add(const ChangedServers());
}
Future<void> removeServer(final Server server) async {
_servers.remove(server);
await _box.put(BNames.servers, _servers);
_statusStreamController.add(const ChangedServers());
}
Future<void> setBackblazeBucket(final BackblazeBucket bucket) async {
@ -146,6 +192,12 @@ class ResourcesModel {
_box.clear();
_box.compact();
_statusStreamController.add(const ClearedModel());
}
void dispose() {
_statusStreamController.close();
}
void init() {
@ -180,6 +232,8 @@ class ResourcesModel {
.map<Server>((final e) => e as Server)
.toList();
_backblazeBucket = _box.get(BNames.backblazeBucket);
_statusStreamController.add(const ResourcesModelLoaded());
}
}

View file

@ -0,0 +1,93 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider.dart';
class ApiAdapter {
ApiAdapter({
final String? token,
final String? tokenId,
}) : _api = BackblazeApi(
isWithToken: true,
token: token ?? '',
tokenId: tokenId ?? '',
);
BackblazeApi api({final bool getInitialized = true}) => getInitialized
? _api
: BackblazeApi(
isWithToken: false,
);
final BackblazeApi _api;
}
class BackblazeBackupsProvider extends BackupsProvider {
BackblazeBackupsProvider() : _adapter = ApiAdapter();
BackblazeBackupsProvider.load(
final bool isAuthorized,
final String? token,
final String? tokenId,
) : _adapter = ApiAdapter(
token: token,
tokenId: tokenId,
);
final ApiAdapter _adapter;
@override
BackupsProviderType get type => BackupsProviderType.backblaze;
@override
String get howToRegister => 'how_backblaze';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);
final result = await api.isApiTokenValid(token);
if (!result.data || !result.success) {
return result;
}
return result;
}
@override
Future<GenericResult<String>> createStorage(final String desiredName) async {
final api = _adapter.api();
final result = await api.createBucket(desiredName);
return result;
}
@override
Future<GenericResult<BackupsApplicationKey?>> createApplicationKey(
final String storageId,
) async {
final api = _adapter.api();
final result = await api.createKey(storageId);
if (!result.success) {
return GenericResult(
success: false,
data: null,
message: result.message,
);
}
return GenericResult(
success: result.success,
data: BackupsApplicationKey(
applicationKeyId: result.data.applicationKeyId,
applicationKey: result.data.applicationKey,
),
);
}
@override
Future<GenericResult<BackblazeBucket?>> getStorage(
final BackupsCredential credentials,
final BackupConfiguration configuration,
) async {
final api = _adapter.api();
final result = await api.fetchBucket(credentials, configuration);
return result;
}
}

View file

@ -0,0 +1,47 @@
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
class BackupsApplicationKey {
BackupsApplicationKey({
required this.applicationKeyId,
required this.applicationKey,
});
final String applicationKeyId;
final String applicationKey;
}
abstract class BackupsProvider {
/// Returns an assigned enum value, respectively to which
/// provider implements [BackupsProvider] interface.
BackupsProviderType get type;
/// Returns a full url to a guide on how to setup
/// backups provider
String get howToRegister;
/// Tries to access an account linked to the provided token.
///
/// To generate a token for your account follow instructions of your
/// backups provider respectfully.
Future<GenericResult<bool>> tryInitApiByToken(final String token);
/// Creates a storage for backups (for example, a bucket)
/// and returns a storage ID to access it.
Future<GenericResult<String>> createStorage(final String desiredName);
/// Creates the credentials to access the backups storage
/// from the server
Future<GenericResult<BackupsApplicationKey?>> createApplicationKey(
final String storageId,
);
/// Get the backups storage
Future<GenericResult<BackblazeBucket?>> getStorage(
final BackupsCredential credentials,
final BackupConfiguration configuration,
);
}

View file

@ -0,0 +1,30 @@
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backblaze.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
class UnknownProviderException implements Exception {
UnknownProviderException(this.message);
final String message;
}
class BackupsProviderFactory {
static BackupsProvider createBackupsProviderInterface(
final BackupsProviderSettings settings,
) {
switch (settings.provider) {
case BackupsProviderType.backblaze:
return settings.isAuthorized
? BackblazeBackupsProvider.load(
settings.isAuthorized,
settings.token,
settings.tokenId,
)
: BackblazeBackupsProvider();
case BackupsProviderType.none:
case BackupsProviderType.file:
case BackupsProviderType.memory:
throw UnknownProviderException('Unknown server provider');
}
}
}

View file

@ -7,10 +7,12 @@ import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class ApiAdapter {
ApiAdapter({
final bool isWithToken = true,
final String? token,
this.cachedDomain = '',
this.cachedZoneId = '',
}) : _api = CloudflareApi(
isWithToken: isWithToken,
token: token ?? '',
);
CloudflareApi api({final bool getInitialized = true}) => getInitialized
@ -28,8 +30,10 @@ class CloudflareDnsProvider extends DnsProvider {
CloudflareDnsProvider() : _adapter = ApiAdapter();
CloudflareDnsProvider.load(
final bool isAuthorized,
final String? token,
) : _adapter = ApiAdapter(
isWithToken: isAuthorized,
token: token,
);
ApiAdapter _adapter;
@ -38,7 +42,7 @@ class CloudflareDnsProvider extends DnsProvider {
DnsProviderType get type => DnsProviderType.cloudflare;
@override
String get howToRegistar => 'how_fix_domain_cloudflare';
String get howToRegister => 'how_fix_domain_cloudflare';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
@ -47,8 +51,6 @@ class CloudflareDnsProvider extends DnsProvider {
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(isWithToken: true);
return result;
}
@ -306,6 +308,7 @@ class CloudflareDnsProvider extends DnsProvider {
_adapter = ApiAdapter(
isWithToken: true,
token: _adapter.api().token,
cachedDomain: domain,
cachedZoneId: getZoneIdResult.data!,
);

View file

@ -5,9 +5,10 @@ import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
ApiAdapter({final bool isWithToken = true, final String? token})
: _api = DesecApi(
isWithToken: isWithToken,
token: token ?? '',
);
DesecApi api({final bool getInitialized = true}) => getInitialized
@ -23,17 +24,19 @@ class DesecDnsProvider extends DnsProvider {
DesecDnsProvider() : _adapter = ApiAdapter();
DesecDnsProvider.load(
final bool isAuthorized,
final String? token,
) : _adapter = ApiAdapter(
isWithToken: isAuthorized,
token: token,
);
ApiAdapter _adapter;
final ApiAdapter _adapter;
@override
DnsProviderType get type => DnsProviderType.desec;
@override
String get howToRegistar => 'how_fix_domain_desec';
String get howToRegister => 'how_fix_domain_desec';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
@ -42,8 +45,6 @@ class DesecDnsProvider extends DnsProvider {
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(isWithToken: true);
return result;
}

View file

@ -5,9 +5,10 @@ import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
ApiAdapter({final bool isWithToken = true, final String? token})
: _api = DigitalOceanDnsApi(
isWithToken: isWithToken,
token: token ?? '',
);
DigitalOceanDnsApi api({final bool getInitialized = true}) => getInitialized
@ -23,17 +24,19 @@ class DigitalOceanDnsProvider extends DnsProvider {
DigitalOceanDnsProvider() : _adapter = ApiAdapter();
DigitalOceanDnsProvider.load(
final bool isAuthorized,
final String? token,
) : _adapter = ApiAdapter(
isWithToken: isAuthorized,
token: token,
);
ApiAdapter _adapter;
final ApiAdapter _adapter;
@override
DnsProviderType get type => DnsProviderType.digitalOcean;
@override
String get howToRegistar => 'how_fix_domain_digital_ocean';
String get howToRegister => 'how_fix_domain_digital_ocean';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
@ -42,8 +45,6 @@ class DigitalOceanDnsProvider extends DnsProvider {
if (!result.data || !result.success) {
return result;
}
_adapter = ApiAdapter(isWithToken: true);
return result;
}

View file

@ -10,14 +10,12 @@ abstract class DnsProvider {
/// Returns a full url to a guide on how to setup
/// DNS provider nameservers
String get howToRegistar;
String get howToRegister;
/// Tries to access an account linked to the provided token.
///
/// To generate a token for your account follow instructions of your
/// DNS provider respectfully.
///
/// If success, saves it for future usage.
Future<GenericResult<bool>> tryInitApiByToken(final String token);
/// Returns list of all available domain entries assigned to the account.

View file

@ -19,18 +19,21 @@ class DnsProviderFactory {
return settings.isAuthorized
? CloudflareDnsProvider.load(
settings.isAuthorized,
settings.token,
)
: CloudflareDnsProvider();
case DnsProviderType.digitalOcean:
return settings.isAuthorized
? DigitalOceanDnsProvider.load(
settings.isAuthorized,
settings.token,
)
: DigitalOceanDnsProvider();
case DnsProviderType.desec:
return settings.isAuthorized
? DesecDnsProvider.load(
settings.isAuthorized,
settings.token,
)
: DesecDnsProvider();
case DnsProviderType.unknown:

View file

@ -1,24 +1,43 @@
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
class ServerProviderSettings {
ServerProviderSettings({
required this.provider,
this.token,
this.isAuthorized = false,
this.location,
});
final bool isAuthorized;
final ServerProviderType provider;
final String? token;
final String? location;
}
class DnsProviderSettings {
DnsProviderSettings({
required this.provider,
this.token,
this.isAuthorized = false,
});
final bool isAuthorized;
final DnsProviderType provider;
final String? token;
}
class BackupsProviderSettings {
BackupsProviderSettings({
required this.provider,
this.tokenId,
this.token,
this.isAuthorized = false,
});
final bool isAuthorized;
final BackupsProviderType provider;
final String? tokenId;
final String? token;
}

View file

@ -1,3 +1,5 @@
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider.dart';
import 'package:selfprivacy/logic/providers/backups_providers/backups_provider_factory.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider_factory.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
@ -7,6 +9,7 @@ import 'package:selfprivacy/logic/providers/server_providers/server_provider_fac
class ProvidersController {
static ServerProvider? get currentServerProvider => _serverProvider;
static DnsProvider? get currentDnsProvider => _dnsProvider;
static BackupsProvider? get currentBackupsProvider => _backupsProvider;
static void initServerProvider(
final ServerProviderSettings settings,
@ -24,11 +27,21 @@ class ProvidersController {
);
}
static void initBackupsProvider(
final BackupsProviderSettings settings,
) {
_backupsProvider = BackupsProviderFactory.createBackupsProviderInterface(
settings,
);
}
static void clearProviders() {
_serverProvider = null;
_dnsProvider = null;
_backupsProvider = null;
}
static ServerProvider? _serverProvider;
static DnsProvider? _dnsProvider;
static BackupsProvider? _backupsProvider;
}

View file

@ -18,10 +18,14 @@ import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class ApiAdapter {
ApiAdapter({final String? region, final bool isWithToken = true})
: _api = DigitalOceanApi(
ApiAdapter({
final String? region,
final bool isWithToken = true,
final String? token,
}) : _api = DigitalOceanApi(
region: region,
isWithToken: isWithToken,
token: token ?? '',
);
DigitalOceanApi api({final bool getInitialized = true}) => getInitialized
@ -39,9 +43,11 @@ class DigitalOceanServerProvider extends ServerProvider {
DigitalOceanServerProvider.load(
final String? location,
final bool isAuthorized,
final String? token,
) : _adapter = ApiAdapter(
isWithToken: isAuthorized,
region: location,
token: token,
);
ApiAdapter _adapter;
@ -408,10 +414,10 @@ class DigitalOceanServerProvider extends ServerProvider {
);
}
_adapter = ApiAdapter(
isWithToken: true,
region: location,
);
// _adapter = ApiAdapter(
// isWithToken: true,
// region: location,
// );
return success;
}

View file

@ -18,10 +18,14 @@ import 'package:selfprivacy/utils/network_utils.dart';
import 'package:selfprivacy/utils/password_generator.dart';
class ApiAdapter {
ApiAdapter({final String? region, final bool isWithToken = true})
: _api = HetznerApi(
ApiAdapter({
final String? region,
final bool isWithToken = true,
final String? token,
}) : _api = HetznerApi(
region: region,
isWithToken: isWithToken,
token: token ?? '',
);
HetznerApi api({final bool getInitialized = true}) => getInitialized
@ -39,9 +43,11 @@ class HetznerServerProvider extends ServerProvider {
HetznerServerProvider.load(
final String? location,
final bool isAuthorized,
final String? token,
) : _adapter = ApiAdapter(
isWithToken: isAuthorized,
region: location,
token: token,
);
ApiAdapter _adapter;
@ -410,7 +416,7 @@ class HetznerServerProvider extends ServerProvider {
return result;
}
_adapter = ApiAdapter(region: api.region, isWithToken: true);
// _adapter = ApiAdapter(region: api.region, isWithToken: true);
return result;
}

View file

@ -56,8 +56,6 @@ abstract class ServerProvider {
///
/// To generate a token for your account follow instructions of your
/// server provider respectfully.
///
/// If success, saves it for future usage.
Future<GenericResult<bool>> tryInitApiByToken(final String token);
/// Tries to assign the location shortcode for future usage.

View file

@ -19,6 +19,7 @@ class ServerProviderFactory {
? HetznerServerProvider.load(
settings.location,
settings.isAuthorized,
settings.token,
)
: HetznerServerProvider();
case ServerProviderType.digitalOcean:
@ -26,6 +27,7 @@ class ServerProviderFactory {
? DigitalOceanServerProvider.load(
settings.location,
settings.isAuthorized,
settings.token,
)
: DigitalOceanServerProvider();
case ServerProviderType.unknown:

View file

@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/tokens/tokens_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
@ -54,6 +55,8 @@ class BackupDetailsPage extends StatelessWidget {
.where((final job) => job.status != JobStatusEnum.finished)
.toList();
final TokensState tokensState = context.watch<TokensBloc>().state;
if (!isReady) {
return BrandHeroScreen(
heroIcon: BrandIcons.save,
@ -69,7 +72,7 @@ class BackupDetailsPage extends StatelessWidget {
heroTitle: 'backup.card_title'.tr(),
heroSubtitle: 'backup.description'.tr(),
children: [
if (preventActions)
if (preventActions || tokensState.backupsCredentials.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
@ -81,9 +84,11 @@ class BackupDetailsPage extends StatelessWidget {
onPressed: preventActions
? null
: () {
context
.read<BackupsBloc>()
.add(const InitializeBackupsRepository());
context.read<BackupsBloc>().add(
InitializeBackupsRepository(
tokensState.backupsCredentials.first.data,
),
);
},
text: 'backup.initialize'.tr(),
),

View file

@ -59,6 +59,11 @@ class MorePage extends StatelessWidget {
goTo: () => const DevicesRoute(),
title: 'devices.main_screen.header'.tr(),
),
_MoreMenuItem(
iconData: Icons.token_outlined,
title: 'tokens.title'.tr(),
goTo: () => const TokensRoute(),
),
_MoreMenuItem(
title: 'application_settings.title'.tr(),
iconData: Icons.settings_outlined,

View file

@ -0,0 +1,262 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/logic/bloc/tokens/tokens_bloc.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/hive/dns_provider_credential.dart';
import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
@RoutePage()
class TokensPage extends StatelessWidget {
const TokensPage({super.key});
@override
Widget build(final BuildContext context) {
final TokensState state = context.watch<TokensBloc>().state;
return BrandHeroScreen(
heroTitle: 'tokens.title'.tr(),
heroSubtitle: 'tokens.description'.tr(),
heroIcon: Icons.token_outlined,
children: [
FilledCard(
child: Column(
children: [
ListTileOnSurfaceVariant(
title: 'tokens.server_provider_tokens'.tr(),
),
Divider(
height: 0,
color: Theme.of(context).colorScheme.outline,
),
if (state.serverProviderCredentials.isEmpty)
ListTileOnSurfaceVariant(
title: 'tokens.no_tokens'.tr(),
leadingIcon: Icons.warning_amber_outlined,
),
Column(
children: state.serverProviderCredentials
.map(
(final serverProviderCredential) =>
_ServerProviderListItem(
serverProviderCredential: serverProviderCredential,
),
)
.toList(),
),
],
),
),
const Gap(16),
FilledCard(
child: Column(
children: [
ListTileOnSurfaceVariant(
title: 'tokens.dns_provider_tokens'.tr(),
),
Divider(
height: 0,
color: Theme.of(context).colorScheme.outline,
),
if (state.dnsProviderCredentials.isEmpty)
ListTileOnSurfaceVariant(
title: 'tokens.no_tokens'.tr(),
leadingIcon: Icons.warning_amber_outlined,
),
Column(
children: state.dnsProviderCredentials
.map(
(final dnsProviderCredential) => _DnsProviderListItem(
dnsProviderCredential: dnsProviderCredential,
),
)
.toList(),
),
],
),
),
const Gap(16),
FilledCard(
child: Column(
children: [
ListTileOnSurfaceVariant(
title: 'tokens.backup_provider_tokens'.tr(),
),
Divider(
height: 0,
color: Theme.of(context).colorScheme.outline,
),
if (state.backupsCredentials.isEmpty)
ListTileOnSurfaceVariant(
title: 'tokens.no_tokens'.tr(),
leadingIcon: Icons.warning_amber_outlined,
),
Column(
children: state.backupsCredentials
.map(
(final backupProviderCredential) =>
_BackupProviderListItem(
backupProviderCredential: backupProviderCredential,
),
)
.toList(),
),
],
),
),
const Gap(16),
ListTile(
title: Text('tokens.check_again'.tr()),
onTap: (state is TokensInitial)
? null
: () => context.read<TokensBloc>().add(const RevalidateTokens()),
leading: const Icon(Icons.refresh_outlined),
enabled: state is! TokensInitial,
),
],
);
}
}
class _ServerProviderListItem extends StatelessWidget {
const _ServerProviderListItem({
required this.serverProviderCredential,
});
final TokenStatusWrapper<ServerProviderCredential> serverProviderCredential;
String getSubtitle(final BuildContext context) {
String subtitle = '';
subtitle += serverProviderCredential.status.statusText;
if (serverProviderCredential.data.associatedServerIds.isNotEmpty) {
final String serverDomains =
serverProviderCredential.data.associatedServerIds
.map(
(final int serverId) => context
.read<TokensBloc>()
.state
.getServerById(serverId)
.domain
.domainName,
)
.join(', ');
subtitle +=
'. ${'tokens.used_by'.tr(namedArgs: {'servers': serverDomains})}';
}
return subtitle;
}
@override
Widget build(final BuildContext context) => Column(
children: [
ListTileOnSurfaceVariant(
title:
'${serverProviderCredential.data.provider.displayName} (${serverProviderCredential.data.tokenPrefix}...)',
subtitle: getSubtitle(context),
leadingIcon: serverProviderCredential.status.icon,
),
],
);
}
class _DnsProviderListItem extends StatelessWidget {
const _DnsProviderListItem({
required this.dnsProviderCredential,
});
final TokenStatusWrapper<DnsProviderCredential> dnsProviderCredential;
String getSubtitle(final BuildContext context) {
String subtitle = '';
subtitle += dnsProviderCredential.status.statusText;
if (dnsProviderCredential.data.associatedDomainNames.isNotEmpty) {
final String serverDomains =
dnsProviderCredential.data.associatedDomainNames.join(', ');
subtitle +=
'. ${'tokens.used_by'.tr(namedArgs: {'servers': serverDomains})}';
}
return subtitle;
}
@override
Widget build(final BuildContext context) => Column(
children: [
ListTileOnSurfaceVariant(
title:
'${dnsProviderCredential.data.provider.displayName} (${dnsProviderCredential.data.tokenPrefix}...)',
subtitle: getSubtitle(context),
leadingIcon: dnsProviderCredential.status.icon,
),
],
);
}
class _BackupProviderListItem extends StatelessWidget {
const _BackupProviderListItem({
required this.backupProviderCredential,
});
final TokenStatusWrapper<BackupsCredential> backupProviderCredential;
String getSubtitle(final BuildContext context) {
String subtitle = '';
subtitle += backupProviderCredential.status.statusText;
return subtitle;
}
@override
Widget build(final BuildContext context) => Column(
children: [
ListTileOnSurfaceVariant(
title:
'${backupProviderCredential.data.provider.name} (${backupProviderCredential.data.tokenPrefix})',
subtitle: getSubtitle(context),
leadingIcon: backupProviderCredential.status.icon,
),
],
);
}
extension on TokenStatus {
String get statusText {
switch (this) {
case TokenStatus.valid:
return 'tokens.valid'.tr();
case TokenStatus.invalid:
return 'tokens.invalid'.tr();
case TokenStatus.noAccess:
return 'tokens.no_access'.tr();
case TokenStatus.loading:
return 'tokens.loading'.tr();
}
}
IconData get icon {
switch (this) {
case TokenStatus.valid:
return Icons.check_circle_outlined;
case TokenStatus.invalid:
return Icons.error_outline_outlined;
case TokenStatus.noAccess:
return Icons.no_encryption_outlined;
case TokenStatus.loading:
return Icons.sync_outlined;
}
}
}
extension on ServerProviderCredential {
String get tokenPrefix => tokenId ?? token.substring(0, 8);
}
extension on DnsProviderCredential {
String get tokenPrefix => tokenId ?? token.substring(0, 8);
}
extension on BackupsCredential {
String get tokenPrefix => keyId;
}

View file

@ -23,7 +23,7 @@ class BrokenDomainOutlinedCard extends StatelessWidget {
child: InkResponse(
highlightShape: BoxShape.rectangle,
onTap: () => context.read<SupportSystemCubit>().showArticle(
article: dnsProvider.howToRegistar,
article: dnsProvider.howToRegister,
context: context,
),
child: Padding(

View file

@ -12,6 +12,7 @@ import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart';
import 'package:selfprivacy/ui/pages/more/app_settings/developer_settings.dart';
import 'package:selfprivacy/ui/pages/more/console/console_page.dart';
import 'package:selfprivacy/ui/pages/more/more.dart';
import 'package:selfprivacy/ui/pages/more/tokens/tokens_page.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/providers/providers.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
@ -107,6 +108,7 @@ class RootRouter extends _$RootRouter {
AutoRoute(page: ExtendingVolumeRoute.page),
AutoRoute(page: ServerSettingsRoute.page),
AutoRoute(page: ServerLogsRoute.page),
AutoRoute(page: TokensRoute.page),
],
),
AutoRoute(page: ServicesMigrationRoute.page),
@ -162,6 +164,8 @@ String getRouteTitle(final String routeName) {
return 'storage.card_title';
case 'ExtendingVolumeRoute':
return 'storage.extending_volume_title';
case 'TokensRoute':
return 'tokens.title';
default:
return routeName;
}

View file

@ -192,6 +192,12 @@ abstract class _$RootRouter extends RootStackRouter {
child: const ServicesPage(),
);
},
TokensRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const TokensPage(),
);
},
UserDetailsRoute.name: (routeData) {
final args = routeData.argsAs<UserDetailsRouteArgs>();
return AutoRoutePage<dynamic>(
@ -720,6 +726,20 @@ class ServicesRoute extends PageRouteInfo<void> {
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [TokensPage]
class TokensRoute extends PageRouteInfo<void> {
const TokensRoute({List<PageRouteInfo>? children})
: super(
TokensRoute.name,
initialChildren: children,
);
static const String name = 'TokensRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [UserDetailsPage]
class UserDetailsRoute extends PageRouteInfo<UserDetailsRouteArgs> {