diff --git a/lib/analytics/analytics_service.dart b/lib/analytics/analytics_service.dart index aa240b9..eb75667 100644 --- a/lib/analytics/analytics_service.dart +++ b/lib/analytics/analytics_service.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; /// Simplified analytics client for M0 (report 13 + ticket 19): track events @@ -10,21 +9,18 @@ import 'package:uuid/uuid.dart'; /// silently discarded (no retry as spec'd); privacy red-line enforced locally. class AnalyticsService { AnalyticsService({ - required String apiBaseUrl, + required this.apiBaseUrl, required this.getAccessToken, String? anonymousId, - String? userId, - }) : _apiBaseUrl = apiBaseUrl, - _anonymousId = anonymousId ?? const Uuid().v4(), - _userId = userId; + this._userId, + }) : _anonymousId = anonymousId ?? const Uuid().v4(); - static const _queueKey = 'patbond_analytics_queue'; - static const _queueMaxSize = 500; + // M0:内存队列,满 _flushThreshold 条上传一次;持久化队列留 M1(报告 19 §3)。 static const _flushThreshold = 20; - final String _apiBaseUrl; + final String apiBaseUrl; final String Function()? getAccessToken; - String _anonymousId; + final String _anonymousId; String? _userId; final List> _pendingEvents = []; @@ -90,13 +86,8 @@ class AnalyticsService { Future _upload(List> events) async { final token = getAccessToken?.call(); - final headers = { - 'Content-Type': 'application/json', - if (token != null) 'Authorization': 'Bearer $token', - }; - final request = - await HttpClient().postUrl(Uri.parse('$_apiBaseUrl/api/v1/events')) + await HttpClient().postUrl(Uri.parse('$apiBaseUrl/api/v1/events')) ..headers.contentType = ContentType.json; if (token != null) { request.headers.add('Authorization', 'Bearer $token'); @@ -110,8 +101,10 @@ class AnalyticsService { } bool _containsForbiddenField(Map props) { + // Dart RegExp 不支持 (?i) 内联标志(原写法构造即抛异常,事件被静默丢弃)。 final pattern = RegExp( - r'(?i).*(password|token|secret|phone|mobile|email|credential|idfa|gaid).*', + r'password|token|secret|phone|mobile|email|credential|idfa|gaid', + caseSensitive: false, ); return props.keys.any((key) => pattern.hasMatch(key)); } diff --git a/lib/features/auth/auth_repository.dart b/lib/features/auth/auth_repository.dart index 610be90..735d453 100644 --- a/lib/features/auth/auth_repository.dart +++ b/lib/features/auth/auth_repository.dart @@ -35,9 +35,9 @@ class ApiAuthRepository implements AuthRepository { required this._api, required this._session, required this._refresher, - AnalyticsService? analytics, + this._analytics, this._uuid = const Uuid(), - }) : _analytics = analytics; + }); final ApiClient _api; final SessionManager _session; diff --git a/test_e2e_manual.dart b/test_e2e_manual.dart index 8adacd4..c3ed6ce 100755 --- a/test_e2e_manual.dart +++ b/test_e2e_manual.dart @@ -1,8 +1,12 @@ #!/usr/bin/env dart + +// ignore_for_file: avoid_print — 手动 E2E 脚本,print 即输出。 /// E2E 手动测试脚本:对真实后端的完整登录纵切流程(独立脚本,无 Flutter 运行时)。 /// /// 前置条件:patbond-api 目录执行 `docker compose up -d` /// 运行方式:dart run test_e2e_manual.dart +library; + /// /// 本脚本直接通过 HTTP 客户端验证契约实现,不依赖 Flutter widget。 @@ -33,13 +37,13 @@ void main() async { try { // 1. 注册 print('[1/7] POST /api/v1/auth/register'); - final registerReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/register')); + final registerReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/register'), + ); registerReq.headers.contentType = ContentType.json; - registerReq.write(jsonEncode({ - 'username': username, - 'phone': phone, - 'password': password, - })); + registerReq.write( + jsonEncode({'username': username, 'phone': phone, 'password': password}), + ); final registerResp = await registerReq.close(); final registerBody = await utf8.decodeStream(registerResp); print(' Status: ${registerResp.statusCode}'); @@ -67,7 +71,9 @@ void main() async { print(' accessToken: ${accessToken.substring(0, 20)}...'); print(' refreshToken: ${refreshToken.substring(0, 20)}...'); print(' accessTokenExpiresAt: ${registerData['accessTokenExpiresAt']}'); - print(' refreshTokenExpiresAt: ${registerData['refreshTokenExpiresAt']}'); + print( + ' refreshTokenExpiresAt: ${registerData['refreshTokenExpiresAt']}', + ); print(''); // 2. 获取当前用户 (me) @@ -106,7 +112,9 @@ void main() async { // 3. 刷新 token print('[3/7] POST /api/v1/auth/refresh'); final oldRefreshToken = refreshToken; - final refreshReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/refresh')); + final refreshReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/refresh'), + ); refreshReq.headers.contentType = ContentType.json; refreshReq.write(jsonEncode({'refreshToken': refreshToken})); final refreshResp = await refreshReq.close(); @@ -131,7 +139,9 @@ void main() async { print(' ✓ Token 刷新成功'); print(' 新 accessToken: ${newAccessToken.substring(0, 20)}...'); - print(' 新 refreshToken: ${newRefreshToken.substring(0, 20)}...'); + print( + ' 新 refreshToken: ${newRefreshToken.substring(0, 20)}...', + ); if (newAccessToken == accessToken) { print(' ✗ access token 未轮换'); @@ -148,7 +158,9 @@ void main() async { // 4. 验证旧 refresh token 不可用(轮换生效) print('[4/7] POST /api/v1/auth/refresh(用已轮换的旧 token,应 401)'); - final oldRefreshReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/refresh')); + final oldRefreshReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/refresh'), + ); oldRefreshReq.headers.contentType = ContentType.json; oldRefreshReq.write(jsonEncode({'refreshToken': oldRefreshToken})); final oldRefreshResp = await oldRefreshReq.close(); @@ -168,7 +180,9 @@ void main() async { // 5. 退出登录 print('[5/7] POST /api/v1/auth/logout'); - final logoutReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/logout')); + final logoutReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/logout'), + ); logoutReq.headers.contentType = ContentType.json; logoutReq.headers.set('Authorization', 'Bearer $accessToken'); logoutReq.write(jsonEncode({'refreshToken': refreshToken})); @@ -193,15 +207,20 @@ void main() async { // 6. 验证退出后 refresh token 不可用 print('[6/7] POST /api/v1/auth/refresh(退出后,应 401)'); - final postLogoutRefreshReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/refresh')); + final postLogoutRefreshReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/refresh'), + ); postLogoutRefreshReq.headers.contentType = ContentType.json; postLogoutRefreshReq.write(jsonEncode({'refreshToken': refreshToken})); final postLogoutRefreshResp = await postLogoutRefreshReq.close(); - final postLogoutRefreshBody = await utf8.decodeStream(postLogoutRefreshResp); + final postLogoutRefreshBody = await utf8.decodeStream( + postLogoutRefreshResp, + ); print(' Status: ${postLogoutRefreshResp.statusCode}'); if (postLogoutRefreshResp.statusCode == 401) { - final postLogoutJson = jsonDecode(postLogoutRefreshBody) as Map; + final postLogoutJson = + jsonDecode(postLogoutRefreshBody) as Map; print(' ✓ 退出后 refresh token 已失效'); print(' code: ${postLogoutJson['code']}'); print(' message: ${postLogoutJson['message']}'); @@ -217,17 +236,20 @@ void main() async { print('[7/7] POST /api/v1/auth/login(5 次错误密码 → 第 6 次触发 423/42300)'); for (int i = 1; i <= 5; i++) { print(' 错误密码尝试 $i/5...'); - final badLoginReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/login')); + final badLoginReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/login'), + ); badLoginReq.headers.contentType = ContentType.json; - badLoginReq.write(jsonEncode({ - 'username': username, - 'password': 'WrongPassword$i', - })); + badLoginReq.write( + jsonEncode({'username': username, 'password': 'WrongPassword$i'}), + ); final badLoginResp = await badLoginReq.close(); final badLoginBody = await utf8.decodeStream(badLoginResp); final badLoginJson = jsonDecode(badLoginBody) as Map; - print(' → HTTP ${badLoginResp.statusCode} / code ${badLoginJson['code']}: ' - '${badLoginJson['message']}'); + print( + ' → HTTP ${badLoginResp.statusCode} / code ${badLoginJson['code']}: ' + '${badLoginJson['message']}', + ); if (badLoginResp.statusCode == 423) { // 之前的失败计数已触发锁定(本脚本重复运行时会出现),也算验证通过 print(' ✓ 账号已处于锁定状态(423/42300)'); @@ -238,12 +260,13 @@ void main() async { // 第 6 次:用正确密码验证「锁定期间即使密码正确也返回 423/42300」 print(' 第 6 次尝试(正确密码,应因锁定被拒绝)...'); - final lockedLoginReq = await client.postUrl(Uri.parse('$baseUrl/api/v1/auth/login')); + final lockedLoginReq = await client.postUrl( + Uri.parse('$baseUrl/api/v1/auth/login'), + ); lockedLoginReq.headers.contentType = ContentType.json; - lockedLoginReq.write(jsonEncode({ - 'username': username, - 'password': password, - })); + lockedLoginReq.write( + jsonEncode({'username': username, 'password': password}), + ); final lockedLoginResp = await lockedLoginReq.close(); final lockedLoginBody = await utf8.decodeStream(lockedLoginResp); print(' Status: ${lockedLoginResp.statusCode}');