import 'dart:developer' as developer; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../auth/auth_provider.dart'; import '../config/app_config.dart'; import '../error/app_exception.dart'; /// Adds authentication token to requests. /// /// Skipped entirely when [AppConfig.requiresAuth] is false (LAN development). class AuthInterceptor extends Interceptor { AuthInterceptor(this._ref); final Ref _ref; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { // Skip auth for LAN development if (!AppConfig.requiresAuth) { handler.next(options); return; } final authState = _ref.read(authNotifierProvider); authState.whenData((auth) { if (auth.isAuthenticated && auth.accessToken != null) { options.headers['Authorization'] = 'Bearer ${auth.accessToken}'; } }); handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) { // Skip auth error handling for LAN development if (!AppConfig.requiresAuth) { handler.next(err); return; } if (err.response?.statusCode == 401) { // Token expired - trigger re-authentication _ref.read(authNotifierProvider.notifier).signOut(); } handler.next(err); } } /// Logs requests and responses in debug mode. class LoggingInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { developer.log( '→ ${options.method} ${options.uri}', name: 'api', ); handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { developer.log( '← ${response.statusCode} ${response.requestOptions.uri}', name: 'api', ); handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) { developer.log( '✗ ${err.response?.statusCode ?? 'NETWORK'} ${err.requestOptions.uri}: ${err.message}', name: 'api', error: err, ); handler.next(err); } } /// Converts Dio errors to AppException types. class ErrorInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) { final exception = _mapToAppException(err); handler.reject( DioException( requestOptions: err.requestOptions, response: err.response, type: err.type, error: exception, ), ); } AppException _mapToAppException(DioException err) { switch (err.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return NetworkException( message: 'Connection timed out', cause: err, ); case DioExceptionType.connectionError: return NetworkException( message: 'Unable to connect to server', cause: err, ); case DioExceptionType.badResponse: final statusCode = err.response?.statusCode ?? 0; final data = err.response?.data; String message = 'Request failed'; String? code; if (data is Map) { final error = data['error']; if (error is Map) { message = error['message'] as String? ?? message; code = error['code'] as String?; } else if (error is String) { message = error; } } return ApiException( message: message, statusCode: statusCode, code: code, cause: err, ); case DioExceptionType.cancel: return const NetworkException(message: 'Request cancelled'); case DioExceptionType.badCertificate: return const NetworkException(message: 'Invalid SSL certificate'); case DioExceptionType.unknown: return NetworkException( message: err.message ?? 'Unknown error', cause: err, ); } } }