// auth_provider.dart — 认证状态(JWT 令牌生命周期) import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../services/api_config.dart'; import '../services/auth_api.dart'; import '../services/token_store.dart'; // ── 状态 ──────────────────────────────────────────────────────────── class AuthState { const AuthState({this.accessToken, this.isLoading = false}); final String? accessToken; final bool isLoading; bool get isLoggedIn => accessToken != null && accessToken!.isNotEmpty; AuthState copyWith({String? accessToken, bool? isLoading}) => AuthState( accessToken: accessToken ?? this.accessToken, isLoading: isLoading ?? this.isLoading, ); } // ── 状态机 ─────────────────────────────────────────────────────────── class AuthNotifier extends StateNotifier { AuthNotifier(this._store, {AuthApi? api}) : _api = api ?? AuthApi(baseUrl: kApiBaseUrl), super(const AuthState(isLoading: true)) { _loadFromStore(); } final TokenStore _store; final AuthApi _api; /// 进行中的刷新(single-flight)。refresh token 是「单次使用 + 轮换」的—— /// 服务端每次刷新会作废旧 JTI、发新的。冷启动/进页面会并发打多个接口,过期后 /// 会同时多次 401 → 各自拿同一个旧 refresh token 去刷新:第一个成功轮换,其余 /// 拿已作废的 JTI 必被拒 → 误触发 logout 把用户踢下线。用一个共享的 in-flight /// future 合并并发刷新,旧 token 只被消费一次。 Future? _refreshInFlight; Future _loadFromStore() async { try { final token = await _store.loadAccessToken(); state = AuthState(accessToken: token); } catch (_) { // FlutterSecureStorage 在测试环境 / 未初始化时抛异常,视为未登录。 state = const AuthState(); } } /// 登录 / 注册成功后保存令牌。 Future saveTokens(AuthTokens tokens) async { await _store.saveTokens( access: tokens.accessToken, refresh: tokens.refreshToken, ); state = AuthState(accessToken: tokens.accessToken); } /// 用存储的 refresh token 刷新令牌(供 ApiClient 在 401 时调用)。 /// 成功写入新令牌并更新状态返回 true;无 refresh token / 刷新失败返回 false。 /// single-flight:并发调用共用同一次刷新,避免单次使用的 refresh token 被多个 /// 并发 401 重复消费 → 误判失效把用户踢下线。 Future refresh() { return _refreshInFlight ??= _doRefresh().whenComplete(() => _refreshInFlight = null); } Future _doRefresh() async { final rt = await _store.loadRefreshToken(); if (rt == null || rt.isEmpty) return false; try { await saveTokens(await _api.refresh(rt)); return true; } on AuthApiException catch (e) { // 服务端明确拒绝(refresh 过期/失效,超出 7 天)→ 退出登录回登录页; // 网络错误(statusCode<=0)则保留会话,稍后重试。 if (e.statusCode > 0) await logout(); return false; } catch (_) { return false; } } /// 退出登录:清除本地令牌。 Future logout() async { await _store.clear(); state = const AuthState(); } } // ── Providers ──────────────────────────────────────────────────────── /// 可在测试中 override,注入 stub TokenStore(避免 FlutterSecureStorage 平台依赖)。 final tokenStoreProvider = Provider((_) => const TokenStore()); /// 登录时活跃设备超套餐上限的信号(来自登录响应 device_limit)。非 null → 顶层路由 /// 挡在「移除设备」页(登录已成功、token 已发,但先降到上限内才进主界面)。用户在 /// DeviceLimitScreen 移除设备降到上限内后置回 null,放行进主界面;登出时也清空。 final deviceLimitProvider = StateProvider((_) => null); final authProvider = StateNotifierProvider( (ref) => AuthNotifier(ref.watch(tokenStoreProvider)), );