From 8f42d89457fa1ecc29c206e1700f590d3f218038 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 23 Aug 2024 12:42:07 +0300 Subject: [PATCH] feat: Allow refreshing device token for Server API --- assets/translations/en.json | 8 +++ .../graphql_maps/server_api/server_api.dart | 35 ++++++++++++ lib/logic/bloc/tokens/tokens_bloc.dart | 32 +++++++++++ lib/logic/bloc/tokens/tokens_event.dart | 7 +++ .../get_it/api_connection_repository.dart | 10 ++++ lib/logic/models/hive/server_details.dart | 3 +- lib/theming/app_theme_factory.dart | 11 ++-- lib/ui/pages/devices/devices.dart | 53 ++++++++++++++++++- 8 files changed, 154 insertions(+), 5 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index a692bd96..5deb5eae 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -569,6 +569,14 @@ "description": "The device {} will no longer have access to the server.", "yes": "Revoke", "no": "Cancel" + }, + "refresh_token_alert": { + "header": "Refresh access token?", + "description": "A new token will be generated for this device to connect to the server.", + "yes": "Confirm", + "no": "Cancel", + "failed_to_refresh_token": "Couldn't refresh the token. Please try again.", + "success_refresh_token": "Token refreshed successfully." } }, "recovery_key": { diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart index 8e9f4a5c..b693935d 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart @@ -550,6 +550,41 @@ class ServerApi extends GraphQLApiMap return token; } + Future> refreshDeviceApiToken() async { + GenericResult token; + QueryResult response; + + try { + final GraphQLClient client = await getClient(); + + final mutation = Options$Mutation$RefreshDeviceApiToken(); + response = await client.mutate$RefreshDeviceApiToken( + mutation, + ); + if (response.hasException) { + print(response.exception.toString()); + token = GenericResult( + success: false, + data: '', + message: response.exception.toString(), + ); + } + token = GenericResult( + success: true, + data: response.parsedData!.api.refreshDeviceApiToken.token!, + ); + } catch (e) { + print(e); + token = GenericResult( + success: false, + data: '', + message: e.toString(), + ); + } + + return token; + } + Future isHttpServerWorking() async => (await getApiVersion()) != null; Future> authorizeDevice( diff --git a/lib/logic/bloc/tokens/tokens_bloc.dart b/lib/logic/bloc/tokens/tokens_bloc.dart index 89a1a294..e156b8f5 100644 --- a/lib/logic/bloc/tokens/tokens_bloc.dart +++ b/lib/logic/bloc/tokens/tokens_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc_concurrency/bloc_concurrency.dart'; +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'; @@ -32,6 +33,10 @@ class TokensBloc extends Bloc { on( connectServerToProviderToken, ); + on( + refreshServerApiToken, + transformer: droppable(), + ); add(const RevalidateTokens()); @@ -208,6 +213,33 @@ class TokensBloc extends Bloc { await getIt().updateServerByDomain(newServerData); } + Future refreshServerApiToken( + final RefreshServerApiTokenEvent event, + final Emitter emit, + ) async { + final (success, newToken) = + await getIt().refreshDeviceToken(); + if (!success) { + getIt().showSnackBar( + 'devices.refresh_token_alert.failed_to_refresh_token'.tr(), + ); + return; + } + final Server serverDetails = state.servers.first; + final hostingDetails = serverDetails.hostingDetails.copyWith( + apiToken: newToken, + ); + await getIt().updateServerByDomain( + Server( + hostingDetails: hostingDetails, + domain: serverDetails.domain, + ), + ); + getIt().showSnackBar( + 'devices.refresh_token_alert.success_refresh_token'.tr(), + ); + } + @override Future close() { _resourcesModelSubscription.cancel(); diff --git a/lib/logic/bloc/tokens/tokens_event.dart b/lib/logic/bloc/tokens/tokens_event.dart index deb1133c..85fb6436 100644 --- a/lib/logic/bloc/tokens/tokens_event.dart +++ b/lib/logic/bloc/tokens/tokens_event.dart @@ -34,3 +34,10 @@ class ServerSelectedForProviderToken extends TokensEvent { @override List get props => [providerServer, server, serverProviderCredential]; } + +class RefreshServerApiTokenEvent extends TokensEvent { + const RefreshServerApiTokenEvent(); + + @override + List get props => []; +} diff --git a/lib/logic/get_it/api_connection_repository.dart b/lib/logic/get_it/api_connection_repository.dart index 885c90f0..16550596 100644 --- a/lib/logic/get_it/api_connection_repository.dart +++ b/lib/logic/get_it/api_connection_repository.dart @@ -257,6 +257,16 @@ class ApiConnectionRepository { } } + Future<(bool, String)> refreshDeviceToken() async { + final GenericResult result = await api.refreshDeviceApiToken(); + _apiData.devices.invalidate(); + if (result.success) { + return (true, result.data); + } else { + return (false, result.message ?? 'jobs.generic_error'.tr()); + } + } + void dispose() { _dataStream.close(); _connectionStatusStream.close(); diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart index b24ee30f..f978b1eb 100644 --- a/lib/logic/models/hive/server_details.dart +++ b/lib/logic/models/hive/server_details.dart @@ -50,16 +50,17 @@ class ServerHostingDetails { final DateTime? startTime, final String? serverLocation, final String? serverType, + final String? apiToken, }) => ServerHostingDetails( startTime: startTime ?? this.startTime, serverLocation: serverLocation ?? this.serverLocation, serverType: serverType ?? this.serverType, + apiToken: apiToken ?? this.apiToken, createTime: createTime, id: id, ip4: ip4, volume: volume, - apiToken: apiToken, provider: provider, ); diff --git a/lib/theming/app_theme_factory.dart b/lib/theming/app_theme_factory.dart index 414fd16d..ff528ef9 100644 --- a/lib/theming/app_theme_factory.dart +++ b/lib/theming/app_theme_factory.dart @@ -52,7 +52,8 @@ abstract class AppThemeFactory { } static Future _getDynamicColors( - final Brightness brightness) async { + final Brightness brightness, + ) async { List extractAdditionalColours(final ColorScheme scheme) => [ scheme.surface, scheme.surfaceDim, @@ -65,7 +66,9 @@ abstract class AppThemeFactory { ]; ColorScheme insertAdditionalColours( - final ColorScheme scheme, final List additionalColours) => + final ColorScheme scheme, + final List additionalColours, + ) => scheme.copyWith( surface: additionalColours[0], surfaceDim: additionalColours[1], @@ -88,7 +91,9 @@ abstract class AppThemeFactory { } // Workaround as dynamic_color doesn't add new color roles final fromSeed = ColorScheme.fromSeed( - seedColor: colorScheme.primary, brightness: brightness); + seedColor: colorScheme.primary, + brightness: brightness, + ); final additionalColours = extractAdditionalColours(fromSeed); final updatedColorScheme = insertAdditionalColours(colorScheme, additionalColours); diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart index 6c2992c8..0051fc62 100644 --- a/lib/ui/pages/devices/devices.dart +++ b/lib/ui/pages/devices/devices.dart @@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/bloc/devices/devices_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/json/api_token.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; @@ -121,7 +122,7 @@ class _DeviceTile extends StatelessWidget { .tr(args: [DateFormat.yMMMMd().format(device.date)]), ), onTap: device.isCaller - ? null + ? () => _showTokenRefreshDialog(context, device) : () => _showConfirmationDialog(context, device), ); @@ -172,4 +173,54 @@ class _DeviceTile extends StatelessWidget { ], ), ); + + Future _showTokenRefreshDialog( + final BuildContext context, + final ApiToken device, + ) => + showDialog( + context: context, + builder: (final context) => AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.update_outlined), + const SizedBox(height: 16), + Text( + 'devices.refresh_token_alert.header'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'devices.refresh_token_alert.description' + .tr(args: [device.name]), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + actions: [ + TextButton( + child: Text('devices.refresh_token_alert.no'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('devices.refresh_token_alert.yes'.tr()), + onPressed: () { + context + .read() + .add(const RefreshServerApiTokenEvent()); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); }