1import 'package:dio/dio.dart'; 2import 'package:flutter/foundation.dart'; 3 4import '../models/coves_session.dart'; 5 6/// Creates a Dio interceptor that handles authentication and automatic 7/// token refresh on 401 errors. 8/// 9/// This shared utility eliminates duplication between VoteService and 10/// CommentService by providing a single implementation of: 11/// - Adding Authorization headers with fresh tokens on each request 12/// - Automatic retry with token refresh on 401 responses 13/// - Sign-out handling when refresh fails 14/// 15/// Usage: 16/// ```dart 17/// _dio.interceptors.add( 18/// createAuthInterceptor( 19/// sessionGetter: () async => authProvider.session, 20/// tokenRefresher: authProvider.refreshToken, 21/// signOutHandler: authProvider.signOut, 22/// serviceName: 'MyService', 23/// ), 24/// ); 25/// ``` 26InterceptorsWrapper createAuthInterceptor({ 27 required Future<CovesSession?> Function()? sessionGetter, 28 required Future<bool> Function()? tokenRefresher, 29 required Future<void> Function()? signOutHandler, 30 required String serviceName, 31 required Dio dio, 32}) { 33 return InterceptorsWrapper( 34 onRequest: (options, handler) async { 35 // Fetch fresh token before each request 36 final session = await sessionGetter?.call(); 37 if (session != null) { 38 options.headers['Authorization'] = 'Bearer ${session.token}'; 39 if (kDebugMode) { 40 debugPrint('🔐 $serviceName: Adding fresh Authorization header'); 41 } 42 } else { 43 if (kDebugMode) { 44 debugPrint( 45 '⚠️ $serviceName: Session getter returned null - ' 46 'making unauthenticated request', 47 ); 48 } 49 } 50 return handler.next(options); 51 }, 52 onError: (error, handler) async { 53 // Handle 401 errors with automatic token refresh 54 if (error.response?.statusCode == 401 && tokenRefresher != null) { 55 if (kDebugMode) { 56 debugPrint( 57 '🔄 $serviceName: 401 detected, attempting token refresh...', 58 ); 59 } 60 61 // Check if we already retried this request (prevent infinite loop) 62 if (error.requestOptions.extra['retried'] == true) { 63 if (kDebugMode) { 64 debugPrint( 65 '⚠️ $serviceName: Request already retried after token refresh, ' 66 'signing out user', 67 ); 68 } 69 // Already retried once, don't retry again 70 if (signOutHandler != null) { 71 await signOutHandler(); 72 } 73 return handler.next(error); 74 } 75 76 try { 77 // Attempt to refresh the token 78 final refreshSucceeded = await tokenRefresher(); 79 80 if (refreshSucceeded) { 81 if (kDebugMode) { 82 debugPrint( 83 '$serviceName: Token refresh successful, retrying request', 84 ); 85 } 86 87 // Get the new session 88 final newSession = await sessionGetter?.call(); 89 90 if (newSession != null) { 91 // Mark this request as retried to prevent infinite loops 92 error.requestOptions.extra['retried'] = true; 93 94 // Update the Authorization header with the new token 95 error.requestOptions.headers['Authorization'] = 96 'Bearer ${newSession.token}'; 97 98 // Retry the original request with the new token 99 try { 100 final response = await dio.fetch(error.requestOptions); 101 return handler.resolve(response); 102 } on DioException catch (retryError) { 103 // If retry failed with 401 and already retried, we already 104 // signed out in the retry limit check above, so just pass 105 // the error through without signing out again 106 if (retryError.response?.statusCode == 401 && 107 retryError.requestOptions.extra['retried'] == true) { 108 return handler.next(retryError); 109 } 110 // For other errors during retry, rethrow to outer catch 111 rethrow; 112 } 113 } 114 } 115 116 // Refresh failed, sign out the user 117 if (kDebugMode) { 118 debugPrint( 119 '$serviceName: Token refresh failed, signing out user', 120 ); 121 } 122 if (signOutHandler != null) { 123 await signOutHandler(); 124 } 125 } on Exception catch (e) { 126 if (kDebugMode) { 127 debugPrint('$serviceName: Error during token refresh: $e'); 128 } 129 // Only sign out if we haven't already (avoid double sign-out) 130 // Check if this is a DioException from a retried request 131 final isRetriedRequest = 132 e is DioException && 133 e.response?.statusCode == 401 && 134 e.requestOptions.extra['retried'] == true; 135 136 if (!isRetriedRequest && signOutHandler != null) { 137 await signOutHandler(); 138 } 139 } 140 } 141 142 // Log the error for debugging 143 if (kDebugMode) { 144 debugPrint('$serviceName API Error: ${error.message}'); 145 if (error.response != null) { 146 debugPrint(' Status: ${error.response?.statusCode}'); 147 debugPrint(' Data: ${error.response?.data}'); 148 } 149 } 150 return handler.next(error); 151 }, 152 ); 153}