// auth_api.dart — 控制面认证 HTTP 客户端 // // 职责:封装 /v1/auth/* 接口调用。 // 关键约束:所有 HTTP 错误统一包装为 AuthApiException, // UI 层通过 e.statusCode / e.messageZh 显示错误文案。 import 'dart:convert'; import 'package:http/http.dart' as http; /// 认证接口调用失败时抛出。 /// [statusCode] < 0 表示网络层错误。 class AuthApiException implements Exception { const AuthApiException({ required this.statusCode, required this.messageZh, required this.messageEn, }); final int statusCode; final String messageZh; final String messageEn; @override String toString() => 'AuthApiException($statusCode): $messageZh'; } /// 登录 / 注册成功后返回的 JWT 令牌对。 class AuthTokens { const AuthTokens({ required this.accessToken, required this.refreshToken, this.deviceLimit, }); final String accessToken; final String refreshToken; /// 非 null 表示登录成功但账户活跃设备数超套餐上限——登录仍生效(token 已发), /// 客户端须先让用户移除设备(降到上限内)才进主界面。见 device_limit 挡板。 final DeviceLimit? deviceLimit; factory AuthTokens.fromJson(Map m) => AuthTokens( accessToken: m['access_token'] as String? ?? '', refreshToken: m['refresh_token'] as String? ?? '', deviceLimit: m['device_limit'] is Map ? DeviceLimit.fromJson(Map.from(m['device_limit'] as Map)) : null, ); } /// 超限信号:当前套餐上限 + 账户活跃设备列表(供「移除设备」页展示)。 class DeviceLimit { const DeviceLimit({required this.maxDevices, required this.devices}); final int maxDevices; final List devices; factory DeviceLimit.fromJson(Map m) => DeviceLimit( maxDevices: (m['max_devices'] as num?)?.toInt() ?? 0, devices: ((m['devices'] as List?) ?? const []) .whereType() .map((e) => DeviceBrief.fromJson(Map.from(e))) .toList(), ); } /// 超限页用的精简设备视图。 class DeviceBrief { const DeviceBrief({ required this.uuid, required this.name, required this.platform, this.lastSeen, }); final String uuid; final String name; final String platform; final String? lastSeen; // RFC 3339 UTC factory DeviceBrief.fromJson(Map m) => DeviceBrief( uuid: m['uuid'] as String? ?? '', name: m['name'] as String? ?? '', platform: m['platform'] as String? ?? '', lastSeen: m['last_seen'] as String?, ); } /// [AuthApi] 封装 /v1/auth 接口族。 class AuthApi { AuthApi({required this.baseUrl, http.Client? client}) : _client = client ?? http.Client(); final String baseUrl; final http.Client _client; // ── 发送验证码:POST /v1/auth/code ───────────────────────────── /// 向 [email] 发送 6 位验证码。成功无返回,失败抛 [AuthApiException]。 Future sendCode(String email) async { final resp = await _post('/v1/auth/code', {'email': email}); if (resp.statusCode != 204 && resp.statusCode != 200) { _throwFromResponse(resp); } } // ── 注册:POST /v1/auth/register ─────────────────────────────── Future register({ required String email, required String code, required String password, Map? device, }) async { final resp = await _post('/v1/auth/register', { 'email': email, 'code': code, 'password': password, if (device != null) 'device': device, }); if (resp.statusCode != 200 && resp.statusCode != 201) { _throwFromResponse(resp); } return AuthTokens.fromJson(jsonDecode(resp.body) as Map); } // ── 登录:POST /v1/auth/login ─────────────────────────────────── Future login({ required String email, required String password, Map? device, }) async { final resp = await _post('/v1/auth/login', { 'email': email, 'password': password, if (device != null) 'device': device, }); if (resp.statusCode != 200) _throwFromResponse(resp); return AuthTokens.fromJson(jsonDecode(resp.body) as Map); } // ── 刷新 token:POST /v1/auth/refresh ────────────────────────── Future refresh(String refreshToken) async { final resp = await _post('/v1/auth/refresh', {'refresh_token': refreshToken}); if (resp.statusCode != 200) _throwFromResponse(resp); return AuthTokens.fromJson(jsonDecode(resp.body) as Map); } // ── 内部 ──────────────────────────────────────────────────────── Future _post(String path, Map body) async { final uri = Uri.parse('$baseUrl$path'); try { final resp = await _client .post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode(body), ) .timeout(const Duration(seconds: 15)); return resp; } on Exception catch (e) { throw AuthApiException( statusCode: -1, messageZh: '网络请求失败,请检查连接后重试', messageEn: 'Network error: $e', ); } } void _throwFromResponse(http.Response resp) { String messageZh = '操作失败 (HTTP ${resp.statusCode})'; String messageEn = 'Request failed (HTTP ${resp.statusCode})'; try { final body = jsonDecode(resp.body) as Map; messageZh = body['message_zh'] as String? ?? messageZh; messageEn = body['message_en'] as String? ?? messageEn; } catch (_) {} throw AuthApiException( statusCode: resp.statusCode, messageZh: messageZh, messageEn: messageEn, ); } void dispose() => _client.close(); }