Files
pangolin/client/lib/services/auth_api.dart
T
wangjia c0c4b94e29
ci-pangolin / Lint — shellcheck (push) Successful in 8s
ci-pangolin / OpenAPI Sync Check (push) Successful in 16s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 7s
ci-pangolin / Flutter — analyze + test (push) Successful in 26s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 15s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m10s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s
feat(devices): P1 设备注册打通 —— 登录/注册即写 devices 表
后端:auth.Service 加 DeviceMeta + DeviceRegistrar 接口(consumer-side 解耦),
Login/Register 成功签发后 best-effort 注册设备(不强制设备上限,避免免费档重装
churn 锁死用户);handler 加 device 请求体;main 用 authDeviceRegistrar 适配
devices.Service 注入;normalizePlatform 加 linux。
客户端:新 device_identity.dart(SecureKV 接缝 + 稳定 UUIDv4 device_id 持久化 +
名称/平台/版本);弃用硬编码 'mac-001';auth_api login/register + connect 携带
device 元数据。加 uuid + device_info_plus 依赖。
测试:auth 设备注册(触发/best-effort/空 meta) + device_identity(生成/持久/
读失败不重生成/UUIDv4 形态);normalizePlatform linux=true。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 00:28:13 +08:00

141 lines
4.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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});
final String accessToken;
final String refreshToken;
factory AuthTokens.fromJson(Map<String, dynamic> m) => AuthTokens(
accessToken: m['access_token'] as String? ?? '',
refreshToken: m['refresh_token'] 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<void> 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<AuthTokens> register({
required String email,
required String code,
required String password,
Map<String, dynamic>? 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<String, dynamic>);
}
// ── 登录:POST /v1/auth/login ───────────────────────────────────
Future<AuthTokens> login({
required String email,
required String password,
Map<String, dynamic>? 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<String, dynamic>);
}
// ── 刷新 tokenPOST /v1/auth/refresh ──────────────────────────
Future<AuthTokens> 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<String, dynamic>);
}
// ── 内部 ────────────────────────────────────────────────────────
Future<http.Response> _post(String path, Map<String, dynamic> 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<String, dynamic>;
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();
}