Files
easynode/native/lib/core/api/api_client.dart
2026-06-07 00:01:54 +08:00

196 lines
5.4 KiB
Dart

import 'dart:io' show Platform;
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'api_result.dart';
import 'cookie_store.dart';
const String _fallbackNativeAppVersion = 'unknown';
String buildNativeUserAgent({String? appVersion}) {
String clientName;
if (Platform.isAndroid) {
clientName = 'Android';
} else if (Platform.isIOS) {
clientName = 'iOS';
} else if (Platform.isMacOS) {
clientName = 'macOS';
} else if (Platform.isWindows) {
clientName = 'Windows';
} else if (Platform.isLinux) {
clientName = 'Linux';
} else {
clientName = 'Native';
}
final sanitizedVersion = Platform.operatingSystemVersion
.replaceAll('(', '[')
.replaceAll(')', ']')
.trim();
final version = appVersion?.trim().isNotEmpty == true
? appVersion!.trim()
: _fallbackNativeAppVersion;
return 'EasyNode-$clientName/$version ($sanitizedVersion)';
}
class ApiClient {
ApiClient({
required String serverAddress,
required SessionCookieStore cookieStore,
String? token,
Future<void> Function(String? message)? onUnauthorized,
String? appVersion,
Dio? dio,
}) : _cookieStore = cookieStore,
_token = token,
_onUnauthorized = onUnauthorized,
_dio =
dio ??
Dio(
BaseOptions(
baseUrl: '$serverAddress/api/v1',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'User-Agent': buildNativeUserAgent(appVersion: appVersion),
},
),
) {
if (kDebugMode) {
_dio.interceptors.add(
LogInterceptor(
requestHeader: true,
requestBody: true,
responseHeader: false,
responseBody: true,
error: true,
logPrint: (object) => debugPrint(object.toString(), wrapWidth: 1024),
),
);
}
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
if (_token != null && _token!.isNotEmpty) {
options.headers['token'] = _token;
}
final cookie = await _cookieStore.readCookieHeader();
if (cookie != null && cookie.isNotEmpty) {
options.headers['Cookie'] = cookie;
}
handler.next(options);
},
onResponse: (response, handler) async {
final cookies = response.headers.map['set-cookie'];
if (cookies != null) {
await _cookieStore.saveFromSetCookieHeaders(cookies);
}
handler.next(response);
},
onError: (error, handler) {
final status = error.response?.statusCode;
if (status == 401 || status == 403) {
final cb = _onUnauthorized;
if (cb != null) {
final body = error.response?.data;
String? msg;
if (body is Map && body['msg'] is String) {
msg = body['msg'] as String;
}
_signOutFuture ??= cb(msg);
}
}
handler.next(error);
},
),
);
}
final Dio _dio;
final SessionCookieStore _cookieStore;
Future<void> Function(String? message)? _onUnauthorized;
Future<void>? _signOutFuture;
String? _token;
void setToken(String? token) {
_token = token;
}
void setOnUnauthorized(Future<void> Function(String? message)? cb) {
_onUnauthorized = cb;
}
Future<String> getPublicKey() async {
final json = await getJson('/get-pub-pem');
final data = json['data'];
if (data is String && data.isNotEmpty) return data;
throw ApiFailure('Server public key is missing');
}
Future<Map<String, dynamic>> getJson(String path) async {
try {
final response = await _dio.get(path);
return _asJson(response);
} on DioException catch (error) {
throw _toFailure(error);
}
}
Future<Map<String, dynamic>> postJson(
String path,
Map<String, dynamic> data,
) async {
try {
final response = await _dio.post(path, data: data);
return _asJson(response);
} on DioException catch (error) {
throw _toFailure(error);
}
}
Future<Map<String, dynamic>> putJson(
String path,
Map<String, dynamic> data,
) async {
try {
final response = await _dio.put(path, data: data);
return _asJson(response);
} on DioException catch (error) {
throw _toFailure(error);
}
}
Future<Map<String, dynamic>> deleteJson(String path) async {
try {
final response = await _dio.delete(path);
return _asJson(response);
} on DioException catch (error) {
throw _toFailure(error);
}
}
Map<String, dynamic> _asJson(Response response) {
final data = response.data;
if (data is Map<String, dynamic>) return data;
throw ApiFailure('Unexpected server response');
}
ApiFailure _toFailure(DioException error) {
final body = error.response?.data;
String? msg;
Object? data;
if (body is Map && body['msg'] is String) {
msg = body['msg'] as String;
}
if (body is Map) {
data = body['data'];
}
final statusCode = error.response?.statusCode;
final message = msg ?? error.message ?? 'Network error';
if (statusCode == 401 || statusCode == 403) {
return UnauthorizedFailure(message, statusCode: statusCode, data: data);
}
return ApiFailure(message, statusCode: statusCode, data: data);
}
}