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.",
"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": {

View file

@ -550,6 +550,41 @@ class ServerApi extends GraphQLApiMap
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<GenericResult<String>> authorizeDevice(

View file

@ -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<TokensEvent, TokensState> {
on<ServerSelectedForProviderToken>(
connectServerToProviderToken,
);
on<RefreshServerApiTokenEvent>(
refreshServerApiToken,
transformer: droppable(),
);
add(const RevalidateTokens());
@ -208,6 +213,33 @@ class TokensBloc extends Bloc<TokensEvent, TokensState> {
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
Future<void> close() {
_resourcesModelSubscription.cancel();

View file

@ -34,3 +34,10 @@ class ServerSelectedForProviderToken extends TokensEvent {
@override
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() {
_dataStream.close();
_connectionStatusStream.close();

View file

@ -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,
);

View file

@ -52,7 +52,8 @@ abstract class AppThemeFactory {
}
static Future<ColorScheme?> _getDynamicColors(
final Brightness brightness) async {
final Brightness brightness,
) async {
List<Color> extractAdditionalColours(final ColorScheme scheme) => [
scheme.surface,
scheme.surfaceDim,
@ -65,7 +66,9 @@ abstract class AppThemeFactory {
];
ColorScheme insertAdditionalColours(
final ColorScheme scheme, final List<Color> additionalColours) =>
final ColorScheme scheme,
final List<Color> 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);

View file

@ -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: <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();
},
),
],
),
);
}