docs(webrtc_controller_flutter): 更新项目文档以反映重构后的架构
AGENTS.md 与 README.md 同步更新:根据实际代码结构重写目录树、技术栈、架构分层及编码规范,移除旧版内联示例并补充新的开发约定与代码生成命令。
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../domain/auth_repository.dart';
|
||||
import 'dio_auth_repository.dart';
|
||||
|
||||
/// 认证仓库单例(登录 / 刷新 / 绑定列表 / TURN 凭证)。
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
return DioAuthRepository();
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../app/constants/app_constants.dart';
|
||||
import '../../../core/network/api_exception.dart';
|
||||
import '../../../core/storage/token_storage.dart';
|
||||
import '../domain/auth_repository.dart';
|
||||
|
||||
/// 基于 Dio 的 [AuthRepository] 实现。
|
||||
class DioAuthRepository implements AuthRepository {
|
||||
DioAuthRepository({Dio? dio, TokenStorage? storage})
|
||||
: _dio = dio ?? Dio(_baseOptions),
|
||||
_storage = storage ?? const TokenStorage();
|
||||
|
||||
static final BaseOptions _baseOptions = BaseOptions(
|
||||
baseUrl: kApiBase,
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
final Dio _dio;
|
||||
final TokenStorage _storage;
|
||||
|
||||
// 刷新单飞:避免并发触发多次刷新。
|
||||
Future<Map<String, dynamic>>? _refreshing;
|
||||
|
||||
String? _accessToken;
|
||||
|
||||
@override
|
||||
String? get accessToken => _accessToken;
|
||||
|
||||
@override
|
||||
Future<void> restore() async {
|
||||
_accessToken = await _storage.readAccess();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasRefreshToken() async =>
|
||||
await _storage.readRefresh() != null;
|
||||
|
||||
@override
|
||||
Future<String?> getUsername() => _storage.readUsername();
|
||||
|
||||
Future<void> _saveTokens({
|
||||
required String accessToken,
|
||||
String? refreshToken,
|
||||
String? username,
|
||||
}) async {
|
||||
_accessToken = accessToken;
|
||||
await _storage.writeAccess(accessToken);
|
||||
if (refreshToken != null) {
|
||||
await _storage.writeRefresh(refreshToken);
|
||||
}
|
||||
if (username != null) {
|
||||
await _storage.writeUsername(username);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> 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;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> register(String username, String password) =>
|
||||
_post('/api/auth/register',
|
||||
body: {'username': username, 'password': password});
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> refresh() async {
|
||||
final rt = await _storage.readRefresh();
|
||||
if (rt == null) {
|
||||
await clear();
|
||||
throw const ApiException('NO_REFRESH_TOKEN', 401);
|
||||
}
|
||||
_refreshing ??= _doRefresh(rt).whenComplete(() => _refreshing = null);
|
||||
return _refreshing!;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _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;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> verify() => _get('/api/client/verify');
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> bindings() => _get('/api/client/bindings');
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> turnCredentials() async {
|
||||
try {
|
||||
return await _get('/api/client/turn-credentials');
|
||||
} on ApiException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
_accessToken = null;
|
||||
await _storage.clear();
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _post(
|
||||
String path, {
|
||||
required Map<String, dynamic> body,
|
||||
}) async {
|
||||
final res = await _dio.post<dynamic>(
|
||||
path,
|
||||
data: jsonEncode(body),
|
||||
);
|
||||
return _handle(res);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _get(String path) async {
|
||||
final res = await _dio.get<dynamic>(
|
||||
path,
|
||||
options: Options(
|
||||
headers: {
|
||||
if (_accessToken != null) 'Authorization': 'Bearer $_accessToken',
|
||||
},
|
||||
),
|
||||
);
|
||||
return _handle(res);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _handle(Response<dynamic> res) {
|
||||
final data = res.data;
|
||||
final body = (data is Map)
|
||||
? data.cast<String, dynamic>()
|
||||
: <String, dynamic>{};
|
||||
if (res.statusCode == 401) {
|
||||
throw ApiException(body['code'] as String? ?? 'UNAUTHORIZED', 401);
|
||||
}
|
||||
if (res.statusCode == null || res.statusCode! < 200 || res.statusCode! >= 300) {
|
||||
throw ApiException(
|
||||
body['error'] as String? ?? 'HTTP ${res.statusCode}',
|
||||
res.statusCode ?? -1,
|
||||
);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user