In my project, I use the fresh_graphql package to refresh tokens at the GraphQLClient
link level. This package adds the access token to headers, manages token storage, and handles authorization errors, after which it refreshes the tokens.
I plan to create a similar link that will only add tokens to headers and handle authorization errors, so that token management can be done manually in the authorization service.
I have an AuthService
that uses AuthRepository
to make requests to the server. AuthRepository
uses GraphQLClient
to make requests to the server. GraphQLClient
uses FreshLink
to automatically refresh tokens. FreshLink
uses AuthService
to get the current tokens.
Listing AuthService
:
class AuthService {
final AuthRepository _authRepository;
final _authenticationStatusController = StreamController<bool>.broadcast();
const AuthService({
required AuthRepository authRepository,
}) : _authRepository = authRepository;
bool get isAuthenticated => _authRepository.currentAuthTokens != null;
Stream<bool> get authenticationStatus => _authenticationStatusController.stream;
Future<void> login(String email, String password) async {
final authTokens = await _authRepository.login(email, password);
_authenticationStatusController.add(true);
await _authRepository.saveAuthTokens(authTokens);
}
Future<void> logout() async {
await _authRepository.logout();
await _authRepository.deleteAuthTokens();
_authenticationStatusController.add(false);
}
Future<void> refreshTokens() async {
final authTokens = await _authRepository.refreshTokens();
await _authRepository.saveAuthTokens(authTokens);
}
Future<void> revokeTokens() async {
await _authRepository.deleteAuthTokens();
_authenticationStatusController.add(false);
}
void dispose() {
_authenticationStatusController.close();
}
}
Listing AuthRepository
:
class AuthRepository {
final GraphQLClient _graphQLClient;
const AuthRepository({
required GraphQLClient graphQLClient,
}) : _graphQLClient = graphQLClient;
Future<AuthTokens> login(String email, String password) async {
const query = r'''
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) { accessToken refreshToken }
}
''';
final options = MutationOptions(
document: gql(query),
variables: {
'email': email,
'password': password,
},
);
final result = await _graphQLClient.mutate(options);
if (result.hasException) throw result.exception!;
return AuthTokensGraphQL.fromJson(result.data!['login']);
}
Future<void> logout() async {
// logout
}
AuthTokens? get currentAuthTokens {
// fetch from local storage
}
Future<void> saveAuthTokens(AuthTokens authTokens) async {
// save to local storage
}
Future<void> deleteAuthTokens() async {
// delete from local storage
}
}
Listing FreshLink
:
class FreshLink extends Link {
final AuthService _authService;
FreshLink({
required AuthService authService,
}) : _authService = authService;
@override
Stream<Response> request(Request request, [NextLink? forward]) async* {
// add access token to headers
// do request
// handle unauthorized error
}
}
Listing GraphQLClient
:
final freshLink = FreshLink(
authService: authService,
);
final httpLink = HttpLink(
'https://example.app/graphql',
);
final links = Link.from([freshLink, httpLink]);
final graphQLClient = GraphQLClient(
cache: GraphQLCache(),
link: links,
);
As you can see, AuthService
uses AuthRepository
, which uses GraphQLClient
, which uses FreshLink
, which uses AuthService
.
How can I fix this problem?