// 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 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 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; } if (refreshExpiresIn != null) { _cachedRefreshExpiresAt = now + refreshExpiresIn * 1000; } 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(), ), if (userId != null) _storage.write(key: _userIdKey, value: userId), // برای سازگاری با کدهایی که هنوز کلید "token" را می‌خوانند: _storage.write(key: _legacyTokenKey, value: accessToken), ]); } /// ذخیره id کاربر (Keycloak sub) به طور جداگانه. static Future setUserId(String userId) async { _cachedUserId = userId; await _storage.write(key: _userIdKey, value: userId); } /// پاک کردن همه‌ی توکن‌ها (logout). static Future 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), ]); } }