All checks were successful
ci/woodpecker/push/build Pipeline was successful
Signed-off-by: Nikolai Rodionov <allanger@posteo.com>
168 lines
5.3 KiB
Dart
168 lines
5.3 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
|
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pb.dart';
|
|
import 'package:softplayer_web/core/api/v1/accounts.dart';
|
|
import 'package:softplayer_web/core/api/v1/refresh_session.dart';
|
|
import 'package:softplayer_web/core/grpc/grpc_client.dart';
|
|
import 'package:softplayer_web/core/tokens/data/token_storage_repository.dart';
|
|
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';
|
|
|
|
class TokenState {
|
|
final String? accessToken;
|
|
final String? refreshToken;
|
|
const TokenState({this.accessToken, this.refreshToken});
|
|
|
|
TokenState copyWith({String? accessToken, String? refreshToken}) {
|
|
return TokenState(
|
|
refreshToken: refreshToken ?? this.refreshToken,
|
|
accessToken: accessToken ?? this.accessToken,
|
|
);
|
|
}
|
|
|
|
// Get an access token from the state
|
|
String getAccessToken() {
|
|
return accessToken ?? "";
|
|
}
|
|
|
|
// Get a refresh token from the state
|
|
String getRefreshToken() {
|
|
return refreshToken ?? "";
|
|
}
|
|
}
|
|
|
|
const accessTokenHeader = "x-access-token";
|
|
const refreshTokenHeader = "x-refresh-token";
|
|
|
|
class Tokens {
|
|
final String accessToken;
|
|
final String refreshToken;
|
|
|
|
const Tokens({required this.accessToken, required this.refreshToken});
|
|
|
|
factory Tokens.fromProto(TokenPair pair) {
|
|
return Tokens(
|
|
accessToken: pair.accessToken,
|
|
refreshToken: pair.refreshToken,
|
|
);
|
|
}
|
|
}
|
|
|
|
final tokensControllerProvider =
|
|
AsyncNotifierProvider<TokensController, TokenState>(TokensController.new);
|
|
|
|
class TokensController extends AsyncNotifier<TokenState> {
|
|
@override
|
|
Future<TokenState> build() async {
|
|
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
|
|
final accessToken = await tokenRepo.getAccessToken();
|
|
final refreshToken = await tokenRepo.getRefreshToken();
|
|
|
|
// If refresh token is not valid, we just drop the session,
|
|
// even if there is an active access token, and return
|
|
// an empty state
|
|
if (!verifyJWT(refreshToken)) {
|
|
log("Refresh token is not valid, logging out");
|
|
await tokenRepo.clearStorage();
|
|
return TokenState();
|
|
}
|
|
|
|
// If access token is not valid, refresh it using the refresh token
|
|
if (verifyJWT(accessToken)) {
|
|
log("Access token is valid");
|
|
return TokenState(refreshToken: refreshToken, accessToken: accessToken);
|
|
}
|
|
log("Only refresh token is valid");
|
|
return TokenState(refreshToken: refreshToken);
|
|
}
|
|
|
|
Future<void> resetTokens() async {
|
|
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
|
|
await tokenRepo.clearStorage();
|
|
state = await AsyncValue.guard(() async {
|
|
return TokenState();
|
|
});
|
|
}
|
|
|
|
// Store an access token to the storage and save it to the state
|
|
Future<void> writeAccessToken(String token) async {
|
|
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
|
|
await tokenRepo.storeAccessToken(token);
|
|
state = await AsyncValue.guard(() async {
|
|
return state.value!.copyWith(accessToken: token);
|
|
});
|
|
}
|
|
|
|
// Store a pair of tokens to the storage and refresh the state
|
|
Future<void> writeTokenPair(Tokens pair) async {
|
|
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
|
|
log("Writing tokens");
|
|
await tokenRepo.storeRefreshToken(pair.refreshToken);
|
|
await tokenRepo.storeAccessToken(pair.accessToken);
|
|
state = await AsyncValue.guard(() async {
|
|
return state.value!.copyWith(
|
|
accessToken: pair.accessToken,
|
|
refreshToken: pair.refreshToken,
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> checkTokens() async {
|
|
final currentState = state.value;
|
|
if (currentState == null ||
|
|
currentState.accessToken == null ||
|
|
currentState.refreshToken == null) {
|
|
log("Trying to get tokens from the storage");
|
|
state = await AsyncValue.guard(() async {
|
|
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
|
|
return TokenState(
|
|
accessToken: await tokenRepo.getAccessToken(),
|
|
refreshToken: await tokenRepo.getRefreshToken(),
|
|
);
|
|
});
|
|
}
|
|
log("checking tokens");
|
|
|
|
final inMemAccessToken = state.value!.getAccessToken();
|
|
// If we have a valid token in memory, nothing must be done
|
|
// TODO: Store the expiration in memory, to make it cheaper to check
|
|
if (verifyJWT(inMemAccessToken)) {
|
|
return;
|
|
} else {
|
|
log("Access it not valid");
|
|
final inMemRefreshToken = state.value!.getRefreshToken();
|
|
if (verifyJWT(inMemRefreshToken)) {
|
|
log("Trying to refresh");
|
|
try {
|
|
final refreshSessionGrpc = ref.read(refreshSessionGrpcProvider);
|
|
log("Trying to refresh");
|
|
final response = await refreshSessionGrpc.refreshSession(
|
|
inMemRefreshToken,
|
|
);
|
|
await writeTokenPair(Tokens.fromProto(response.tokenPair));
|
|
return;
|
|
} catch (e) {
|
|
log(e.toString());
|
|
}
|
|
}
|
|
}
|
|
log("Refrsh it not valid");
|
|
await resetTokens();
|
|
ref.read(authorizationControllerProvider.notifier).logout();
|
|
}
|
|
|
|
bool verifyJWT(String? token) {
|
|
if (token == null || token.isEmpty) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
bool isExpired = JwtDecoder.isExpired(token);
|
|
return !isExpired;
|
|
} catch (_) {
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|