Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[77] [Fix] Token doesnt refresh after expires #80

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions lib/gen/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lib/modules/landing/landing_interactor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ part of 'landing_module.dart';

abstract class LandingInteractor extends Interactor<LandingInteractorDelegate> {
void validateAuthentication();
void logout();
}

abstract class LandingInteractorDelegate {
Expand All @@ -21,4 +22,9 @@ class LandingInteractorImpl extends LandingInteractor {
delegate?.authenticationDidFailToValidate.add(exception);
});
}

@override
void logout() {
_authRepository.logout().then((value) => null);
}
}
3 changes: 2 additions & 1 deletion lib/modules/landing/landing_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import 'package:survey/modules/home/home_module.dart';
import 'package:survey/modules/login/login_module.dart';
import 'package:survey/modules/screen.dart';
import 'package:survey/core/extensions/build_context.dart';
import 'package:survey/repositories/auth_repository.dart';
import 'package:survey/repositories/auth/auth_repository.dart';
import 'package:survey/services/api/api_service.dart';
import 'package:survey/services/locator/locator_service.dart';

part 'landing_presenter.dart';
Expand Down
10 changes: 8 additions & 2 deletions lib/modules/landing/landing_presenter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ class LandingPresenterImpl extends LandingPresenter
interactor.validateAuthentication();
}

void _authenticationDidFailToValidate(Object error) {
view.alert(error);
void _authenticationDidFailToValidate(Exception exception) {
if (exception == ApiException.invalidToken) {
interactor.logout();
router.replaceToLoginScreen(context: view.context);
return;
}

view.alert(exception);
}

void _didAllFinish(bool isAuthenticated) {
Expand Down
2 changes: 1 addition & 1 deletion lib/modules/login/login_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'package:survey/gen/assets.gen.dart';
import 'package:survey/modules/forgot_password/forgot_password_module.dart';
import 'package:survey/modules/home/home_module.dart';
import 'package:survey/modules/screen.dart';
import 'package:survey/repositories/auth_repository.dart';
import 'package:survey/repositories/auth/auth_repository.dart';
import 'package:survey/services/locator/locator_service.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Expand Down
25 changes: 25 additions & 0 deletions lib/repositories/auth/auth_refresh_token_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
part of 'auth_repository.dart';

class AuthRefreshTokenInterceptor extends HttpInterceptor {
final AuthRepository _authRepository = locator.get();

@override
final identifier = "auth_refresh_token";

@override
Future<void> onException(
HttpException exception, HttpExceptionInterceptorHandler handler) async {
final apiException = ApiException.fromHttpException(exception);
if (apiException == null || apiException != ApiException.invalidToken) {
return handler.next(exception);
}

try {
await _authRepository.refreshToken();
} on Exception {
return handler.next(exception);
}

return handler.retry(exception);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import 'package:survey/models/auth_token_info.dart';
import 'package:survey/services/api/api_service.dart';
import 'package:survey/services/api/auth/auth_api_service.dart';
import 'package:survey/services/api/user/user_api_service.dart';
import 'package:survey/services/http/http_service.dart';
import 'package:survey/services/local_storage/local_storage_service.dart';
import 'package:survey/services/locator/locator_service.dart';
import 'package:survey/models/user_info.dart';

part 'auth_refresh_token_interceptor.dart';

abstract class AuthRepository {
static const tokenLocalStorageKey = "auth_repository_token";

Expand All @@ -27,6 +30,8 @@ abstract class AuthRepository {
Future<void> fetchUser();

Future<void> attemptAndFetchUser();

Future<void> refreshToken();
}

class AuthRepositoryImpl implements AuthRepository {
Expand Down Expand Up @@ -83,6 +88,7 @@ class AuthRepositoryImpl implements AuthRepository {
}

_accessToken = token.accessToken;
_apiService.addGlobalInterceptors([AuthRefreshTokenInterceptor()]);
_apiService.configureGlobalToken(_accessToken, token.tokenType);
}

Expand All @@ -99,4 +105,18 @@ class AuthRepositoryImpl implements AuthRepository {
await attempt();
await fetchUser();
}

@override
Future<void> refreshToken() async {
final oldToken = await _localStorageService
.getObject<AuthTokenInfo>(AuthRepository.tokenLocalStorageKey);

final params =
AuthRefreshTokenParams(refreshToken: oldToken!.refreshToken!);
final token = await _authApiService.refreshToken(params: params);

await _localStorageService.setObject(token,
key: AuthRepository.tokenLocalStorageKey);
await attemptAndFetchUser();
}
}
14 changes: 12 additions & 2 deletions lib/services/api/api_exception.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of 'api_service.dart';

class ApiException implements LocalizedException {
class ApiException extends Equatable implements LocalizedException {
const ApiException({
required this.source,
required this.message,
Expand All @@ -26,13 +26,23 @@ class ApiException implements LocalizedException {
}

final String? source;
final String code;

@override
final String message;
final String code;

@override
List<Object?> get props => [source, code];

static const invalidResponseStructure = ApiException(
source: "local",
message: "Wrong response structure",
code: "wrong_response_structure",
);

static const invalidToken = ApiException(
source: "unauthorized",
message: "The access token is invalid",
code: "invalid_token",
);
}
14 changes: 14 additions & 0 deletions lib/services/api/api_service.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:survey/core/classes/localized_exception.dart';
import 'package:survey/gen/configs.gen.dart';
import 'package:survey/services/http/http_service.dart';
Expand Down Expand Up @@ -36,12 +37,16 @@ abstract class ApiService {
void configureGlobalBaseUrl(String? baseUrl);

void configureGlobalToken(String? token, String? tokenType);

void addGlobalInterceptors(List<HttpInterceptor> interceptor);
}

class ApiServiceImpl implements ApiService {
static String? _baseUrl;
static String? _token;
static String _tokenType = "Bearer";
static final List<HttpInterceptor> _interceptor = [];

final HttpService _httpService = locator.get();

@override
Expand Down Expand Up @@ -111,6 +116,14 @@ class ApiServiceImpl implements ApiService {
}
}

@override
void addGlobalInterceptors(List<HttpInterceptor> interceptor) {
final identifiers = interceptor.map((e) => e.identifier);
_interceptor
.removeWhere((element) => identifiers.contains(element.identifier));
_interceptor.addAll(interceptor);
}

Future<Map<String, dynamic>> _request({
required HttpMethod method,
String? baseUrl,
Expand Down Expand Up @@ -139,6 +152,7 @@ class ApiServiceImpl implements ApiService {
data: params?.toJson(),
url: url,
headers: headers,
interceptors: _interceptor,
) as Map<String, dynamic>;
} on HttpException catch (e) {
throw ApiException.fromHttpException(e) ?? e;
Expand Down
19 changes: 16 additions & 3 deletions lib/services/api/auth/auth_api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import 'package:survey/services/http/http_service.dart';

part 'params/auth_login_params.dart';

part 'params/auth_refresh_token_params.dart';

abstract class AuthApiService {
static const loginEndpoint = "/oauth/token";
static const logoutEndpoint = "/oauth/revoke";
static const refreshTokenEndpoint = loginEndpoint;

static const preferenceTokenKey = "auth_service_preference_token";

Future<AuthTokenInfo> login({
required AuthLoginParams params,
});
Future<AuthTokenInfo> login({required AuthLoginParams params});

Future<void> logout();

Future<AuthTokenInfo> refreshToken({required AuthRefreshTokenParams params});
}

class AuthApiServiceImpl implements AuthApiService {
Expand All @@ -40,4 +44,13 @@ class AuthApiServiceImpl implements AuthApiService {
endpoint: AuthApiService.logoutEndpoint,
);
}

@override
Future<AuthTokenInfo> refreshToken({required AuthRefreshTokenParams params}) {
return _apiService.call(
method: HttpMethod.post,
endpoint: AuthApiService.refreshTokenEndpoint,
params: params,
);
}
}
36 changes: 36 additions & 0 deletions lib/services/api/auth/params/auth_refresh_token_params.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
part of '../auth_api_service.dart';

class AuthRefreshTokenParams extends ApiParams {
factory AuthRefreshTokenParams({
required String refreshToken,
String? clientId,
String? clientSecret,
}) {
return AuthRefreshTokenParams._(
refreshToken: refreshToken,
clientId: clientId ?? Configs.app.api.clientId,
clientSecret: clientSecret ?? Configs.app.api.clientSecret,
);
}

AuthRefreshTokenParams._({
required this.refreshToken,
required this.clientId,
required this.clientSecret,
});

String refreshToken;
String clientId;
String clientSecret;
String grantType = "refresh_token";

@override
void mapping(Mapper map) {
map<String>(
"refresh_token", refreshToken, (v) => refreshToken = v as String);
map<String>("client_id", clientId, (v) => clientId = v as String);
map<String>(
"client_secret", clientSecret, (v) => clientSecret = v as String);
map<String>("grant_type", grantType, (v) => grantType = v as String);
}
}
2 changes: 1 addition & 1 deletion lib/services/http/http_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class HttpException implements Exception {
return HttpException(
response: response,
type: type,
error: error.error,
error: error,
);
}

Expand Down
51 changes: 51 additions & 0 deletions lib/services/http/http_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
part of 'http_service.dart';

abstract class HttpInterceptor {
String get identifier;

void onException(
HttpException exception,
HttpExceptionInterceptorHandler handler,
) =>
handler.next(exception);

Interceptor toInterceptor(Dio dio) {
return InterceptorsWrapper(onError: (e, handler) {
onException(
HttpException.fromDioError(e),
HttpExceptionInterceptorHandler._(dio: dio, handler: handler),
);
});
}
}

class HttpExceptionInterceptorHandler {
const HttpExceptionInterceptorHandler._({
required Dio dio,
required ErrorInterceptorHandler handler,
}) : _dio = dio,
_handler = handler;

final ErrorInterceptorHandler _handler;
final Dio _dio;

void next(HttpException exception) {
_handler.next(exception.error as DioError);
}

void reject(HttpException exception) {
_handler.reject(exception.error as DioError);
}

Future<void> retry(HttpException exception) {
final requestOptions = (exception.error as DioError).requestOptions;

return _dio.request(requestOptions.path,
cancelToken: requestOptions.cancelToken,
data: requestOptions.data,
onReceiveProgress: requestOptions.onReceiveProgress,
onSendProgress: requestOptions.onSendProgress,
queryParameters: requestOptions.queryParameters,
options: requestOptions as Options);
}
}
10 changes: 10 additions & 0 deletions lib/services/http/http_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import 'package:enumerated_class/enumerated_class.dart';
import 'package:flutter/foundation.dart';

part 'http_method.dart';

part 'http_exception.dart';

part 'http_response.dart';

part 'http_interceptor.dart';

abstract class HttpService {
Future<dynamic> request({
required HttpMethod method,
dynamic data,
required String url,
Map<String, dynamic>? headers,
List<HttpInterceptor> interceptors = const [],
});
}

Expand All @@ -33,7 +38,12 @@ class HttpServiceImpl implements HttpService {
dynamic data,
required String url,
Map<String, dynamic>? headers,
List<HttpInterceptor> interceptors = const [],
}) async {
_dio.interceptors.addAll(interceptors.map(
(e) => e.toInterceptor(_dio),
));

final options = Options(method: method.rawValue, headers: headers);
try {
final response = await _dio.request(url,
Expand Down
Loading