chore: initial release

This commit is contained in:
cogwheel0
2025-08-10 01:20:45 +05:30
commit 758615813f
218 changed files with 67743 additions and 0 deletions

View File

@@ -0,0 +1,259 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:crypto/crypto.dart';
/// JWT token validation utilities
class TokenValidator {
static const Duration _validationTimeout = Duration(seconds: 5);
/// Validate JWT token format and expiry without network call
static TokenValidationResult validateTokenFormat(String token) {
try {
// Basic format check
if (token.isEmpty || token.length < 10) {
return TokenValidationResult.invalid('Token too short');
}
// Check if it looks like a JWT (has at least 2 dots)
final parts = token.split('.');
if (parts.length < 3) {
return TokenValidationResult.invalid('Invalid JWT format');
}
// Try to decode the payload to check expiry
try {
final payload = _decodeJWTPayload(parts[1]);
final exp = payload['exp'] as int?;
if (exp != null) {
final expiryTime = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
final now = DateTime.now();
if (expiryTime.isBefore(now)) {
return TokenValidationResult.expired('Token expired');
}
// Check if token expires soon (within 5 minutes)
final fiveMinutesFromNow = now.add(const Duration(minutes: 5));
if (expiryTime.isBefore(fiveMinutesFromNow)) {
return TokenValidationResult.expiringSoon(
'Token expires soon',
expiryTime,
);
}
}
return TokenValidationResult.valid(
'Token format valid',
expiryData: exp != null
? DateTime.fromMillisecondsSinceEpoch(exp * 1000)
: null,
);
} catch (e) {
// If we can't decode JWT, treat as opaque token
debugPrint(
'DEBUG: Could not decode JWT payload, treating as opaque token: $e',
);
return TokenValidationResult.valid('Opaque token format valid');
}
} catch (e) {
return TokenValidationResult.invalid('Token validation error: $e');
}
}
/// Validate token with server (async with timeout)
static Future<TokenValidationResult> validateTokenWithServer(
String token,
Future<dynamic> Function() serverValidationCall,
) async {
try {
// First check format
final formatResult = validateTokenFormat(token);
if (!formatResult.isValid) {
return formatResult;
}
// If format is good, try server validation with timeout
final validationFuture = serverValidationCall();
final result = await validationFuture.timeout(
_validationTimeout,
onTimeout: () => throw Exception('Token validation timeout'),
);
return TokenValidationResult.valid(
'Server validation successful',
serverData: result,
);
} catch (e) {
if (e.toString().contains('timeout')) {
return TokenValidationResult.networkError(
'Validation timeout - using cached result',
);
} else if (e.toString().contains('401') || e.toString().contains('403')) {
return TokenValidationResult.invalid('Server rejected token');
} else {
return TokenValidationResult.networkError(
'Network error during validation: $e',
);
}
}
}
/// Decode JWT payload (without signature verification)
static Map<String, dynamic> _decodeJWTPayload(String base64Payload) {
// Add padding if needed
String padded = base64Payload;
while (padded.length % 4 != 0) {
padded += '=';
}
// Decode base64
final decoded = base64Url.decode(padded);
final jsonString = utf8.decode(decoded);
return jsonDecode(jsonString) as Map<String, dynamic>;
}
/// Extract user information from JWT token (if available)
static Map<String, dynamic>? extractUserInfo(String token) {
try {
final parts = token.split('.');
if (parts.length < 3) return null;
final payload = _decodeJWTPayload(parts[1]);
// Extract common user fields
return {
'sub': payload['sub'], // Subject (user ID)
'username':
payload['username'] ??
payload['name'] ??
payload['preferred_username'],
'email': payload['email'],
'roles': payload['roles'] ?? payload['groups'],
'exp': payload['exp'],
'iat': payload['iat'], // Issued at
};
} catch (e) {
debugPrint('DEBUG: Could not extract user info from token: $e');
return null;
}
}
/// Generate a cache key for token validation results
static String generateCacheKey(String token) {
final bytes = utf8.encode(token);
final digest = sha256.convert(bytes);
return digest.toString().substring(
0,
16,
); // Use first 16 chars as cache key
}
}
/// Result of token validation
class TokenValidationResult {
const TokenValidationResult._(
this.isValid,
this.status,
this.message, {
this.expiryData,
this.serverData,
});
const TokenValidationResult.valid(
String message, {
DateTime? expiryData,
dynamic serverData,
}) : this._(
true,
TokenValidationStatus.valid,
message,
expiryData: expiryData,
serverData: serverData,
);
const TokenValidationResult.invalid(String message)
: this._(false, TokenValidationStatus.invalid, message);
const TokenValidationResult.expired(String message)
: this._(false, TokenValidationStatus.expired, message);
const TokenValidationResult.expiringSoon(String message, DateTime expiryTime)
: this._(
true,
TokenValidationStatus.expiringSoon,
message,
expiryData: expiryTime,
);
const TokenValidationResult.networkError(String message)
: this._(false, TokenValidationStatus.networkError, message);
final bool isValid;
final TokenValidationStatus status;
final String message;
final DateTime? expiryData;
final dynamic serverData;
bool get isExpired => status == TokenValidationStatus.expired;
bool get isExpiringSoon => status == TokenValidationStatus.expiringSoon;
bool get hasNetworkError => status == TokenValidationStatus.networkError;
@override
String toString() =>
'TokenValidationResult(isValid: $isValid, status: $status, message: $message)';
}
enum TokenValidationStatus {
valid,
invalid,
expired,
expiringSoon,
networkError,
}
/// Cache for token validation results
class TokenValidationCache {
static final Map<String, _CacheEntry> _cache = {};
static const Duration _cacheTimeout = Duration(minutes: 5);
static void cacheResult(String token, TokenValidationResult result) {
final key = TokenValidator.generateCacheKey(token);
_cache[key] = _CacheEntry(result, DateTime.now());
// Clean old entries
_cleanCache();
}
static TokenValidationResult? getCachedResult(String token) {
final key = TokenValidator.generateCacheKey(token);
final entry = _cache[key];
if (entry != null &&
DateTime.now().difference(entry.timestamp) < _cacheTimeout) {
return entry.result;
}
return null;
}
static void clearCache() {
_cache.clear();
}
static void _cleanCache() {
final now = DateTime.now();
_cache.removeWhere(
(key, entry) => now.difference(entry.timestamp) > _cacheTimeout,
);
}
}
class _CacheEntry {
const _CacheEntry(this.result, this.timestamp);
final TokenValidationResult result;
final DateTime timestamp;
}