feat: Allow setting the provider token after recovery

This commit is contained in:
Inex Code 2024-07-30 18:22:32 +03:00
parent 51b6e0ab41
commit efed52f3ec
14 changed files with 512 additions and 40 deletions

View file

@ -693,6 +693,11 @@
"loading": "Loading token status",
"used_by": "Used for {servers}.",
"check_again": "Check again",
"no_tokens": "No tokens"
"no_tokens": "No tokens",
"server_without_token": "{server_domain} has no token for {provider}.",
"tap_to_add_token": "Tap here to add a token",
"add_server_provider_token": "Add server provider token",
"server_provider_unknown": "Unknown server provider",
"server_provider_unknown_description": "Your server provider is not supported by this app version."
}
}

View file

@ -8,10 +8,13 @@ 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_details.dart';
import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/logic/models/server_basic_info.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/providers_controller.dart';
import 'package:selfprivacy/logic/providers/server_providers/server_provider_factory.dart';
part 'tokens_event.dart';
@ -23,6 +26,12 @@ class TokensBloc extends Bloc<TokensEvent, TokensState> {
validateTokens,
transformer: droppable(),
);
on<AddServerProviderToken>(
addServerProviderCredential,
);
on<ServerSelectedForProviderToken>(
connectServerToProviderToken,
);
add(const RevalidateTokens());
@ -154,6 +163,51 @@ class TokensBloc extends Bloc<TokensEvent, TokensState> {
return TokenStatus.valid;
}
Future<void> addServerProviderCredential(
final AddServerProviderToken event,
final Emitter<TokensState> emit,
) async {
await getIt<ResourcesModel>()
.addServerProviderToken(event.serverProviderCredential);
final ServerProviderSettings settings = ServerProviderSettings(
provider: event.serverProviderCredential.provider,
token: event.serverProviderCredential.token,
isAuthorized: true,
);
ProvidersController.initServerProvider(settings);
}
Future<void> connectServerToProviderToken(
final ServerSelectedForProviderToken event,
final Emitter<TokensState> emit,
) async {
await getIt<ResourcesModel>().associateServerWithToken(
event.providerServer.id,
event.serverProviderCredential.token,
);
final Server newServerData = Server(
domain: event.server.domain,
hostingDetails: ServerHostingDetails(
ip4: event.providerServer.ip,
id: event.providerServer.id,
createTime: event.providerServer.created,
volume: ServerProviderVolume(
id: 0,
name: 'recovered_volume',
sizeByte: 0,
serverId: event.providerServer.id,
linuxDevice: '',
),
apiToken: event.server.hostingDetails.apiToken,
provider: event.serverProviderCredential.provider,
serverLocation: event.server.hostingDetails.serverLocation,
serverType: event.server.hostingDetails.serverType,
),
);
await getIt<ResourcesModel>().updateServerByDomain(newServerData);
}
@override
Future<void> close() {
_resourcesModelSubscription.cancel();

View file

@ -10,3 +10,27 @@ class RevalidateTokens extends TokensEvent {
@override
List<Object> get props => [];
}
class AddServerProviderToken extends TokensEvent {
const AddServerProviderToken(this.serverProviderCredential);
final ServerProviderCredential serverProviderCredential;
@override
List<Object> get props => [serverProviderCredential];
}
class ServerSelectedForProviderToken extends TokensEvent {
const ServerSelectedForProviderToken(
this.providerServer,
this.server,
this.serverProviderCredential,
);
final ServerBasicInfoWithValidators providerServer;
final Server server;
final ServerProviderCredential serverProviderCredential;
@override
List<Object> get props => [providerServer, server, serverProviderCredential];
}

View file

@ -26,6 +26,21 @@ sealed class TokensState extends Equatable {
List<TokenStatusWrapper<BackupsCredential>> get backupsCredentials;
List<Server> get servers => _servers;
List<Server> get serversWithoutProviderCredentials => servers
.where(
(final Server server) =>
server.hostingDetails.provider != ServerProviderType.unknown &&
serverProviderCredentials.every(
(
final TokenStatusWrapper<ServerProviderCredential>
serverProviderCredential,
) =>
!serverProviderCredential.data.associatedServerIds
.contains(server.hostingDetails.id),
),
)
.toList();
Server getServerById(final int serverId) => servers.firstWhere(
(final Server server) => server.hostingDetails.id == serverId,
);

View file

@ -6,6 +6,7 @@ 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/generic_result.dart';
import 'package:selfprivacy/logic/get_it/resources_model.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
@ -68,10 +69,18 @@ class VolumesBloc extends Bloc<VolumesEvent, VolumesState> {
}
},
);
_resourcesModelSubscription =
getIt<ResourcesModel>().statusStream.listen((final event) {
if (event is ChangedServerProviderCredentials) {
add(const VolumesServerLoaded());
}
});
}
late StreamSubscription _apiStatusSubscription;
late StreamSubscription _apiDataSubscription;
late StreamSubscription _resourcesModelSubscription;
bool isLoaded = false;
Future<Price?> getPricePerGb() async {
@ -145,6 +154,7 @@ class VolumesBloc extends Bloc<VolumesEvent, VolumesState> {
Future<void> close() async {
await _apiStatusSubscription.cancel();
await _apiDataSubscription.cancel();
await _resourcesModelSubscription.cancel();
await super.close();
}

View file

@ -0,0 +1,54 @@
import 'dart:async';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
class AddServerProviderToExistingServerFormCubit extends FormCubit {
AddServerProviderToExistingServerFormCubit(
this.serverInstallationCubit,
this.setServerProviderKey,
) {
apiKey = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('validations.required'.tr()),
],
);
super.addFields([apiKey]);
}
@override
FutureOr<void> onSubmit() async {
// serverInstallationCubit.setServerProviderKey(apiKey.state.value);
setServerProviderKey(apiKey.state.value);
}
final ServerInstallationCubit serverInstallationCubit;
final Function(String) setServerProviderKey;
late final FieldCubit<String> apiKey;
@override
FutureOr<bool> asyncValidation() async {
bool? isKeyValid;
try {
isKeyValid = await serverInstallationCubit
.isServerProviderApiTokenValid(apiKey.state.value);
} catch (e) {
addError(e);
}
if (isKeyValid == null) {
apiKey.setError('');
return false;
}
if (!isKeyValid) {
apiKey.setError('initializing.provider_bad_key_error'.tr());
}
return isKeyValid;
}
}

View file

@ -11,6 +11,7 @@ import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/hive/server.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
@ -708,29 +709,54 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
);
}
Future<List<ServerBasicInfoWithValidators>> getAvailableServers() async {
final ServerInstallationRecovery dataState =
state as ServerInstallationRecovery;
final List<ServerBasicInfo> servers =
await repository.getServersOnProviderAccount();
Future<List<ServerBasicInfoWithValidators>> getAvailableServers({
final Server? server,
}) async {
List<ServerBasicInfoWithValidators> validatedList = [];
try {
final Iterable<ServerBasicInfoWithValidators> validated = servers.map(
(final ServerBasicInfo server) =>
ServerBasicInfoWithValidators.fromServerBasicInfo(
serverBasicInfo: server,
isIpValid: server.ip == dataState.serverDetails?.ip4,
isReverseDnsValid:
server.reverseDns == dataState.serverDomain?.domainName ||
server.reverseDns ==
dataState.serverDomain?.domainName.split('.')[0],
),
);
validatedList = validated.toList();
} catch (e) {
print(e);
getIt<NavigationService>()
.showSnackBar('modals.server_validators_error'.tr());
if (server != null) {
final List<ServerBasicInfo> servers =
await repository.getServersOnProviderAccount();
try {
final Iterable<ServerBasicInfoWithValidators> validated = servers.map(
(final ServerBasicInfo hostingServer) =>
ServerBasicInfoWithValidators.fromServerBasicInfo(
serverBasicInfo: hostingServer,
isIpValid: hostingServer.ip == server.hostingDetails.ip4,
isReverseDnsValid:
hostingServer.reverseDns == server.domain.domainName ||
hostingServer.reverseDns ==
server.domain.domainName.split('.')[0],
),
);
validatedList = validated.toList();
} catch (e) {
print(e);
getIt<NavigationService>()
.showSnackBar('modals.server_validators_error'.tr());
}
} else {
final ServerInstallationRecovery dataState =
state as ServerInstallationRecovery;
final List<ServerBasicInfo> servers =
await repository.getServersOnProviderAccount();
try {
final Iterable<ServerBasicInfoWithValidators> validated = servers.map(
(final ServerBasicInfo server) =>
ServerBasicInfoWithValidators.fromServerBasicInfo(
serverBasicInfo: server,
isIpValid: server.ip == dataState.serverDetails?.ip4,
isReverseDnsValid:
server.reverseDns == dataState.serverDomain?.domainName ||
server.reverseDns ==
dataState.serverDomain?.domainName.split('.')[0],
),
);
validatedList = validated.toList();
} catch (e) {
print(e);
getIt<NavigationService>()
.showSnackBar('modals.server_validators_error'.tr());
}
}
return validatedList;

View file

@ -173,6 +173,17 @@ class ResourcesModel {
_statusStreamController.add(const ChangedServers());
}
Future<void> updateServerByDomain(final Server server) async {
final index = _servers.indexWhere(
(final s) => s.domain.domainName == server.domain.domainName,
);
if (index != -1) {
_servers[index] = server;
await _box.put(BNames.servers, _servers);
_statusStreamController.add(const ChangedServers());
}
}
Future<void> setBackblazeBucket(final BackblazeBucket bucket) async {
_backblazeBucket = bucket;
await _box.put(BNames.backblazeBucket, _backblazeBucket);

View file

@ -117,6 +117,12 @@ enum ServerProviderType {
hetzner => 'Hetzner Cloud',
unknown => 'Unknown',
};
String get supportArticle => switch (this) {
digitalOcean => 'how_digital_ocean',
hetzner => 'how_hetzner',
unknown => '',
};
}
extension ServerProviderTypeIsSpecified on ServerProviderType? {

View file

@ -0,0 +1,167 @@
import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/logic/bloc/tokens/tokens_bloc.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/add_server_provider_to_exsisting_server_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart';
@RoutePage()
class AddServerProviderTokenPage extends StatefulWidget {
const AddServerProviderTokenPage({
required this.server,
super.key,
});
final Server server;
@override
State<AddServerProviderTokenPage> createState() =>
_AddServerProviderTokenPageState();
}
class _AddServerProviderTokenPageState
extends State<AddServerProviderTokenPage> {
bool isChoosingServer = false;
ServerProviderCredential? credential;
@override
Widget build(final BuildContext context) {
final ServerInstallationCubit appConfig =
context.watch<ServerInstallationCubit>();
void setServerProviderKey(final String key) {
final newCredential = ServerProviderCredential(
token: key,
provider: widget.server.hostingDetails.provider,
tokenId: null,
associatedServerIds: [],
);
context.read<TokensBloc>().add(
AddServerProviderToken(newCredential),
);
setState(() {
isChoosingServer = true;
credential = newCredential;
});
}
if (isChoosingServer && credential != null) {
return RecoveryConfirmServer(
server: widget.server,
serverProviderCredential: credential,
submitCallback: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
);
}
if (widget.server.hostingDetails.provider == ServerProviderType.unknown) {
return BrandHeroScreen(
heroTitle: 'tokens.server_provider_unknown'.tr(),
heroSubtitle: 'tokens.server_provider_unknown_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
ignoreBreakpoints: true,
onBackButtonPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
children: [
BrandButton.filled(
text: 'basis.close'.tr(),
onPressed: () =>
Navigator.of(context).popUntil((final route) => route.isFirst),
),
],
);
}
return _TokenProviderInput(
appConfig: appConfig,
server: widget.server,
setServerProviderKey: setServerProviderKey,
);
}
}
class _TokenProviderInput extends StatelessWidget {
const _TokenProviderInput({
required this.appConfig,
required this.server,
required this.setServerProviderKey,
});
final ServerInstallationCubit appConfig;
final Server server;
final Function(String) setServerProviderKey;
@override
Widget build(final BuildContext context) => BlocProvider(
create: (final BuildContext context) =>
AddServerProviderToExistingServerFormCubit(
appConfig,
setServerProviderKey,
),
child: Builder(
builder: (final BuildContext context) => BrandHeroScreen(
heroTitle: 'recovering.provider_connected'.tr(
args: [
server.hostingDetails.provider.displayName,
],
),
heroSubtitle: 'recovering.provider_connected_description'.tr(
args: [server.domain.domainName],
),
hasBackButton: true,
hasFlashButton: false,
hasSupportDrawer: true,
onBackButtonPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
children: [
CubitFormTextField(
autofocus: true,
formFieldCubit: context
.read<AddServerProviderToExistingServerFormCubit>()
.apiKey,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'recovering.provider_connected_placeholder'.tr(
args: [
server.hostingDetails.provider.displayName,
],
),
),
),
const Gap(16),
BrandButton.filled(
onPressed: () => context
.read<AddServerProviderToExistingServerFormCubit>()
.trySubmit(),
child: Text('basis.continue'.tr()),
),
const Gap(16),
Builder(
builder: (final context) => BrandButton.text(
title: 'initializing.how'.tr(),
onPressed: () => context
.read<SupportSystemCubit>()
.showArticle(
article: server.hostingDetails.provider.supportArticle,
context: context,
),
),
),
],
),
),
);
}

View file

@ -10,6 +10,7 @@ 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';
import 'package:selfprivacy/ui/router/router.dart';
@RoutePage()
class TokensPage extends StatelessWidget {
@ -29,10 +30,7 @@ class TokensPage extends StatelessWidget {
ListTileOnSurfaceVariant(
title: 'tokens.server_provider_tokens'.tr(),
),
Divider(
height: 0,
color: Theme.of(context).colorScheme.outline,
),
const Divider(height: 0),
if (state.serverProviderCredentials.isEmpty)
ListTileOnSurfaceVariant(
title: 'tokens.no_tokens'.tr(),
@ -48,6 +46,34 @@ class TokensPage extends StatelessWidget {
)
.toList(),
),
if (state.serversWithoutProviderCredentials.isNotEmpty)
Column(
children: [
const Divider(height: 0),
Column(
children: state.serversWithoutProviderCredentials
.map(
(final server) => ListTileOnSurfaceVariant(
title: 'tokens.server_without_token'.tr(
namedArgs: {
'server_domain': server.domain.domainName,
'provider': server
.hostingDetails.provider.displayName,
},
),
subtitle: 'tokens.tap_to_add_token'.tr(),
leadingIcon: Icons.add_circle_outline,
onTap: () => context.router.push(
AddServerProviderTokenRoute(
server: server,
),
),
),
)
.toList(),
),
],
),
],
),
),
@ -58,10 +84,7 @@ class TokensPage extends StatelessWidget {
ListTileOnSurfaceVariant(
title: 'tokens.dns_provider_tokens'.tr(),
),
Divider(
height: 0,
color: Theme.of(context).colorScheme.outline,
),
const Divider(height: 0),
if (state.dnsProviderCredentials.isEmpty)
ListTileOnSurfaceVariant(
title: 'tokens.no_tokens'.tr(),
@ -86,10 +109,7 @@ class TokensPage extends StatelessWidget {
ListTileOnSurfaceVariant(
title: 'tokens.backup_provider_tokens'.tr(),
),
Divider(
height: 0,
color: Theme.of(context).colorScheme.outline,
),
const Divider(height: 0),
if (state.backupsCredentials.isEmpty)
ListTileOnSurfaceVariant(
title: 'tokens.no_tokens'.tr(),

View file

@ -1,13 +1,25 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/hive/server.dart';
import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class RecoveryConfirmServer extends StatefulWidget {
const RecoveryConfirmServer({super.key});
const RecoveryConfirmServer({
this.server,
this.serverProviderCredential,
this.submitCallback,
super.key,
});
final Server? server;
final ServerProviderCredential? serverProviderCredential;
final Function? submitCallback;
@override
State<RecoveryConfirmServer> createState() => _RecoveryConfirmServerState();
@ -45,8 +57,9 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
hasFlashButton: false,
children: [
FutureBuilder<List<ServerBasicInfoWithValidators>>(
future:
context.read<ServerInstallationCubit>().getAvailableServers(),
future: context
.read<ServerInstallationCubit>()
.getAvailableServers(server: widget.server),
builder: (final context, final snapshot) {
if (snapshot.hasData) {
final servers = snapshot.data;
@ -248,8 +261,21 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
TextButton(
child: Text('modals.yes'.tr()),
onPressed: () {
context.read<ServerInstallationCubit>().setServerId(server);
Navigator.of(context).pop();
if (widget.server != null &&
widget.serverProviderCredential != null) {
context.read<TokensBloc>().add(
ServerSelectedForProviderToken(
server,
widget.server!,
widget.serverProviderCredential!,
),
);
Navigator.of(context).pop();
widget.submitCallback?.call();
} else {
context.read<ServerInstallationCubit>().setServerId(server);
Navigator.of(context).pop();
}
},
),
],

View file

@ -2,6 +2,7 @@ import 'package:animations/animations.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/hive/server.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/pages/backups/backup_details.dart';
import 'package:selfprivacy/ui/pages/backups/backups_list.dart';
@ -12,6 +13,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/add_server_provider_token.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';
@ -111,6 +113,7 @@ class RootRouter extends _$RootRouter {
AutoRoute(page: ServerLogsRoute.page),
AutoRoute(page: TokensRoute.page),
AutoRoute(page: MemoryUsageByServiceRoute.page),
AutoRoute(page: AddServerProviderTokenRoute.page),
],
),
AutoRoute(page: ServicesMigrationRoute.page),
@ -170,6 +173,8 @@ String getRouteTitle(final String routeName) {
return 'tokens.title';
case 'MemoryUsageByServiceRoute':
return 'resource_chart.memory';
case 'AddServerProviderTokenPage':
return 'tokens.add_server_provider_token';
default:
return routeName;
}

View file

@ -21,6 +21,16 @@ abstract class _$RootRouter extends RootStackRouter {
child: const AboutApplicationPage(),
);
},
AddServerProviderTokenRoute.name: (routeData) {
final args = routeData.argsAs<AddServerProviderTokenRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: AddServerProviderTokenPage(
server: args.server,
key: args.key,
),
);
},
AppSettingsRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
@ -242,6 +252,45 @@ class AboutApplicationRoute extends PageRouteInfo<void> {
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [AddServerProviderTokenPage]
class AddServerProviderTokenRoute
extends PageRouteInfo<AddServerProviderTokenRouteArgs> {
AddServerProviderTokenRoute({
required Server server,
Key? key,
List<PageRouteInfo>? children,
}) : super(
AddServerProviderTokenRoute.name,
args: AddServerProviderTokenRouteArgs(
server: server,
key: key,
),
initialChildren: children,
);
static const String name = 'AddServerProviderTokenRoute';
static const PageInfo<AddServerProviderTokenRouteArgs> page =
PageInfo<AddServerProviderTokenRouteArgs>(name);
}
class AddServerProviderTokenRouteArgs {
const AddServerProviderTokenRouteArgs({
required this.server,
this.key,
});
final Server server;
final Key? key;
@override
String toString() {
return 'AddServerProviderTokenRouteArgs{server: $server, key: $key}';
}
}
/// generated route for
/// [AppSettingsPage]
class AppSettingsRoute extends PageRouteInfo<void> {