fix: improve transfer recognition and local data safety

This commit is contained in:
2026-07-26 21:36:46 +08:00
parent 7df25edd96
commit 882293343e
21 changed files with 505 additions and 128 deletions
+14 -42
View File
@@ -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) =>
+23 -6
View File
@@ -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();
}
}