From 426f8ad7f7797b7dffe36a9b1dd8f006c8dbef43 Mon Sep 17 00:00:00 2001 From: Lzyct Date: Sat, 9 Aug 2025 15:01:06 +0800 Subject: [PATCH 1/2] feat: implement refresh token mechanism in DioInterceptor and update auth handling --- lib/core/api/dio_interceptor.dart | 69 ++++++++++++++++--- lib/core/api/list_api.dart | 1 + .../auth/data/models/login_response.dart | 1 + .../repositories/auth_repository_impl.dart | 40 +++++------ lib/utils/services/hive/main_box.dart | 1 + 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/lib/core/api/dio_interceptor.dart b/lib/core/api/dio_interceptor.dart index 3bc14ce..69d3243 100644 --- a/lib/core/api/dio_interceptor.dart +++ b/lib/core/api/dio_interceptor.dart @@ -2,21 +2,20 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_auth_app/core/core.dart'; +import 'package:flutter_auth_app/features/auth/auth.dart'; import 'package:flutter_auth_app/utils/utils.dart'; // coverage:ignore-start -class DioInterceptor extends Interceptor with FirebaseCrashLogger { +class DioInterceptor extends Interceptor + with FirebaseCrashLogger, MainBoxMixin { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { String headerMessage = ''; options.headers.forEach((k, v) => headerMessage += '► $k: $v\n'); try { - options.queryParameters.forEach( - (k, v) => debugPrint( - '► $k: $v', - ), - ); + options.queryParameters.forEach((k, v) => debugPrint('► $k: $v')); } catch (_) {} try { const JsonEncoder encoder = JsonEncoder.withIndent(' '); @@ -38,14 +37,67 @@ class DioInterceptor extends Interceptor with FirebaseCrashLogger { } @override - void onError(DioException dioException, ErrorInterceptorHandler handler) { + Future onError( + DioException dioException, + ErrorInterceptorHandler handler, + ) async { log.e( "<-- ${dioException.message} ${dioException.response?.requestOptions != null ? (dioException.response!.requestOptions.baseUrl + dioException.response!.requestOptions.path) : 'URL'}\n\n" "${dioException.response != null ? dioException.response!.data : 'Unknown Error'}", ); nonFatalError(error: dioException, stackTrace: dioException.stackTrace); - super.onError(dioException, handler); + if (dioException.response?.statusCode == 401 && + dioException.response?.data['meta']['description'] == + 'Unauthenticated.') { + if (getData(MainBoxKeys.refreshToken) != null) { + await refreshToken(); + + // Retry the request with the new token + return handler.resolve(await _retry(dioException.requestOptions)); + } else { + logoutBox(); + } + } + return handler.next(dioException); + } + + Future> _retry(RequestOptions requestOptions) { + final options = Options( + method: requestOptions.method, + headers: requestOptions.headers, + ); + + return DioClient().dio.request( + requestOptions.path, + data: requestOptions.data, + queryParameters: requestOptions.queryParameters, + options: options, + ); + } + + Future refreshToken() async { + /// Call API Refresh token + final response = await DioClient().postRequest( + ListAPI.generalToken, + data: { + 'clientId': const String.fromEnvironment('USER_CLIENT_ID'), + 'clientSecret': const String.fromEnvironment('USER_CLIENT_SECRET'), + 'grantType': 'refresh_token', + 'refreshToken': getData(MainBoxKeys.refreshToken), + }, + converter: (response) => + LoginResponse.fromJson(response as Map), + ); + + response.fold((l) => logoutBox(), (r) { + final data = r.data; + addData( + MainBoxKeys.refreshToken, + '${data?.tokenType} ${data?.refreshToken}', + ); + addData(MainBoxKeys.authToken, '${data?.tokenType} ${data?.token}'); + }); } @override @@ -66,4 +118,5 @@ class DioInterceptor extends Interceptor with FirebaseCrashLogger { super.onResponse(response, handler); } } + // coverage:ignore-end diff --git a/lib/core/api/list_api.dart b/lib/core/api/list_api.dart index 1d69fe2..0164b51 100644 --- a/lib/core/api/list_api.dart +++ b/lib/core/api/list_api.dart @@ -3,6 +3,7 @@ class ListAPI { /// Auth static const String generalToken = '/v1/api/auth/general'; + static const String refreshToken = '/v1/api/auth/refresh'; static const String user = '/v1/api/user'; static const String login = '/v1/api/auth/login'; static const String logout = '/v1/api/auth/logout'; diff --git a/lib/features/auth/data/models/login_response.dart b/lib/features/auth/data/models/login_response.dart index 6fd2688..8e05ff0 100644 --- a/lib/features/auth/data/models/login_response.dart +++ b/lib/features/auth/data/models/login_response.dart @@ -25,6 +25,7 @@ sealed class DataLogin with _$DataLogin { const factory DataLogin({ @JsonKey(name: 'token') String? token, @JsonKey(name: 'tokenType') String? tokenType, + @JsonKey(name: 'refreshToken') String? refreshToken, }) = _DataLogin; factory DataLogin.fromJson(Map json) => diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/lib/features/auth/data/repositories/auth_repository_impl.dart index a3bdad5..c4b8c84 100644 --- a/lib/features/auth/data/repositories/auth_repository_impl.dart +++ b/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -14,18 +14,19 @@ class AuthRepositoryImpl implements AuthRepository { Future> login(LoginParams params) async { final response = await authRemoteDatasource.login(params); - return response.fold( - (failure) => Left(failure), - (loginResponse) { - mainBoxMixin.addData(MainBoxKeys.isLogin, true); - mainBoxMixin.addData( - MainBoxKeys.authToken, - '${loginResponse.data?.tokenType} ${loginResponse.data?.token}', - ); + return response.fold((failure) => Left(failure), (loginResponse) { + mainBoxMixin.addData(MainBoxKeys.isLogin, true); + mainBoxMixin.addData( + MainBoxKeys.authToken, + '${loginResponse.data?.tokenType} ${loginResponse.data?.token}', + ); + mainBoxMixin.addData( + MainBoxKeys.refreshToken, + '${loginResponse.data?.tokenType} ${loginResponse.data?.refreshToken}', + ); - return Right(loginResponse.toEntity()); - }, - ); + return Right(loginResponse.toEntity()); + }); } @override @@ -44,17 +45,14 @@ class AuthRepositoryImpl implements AuthRepository { ) async { final response = await authRemoteDatasource.generalToken(params); - return response.fold( - (failure) => Left(failure), - (loginResponse) { - mainBoxMixin.addData( - MainBoxKeys.generalToken, - '${loginResponse.data?.tokenType} ${loginResponse.data?.token}', - ); + return response.fold((failure) => Left(failure), (loginResponse) { + mainBoxMixin.addData( + MainBoxKeys.generalToken, + '${loginResponse.data?.tokenType} ${loginResponse.data?.token}', + ); - return Right(loginResponse.toEntity()); - }, - ); + return Right(loginResponse.toEntity()); + }); } @override diff --git a/lib/utils/services/hive/main_box.dart b/lib/utils/services/hive/main_box.dart index 74caab6..0f1ed11 100644 --- a/lib/utils/services/hive/main_box.dart +++ b/lib/utils/services/hive/main_box.dart @@ -14,6 +14,7 @@ enum ActiveTheme { enum MainBoxKeys { generalToken, authToken, + refreshToken, fcm, language, theme, From 423463743906931549bfa4f039fdbb09e99292c6 Mon Sep 17 00:00:00 2001 From: Lzyct Date: Sat, 9 Aug 2025 17:00:04 +0800 Subject: [PATCH 2/2] feat: add refresh token to login response and test cases --- .../auth/data/datasources/models/login_response_test.dart | 4 ++++ test/helpers/stubs/login_response_200.json | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/features/auth/data/datasources/models/login_response_test.dart b/test/features/auth/data/datasources/models/login_response_test.dart index 8ae9f88..680f9a7 100644 --- a/test/features/auth/data/datasources/models/login_response_test.dart +++ b/test/features/auth/data/datasources/models/login_response_test.dart @@ -13,6 +13,8 @@ void main() { token: 'lazycatlabs.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdXRoX2FwcCIsImlhdCI6MTY5OTExMDMxOSwiZXhwIjoxNjk5NzE1MTE5fQ.SlybkWOQs9JJpTG-tRYJapalESGNE63atPEfw6ry7NdcoZFkjYDAdlYfnIBlp9eUISAjSc7IUtvKGks0jEJ27V_iUBdKcS0aTVvtd8g1yW14UBGW6jKsOn9QtgxWnPELP0GZ1TRzObZW3bYAXpiVsC9o0LnONmq5ehMUgHVYknF_wTfHwSB2pb77pAZguwK4I9MI4BoqcvcuET36MEgYs9vY-e0f2y50nHN4kbjVe9iFay0GeNIRQsWzzmyN5Xd9Zv5HiSCgbB80UA6SrneoExBi-fNIlxrOxJRaVt16-1ElXu04W5Y_FIoY-jekmMWusE54csh3Woo6ChQQJEopfuU6prdP50TN7UpqiH_o3R77MdgcYBdJ-puZOt-XsplOHNAjDtp2rpo9UExQUlOVxQFuvSKkanxaOSsAXYuOaEh9iBoq0LQ_JiaIbrZBn7EVxKhFnUJokv7SvPMg2LG7p7wczgxYjnuxG0fDRRjK2vAQyAj0rIigd6xpA6g-ii5VWRsk_sMJw-QJW_ivZdQZwjlXeH-EcVeTaZ9yn2zmmavF6sxDxC1SDGGkbKjUpfIdQYa-t82sPO0HUd_OBQ8ZiBGmSV1gi-8lAat1XtJTgsgM0zTxqK5kwekc3gsoZfVdlhJ8SyN6ohyOMU8Hv8M2H7lG8u1DTq1jj7rb1BEtwGw', tokenType: 'Bearer', + refreshToken: + 'lazycatlabs.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdXRoX2FwcCIsImlhdCI6MTY5OTExMDMxOSwiZXhwIjoxNjk5NzE1MTE5fQ.SlybkWOQs9JJpTG-tRYJapalESGNE63atPEfw6ry7NdcoZFkjYDAdlYfnIBlp9eUISAjSc7IUtvKGks0jEJ27V_iUBdKcS0aTVvtd8g1yW14UBGW6jKsOn9QtgxWnPELP0GZ1TRzObZW3bYAXpiVsC9o0LnONmq5ehMUgHVYknF_wTfHwSB2pb77pAZguwK4I9MI4BoqcvcuET36MEgYs9vY-e0f2y50nHN4kbjVe9iFay0GeNIRQsWzzmyN5Xd9Zv5HiSCgbB80UA6SrneoExBi-fNIlxrOxJRaVt16-1ElXu04W5Y_FIoY-jekmMWusE54csh3Woo6ChQQJEopfuU6prdP50TN7UpqiH_o3R77MdgcYBdJ-puZOt-XsplOHNAjDtp2rpo9UExQUlOVxQFuvSKkanxaOSsAXYuOaEh9iBoq0LQ_JiaIbrZBn7EVxKhFnUJokv7SvPMg2LG7p7wczgxYjnuxG0fDRRjK2vAQyAj0rIigd6xpA6g-ii5VWRsk_sMJw-QJW_ivZdQZwjlXeH-EcVeTaZ9yn2zmmavF6sxDxC1SDGGkbKjUpfIdQYa-t82sPO0HUd_OBQ8ZiBGmSV1gi-8lAat1XtJTgsgM0zTxqK5kwekc3gsoZfVdlhJ8SyN6ohyOMU8Hv8', ), ); @@ -38,6 +40,8 @@ void main() { 'token': 'lazycatlabs.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdXRoX2FwcCIsImlhdCI6MTY5OTExMDMxOSwiZXhwIjoxNjk5NzE1MTE5fQ.SlybkWOQs9JJpTG-tRYJapalESGNE63atPEfw6ry7NdcoZFkjYDAdlYfnIBlp9eUISAjSc7IUtvKGks0jEJ27V_iUBdKcS0aTVvtd8g1yW14UBGW6jKsOn9QtgxWnPELP0GZ1TRzObZW3bYAXpiVsC9o0LnONmq5ehMUgHVYknF_wTfHwSB2pb77pAZguwK4I9MI4BoqcvcuET36MEgYs9vY-e0f2y50nHN4kbjVe9iFay0GeNIRQsWzzmyN5Xd9Zv5HiSCgbB80UA6SrneoExBi-fNIlxrOxJRaVt16-1ElXu04W5Y_FIoY-jekmMWusE54csh3Woo6ChQQJEopfuU6prdP50TN7UpqiH_o3R77MdgcYBdJ-puZOt-XsplOHNAjDtp2rpo9UExQUlOVxQFuvSKkanxaOSsAXYuOaEh9iBoq0LQ_JiaIbrZBn7EVxKhFnUJokv7SvPMg2LG7p7wczgxYjnuxG0fDRRjK2vAQyAj0rIigd6xpA6g-ii5VWRsk_sMJw-QJW_ivZdQZwjlXeH-EcVeTaZ9yn2zmmavF6sxDxC1SDGGkbKjUpfIdQYa-t82sPO0HUd_OBQ8ZiBGmSV1gi-8lAat1XtJTgsgM0zTxqK5kwekc3gsoZfVdlhJ8SyN6ohyOMU8Hv8M2H7lG8u1DTq1jj7rb1BEtwGw', 'tokenType': 'Bearer', + 'refreshToken': + 'lazycatlabs.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdXRoX2FwcCIsImlhdCI6MTY5OTExMDMxOSwiZXhwIjoxNjk5NzE1MTE5fQ.SlybkWOQs9JJpTG-tRYJapalESGNE63atPEfw6ry7NdcoZFkjYDAdlYfnIBlp9eUISAjSc7IUtvKGks0jEJ27V_iUBdKcS0aTVvtd8g1yW14UBGW6jKsOn9QtgxWnPELP0GZ1TRzObZW3bYAXpiVsC9o0LnONmq5ehMUgHVYknF_wTfHwSB2pb77pAZguwK4I9MI4BoqcvcuET36MEgYs9vY-e0f2y50nHN4kbjVe9iFay0GeNIRQsWzzmyN5Xd9Zv5HiSCgbB80UA6SrneoExBi-fNIlxrOxJRaVt16-1ElXu04W5Y_FIoY-jekmMWusE54csh3Woo6ChQQJEopfuU6prdP50TN7UpqiH_o3R77MdgcYBdJ-puZOt-XsplOHNAjDtp2rpo9UExQUlOVxQFuvSKkanxaOSsAXYuOaEh9iBoq0LQ_JiaIbrZBn7EVxKhFnUJokv7SvPMg2LG7p7wczgxYjnuxG0fDRRjK2vAQyAj0rIigd6xpA6g-ii5VWRsk_sMJw-QJW_ivZdQZwjlXeH-EcVeTaZ9yn2zmmavF6sxDxC1SDGGkbKjUpfIdQYa-t82sPO0HUd_OBQ8ZiBGmSV1gi-8lAat1XtJTgsgM0zTxqK5kwekc3gsoZfVdlhJ8SyN6ohyOMU8Hv8', }, }; diff --git a/test/helpers/stubs/login_response_200.json b/test/helpers/stubs/login_response_200.json index d41c1a0..c5a7114 100644 --- a/test/helpers/stubs/login_response_200.json +++ b/test/helpers/stubs/login_response_200.json @@ -5,6 +5,7 @@ }, "data": { "token": "lazycatlabs.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdXRoX2FwcCIsImlhdCI6MTY5OTExMDMxOSwiZXhwIjoxNjk5NzE1MTE5fQ.SlybkWOQs9JJpTG-tRYJapalESGNE63atPEfw6ry7NdcoZFkjYDAdlYfnIBlp9eUISAjSc7IUtvKGks0jEJ27V_iUBdKcS0aTVvtd8g1yW14UBGW6jKsOn9QtgxWnPELP0GZ1TRzObZW3bYAXpiVsC9o0LnONmq5ehMUgHVYknF_wTfHwSB2pb77pAZguwK4I9MI4BoqcvcuET36MEgYs9vY-e0f2y50nHN4kbjVe9iFay0GeNIRQsWzzmyN5Xd9Zv5HiSCgbB80UA6SrneoExBi-fNIlxrOxJRaVt16-1ElXu04W5Y_FIoY-jekmMWusE54csh3Woo6ChQQJEopfuU6prdP50TN7UpqiH_o3R77MdgcYBdJ-puZOt-XsplOHNAjDtp2rpo9UExQUlOVxQFuvSKkanxaOSsAXYuOaEh9iBoq0LQ_JiaIbrZBn7EVxKhFnUJokv7SvPMg2LG7p7wczgxYjnuxG0fDRRjK2vAQyAj0rIigd6xpA6g-ii5VWRsk_sMJw-QJW_ivZdQZwjlXeH-EcVeTaZ9yn2zmmavF6sxDxC1SDGGkbKjUpfIdQYa-t82sPO0HUd_OBQ8ZiBGmSV1gi-8lAat1XtJTgsgM0zTxqK5kwekc3gsoZfVdlhJ8SyN6ohyOMU8Hv8M2H7lG8u1DTq1jj7rb1BEtwGw", - "tokenType": "Bearer" + "tokenType": "Bearer", + "refreshToken": "lazycatlabs.eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhdXRoX2FwcCIsImlhdCI6MTY5OTExMDMxOSwiZXhwIjoxNjk5NzE1MTE5fQ.SlybkWOQs9JJpTG-tRYJapalESGNE63atPEfw6ry7NdcoZFkjYDAdlYfnIBlp9eUISAjSc7IUtvKGks0jEJ27V_iUBdKcS0aTVvtd8g1yW14UBGW6jKsOn9QtgxWnPELP0GZ1TRzObZW3bYAXpiVsC9o0LnONmq5ehMUgHVYknF_wTfHwSB2pb77pAZguwK4I9MI4BoqcvcuET36MEgYs9vY-e0f2y50nHN4kbjVe9iFay0GeNIRQsWzzmyN5Xd9Zv5HiSCgbB80UA6SrneoExBi-fNIlxrOxJRaVt16-1ElXu04W5Y_FIoY-jekmMWusE54csh3Woo6ChQQJEopfuU6prdP50TN7UpqiH_o3R77MdgcYBdJ-puZOt-XsplOHNAjDtp2rpo9UExQUlOVxQFuvSKkanxaOSsAXYuOaEh9iBoq0LQ_JiaIbrZBn7EVxKhFnUJokv7SvPMg2LG7p7wczgxYjnuxG0fDRRjK2vAQyAj0rIigd6xpA6g-ii5VWRsk_sMJw-QJW_ivZdQZwjlXeH-EcVeTaZ9yn2zmmavF6sxDxC1SDGGkbKjUpfIdQYa-t82sPO0HUd_OBQ8ZiBGmSV1gi-8lAat1XtJTgsgM0zTxqK5kwekc3gsoZfVdlhJ8SyN6ohyOMU8Hv8" } -} \ No newline at end of file +}