import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../config/app_config.dart'; const _kAccessToken = 'access_token'; const _kRefreshToken = 'refresh_token'; const _kUsername = 'username'; const _kRealName = 'real_name'; const _kShopNo = 'shop_no'; const _kShopId = 'shop_id'; const _kRole = 'role'; class AuthUser { final String accessToken; final String refreshToken; final String username; final String realName; final String shopNo; final int shopId; final String role; const AuthUser({ required this.accessToken, required this.refreshToken, required this.username, required this.realName, required this.shopNo, required this.shopId, this.role = 'operator', }); // Kept for ApiClient compatibility String get token => accessToken; } class AuthState { final AuthUser? user; final bool initialized; const AuthState({this.user, this.initialized = false}); bool get isLoggedIn => user != null; } class AuthNotifier extends StateNotifier { AuthNotifier() : super(const AuthState()); /// Called at app startup to restore persisted session. Future restore() async { try { final prefs = await SharedPreferences.getInstance(); final accessToken = prefs.getString(_kAccessToken); final refreshToken = prefs.getString(_kRefreshToken); final username = prefs.getString(_kUsername); final realName = prefs.getString(_kRealName); final shopNo = prefs.getString(_kShopNo); final shopIdStr = prefs.getString(_kShopId); final role = prefs.getString(_kRole); if (accessToken != null && refreshToken != null && username != null) { state = AuthState( initialized: true, user: AuthUser( accessToken: accessToken, refreshToken: refreshToken, username: username, realName: realName ?? username, shopNo: shopNo ?? '', shopId: int.tryParse(shopIdStr ?? '') ?? 0, role: role ?? 'operator', ), ); return; } } catch (_) { // Storage unavailable — fall through to logged-out state } state = const AuthState(initialized: true); } Future login(AuthUser user) async { debugPrint('[Auth] login() called, username=${user.username} shopId=${user.shopId}'); final prefs = await SharedPreferences.getInstance(); await prefs.setString(_kAccessToken, user.accessToken); await prefs.setString(_kRefreshToken, user.refreshToken); await prefs.setString(_kUsername, user.username); await prefs.setString(_kRealName, user.realName); await prefs.setString(_kShopNo, user.shopNo); await prefs.setString(_kShopId, user.shopId.toString()); await prefs.setString(_kRole, user.role); debugPrint('[Auth] login() setting state, token prefix: ${user.accessToken.substring(0, user.accessToken.length.clamp(0, 20))}...'); state = AuthState(initialized: true, user: user); debugPrint('[Auth] login() state set, isLoggedIn=${state.isLoggedIn}'); } void updateAccessToken(String newToken) { if (state.user == null) return; SharedPreferences.getInstance() .then((prefs) => prefs.setString(_kAccessToken, newToken)); state = AuthState( initialized: true, user: AuthUser( accessToken: newToken, refreshToken: state.user!.refreshToken, username: state.user!.username, realName: state.user!.realName, shopNo: state.user!.shopNo, shopId: state.user!.shopId, role: state.user!.role, ), ); } Future logout() async { debugPrint('[Auth] logout() called! stack: ${StackTrace.current}'); // 尽力通知后端撤销当前会话(离线/失败均忽略,不阻塞本地登出) final token = state.user?.accessToken; if (token != null && token.isNotEmpty) { try { await Dio(BaseOptions( baseUrl: AppConfig.apiBaseUrl, connectTimeout: const Duration(seconds: 4), receiveTimeout: const Duration(seconds: 4), headers: {'Authorization': 'Bearer $token'}, )).post('/auth/logout'); } catch (_) { // ignore: 后端不可达或会话已失效都无所谓 } } final prefs = await SharedPreferences.getInstance(); await prefs.remove(_kAccessToken); await prefs.remove(_kRefreshToken); await prefs.remove(_kUsername); await prefs.remove(_kRealName); await prefs.remove(_kShopNo); await prefs.remove(_kShopId); await prefs.remove(_kRole); state = const AuthState(initialized: true); } } final authStateProvider = StateNotifierProvider( (ref) => AuthNotifier(), ); /// 会话结束提示语(被踢下线 / 会话失效)。登录页监听后弹出提示并清空。 final sessionEndedMessageProvider = StateProvider((ref) => null); /// 当前登录用户是否为只读角色(role == 'readonly')。 /// 只读用户禁止任何写操作:UI 据此隐藏新增/编辑/删除/审核等按钮, /// 后端亦有 middleware.ReadOnly() 兜底返回 403。 final isReadonlyProvider = Provider((ref) { final role = ref.watch(authStateProvider.select((s) => s.user?.role)); return role == 'readonly'; });