import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; /// 服务端 HTTP 基址(与信令同源)。部署时通过 --dart-define=API_BASE= 注入。 const String kApiBase = String.fromEnvironment( 'API_BASE', defaultValue: 'https://www.ttstd.com', ); /// 账号体系与自助接口封装(对应服务端 /api/auth/* 与 /api/client/*)。 /// /// 令牌存储策略: /// - accessToken:内存保存(掉线即失,需重新登录); /// - refreshToken:flutter_secure_storage(Keychain / EncryptedSharedPreferences,一次性,ses_ 前缀)。 class ApiClient { ApiClient({http.Client? client}) : _http = client ?? http.Client(); final http.Client _http; static const _storage = FlutterSecureStorage(); static const _kAccess = 'ttstd.accessToken'; static const _kRefresh = 'ttstd.refreshToken'; static const _kUsername = 'ttstd.username'; // 刷新单飞:避免并发触发多次刷新。 Future>? _refreshing; String? _accessToken; String? get accessToken => _accessToken; /// 从安全存储恢复令牌(应用启动时调用)。 Future restore() async { _accessToken = await _storage.read(key: _kAccess); } bool get hasTokens { // 同时读取内存 accessToken 与持久 refreshToken 判断。 return _accessToken != null; } Future hasRefreshToken() async => (await _storage.read(key: _kRefresh)) != null; Future saveTokens({ required String accessToken, String? refreshToken, String? username, }) async { _accessToken = accessToken; await _storage.write(key: _kAccess, value: accessToken); if (refreshToken != null) { await _storage.write(key: _kRefresh, value: refreshToken); } if (username != null) { await _storage.write(key: _kUsername, value: username); } } Future getUsername() => _storage.read(key: _kUsername); Future clear() async { _accessToken = null; await _storage.delete(key: _kAccess); await _storage.delete(key: _kRefresh); await _storage.delete(key: _kUsername); } Future> login(String username, String password) async { final data = await _post('/api/auth/login', body: {'username': username, 'password': password}); await saveTokens( accessToken: data['accessToken'] as String, refreshToken: data['refreshToken'] as String?, username: username, ); return data; } Future> register(String username, String password) => _post('/api/auth/register', body: {'username': username, 'password': password}); /// 刷新令牌:带单飞锁,并发调用共享同一次刷新结果。可能轮换 refreshToken。 Future> refresh() async { final rt = await _storage.read(key: _kRefresh); if (rt == null) { await clear(); throw ApiException('NO_REFRESH_TOKEN', 401); } _refreshing ??= _doRefresh(rt).whenComplete(() => _refreshing = null); return _refreshing!; } Future> _doRefresh(String rt) async { final data = await _post('/api/auth/refresh', body: {'refreshToken': rt}); await saveTokens( accessToken: data['accessToken'] as String, refreshToken: data['refreshToken'] as String?, ); return data; } Future> verify() => _get('/api/client/verify'); Future> bindings() => _get('/api/client/bindings'); /// 拉取 TURN 短期凭证(服务端开启时返回 iceServers);关闭时返回空 Map。 Future?> turnCredentials() async { try { return await _get('/api/client/turn-credentials'); } on ApiException { return null; } } Future> _post(String path, {required Map body}) async { final res = await _http.post( Uri.parse('$kApiBase$path'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(body), ); return _handle(res); } Future> _get(String path) async { final res = await _http.get( Uri.parse('$kApiBase$path'), headers: { 'Content-Type': 'application/json', if (_accessToken != null) 'Authorization': 'Bearer $_accessToken', }, ); return _handle(res); } Map _handle(http.Response res) { final body = res.body.isNotEmpty ? jsonDecode(res.body) : {}; if (res.statusCode == 401) { throw ApiException(body['code'] ?? 'UNAUTHORIZED', 401); } if (res.statusCode < 200 || res.statusCode >= 300) { throw ApiException(body['error'] ?? 'HTTP ${res.statusCode}', res.statusCode); } return body as Map; } } /// API 调用异常,携带服务端 code 与 HTTP 状态码。 class ApiException implements Exception { final String code; final int httpCode; ApiException(this.code, this.httpCode); @override String toString() => code; }