add auth, format
This commit is contained in:
@@ -10,11 +10,12 @@ library;
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
|
||||
|
||||
part 'api_interceptor.g.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ part 'api_interceptor.g.dart';
|
||||
// ============================================================================
|
||||
|
||||
/// Keys for storing auth tokens in SharedPreferences
|
||||
/// @deprecated Use AuthLocalDataSource with Hive instead
|
||||
class AuthStorageKeys {
|
||||
static const String accessToken = 'auth_access_token';
|
||||
static const String refreshToken = 'auth_refresh_token';
|
||||
@@ -33,12 +35,15 @@ class AuthStorageKeys {
|
||||
// Auth Interceptor
|
||||
// ============================================================================
|
||||
|
||||
/// Interceptor for adding authentication tokens to requests
|
||||
/// Interceptor for adding ERPNext session tokens to requests
|
||||
///
|
||||
/// Adds SID (Session ID) and CSRF token from Hive storage to request headers.
|
||||
class AuthInterceptor extends Interceptor {
|
||||
AuthInterceptor(this._prefs, this._dio);
|
||||
AuthInterceptor(this._prefs, this._dio, this._authLocalDataSource);
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
final Dio _dio;
|
||||
final AuthLocalDataSource _authLocalDataSource;
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
@@ -47,10 +52,19 @@ class AuthInterceptor extends Interceptor {
|
||||
) async {
|
||||
// Check if this endpoint requires authentication
|
||||
if (_requiresAuth(options.path)) {
|
||||
final token = await _getAccessToken();
|
||||
// Get session data from secure storage (async)
|
||||
final sid = await _authLocalDataSource.getSid();
|
||||
final csrfToken = await _authLocalDataSource.getCsrfToken();
|
||||
|
||||
if (sid != null && csrfToken != null) {
|
||||
// Add ERPNext session headers
|
||||
options.headers['Cookie'] = 'sid=$sid';
|
||||
options.headers['X-Frappe-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
// Legacy: Also check for access token (for backward compatibility)
|
||||
final token = await _getAccessToken();
|
||||
if (token != null) {
|
||||
// Add bearer token to headers
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
}
|
||||
@@ -66,10 +80,7 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// Check if error is 401 Unauthorized
|
||||
if (err.response?.statusCode == 401) {
|
||||
// Try to refresh token
|
||||
@@ -113,15 +124,16 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
/// Check if token is expired
|
||||
Future<bool> _isTokenExpired() async {
|
||||
final expiryString = _prefs.getString(AuthStorageKeys.tokenExpiry);
|
||||
if (expiryString == null) return true;
|
||||
|
||||
final expiry = DateTime.tryParse(expiryString);
|
||||
if (expiry == null) return true;
|
||||
|
||||
return DateTime.now().isAfter(expiry);
|
||||
}
|
||||
// TODO: Use this method when implementing token refresh logic
|
||||
// Future<bool> _isTokenExpired() async {
|
||||
// final expiryString = _prefs.getString(AuthStorageKeys.tokenExpiry);
|
||||
// if (expiryString == null) return true;
|
||||
//
|
||||
// final expiry = DateTime.tryParse(expiryString);
|
||||
// if (expiry == null) return true;
|
||||
//
|
||||
// return DateTime.now().isAfter(expiry);
|
||||
// }
|
||||
|
||||
/// Refresh access token using refresh token
|
||||
Future<bool> _refreshAccessToken() async {
|
||||
@@ -135,11 +147,7 @@ class AuthInterceptor extends Interceptor {
|
||||
// Call refresh token endpoint
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.apiBaseUrl}${ApiConstants.refreshToken}',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': 'Bearer $refreshToken',
|
||||
},
|
||||
),
|
||||
options: Options(headers: {'Authorization': 'Bearer $refreshToken'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -185,10 +193,7 @@ class AuthInterceptor extends Interceptor {
|
||||
|
||||
final options = Options(
|
||||
method: requestOptions.method,
|
||||
headers: {
|
||||
...requestOptions.headers,
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
headers: {...requestOptions.headers, 'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
return _dio.request(
|
||||
@@ -217,19 +222,13 @@ class LoggingInterceptor extends Interceptor {
|
||||
final bool enableErrorLogging;
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) {
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (enableRequestLogging) {
|
||||
developer.log(
|
||||
'╔══════════════════════════════════════════════════════════════',
|
||||
name: 'HTTP Request',
|
||||
);
|
||||
developer.log(
|
||||
'║ ${options.method} ${options.uri}',
|
||||
name: 'HTTP Request',
|
||||
);
|
||||
developer.log('║ ${options.method} ${options.uri}', name: 'HTTP Request');
|
||||
developer.log(
|
||||
'║ Headers: ${_sanitizeHeaders(options.headers)}',
|
||||
name: 'HTTP Request',
|
||||
@@ -290,10 +289,7 @@ class LoggingInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (enableErrorLogging) {
|
||||
developer.log(
|
||||
'╔══════════════════════════════════════════════════════════════',
|
||||
@@ -303,18 +299,12 @@ class LoggingInterceptor extends Interceptor {
|
||||
'║ ${err.requestOptions.method} ${err.requestOptions.uri}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log(
|
||||
'║ Error Type: ${err.type}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log('║ Error Type: ${err.type}', name: 'HTTP Error');
|
||||
developer.log(
|
||||
'║ Status Code: ${err.response?.statusCode}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log(
|
||||
'║ Message: ${err.message}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log('║ Message: ${err.message}', name: 'HTTP Error');
|
||||
|
||||
if (err.response?.data != null) {
|
||||
developer.log(
|
||||
@@ -389,10 +379,7 @@ class LoggingInterceptor extends Interceptor {
|
||||
/// Interceptor for transforming Dio errors into custom exceptions
|
||||
class ErrorTransformerInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
Exception exception;
|
||||
|
||||
switch (err.type) {
|
||||
@@ -415,9 +402,7 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
break;
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
exception = NetworkException(
|
||||
'Lỗi không xác định: ${err.message}',
|
||||
);
|
||||
exception = NetworkException('Lỗi không xác định: ${err.message}');
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -447,7 +432,8 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
// Extract error message from response
|
||||
String? message;
|
||||
if (data is Map<String, dynamic>) {
|
||||
message = data['message'] as String? ??
|
||||
message =
|
||||
data['message'] as String? ??
|
||||
data['error'] as String? ??
|
||||
data['msg'] as String?;
|
||||
}
|
||||
@@ -460,9 +446,7 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
final validationErrors = errors.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value is List
|
||||
? value.cast<String>()
|
||||
: [value.toString()],
|
||||
value is List ? value.cast<String>() : [value.toString()],
|
||||
),
|
||||
);
|
||||
return ValidationException(
|
||||
@@ -498,9 +482,7 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
final validationErrors = errors.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value is List
|
||||
? value.cast<String>()
|
||||
: [value.toString()],
|
||||
value is List ? value.cast<String>() : [value.toString()],
|
||||
),
|
||||
);
|
||||
return ValidationException(
|
||||
@@ -513,7 +495,9 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
|
||||
case 429:
|
||||
final retryAfter = response.headers.value('retry-after');
|
||||
final retrySeconds = retryAfter != null ? int.tryParse(retryAfter) : null;
|
||||
final retrySeconds = retryAfter != null
|
||||
? int.tryParse(retryAfter)
|
||||
: null;
|
||||
return RateLimitException(message ?? 'Quá nhiều yêu cầu', retrySeconds);
|
||||
|
||||
case 500:
|
||||
@@ -549,7 +533,15 @@ Future<SharedPreferences> sharedPreferences(Ref ref) async {
|
||||
@riverpod
|
||||
Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
|
||||
final prefs = await ref.watch(sharedPreferencesProvider.future);
|
||||
return AuthInterceptor(prefs, dio);
|
||||
|
||||
// Create AuthLocalDataSource with FlutterSecureStorage
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
);
|
||||
final authLocalDataSource = AuthLocalDataSource(secureStorage);
|
||||
|
||||
return AuthInterceptor(prefs, dio, authLocalDataSource);
|
||||
}
|
||||
|
||||
/// Provider for LoggingInterceptor
|
||||
|
||||
@@ -114,7 +114,7 @@ final class AuthInterceptorProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$authInterceptorHash() => r'b54ba9af62c3cd7b922ef4030a8e2debb0220e10';
|
||||
String _$authInterceptorHash() => r'3f964536e03e204d09cc9120dd9d961b6d6d4b71';
|
||||
|
||||
/// Provider for AuthInterceptor
|
||||
|
||||
|
||||
@@ -215,14 +215,14 @@ class DioClient {
|
||||
/// Clear all cached responses
|
||||
Future<void> clearCache() async {
|
||||
if (_cacheStore != null) {
|
||||
await _cacheStore!.clean();
|
||||
await _cacheStore.clean();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear specific cached response by key
|
||||
Future<void> clearCacheByKey(String key) async {
|
||||
if (_cacheStore != null) {
|
||||
await _cacheStore!.delete(key);
|
||||
await _cacheStore.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ class DioClient {
|
||||
final key = CacheOptions.defaultCacheKeyBuilder(
|
||||
RequestOptions(path: path),
|
||||
);
|
||||
await _cacheStore!.delete(key);
|
||||
await _cacheStore.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,10 +258,7 @@ class RetryInterceptor extends Interceptor {
|
||||
final double delayMultiplier;
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// Get retry count from request extra
|
||||
final retries = err.requestOptions.extra['retries'] as int? ?? 0;
|
||||
|
||||
@@ -279,8 +276,9 @@ class RetryInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
// Calculate delay with exponential backoff
|
||||
final delayMs = (initialDelay.inMilliseconds *
|
||||
(delayMultiplier * (retries + 1))).toInt();
|
||||
final delayMs =
|
||||
(initialDelay.inMilliseconds * (delayMultiplier * (retries + 1)))
|
||||
.toInt();
|
||||
final delay = Duration(
|
||||
milliseconds: delayMs.clamp(
|
||||
initialDelay.inMilliseconds,
|
||||
@@ -341,10 +339,7 @@ class RetryInterceptor extends Interceptor {
|
||||
@riverpod
|
||||
Future<CacheStore> cacheStore(Ref ref) async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
return HiveCacheStore(
|
||||
directory.path,
|
||||
hiveBoxName: 'dio_cache',
|
||||
);
|
||||
return HiveCacheStore(directory.path, hiveBoxName: 'dio_cache');
|
||||
}
|
||||
|
||||
/// Provider for cache options
|
||||
@@ -371,31 +366,32 @@ Future<Dio> dio(Ref ref) async {
|
||||
// Base configuration
|
||||
dio
|
||||
..options = BaseOptions(
|
||||
baseUrl: ApiConstants.apiBaseUrl,
|
||||
connectTimeout: ApiConstants.connectionTimeout,
|
||||
receiveTimeout: ApiConstants.receiveTimeout,
|
||||
sendTimeout: ApiConstants.sendTimeout,
|
||||
headers: {
|
||||
'Content-Type': ApiConstants.contentTypeJson,
|
||||
'Accept': ApiConstants.acceptJson,
|
||||
'Accept-Language': ApiConstants.acceptLanguageVi,
|
||||
},
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) {
|
||||
// Accept all status codes and handle errors in interceptor
|
||||
return status != null && status < 500;
|
||||
},
|
||||
)
|
||||
|
||||
// Add interceptors in order
|
||||
|
||||
baseUrl: ApiConstants.apiBaseUrl,
|
||||
connectTimeout: ApiConstants.connectionTimeout,
|
||||
receiveTimeout: ApiConstants.receiveTimeout,
|
||||
sendTimeout: ApiConstants.sendTimeout,
|
||||
headers: {
|
||||
'Content-Type': ApiConstants.contentTypeJson,
|
||||
'Accept': ApiConstants.acceptJson,
|
||||
'Accept-Language': ApiConstants.acceptLanguageVi,
|
||||
},
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) {
|
||||
// Accept all status codes and handle errors in interceptor
|
||||
return status != null && status < 500;
|
||||
},
|
||||
)
|
||||
// Add interceptors in order
|
||||
// 1. Logging interceptor (first to log everything)
|
||||
..interceptors.add(ref.watch(loggingInterceptorProvider))
|
||||
|
||||
// 2. Auth interceptor (add tokens to requests)
|
||||
..interceptors.add(await ref.watch(authInterceptorProvider(dio).future))
|
||||
// 3. Cache interceptor
|
||||
..interceptors.add(DioCacheInterceptor(options: await ref.watch(cacheOptionsProvider.future)))
|
||||
..interceptors.add(
|
||||
DioCacheInterceptor(
|
||||
options: await ref.watch(cacheOptionsProvider.future),
|
||||
),
|
||||
)
|
||||
// 4. Retry interceptor
|
||||
..interceptors.add(RetryInterceptor(ref.watch(networkInfoProvider)))
|
||||
// 5. Error transformer (last to transform all errors)
|
||||
@@ -430,9 +426,7 @@ class ApiRequestOptions {
|
||||
final bool forceRefresh;
|
||||
|
||||
/// Options with cache enabled
|
||||
static const cached = ApiRequestOptions(
|
||||
cachePolicy: CachePolicy.forceCache,
|
||||
);
|
||||
static const cached = ApiRequestOptions(cachePolicy: CachePolicy.forceCache);
|
||||
|
||||
/// Options with network-first strategy
|
||||
static const networkFirst = ApiRequestOptions(
|
||||
@@ -449,12 +443,9 @@ class ApiRequestOptions {
|
||||
Options toDioOptions() {
|
||||
return Options(
|
||||
extra: <String, dynamic>{
|
||||
if (cachePolicy != null)
|
||||
CacheResponse.cacheKey: cachePolicy!.index,
|
||||
if (cacheDuration != null)
|
||||
'maxStale': cacheDuration,
|
||||
if (forceRefresh)
|
||||
'policy': CachePolicy.refresh.index,
|
||||
if (cachePolicy != null) CacheResponse.cacheKey: cachePolicy!.index,
|
||||
if (cacheDuration != null) 'maxStale': cacheDuration,
|
||||
if (forceRefresh) 'policy': CachePolicy.refresh.index,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -487,10 +478,10 @@ class QueuedRequest {
|
||||
final DateTime timestamp;
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'method': method,
|
||||
'path': path,
|
||||
'data': data,
|
||||
'queryParameters': queryParameters,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
'method': method,
|
||||
'path': path,
|
||||
'data': data,
|
||||
'queryParameters': queryParameters,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,7 +191,9 @@ class NetworkInfoImpl implements NetworkInfo {
|
||||
return !results.contains(ConnectivityResult.none);
|
||||
}
|
||||
|
||||
NetworkConnectionType _mapConnectivityResult(List<ConnectivityResult> results) {
|
||||
NetworkConnectionType _mapConnectivityResult(
|
||||
List<ConnectivityResult> results,
|
||||
) {
|
||||
if (results.isEmpty || results.contains(ConnectivityResult.none)) {
|
||||
return NetworkConnectionType.none;
|
||||
}
|
||||
@@ -273,14 +275,11 @@ class NetworkStatusNotifier extends _$NetworkStatusNotifier {
|
||||
final status = await networkInfo.networkStatus;
|
||||
|
||||
// Listen to network changes
|
||||
ref.listen(
|
||||
networkStatusStreamProvider,
|
||||
(_, next) {
|
||||
next.whenData((newStatus) {
|
||||
state = AsyncValue.data(newStatus);
|
||||
});
|
||||
},
|
||||
);
|
||||
ref.listen(networkStatusStreamProvider, (_, next) {
|
||||
next.whenData((newStatus) {
|
||||
state = AsyncValue.data(newStatus);
|
||||
});
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user