mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-09-09 09:18:08 +08:00
196 lines
5.4 KiB
Dart
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);
|
|
}
|
|
}
|