2024-11-23 11:54:38 +00:00
|
|
|
import 'dart:async';
|
2024-11-09 10:28:45 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:developer';
|
2024-12-13 17:32:13 +00:00
|
|
|
import 'dart:io';
|
2024-11-08 16:09:46 +00:00
|
|
|
|
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
2024-12-13 17:32:13 +00:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
2024-11-10 14:14:27 +00:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2024-11-23 11:54:38 +00:00
|
|
|
import 'package:synchronized/synchronized.dart';
|
2024-11-08 16:09:46 +00:00
|
|
|
|
2024-11-09 10:28:45 +00:00
|
|
|
const kAtkStoreKey = 'nex_user_atk';
|
|
|
|
const kRtkStoreKey = 'nex_user_rtk';
|
|
|
|
|
2024-12-07 17:15:17 +00:00
|
|
|
const kNetworkServerDefault = 'https://api.sn.solsynth.dev';
|
2024-11-10 14:14:27 +00:00
|
|
|
const kNetworkServerStoreKey = 'app_server_url';
|
|
|
|
|
|
|
|
const kNetworkServerDirectory = [
|
2024-12-07 17:15:17 +00:00
|
|
|
('Solar Network', 'https://api.sn.solsynth.dev'),
|
2024-11-10 14:14:27 +00:00
|
|
|
('Local', 'http://localhost:8001'),
|
|
|
|
];
|
|
|
|
|
2024-11-08 16:09:46 +00:00
|
|
|
class SnNetworkProvider {
|
2024-11-24 02:54:55 +00:00
|
|
|
late final Dio client;
|
2024-11-08 16:09:46 +00:00
|
|
|
|
2024-11-10 14:14:27 +00:00
|
|
|
late final SharedPreferences _prefs;
|
2024-11-09 10:28:45 +00:00
|
|
|
|
2024-12-13 17:32:13 +00:00
|
|
|
String? _userAgent;
|
|
|
|
|
2024-11-08 16:09:46 +00:00
|
|
|
SnNetworkProvider() {
|
|
|
|
client = Dio();
|
|
|
|
|
2024-11-18 15:04:36 +00:00
|
|
|
client.interceptors.add(RetryInterceptor(
|
|
|
|
dio: client,
|
|
|
|
retries: 3,
|
|
|
|
retryDelays: const [
|
|
|
|
Duration(milliseconds: 300),
|
|
|
|
Duration(milliseconds: 1000),
|
|
|
|
Duration(milliseconds: 3000),
|
|
|
|
],
|
|
|
|
));
|
2024-11-08 16:09:46 +00:00
|
|
|
|
2024-11-09 10:28:45 +00:00
|
|
|
client.interceptors.add(
|
|
|
|
InterceptorsWrapper(
|
|
|
|
onRequest: (
|
|
|
|
RequestOptions options,
|
|
|
|
RequestInterceptorHandler handler,
|
|
|
|
) async {
|
2024-11-14 16:24:46 +00:00
|
|
|
final atk = await getFreshAtk();
|
|
|
|
if (atk != null) {
|
|
|
|
options.headers['Authorization'] = 'Bearer $atk';
|
2024-11-09 10:28:45 +00:00
|
|
|
}
|
2024-12-13 17:32:13 +00:00
|
|
|
if (_userAgent != null) {
|
|
|
|
options.headers['User-Agent'] = _userAgent!;
|
|
|
|
}
|
2024-11-14 16:24:46 +00:00
|
|
|
return handler.next(options);
|
2024-11-09 10:28:45 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2024-11-10 14:14:27 +00:00
|
|
|
SharedPreferences.getInstance().then((prefs) {
|
|
|
|
_prefs = prefs;
|
2024-12-13 17:32:13 +00:00
|
|
|
client.options.baseUrl = _prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
2024-11-10 14:14:27 +00:00
|
|
|
});
|
2024-11-08 16:09:46 +00:00
|
|
|
}
|
2024-11-09 03:16:14 +00:00
|
|
|
|
2024-12-13 17:32:13 +00:00
|
|
|
Future<void> initializeUserAgent() async {
|
|
|
|
final String platformInfo;
|
|
|
|
if (kIsWeb) {
|
|
|
|
final deviceInfo = await DeviceInfoPlugin().webBrowserInfo;
|
|
|
|
platformInfo = 'Web; ${deviceInfo.vendor}';
|
|
|
|
} else if (Platform.isAndroid) {
|
|
|
|
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
|
|
|
platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
|
|
|
|
} else if (Platform.isIOS) {
|
|
|
|
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
|
|
|
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
|
|
|
|
} else if (Platform.isMacOS) {
|
|
|
|
final deviceInfo = await DeviceInfoPlugin().macOsInfo;
|
|
|
|
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
|
|
|
|
} else if (Platform.isWindows) {
|
|
|
|
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
|
|
|
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
|
|
|
} else if (Platform.isLinux) {
|
|
|
|
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
|
|
|
|
platformInfo = 'Linux; ${deviceInfo.prettyName}';
|
|
|
|
} else {
|
|
|
|
platformInfo = 'Unknown';
|
|
|
|
}
|
|
|
|
|
|
|
|
final packageInfo = await PackageInfo.fromPlatform();
|
|
|
|
|
|
|
|
_userAgent = 'Solian/${packageInfo.version}+${packageInfo.buildNumber} ($platformInfo)';
|
|
|
|
}
|
|
|
|
|
2024-11-23 11:54:38 +00:00
|
|
|
final tkLock = Lock();
|
|
|
|
|
|
|
|
Completer<String?>? _refreshCompleter;
|
|
|
|
|
2024-11-14 16:24:46 +00:00
|
|
|
Future<String?> getFreshAtk() async {
|
2024-11-23 11:54:38 +00:00
|
|
|
if (_refreshCompleter != null) {
|
|
|
|
return await _refreshCompleter!.future;
|
|
|
|
} else {
|
|
|
|
_refreshCompleter = Completer<String?>();
|
|
|
|
}
|
|
|
|
|
2024-11-14 16:24:46 +00:00
|
|
|
try {
|
2024-11-23 11:54:38 +00:00
|
|
|
var atk = _prefs.getString(kAtkStoreKey);
|
2024-11-14 16:24:46 +00:00
|
|
|
if (atk != null) {
|
|
|
|
final atkParts = atk.split('.');
|
|
|
|
if (atkParts.length != 3) {
|
|
|
|
throw Exception('invalid format of access token');
|
|
|
|
}
|
|
|
|
|
|
|
|
var rawPayload = atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
|
|
|
|
switch (rawPayload.length % 4) {
|
|
|
|
case 0:
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
rawPayload += '==';
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
rawPayload += '=';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw Exception('illegal format of access token payload');
|
|
|
|
}
|
|
|
|
|
|
|
|
final b64 = utf8.fuse(base64Url);
|
|
|
|
final payload = b64.decode(rawPayload);
|
|
|
|
final exp = jsonDecode(payload)['exp'];
|
|
|
|
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
|
|
|
log('Access token need refresh, doing it at ${DateTime.now()}');
|
|
|
|
atk = await refreshToken();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (atk != null) {
|
2024-11-23 11:54:38 +00:00
|
|
|
_refreshCompleter!.complete(atk);
|
2024-11-14 16:24:46 +00:00
|
|
|
return atk;
|
|
|
|
} else {
|
|
|
|
log('Access token refresh failed...');
|
2024-11-23 11:54:38 +00:00
|
|
|
_refreshCompleter!.complete(null);
|
2024-11-14 16:24:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
log('Failed to authenticate user: $err');
|
2024-11-23 11:54:38 +00:00
|
|
|
_refreshCompleter!.completeError(err);
|
|
|
|
} finally {
|
|
|
|
_refreshCompleter = null;
|
2024-11-14 16:24:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2024-11-09 03:16:14 +00:00
|
|
|
String getAttachmentUrl(String ky) {
|
2024-11-12 16:13:27 +00:00
|
|
|
if (ky.startsWith("http")) return ky;
|
2024-11-09 03:16:14 +00:00
|
|
|
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
|
|
|
}
|
2024-11-09 10:28:45 +00:00
|
|
|
|
2024-11-23 11:54:38 +00:00
|
|
|
void setTokenPair(String atk, String rtk) {
|
|
|
|
_prefs.setString(kAtkStoreKey, atk);
|
|
|
|
_prefs.setString(kRtkStoreKey, rtk);
|
2024-11-09 10:28:45 +00:00
|
|
|
}
|
|
|
|
|
2024-11-23 11:54:38 +00:00
|
|
|
void clearTokenPair() {
|
|
|
|
_prefs.remove(kAtkStoreKey);
|
|
|
|
_prefs.remove(kRtkStoreKey);
|
2024-11-09 10:28:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<String?> refreshToken() async {
|
2024-11-23 11:54:38 +00:00
|
|
|
final rtk = _prefs.getString(kRtkStoreKey);
|
2024-11-09 10:28:45 +00:00
|
|
|
if (rtk == null) return null;
|
|
|
|
|
2024-11-09 11:32:21 +00:00
|
|
|
final dio = Dio();
|
2024-11-10 14:14:27 +00:00
|
|
|
dio.options.baseUrl = client.options.baseUrl;
|
2024-11-09 11:32:21 +00:00
|
|
|
|
|
|
|
final resp = await dio.post('/cgi/id/auth/token', data: {
|
2024-11-09 10:28:45 +00:00
|
|
|
'grant_type': 'refresh_token',
|
|
|
|
'refresh_token': rtk,
|
|
|
|
});
|
|
|
|
|
|
|
|
final atk = resp.data['access_token'];
|
|
|
|
final nRtk = resp.data['refresh_token'];
|
2024-11-23 11:54:38 +00:00
|
|
|
setTokenPair(atk, nRtk);
|
2024-11-09 10:28:45 +00:00
|
|
|
|
|
|
|
return atk;
|
|
|
|
}
|
2024-11-10 14:14:27 +00:00
|
|
|
|
|
|
|
void setBaseUrl(String url) {
|
|
|
|
client.options.baseUrl = url;
|
|
|
|
}
|
2024-11-08 16:09:46 +00:00
|
|
|
}
|