Files
VibeCoding/webrtc_controller_flutter/lib/features/auth/presentation/auth_controller.dart
TongTongStudio 79cffcb041 feat(auth): 添加登录/注册页面及令牌恢复流程
- 新增 SplashPage(启动页)和 LoginPage(登录页),支持路由跳转
- 扩展 AuthState,增加 submitting/alert/busy 状态及消费机制
- AuthController 增加 checkLoginState/register/showAlert 等方法
- 删除旧模型文件(binding_item, token_response, verify_response)
- 删除旧版 LoginPage,迁移至 features/auth 目录
- 更新本地化字符串,增加注册相关文案
- 调整默认 API 地址为本地开发环境
2026-08-03 16:47:08 +08:00

166 lines
4.5 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.
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../core/network/api_exception.dart';
import '../data/auth_providers.dart';
import '../domain/auth_repository.dart';
import '../domain/auth_state.dart';
part 'auth_controller.g.dart';
/// 登录状态控制器。
///
/// 会话级持有(`keepAlive`),负责令牌恢复、登录 / 注册 / 退出,
/// 以及启动期的登录态校验verify + refresh 重试)。
@Riverpod(keepAlive: true)
class AuthController extends _$AuthController {
AuthRepository get _repository => ref.read(authRepositoryProvider);
@override
FutureOr<AuthState> build() async {
final repository = ref.watch(authRepositoryProvider);
await repository.restore();
final loggedIn =
repository.accessToken != null && await repository.hasRefreshToken();
return AuthState(
loggedIn: loggedIn,
username: await repository.getUsername(),
);
}
AuthState get _current => state.valueOrNull ?? const AuthState();
/// 校验本地令牌是否仍然有效(对应启动页流程)。
///
/// 无令牌直接返回 false`verify` 失败时尝试 `refresh` 后重试一次,
/// 仍失败则清空令牌。
Future<bool> checkLoginState() async {
final repository = _repository;
await repository.restore();
if (repository.accessToken == null) {
if (!await repository.hasRefreshToken()) {
await _reset();
return false;
}
}
if (await _verifyQuietly()) {
state = AsyncData(
_current.copyWith(
loggedIn: true,
username: await repository.getUsername(),
),
);
return true;
}
// access token 失效:用 refreshToken 续期后再校验一次。
try {
await repository.refresh();
} on Object {
await _reset();
return false;
}
if (await _verifyQuietly()) {
state = AsyncData(
_current.copyWith(
loggedIn: true,
username: await repository.getUsername(),
),
);
return true;
}
await _reset();
return false;
}
Future<bool> _verifyQuietly() async {
try {
await _repository.verify();
return true;
} on Object {
return false;
}
}
Future<void> _reset() async {
final username = await _repository.getUsername();
await _repository.clear();
state = AsyncData(AuthState(username: username));
}
/// 登录:成功返回 true失败返回 false 并写入 [AuthState.error]。
Future<bool> login(String username, String password) async {
state = AsyncData(_current.copyWith(submitting: true, error: null));
try {
await _repository.login(username, password);
state = AsyncData(AuthState(loggedIn: true, username: username));
return true;
} catch (e) {
state = AsyncData(
_current.copyWith(
submitting: false,
loggedIn: false,
error: _describe(e, '登录失败'),
),
);
return false;
}
}
/// 注册:成功后不自动登录,仅通过 [AuthState.alert] 提示前往登录。
Future<bool> register(String username, String password) async {
state = AsyncData(_current.copyWith(submitting: true, error: null));
try {
await _repository.register(username, password);
state = AsyncData(
_current.copyWith(
submitting: false,
error: null,
alert: '注册成功,请登录',
),
);
return true;
} catch (e) {
state = AsyncData(
_current.copyWith(
submitting: false,
error: _describe(e, '注册失败'),
),
);
return false;
}
}
/// 退出登录:清空令牌并复位状态(保留用户名用于回显)。
Future<void> logout() async {
await _reset();
}
/// 展示一次性提示(如从主页因登录失效跳转时携带的文案)。
void showAlert(String message) {
state = AsyncData(_current.copyWith(alert: message));
}
/// 消费一次性提示,避免重建时重复展示。
void consumeAlert() {
if (_current.alert != null) {
state = AsyncData(_current.copyWith(alert: null));
}
}
/// 清空错误信息。
void clearError() {
if (_current.error != null) {
state = AsyncData(_current.copyWith(error: null));
}
}
/// 优先展示服务端返回的错误码,其余异常回落到通用提示。
String _describe(Object e, String fallback) {
if (e is ApiException) return e.code;
return '$fallback$e';
}
}