Project

General

Profile

Task #28 » authenticated_client.dart

Milad Khnefes, 01/29/2026 02:07 PM

 
import 'dart:async';
import '../constants/constants.dart';
import 'client.dart';
import 'package:http/http.dart' as http;

abstract class AuthenticatedClient extends ApiClient {
// Abstract methods for repositories to implement their token storage
Future<String?> _getAccessTokenFromStorage();
Future<bool> _refreshTokenInStorage();

// Concrete internal token management methods - implemented by AuthenticatedClient
Future<String?> getAccessToken() async {
return await _getAccessTokenFromStorage();
}

Future<bool> refreshToken() async {
return await _refreshTokenInStorage();
}

@override
Future<http.Response> get(
String partOfUrl, {
Map<String, dynamic>? queryParams,
bool needsToken = true,
bool isFirebaseCall = false,
bool isTesting = false,
}) async {
return _sendAuthenticatedRequest(
() => super.get(
partOfUrl,
queryParams: queryParams,
isFirebaseCall: isFirebaseCall,
isTesting: isTesting,
),
partOfUrl,
'GET',
needsToken,
);
}

@override
Future<http.Response> post(
String partOfUrl,
String body, {
bool needsToken = true,
bool isFirebaseCall = false,
bool isTesting = false,
}) async {
return _sendAuthenticatedRequest(
() => super.post(
partOfUrl,
body,
isFirebaseCall: isFirebaseCall,
isTesting: isTesting,
),
partOfUrl,
'POST',
needsToken,
);
}

@override
Future<http.Response> put(
String partOfUrl,
String body, {
bool needsToken = true,
bool isFirebaseCall = false,
bool isTesting = false,
}) async {
return _sendAuthenticatedRequest(
() => super.put(
partOfUrl,
body,
isFirebaseCall: isFirebaseCall,
isTesting: isTesting,
),
partOfUrl,
'PUT',
needsToken,
);
}

@override
Future<http.Response> delete(
String partOfUrl,
String body, {
bool needsToken = true,
bool isFirebaseCall = false,
}) async {
return _sendAuthenticatedRequest(
() => super.delete(
partOfUrl,
body,
isFirebaseCall: isFirebaseCall,
),
partOfUrl,
'DELETE',
needsToken,
);
}

Future<http.Response> _sendAuthenticatedRequest(
Future<http.Response> Function() requestFunction,
String partOfUrl,
String method,
bool needsToken,
) async {
if (!needsToken) {
// No token needed, just make the request
return await requestFunction();
}

// First attempt with current token
final token = await _getAccessTokenFromStorage();
if (token == null || token.isEmpty) {
return http.Response('{}', 404);
}

// Make request with token (we'll need to modify ApiClient to accept custom headers)
final response = await _makeRequestWithToken(requestFunction, token);

// If successful, return response
if (Constants.isSuccessCode(response.statusCode)) {
return response;
}

// If unauthorized, try to refresh token and retry once
if (response.statusCode == 401) {
Constants.logger.i('Token expired, attempting refresh...');
final refreshed = await _refreshTokenInStorage();

if (!refreshed) {
Constants.logger.e('Token refresh failed');
return response;
}

final newToken = await _getAccessTokenFromStorage();
if (newToken == null || newToken.isEmpty) {
Constants.logger.e('No new token available after refresh');
return response;
}

Constants.logger.i('Retrying $method $partOfUrl with new token...');
return await _makeRequestWithToken(requestFunction, newToken);
}

return response;
}

Future<http.Response> _makeRequestWithToken(
Future<http.Response> Function() requestFunction,
String token,
) async {
// TODO:
// For now, we'll make the request and let the calling method handle the response
// In a more sophisticated implementation, we might modify ApiClient to accept custom headers
// But for this separation of concerns demonstration, this approach works

return await requestFunction();
}
}
(2-2/12)