didvan-app/lib/services/auth/token_storage.dart

161 lines
6.4 KiB
Dart

// lib/services/auth/token_storage.dart
//
// مدیریت ذخیره/بازیابی توکن‌های احراز هویت (access, refresh, expiry).
// به جای ذخیره‌ی یک "token" تک‌بعدی، اکنون چند مقدار را به صورت ایمن
// در flutter_secure_storage نگهداری می‌کنیم.
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class TokenStorage {
static const _accessTokenKey = 'access_token';
static const _refreshTokenKey = 'refresh_token';
static const _idTokenKey = 'id_token';
static const _accessExpiresAtKey = 'access_expires_at'; // ms since epoch
static const _refreshExpiresAtKey = 'refresh_expires_at'; // ms since epoch
static const _userIdKey = 'user_id'; // Keycloak sub (UUID)
// برای سازگاری با کد قدیمی که از کلید 'token' استفاده می‌کرد:
static const _legacyTokenKey = 'token';
static const FlutterSecureStorage _storage = FlutterSecureStorage();
/// در حافظه نگه می‌داریم تا در هر درخواست شبکه نیاز به read async نباشد.
static String? _cachedAccessToken;
static String? _cachedRefreshToken;
static String? _cachedIdToken;
static String? _cachedUserId;
static int? _cachedAccessExpiresAt;
static int? _cachedRefreshExpiresAt;
// ----------------- بارگذاری اولیه از Secure Storage -----------------
/// در شروع اپلیکیشن یک‌بار صدا زده می‌شود تا مقادیر در حافظه کش شوند.
static Future<void> init() async {
_cachedAccessToken = await _storage.read(key: _accessTokenKey);
_cachedRefreshToken = await _storage.read(key: _refreshTokenKey);
_cachedIdToken = await _storage.read(key: _idTokenKey);
_cachedUserId = await _storage.read(key: _userIdKey);
_cachedAccessExpiresAt =
int.tryParse(await _storage.read(key: _accessExpiresAtKey) ?? '');
_cachedRefreshExpiresAt =
int.tryParse(await _storage.read(key: _refreshExpiresAtKey) ?? '');
// سازگاری: اگر هنوز توکن قدیمی موجود است ولی access_token نه،
// آن را به عنوان access_token در نظر بگیریم (تا قبل از login مجدد).
if (_cachedAccessToken == null) {
final legacy = await _storage.read(key: _legacyTokenKey);
if (legacy != null && legacy.isNotEmpty) {
_cachedAccessToken = legacy;
}
}
}
// ----------------- Getters -----------------
static String? get accessToken => _cachedAccessToken;
static String? get refreshToken => _cachedRefreshToken;
static String? get idToken => _cachedIdToken;
static String? get userId => _cachedUserId;
/// زمانِ انقضا (ms since epoch) — برای دیباگ/تشخیص.
static int? get accessExpiresAt => _cachedAccessExpiresAt;
static int? get refreshExpiresAt => _cachedRefreshExpiresAt;
/// آیا access token از نظر زمانی هنوز معتبر است؟
static bool get isAccessTokenValid {
if (_cachedAccessToken == null || _cachedAccessToken!.isEmpty) return false;
if (_cachedAccessExpiresAt == null) return true; // فرض می‌کنیم معتبر
return DateTime.now().millisecondsSinceEpoch <
_cachedAccessExpiresAt! - 5000; // 5s buffer
}
/// آیا refresh token از نظر زمانی هنوز معتبر است؟
static bool get isRefreshTokenValid {
if (_cachedRefreshToken == null || _cachedRefreshToken!.isEmpty) {
return false;
}
if (_cachedRefreshExpiresAt == null) return true;
return DateTime.now().millisecondsSinceEpoch <
_cachedRefreshExpiresAt! - 5000;
}
// ----------------- Setters / Persist -----------------
/// مقادیر دریافتی از Keycloak (پاسخ /token) را ذخیره می‌کند.
///
/// [expiresIn] و [refreshExpiresIn] بر حسب ثانیه هستند.
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
String? idToken,
int? expiresIn,
int? refreshExpiresIn,
String? userId,
}) async {
_cachedAccessToken = accessToken;
_cachedRefreshToken = refreshToken;
_cachedIdToken = idToken;
if (userId != null) _cachedUserId = userId;
final now = DateTime.now().millisecondsSinceEpoch;
if (expiresIn != null) {
_cachedAccessExpiresAt = now + expiresIn * 1000;
}
// Offline tokens return refresh_expires_in=0 → treat as "never expires"
// (null cache means isRefreshTokenValid returns true). Only stamp an
// expiry when Keycloak gave us a positive lifetime.
if (refreshExpiresIn != null && refreshExpiresIn > 0) {
_cachedRefreshExpiresAt = now + refreshExpiresIn * 1000;
} else {
_cachedRefreshExpiresAt = null;
}
await Future.wait([
_storage.write(key: _accessTokenKey, value: accessToken),
_storage.write(key: _refreshTokenKey, value: refreshToken),
if (idToken != null) _storage.write(key: _idTokenKey, value: idToken),
if (_cachedAccessExpiresAt != null)
_storage.write(
key: _accessExpiresAtKey,
value: _cachedAccessExpiresAt.toString(),
),
if (_cachedRefreshExpiresAt != null)
_storage.write(
key: _refreshExpiresAtKey,
value: _cachedRefreshExpiresAt.toString(),
)
else
_storage.delete(key: _refreshExpiresAtKey),
if (userId != null) _storage.write(key: _userIdKey, value: userId),
// برای سازگاری با کدهایی که هنوز کلید "token" را می‌خوانند:
_storage.write(key: _legacyTokenKey, value: accessToken),
]);
}
/// ذخیره id کاربر (Keycloak sub) به طور جداگانه.
static Future<void> setUserId(String userId) async {
_cachedUserId = userId;
await _storage.write(key: _userIdKey, value: userId);
}
/// پاک کردن همه‌ی توکن‌ها (logout).
static Future<void> clear() async {
_cachedAccessToken = null;
_cachedRefreshToken = null;
_cachedIdToken = null;
_cachedUserId = null;
_cachedAccessExpiresAt = null;
_cachedRefreshExpiresAt = null;
await Future.wait([
_storage.delete(key: _accessTokenKey),
_storage.delete(key: _refreshTokenKey),
_storage.delete(key: _idTokenKey),
_storage.delete(key: _userIdKey),
_storage.delete(key: _accessExpiresAtKey),
_storage.delete(key: _refreshExpiresAtKey),
_storage.delete(key: _legacyTokenKey),
]);
}
}