Files
b0esche_cloud/b0esche_cloud/lib/services/api_client.dart
2025-12-18 00:02:50 +01:00

154 lines
4.1 KiB
Dart

import 'package:dio/dio.dart';
import '../models/api_error.dart';
import '../blocs/session/session_bloc.dart';
import '../blocs/session/session_event.dart';
import '../blocs/session/session_state.dart';
class ApiClient {
late final Dio _dio;
final SessionBloc _sessionBloc;
ApiClient(this._sessionBloc, {String baseUrl = 'https://go.b0esche.cloud'}) {
_dio = Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Add JWT if available
final token = _getCurrentToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Try refresh
final refreshSuccess = await _tryRefreshToken();
if (refreshSuccess) {
// Retry the request
final token = _getCurrentToken();
if (token != null) {
error.requestOptions.headers['Authorization'] = 'Bearer $token';
try {
final response = await _dio.fetch(error.requestOptions);
return handler.resolve(response);
} catch (e) {
// If retry fails, proceed to error
}
}
}
// If refresh failed, logout
_sessionBloc.add(SessionExpired());
}
return handler.next(error);
},
),
);
}
String? _getCurrentToken() {
// Get from SessionBloc state
final state = _sessionBloc.state;
if (state is SessionActive) {
return state.token;
}
return null;
}
Future<bool> _tryRefreshToken() async {
try {
final response = await _dio.post('/auth/refresh');
if (response.statusCode == 200) {
final newToken = response.data['token'];
_sessionBloc.add(SessionRefreshed(newToken));
return true;
}
} catch (e) {
// Refresh failed
}
return false;
}
Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
required T Function(dynamic data) fromJson,
}) async {
try {
final response = await _dio.get(path, queryParameters: queryParameters);
return fromJson(response.data);
} on DioException catch (e) {
throw _handleError(e);
}
}
Future<T> post<T>(
String path, {
dynamic data,
required T Function(dynamic data) fromJson,
}) async {
try {
final response = await _dio.post(path, data: data);
return fromJson(response.data);
} on DioException catch (e) {
throw _handleError(e);
}
}
Future<List<T>> getList<T>(
String path, {
Map<String, dynamic>? queryParameters,
required T Function(dynamic data) fromJson,
}) async {
try {
final response = await _dio.get(path, queryParameters: queryParameters);
return (response.data as List).map(fromJson).toList();
} on DioException catch (e) {
throw _handleError(e);
}
}
ApiError _handleError(DioException e) {
final status = e.response?.statusCode;
final data = e.response?.data;
if (status == 403) {
return ApiError(
code: 'permission_denied',
message: 'Access denied',
status: status,
);
} else if (status == 404) {
return ApiError(
code: 'not_found',
message: 'Resource not found',
status: status,
);
} else if (status == 409) {
return ApiError(
code: 'conflict',
message: 'Version conflict',
status: status,
);
} else if (status == 401) {
return ApiError(
code: 'unauthorized',
message: 'Unauthorized',
status: status,
);
} else {
return ApiError(
code: 'server_error',
message: data?['message'] ?? 'Server error',
status: status,
);
}
}
}