Files
CloudHome-Ops/lib/providers/auth_provider.dart

159 lines
4.7 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 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/user.dart';
import '../services/api_client.dart';
import '../services/auth_service.dart';
import '../utils/credential_storage.dart';
import '../utils/session_org_scope.dart';
/// AuthService 提供者。
final authServiceProvider = Provider<AuthService>((ref) => AuthService());
/// 登录状态。
class AuthState {
const AuthState({
this.user,
this.isLoading = false,
this.errorMessage,
});
/// 当前登录用户;为 null 表示未登录。
final User? user;
/// 是否正在执行登录请求。
final bool isLoading;
/// 最近一次登录失败的错误信息。
final String? errorMessage;
/// 是否已登录。
bool get isLoggedIn => user != null;
AuthState copyWith({
User? user,
bool? isLoading,
String? errorMessage,
bool clearUser = false,
bool clearError = false,
}) {
return AuthState(
user: clearUser ? null : (user ?? this.user),
isLoading: isLoading ?? this.isLoading,
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
);
}
}
/// 登录状态管理。
class AuthNotifier extends Notifier<AuthState> {
@override
AuthState build() {
// 注册会话失效回调:401 刷新失败 → 清空登录态(含本地持久化),
// 路由 redirect 监听本状态,自动跳转登录页。
ApiClient.instance.onUnauthorized = () {
SessionOrgScope.instance.clear();
unawaited(_clearPersistedSession());
state = const AuthState();
};
// 尝试从本地存储恢复登录态(免登录)。
return _tryRestoreSession();
}
/// 从本地存储恢复登录态。
AuthState _tryRestoreSession() {
final cs = CredentialStorage.instance;
final token = cs.token;
final userMap = cs.savedUserMap;
if (token != null && token.isNotEmpty && userMap != null) {
// 恢复 TokenStore(供 ApiInterceptor 使用)。
TokenStore.instance.save(
token: token,
refreshToken: cs.refreshToken,
);
try {
final user = User.fromJson(userMap);
// 恢复数据权限范围:仅当登录用户 orgId 非空时收敛。
SessionOrgScope.instance.setCurrentOrg(user.orgId);
return AuthState(user: user);
} catch (_) {
// JSON 解析失败,清除残留数据。
_clearPersistedSession();
}
}
return const AuthState();
}
/// 执行登录;成功返回 true,失败返回 false 并写入错误信息。
///
/// [captcha]/[uuid] 仅在后端要求验证码时传值;当前各环境已关闭登录验证码,
/// 调用方(登录页)可不传,由 [AuthService.login] 决定是否写入请求体。
Future<bool> login({
required String account,
required String password,
String captcha = '',
String uuid = '',
bool rememberPassword = false,
}) async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final user = await ref.read(authServiceProvider).login(
account: account,
password: password,
captcha: captcha,
uuid: uuid,
);
// 持久化登录态(token + 用户信息),用于免登录恢复。
await CredentialStorage.instance.saveSession(
token: user.token,
refreshToken: TokenStore.instance.refreshToken,
userJson: jsonEncode(user.toJson()),
);
// 记住密码。
if (rememberPassword) {
await CredentialStorage.instance.saveCredentials(
account: account,
password: password,
);
} else {
await CredentialStorage.instance.clearCredentials();
}
state = AuthState(user: user, isLoading: false);
// 登录成功后写入数据权限范围,供 OrgService 客户端收敛组织/门店数据。
SessionOrgScope.instance.setCurrentOrg(user.orgId);
return true;
} on AuthException catch (e) {
state = state.copyWith(isLoading: false, errorMessage: e.message);
return false;
} catch (_) {
state = state.copyWith(isLoading: false, errorMessage: '登录失败,请稍后重试');
return false;
}
}
/// 登出并清空登录态。
Future<void> logout() async {
await ref.read(authServiceProvider).logout();
SessionOrgScope.instance.clear();
await _clearPersistedSession();
state = const AuthState();
}
/// 清除本地持久化的登录态。
Future<void> _clearPersistedSession() async {
await CredentialStorage.instance.clearSession();
}
}
/// 登录状态提供者。
final authProvider = NotifierProvider<AuthNotifier, AuthState>(
AuthNotifier.new,
);