diff --git a/assets/translations/en.json b/assets/translations/en.json index c35ff0b5..94b250fa 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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" } } diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index e8fff195..db78e435 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -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 { 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 { volumesBloc = VolumesBloc(); serverLogsBloc = ServerLogsBloc(); outdatedServerCheckerBloc = OutdatedServerCheckerBloc(); + tokensBloc = TokensBloc(); } @override @@ -106,6 +109,9 @@ class BlocAndProviderConfigState extends State { BlocProvider( create: (final _) => outdatedServerCheckerBloc, ), + BlocProvider( + create: (final _) => tokensBloc, + ), ], child: widget.child, ); diff --git a/lib/logic/api_maps/rest_maps/backblaze.dart b/lib/logic/api_maps/rest_maps/backblaze.dart index 37d87dd8..017c3489 100644 --- a/lib/logic/api_maps/rest_maps/backblaze.dart +++ b/lib/logic/api_maps/rest_maps/backblaze.dart @@ -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().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 getAuthorizationToken() async { final Dio client = await getClient(); - final BackupsCredential? backblazeCredential = - getIt().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 createBucket(final String bucketName) async { + Future> createBucket(final String bucketName) async { final BackblazeApiAuth auth = await getAuthorizationToken(); - final BackupsCredential? backblazeCredential = - getIt().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 createKey(final String bucketId) async { + Future> 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 fetchBucket( + Future> 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; } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart index aa5bf88e..433ccd02 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart @@ -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().dnsProviderKey; - assert(token != null); + assert(token.isNotEmpty); options.headers = {'Authorization': 'Bearer $token'}; } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart index 2226cdd1..7e069678 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart @@ -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().dnsProviderKey; - assert(token != null); + assert(token.isNotEmpty); options.headers = {'Authorization': 'Token $token'}; } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart index 0a2cea53..090ea5f7 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart @@ -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().dnsProviderKey; - assert(token != null); + assert(token.isNotEmpty); options.headers = {'Authorization': 'Bearer $token'}; } diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart index d9ec1e5b..224900a2 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart @@ -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().serverProviderKey; - assert(token != null); + assert(token.isNotEmpty); options.headers = {'Authorization': 'Bearer $token'}; } diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart index ef731d80..4c1b146a 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart @@ -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().serverProviderKey; - assert(token != null); + assert(token.isNotEmpty); options.headers = {'Authorization': 'Bearer $token'}; } diff --git a/lib/logic/bloc/backups/backups_bloc.dart b/lib/logic/bloc/backups/backups_bloc.dart index 4e8930a6..f7d29b73 100644 --- a/lib/logic/bloc/backups/backups_bloc.dart +++ b/lib/logic/bloc/backups/backups_bloc.dart @@ -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 { } } - final BackblazeApi backblaze = BackblazeApi(); - Future _loadState( final BackupsServerLoaded event, final Emitter emit, @@ -166,6 +166,14 @@ class BackupsBloc extends Bloc { 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() .serverDomain! .domainName @@ -176,9 +184,30 @@ class BackupsBloc extends Bloc { 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().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().showSnackBar( + "Couldn't create application key on your server.", + ); + emit(BackupsUnititialized()); + return; + } + bucket = BackblazeBucket( bucketId: bucketId, bucketName: bucketName, diff --git a/lib/logic/bloc/backups/backups_event.dart b/lib/logic/bloc/backups/backups_event.dart index f2625fd8..135b7c45 100644 --- a/lib/logic/bloc/backups/backups_event.dart +++ b/lib/logic/bloc/backups/backups_event.dart @@ -19,7 +19,11 @@ class BackupsServerReset extends BackupsEvent { } class InitializeBackupsRepository extends BackupsEvent { - const InitializeBackupsRepository(); + const InitializeBackupsRepository( + this.credential, + ); + + final BackupsCredential credential; @override List get props => []; diff --git a/lib/logic/bloc/tokens/tokens_bloc.dart b/lib/logic/bloc/tokens/tokens_bloc.dart new file mode 100644 index 00000000..6ad2c35e --- /dev/null +++ b/lib/logic/bloc/tokens/tokens_bloc.dart @@ -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 { + TokensBloc() : super(const TokensInitial()) { + on( + validateTokens, + transformer: droppable(), + ); + + add(const RevalidateTokens()); + + _resourcesModelSubscription = + getIt().statusStream.listen((final _) { + add(const RevalidateTokens()); + }); + } + + Future validateTokens( + final RevalidateTokens event, + final Emitter emit, + ) async { + emit(const TokensInitial()); + final List serverProviderCredentials = + getIt().serverProviderCredentials; + final List dnsProviderCredentials = + getIt().dnsProviderCredentials; + final List backupsCredentials = + getIt().backupsCredentials; + + final List> + validatedServerProviderCredentials = []; + final List> + validatedDnsProviderCredentials = []; + final List> + 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 _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 _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 _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 close() { + _resourcesModelSubscription.cancel(); + return super.close(); + } + + late StreamSubscription _resourcesModelSubscription; +} diff --git a/lib/logic/bloc/tokens/tokens_event.dart b/lib/logic/bloc/tokens/tokens_event.dart new file mode 100644 index 00000000..b72f808a --- /dev/null +++ b/lib/logic/bloc/tokens/tokens_event.dart @@ -0,0 +1,12 @@ +part of 'tokens_bloc.dart'; + +sealed class TokensEvent extends Equatable { + const TokensEvent(); +} + +class RevalidateTokens extends TokensEvent { + const RevalidateTokens(); + + @override + List get props => []; +} diff --git a/lib/logic/bloc/tokens/tokens_state.dart b/lib/logic/bloc/tokens/tokens_state.dart new file mode 100644 index 00000000..b0c58748 --- /dev/null +++ b/lib/logic/bloc/tokens/tokens_state.dart @@ -0,0 +1,110 @@ +part of 'tokens_bloc.dart'; + +enum TokenStatus { + loading, + valid, + invalid, + noAccess, +} + +class TokenStatusWrapper { + TokenStatusWrapper({ + required this.data, + required this.status, + }); + + final T data; + final TokenStatus status; +} + +sealed class TokensState extends Equatable { + const TokensState(); + + List> + get serverProviderCredentials; + List> get dnsProviderCredentials; + List> get backupsCredentials; + List get servers => _servers; + + Server getServerById(final int serverId) => servers.firstWhere( + (final Server server) => server.hostingDetails.id == serverId, + ); + + List get _serverProviderCredentials => + getIt().serverProviderCredentials; + List get _dnsProviderCredentials => + getIt().dnsProviderCredentials; + List get _backupsCredentials => + getIt().backupsCredentials; + List get _servers => getIt().servers; +} + +final class TokensInitial extends TokensState { + const TokensInitial(); + + @override + List> + get serverProviderCredentials => _serverProviderCredentials + .map( + (final ServerProviderCredential serverProviderCredential) => + TokenStatusWrapper( + data: serverProviderCredential, + status: TokenStatus.loading, + ), + ) + .toList(); + + @override + List> get dnsProviderCredentials => + _dnsProviderCredentials + .map( + (final DnsProviderCredential dnsProviderCredential) => + TokenStatusWrapper( + data: dnsProviderCredential, + status: TokenStatus.loading, + ), + ) + .toList(); + + @override + List> get backupsCredentials => + _backupsCredentials + .map( + (final BackupsCredential backupsCredential) => + TokenStatusWrapper( + data: backupsCredential, + status: TokenStatus.loading, + ), + ) + .toList(); + + @override + List get servers => _servers; + + @override + List get props => []; +} + +final class TokensChecked extends TokensState { + const TokensChecked({ + required this.serverProviderCredentials, + required this.dnsProviderCredentials, + required this.backupsCredentials, + }); + + @override + final List> + serverProviderCredentials; + @override + final List> dnsProviderCredentials; + @override + final List> backupsCredentials; + + @override + List get props => [ + serverProviderCredentials, + dnsProviderCredentials, + backupsCredentials, + servers, + ]; +} diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 4f1a52f3..cd4e14ba 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -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 { ).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().setBackblazeBucket(bucket!); } catch (e) { print(e); diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 995e5922..c823c13a 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -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().serverInstallation!.serverProviderType!, + token: key, + isAuthorized: true, + ), + ); } Future saveServerType(final ServerType serverType) async { @@ -488,6 +498,13 @@ class ServerInstallationRepository { associatedDomainNames: [], ), ); + ProvidersController.initDnsProvider( + DnsProviderSettings( + provider: getIt().serverInstallation!.dnsProviderType!, + token: key, + isAuthorized: true, + ), + ); } Future saveDomain(final ServerDomain serverDomain) async { diff --git a/lib/logic/get_it/resources_model.dart b/lib/logic/get_it/resources_model.dart index b2055381..98204749 100644 --- a/lib/logic/get_it/resources_model.dart +++ b/lib/logic/get_it/resources_model.dart @@ -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.broadcast(); + + Stream get statusStream => + _statusStreamController.stream; + List get serverProviderCredentials => _serverProviderTokens; List get dnsProviderCredentials => _dnsProviderTokens; @@ -55,6 +91,7 @@ class ResourcesModel { ) async { _serverProviderTokens.add(token); await _box.put(BNames.serverProviderTokens, _serverProviderTokens); + _statusStreamController.add(const ChangedServerProviderCredentials()); } Future associateServerWithToken( @@ -68,6 +105,7 @@ class ResourcesModel { .associatedServerIds .add(serverId); await _box.put(BNames.serverProviderTokens, _serverProviderTokens); + _statusStreamController.add(const ChangedServerProviderCredentials()); } Future removeServerProviderToken( @@ -75,6 +113,7 @@ class ResourcesModel { ) async { _serverProviderTokens.remove(token); await _box.put(BNames.serverProviderTokens, _serverProviderTokens); + _statusStreamController.add(const ChangedServerProviderCredentials()); } Future 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 associateDomainWithToken( @@ -98,16 +138,19 @@ class ResourcesModel { .associatedDomainNames .add(domain); await _box.put(BNames.dnsProviderTokens, _dnsProviderTokens); + _statusStreamController.add(const ChangedDnsProviderCredentials()); } Future removeDnsProviderToken(final DnsProviderCredential token) async { _dnsProviderTokens.remove(token); await _box.put(BNames.dnsProviderTokens, _dnsProviderTokens); + _statusStreamController.add(const ChangedDnsProviderCredentials()); } Future addBackupsCredential(final BackupsCredential credential) async { _backupsCredentials.add(credential); await _box.put(BNames.backupsProviderTokens, _backupsCredentials); + _statusStreamController.add(const ChangedBackupsCredentials()); } Future removeBackupsCredential( @@ -115,16 +158,19 @@ class ResourcesModel { ) async { _backupsCredentials.remove(credential); await _box.put(BNames.backupsProviderTokens, _backupsCredentials); + _statusStreamController.add(const ChangedBackupsCredentials()); } Future addServer(final Server server) async { _servers.add(server); await _box.put(BNames.servers, _servers); + _statusStreamController.add(const ChangedServers()); } Future removeServer(final Server server) async { _servers.remove(server); await _box.put(BNames.servers, _servers); + _statusStreamController.add(const ChangedServers()); } Future 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((final e) => e as Server) .toList(); _backblazeBucket = _box.get(BNames.backblazeBucket); + + _statusStreamController.add(const ResourcesModelLoaded()); } } diff --git a/lib/logic/providers/backups_providers/backblaze.dart b/lib/logic/providers/backups_providers/backblaze.dart new file mode 100644 index 00000000..b640baf3 --- /dev/null +++ b/lib/logic/providers/backups_providers/backblaze.dart @@ -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> 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> createStorage(final String desiredName) async { + final api = _adapter.api(); + final result = await api.createBucket(desiredName); + return result; + } + + @override + Future> 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> getStorage( + final BackupsCredential credentials, + final BackupConfiguration configuration, + ) async { + final api = _adapter.api(); + final result = await api.fetchBucket(credentials, configuration); + return result; + } +} diff --git a/lib/logic/providers/backups_providers/backups_provider.dart b/lib/logic/providers/backups_providers/backups_provider.dart new file mode 100644 index 00000000..0503428e --- /dev/null +++ b/lib/logic/providers/backups_providers/backups_provider.dart @@ -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> tryInitApiByToken(final String token); + + /// Creates a storage for backups (for example, a bucket) + /// and returns a storage ID to access it. + Future> createStorage(final String desiredName); + + /// Creates the credentials to access the backups storage + /// from the server + Future> createApplicationKey( + final String storageId, + ); + + /// Get the backups storage + Future> getStorage( + final BackupsCredential credentials, + final BackupConfiguration configuration, + ); +} diff --git a/lib/logic/providers/backups_providers/backups_provider_factory.dart b/lib/logic/providers/backups_providers/backups_provider_factory.dart new file mode 100644 index 00000000..2a61bd1b --- /dev/null +++ b/lib/logic/providers/backups_providers/backups_provider_factory.dart @@ -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'); + } + } +} diff --git a/lib/logic/providers/dns_providers/cloudflare.dart b/lib/logic/providers/dns_providers/cloudflare.dart index 156083a2..68695ca3 100644 --- a/lib/logic/providers/dns_providers/cloudflare.dart +++ b/lib/logic/providers/dns_providers/cloudflare.dart @@ -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> 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!, ); diff --git a/lib/logic/providers/dns_providers/desec.dart b/lib/logic/providers/dns_providers/desec.dart index eb0f2b9a..e619eaf1 100644 --- a/lib/logic/providers/dns_providers/desec.dart +++ b/lib/logic/providers/dns_providers/desec.dart @@ -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> 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; } diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart index bf35b77b..c34f86e7 100644 --- a/lib/logic/providers/dns_providers/digital_ocean_dns.dart +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -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> 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; } diff --git a/lib/logic/providers/dns_providers/dns_provider.dart b/lib/logic/providers/dns_providers/dns_provider.dart index 2066ebcf..03e01657 100644 --- a/lib/logic/providers/dns_providers/dns_provider.dart +++ b/lib/logic/providers/dns_providers/dns_provider.dart @@ -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> tryInitApiByToken(final String token); /// Returns list of all available domain entries assigned to the account. diff --git a/lib/logic/providers/dns_providers/dns_provider_factory.dart b/lib/logic/providers/dns_providers/dns_provider_factory.dart index e02928b6..31b2b346 100644 --- a/lib/logic/providers/dns_providers/dns_provider_factory.dart +++ b/lib/logic/providers/dns_providers/dns_provider_factory.dart @@ -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: diff --git a/lib/logic/providers/provider_settings.dart b/lib/logic/providers/provider_settings.dart index 835697cd..0029eb80 100644 --- a/lib/logic/providers/provider_settings.dart +++ b/lib/logic/providers/provider_settings.dart @@ -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; } diff --git a/lib/logic/providers/providers_controller.dart b/lib/logic/providers/providers_controller.dart index dab74221..bef87b0f 100644 --- a/lib/logic/providers/providers_controller.dart +++ b/lib/logic/providers/providers_controller.dart @@ -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; } diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index bc372ebc..99123750 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -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; } diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index 67037610..26e960cc 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -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; } diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart index bdb56936..ab52fe47 100644 --- a/lib/logic/providers/server_providers/server_provider.dart +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -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> tryInitApiByToken(final String token); /// Tries to assign the location shortcode for future usage. diff --git a/lib/logic/providers/server_providers/server_provider_factory.dart b/lib/logic/providers/server_providers/server_provider_factory.dart index 8c138003..39d6a357 100644 --- a/lib/logic/providers/server_providers/server_provider_factory.dart +++ b/lib/logic/providers/server_providers/server_provider_factory.dart @@ -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: diff --git a/lib/ui/pages/backups/backup_details.dart b/lib/ui/pages/backups/backup_details.dart index 2373d8dd..af28e68b 100644 --- a/lib/ui/pages/backups/backup_details.dart +++ b/lib/ui/pages/backups/backup_details.dart @@ -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().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() - .add(const InitializeBackupsRepository()); + context.read().add( + InitializeBackupsRepository( + tokensState.backupsCredentials.first.data, + ), + ); }, text: 'backup.initialize'.tr(), ), diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index cf297328..3c91f7a2 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -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, diff --git a/lib/ui/pages/more/tokens/tokens_page.dart b/lib/ui/pages/more/tokens/tokens_page.dart new file mode 100644 index 00000000..0fcfef4f --- /dev/null +++ b/lib/ui/pages/more/tokens/tokens_page.dart @@ -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().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().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; + + 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() + .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; + + 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 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; +} diff --git a/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart b/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart index 19d7a175..0fa8da5f 100644 --- a/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart +++ b/lib/ui/pages/setup/initializing/broken_domain_outlined_card.dart @@ -23,7 +23,7 @@ class BrokenDomainOutlinedCard extends StatelessWidget { child: InkResponse( highlightShape: BoxShape.rectangle, onTap: () => context.read().showArticle( - article: dnsProvider.howToRegistar, + article: dnsProvider.howToRegister, context: context, ), child: Padding( diff --git a/lib/ui/router/router.dart b/lib/ui/router/router.dart index ead07b3a..05126d25 100644 --- a/lib/ui/router/router.dart +++ b/lib/ui/router/router.dart @@ -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; } diff --git a/lib/ui/router/router.gr.dart b/lib/ui/router/router.gr.dart index 4fcf7c7c..d30ac51c 100644 --- a/lib/ui/router/router.gr.dart +++ b/lib/ui/router/router.gr.dart @@ -192,6 +192,12 @@ abstract class _$RootRouter extends RootStackRouter { child: const ServicesPage(), ); }, + TokensRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TokensPage(), + ); + }, UserDetailsRoute.name: (routeData) { final args = routeData.argsAs(); return AutoRoutePage( @@ -720,6 +726,20 @@ class ServicesRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [TokensPage] +class TokensRoute extends PageRouteInfo { + const TokensRoute({List? children}) + : super( + TokensRoute.name, + initialChildren: children, + ); + + static const String name = 'TokensRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [UserDetailsPage] class UserDetailsRoute extends PageRouteInfo {