161 lines
6.4 KiB
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),
|
|
]);
|
|
}
|
|
}
|