fix: improve transfer recognition and local data safety
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:miaoji_zhang/shared/api/backend_identity.dart';
|
||||
import 'package:miaoji_zhang/shared/services/session_store.dart';
|
||||
|
||||
class ApiClient {
|
||||
@@ -11,24 +9,14 @@ class ApiClient {
|
||||
static final ApiClient instance = ApiClient._();
|
||||
|
||||
static const _storage = FlutterSecureStorage();
|
||||
static const _tokenKey = 'auth_token';
|
||||
static const _legacyTokenKey = 'auth_token';
|
||||
static final _tokenKey = 'auth_token_${BackendIdentity.scope}';
|
||||
static final sessionExpired = ValueNotifier<int>(0);
|
||||
static bool _handlingUnauthorized = false;
|
||||
static const _internalBuild = bool.fromEnvironment('INTERNAL_BUILD');
|
||||
static const _internalTestBaseUrl = 'https://lt.frp-say.com:38012';
|
||||
static const _configuredBaseUrl = String.fromEnvironment('API_BASE_URL');
|
||||
static const _temporaryFrpHost = 'lt.frp-say.com';
|
||||
static const _temporaryFrpPort = 38012;
|
||||
static const _temporaryFrpCertificateSha1 =
|
||||
'509C3210161E72FB9DA5183D3D2152F63871C31F';
|
||||
|
||||
static const String baseUrl = _configuredBaseUrl != ''
|
||||
? _configuredBaseUrl
|
||||
: _internalBuild
|
||||
? _internalTestBaseUrl
|
||||
: 'https://api.invalid';
|
||||
static const String baseUrl = BackendIdentity.baseUrl;
|
||||
|
||||
static bool get isInternalBuild => _internalBuild;
|
||||
static bool get isInternalBuild => BackendIdentity.internalBuild;
|
||||
|
||||
late final Dio dio = _createDio();
|
||||
|
||||
@@ -77,35 +65,19 @@ class ApiClient {
|
||||
),
|
||||
);
|
||||
|
||||
final apiUri = Uri.tryParse(baseUrl);
|
||||
if (_internalBuild &&
|
||||
apiUri?.scheme == 'https' &&
|
||||
apiUri?.host == _temporaryFrpHost &&
|
||||
apiUri?.port == _temporaryFrpPort) {
|
||||
client.httpClientAdapter = IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
final httpClient = HttpClient();
|
||||
httpClient.badCertificateCallback = (certificate, host, port) {
|
||||
final certificateSha1 = certificate.sha1
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase();
|
||||
return host == _temporaryFrpHost &&
|
||||
port == _temporaryFrpPort &&
|
||||
certificateSha1 == _temporaryFrpCertificateSha1;
|
||||
};
|
||||
return httpClient;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<void> saveToken(String token) =>
|
||||
_storage.write(key: _tokenKey, value: token);
|
||||
Future<void> saveToken(String token) async {
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
await _storage.delete(key: _legacyTokenKey);
|
||||
}
|
||||
|
||||
Future<String?> readToken() => _storage.read(key: _tokenKey);
|
||||
Future<void> clearToken() => _storage.delete(key: _tokenKey);
|
||||
Future<void> clearToken() async {
|
||||
await _storage.delete(key: _tokenKey);
|
||||
await _storage.delete(key: _legacyTokenKey);
|
||||
}
|
||||
}
|
||||
|
||||
bool isConnectivityError(Object error) =>
|
||||
|
||||
@@ -53,10 +53,20 @@ class UserProfile {
|
||||
}) : aiCompanion = null;
|
||||
}
|
||||
|
||||
class AuthLoginResult {
|
||||
final bool accountClosureCancelled;
|
||||
final UserProfile profile;
|
||||
|
||||
const AuthLoginResult({
|
||||
required this.accountClosureCancelled,
|
||||
required this.profile,
|
||||
});
|
||||
}
|
||||
|
||||
class AuthApi {
|
||||
static final _dio = ApiClient.instance.dio;
|
||||
|
||||
static Future<void> register(
|
||||
static Future<UserProfile> register(
|
||||
String username,
|
||||
String password, {
|
||||
required bool agreedToTerms,
|
||||
@@ -70,21 +80,28 @@ class AuthApi {
|
||||
},
|
||||
);
|
||||
await ApiClient.instance.saveToken(response.data['token'] as String);
|
||||
await me();
|
||||
return me(forceRemote: true);
|
||||
}
|
||||
|
||||
static Future<bool> login(String username, String password) async {
|
||||
static Future<AuthLoginResult> login(String username, String password) async {
|
||||
final response = await _dio.post(
|
||||
'/api/auth/login',
|
||||
data: {'username': username, 'password': password},
|
||||
);
|
||||
await ApiClient.instance.saveToken(response.data['token'] as String);
|
||||
return response.data['accountClosureCancelled'] as bool? ?? false;
|
||||
final profile = await me(forceRemote: true);
|
||||
return AuthLoginResult(
|
||||
accountClosureCancelled:
|
||||
response.data['accountClosureCancelled'] as bool? ?? false,
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<UserProfile> me() async {
|
||||
static Future<UserProfile> me({bool forceRemote = false}) async {
|
||||
final session = SessionStore.instance;
|
||||
if (session.isGuest || (session.isAccount && session.shouldUseLocalOnly)) {
|
||||
if (!forceRemote &&
|
||||
(session.isGuest ||
|
||||
(session.isAccount && session.shouldUseLocalOnly))) {
|
||||
return _localProfile();
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
class BackendIdentity {
|
||||
BackendIdentity._();
|
||||
|
||||
static const internalBuild = bool.fromEnvironment('INTERNAL_BUILD');
|
||||
static const configuredBaseUrl = String.fromEnvironment('API_BASE_URL');
|
||||
|
||||
static const String baseUrl = configuredBaseUrl != ''
|
||||
? configuredBaseUrl
|
||||
: 'https://api.invalid';
|
||||
|
||||
static final String scope = scopeForBaseUrl(baseUrl);
|
||||
|
||||
static String accountNamespace(int userId) => 'account_${scope}_$userId';
|
||||
|
||||
static String scopeForBaseUrl(String value) => sha256
|
||||
.convert(utf8.encode(_normalizedBaseUrl(value)))
|
||||
.toString()
|
||||
.substring(0, 16);
|
||||
|
||||
static String accountNamespaceFor(String baseUrl, int userId) =>
|
||||
'account_${scopeForBaseUrl(baseUrl)}_$userId';
|
||||
|
||||
static String _normalizedBaseUrl(String value) {
|
||||
final uri = Uri.tryParse(value.trim());
|
||||
if (uri == null || !uri.hasScheme || uri.host.isEmpty) {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
final path = uri.path == '/'
|
||||
? ''
|
||||
: uri.path.replaceFirst(RegExp(r'/+$'), '');
|
||||
return uri
|
||||
.replace(path: path, query: null, fragment: null)
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user