feat: Allow refreshing device token for Server API

This commit is contained in:
Inex Code 2024-08-23 12:42:07 +03:00
parent 0fb898873c
commit 8f42d89457
8 changed files with 154 additions and 5 deletions

View file

@ -569,6 +569,14 @@
"description": "The device {} will no longer have access to the server.", "description": "The device {} will no longer have access to the server.",
"yes": "Revoke", "yes": "Revoke",
"no": "Cancel" "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": { "recovery_key": {

View file

@ -550,6 +550,41 @@ class ServerApi extends GraphQLApiMap
return token; return token;
} }
Future<GenericResult<String>> refreshDeviceApiToken() async {
GenericResult<String> token;
QueryResult<Mutation$RefreshDeviceApiToken> 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<String>(
success: false,
data: '',
message: response.exception.toString(),
);
}
token = GenericResult<String>(
success: true,
data: response.parsedData!.api.refreshDeviceApiToken.token!,
);
} catch (e) {
print(e);
token = GenericResult<String>(
success: false,
data: '',
message: e.toString(),
);
}
return token;
}
Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null; Future<bool> isHttpServerWorking() async => (await getApiVersion()) != null;
Future<GenericResult<String>> authorizeDevice( Future<GenericResult<String>> authorizeDevice(

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/get_it_config.dart';
@ -32,6 +33,10 @@ class TokensBloc extends Bloc<TokensEvent, TokensState> {
on<ServerSelectedForProviderToken>( on<ServerSelectedForProviderToken>(
connectServerToProviderToken, connectServerToProviderToken,
); );
on<RefreshServerApiTokenEvent>(
refreshServerApiToken,
transformer: droppable(),
);
add(const RevalidateTokens()); add(const RevalidateTokens());
@ -208,6 +213,33 @@ class TokensBloc extends Bloc<TokensEvent, TokensState> {
await getIt<ResourcesModel>().updateServerByDomain(newServerData); await getIt<ResourcesModel>().updateServerByDomain(newServerData);
} }
Future<void> refreshServerApiToken(
final RefreshServerApiTokenEvent event,
final Emitter<TokensState> emit,
) async {
final (success, newToken) =
await getIt<ApiConnectionRepository>().refreshDeviceToken();
if (!success) {
getIt<NavigationService>().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<ResourcesModel>().updateServerByDomain(
Server(
hostingDetails: hostingDetails,
domain: serverDetails.domain,
),
);
getIt<NavigationService>().showSnackBar(
'devices.refresh_token_alert.success_refresh_token'.tr(),
);
}
@override @override
Future<void> close() { Future<void> close() {
_resourcesModelSubscription.cancel(); _resourcesModelSubscription.cancel();

View file

@ -34,3 +34,10 @@ class ServerSelectedForProviderToken extends TokensEvent {
@override @override
List<Object> get props => [providerServer, server, serverProviderCredential]; List<Object> get props => [providerServer, server, serverProviderCredential];
} }
class RefreshServerApiTokenEvent extends TokensEvent {
const RefreshServerApiTokenEvent();
@override
List<Object> get props => [];
}

View file

@ -257,6 +257,16 @@ class ApiConnectionRepository {
} }
} }
Future<(bool, String)> refreshDeviceToken() async {
final GenericResult<String> 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() { void dispose() {
_dataStream.close(); _dataStream.close();
_connectionStatusStream.close(); _connectionStatusStream.close();

View file

@ -50,16 +50,17 @@ class ServerHostingDetails {
final DateTime? startTime, final DateTime? startTime,
final String? serverLocation, final String? serverLocation,
final String? serverType, final String? serverType,
final String? apiToken,
}) => }) =>
ServerHostingDetails( ServerHostingDetails(
startTime: startTime ?? this.startTime, startTime: startTime ?? this.startTime,
serverLocation: serverLocation ?? this.serverLocation, serverLocation: serverLocation ?? this.serverLocation,
serverType: serverType ?? this.serverType, serverType: serverType ?? this.serverType,
apiToken: apiToken ?? this.apiToken,
createTime: createTime, createTime: createTime,
id: id, id: id,
ip4: ip4, ip4: ip4,
volume: volume, volume: volume,
apiToken: apiToken,
provider: provider, provider: provider,
); );

View file

@ -52,7 +52,8 @@ abstract class AppThemeFactory {
} }
static Future<ColorScheme?> _getDynamicColors( static Future<ColorScheme?> _getDynamicColors(
final Brightness brightness) async { final Brightness brightness,
) async {
List<Color> extractAdditionalColours(final ColorScheme scheme) => [ List<Color> extractAdditionalColours(final ColorScheme scheme) => [
scheme.surface, scheme.surface,
scheme.surfaceDim, scheme.surfaceDim,
@ -65,7 +66,9 @@ abstract class AppThemeFactory {
]; ];
ColorScheme insertAdditionalColours( ColorScheme insertAdditionalColours(
final ColorScheme scheme, final List<Color> additionalColours) => final ColorScheme scheme,
final List<Color> additionalColours,
) =>
scheme.copyWith( scheme.copyWith(
surface: additionalColours[0], surface: additionalColours[0],
surfaceDim: additionalColours[1], surfaceDim: additionalColours[1],
@ -88,7 +91,9 @@ abstract class AppThemeFactory {
} }
// Workaround as dynamic_color doesn't add new color roles // Workaround as dynamic_color doesn't add new color roles
final fromSeed = ColorScheme.fromSeed( final fromSeed = ColorScheme.fromSeed(
seedColor: colorScheme.primary, brightness: brightness); seedColor: colorScheme.primary,
brightness: brightness,
);
final additionalColours = extractAdditionalColours(fromSeed); final additionalColours = extractAdditionalColours(fromSeed);
final updatedColorScheme = final updatedColorScheme =
insertAdditionalColours(colorScheme, additionalColours); insertAdditionalColours(colorScheme, additionalColours);

View file

@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.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/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart'; import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.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)]), .tr(args: [DateFormat.yMMMMd().format(device.date)]),
), ),
onTap: device.isCaller onTap: device.isCaller
? null ? () => _showTokenRefreshDialog(context, device)
: () => _showConfirmationDialog(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: <Widget>[
Text(
'devices.refresh_token_alert.description'
.tr(args: [device.name]),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
actions: <Widget>[
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<TokensBloc>()
.add(const RefreshServerApiTokenEvent());
Navigator.of(context).pop();
},
),
],
),
);
} }