add auth, format

This commit is contained in:
Phuoc Nguyen
2025-11-07 11:52:06 +07:00
parent 24a8508fce
commit 3803bd26e0
173 changed files with 8505 additions and 7116 deletions

View File

@@ -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

View File

@@ -114,7 +114,7 @@ final class AuthInterceptorProvider
}
}
String _$authInterceptorHash() => r'b54ba9af62c3cd7b922ef4030a8e2debb0220e10';
String _$authInterceptorHash() => r'3f964536e03e204d09cc9120dd9d961b6d6d4b71';
/// Provider for AuthInterceptor

View File

@@ -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(),
};
}

View File

@@ -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;
}