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

103 lines
3.7 KiB
Dart

// lib/services/auth/jwt_utils.dart
//
// ابزار کوچک برای decode کردن claims یک JWT بدون اعتبارسنجی signature.
// از این فقط برای استخراج اطلاعات داخل توکن استفاده می‌کنیم (مثلا sub).
import 'dart:convert';
class JwtUtils {
/// claims یک JWT را به صورت Map برمی‌گرداند، یا در صورت نامعتبر بودن null.
static Map<String, dynamic>? decodePayload(String token) {
try {
final parts = token.split('.');
if (parts.length < 2) return null;
final payload = _padBase64(parts[1]);
final decoded = utf8.decode(base64Url.decode(payload));
final map = json.decode(decoded);
if (map is Map<String, dynamic>) return map;
return Map<String, dynamic>.from(map as Map);
} catch (_) {
return null;
}
}
/// مقدار claim `sub` (شناسه کاربر در Keycloak) را برمی‌گرداند.
static String? subjectOf(String token) {
final payload = decodePayload(token);
return payload?['sub'] as String?;
}
/// لیست realm role های کاربر — از `realm_access.roles`.
static List<String> realmRolesOf(String token) {
final payload = decodePayload(token);
if (payload == null) return const [];
final realmAccess = payload['realm_access'];
if (realmAccess is Map && realmAccess['roles'] is List) {
return List<String>.from(realmAccess['roles'] as List);
}
return const [];
}
/// لیست group path های کاربر — از claim `groups`.
static List<String> groupsOf(String token) {
final payload = decodePayload(token);
if (payload == null) return const [];
final g = payload['groups'];
if (g is List) return List<String>.from(g);
return const [];
}
/// tenant slug کاربر — از `tenant_id` یا استخراج از groups.
static String tenantSlugOf(String token) {
final payload = decodePayload(token);
if (payload == null) return '';
final direct = payload['tenant_id'];
if (direct is String && direct.isNotEmpty) return direct;
// Fallback: از groups استخراج کن
final groups = groupsOf(token);
const groupToSlug = {
'Fartak': 'fartak',
'MobarakehSteel': 'mobarakeh',
'GoharZamin': 'gohar-zamin',
};
for (final path in groups) {
for (final segment in path.split('/')) {
final slug = groupToSlug[segment];
if (slug != null) return slug;
}
}
return '';
}
/// True اگر کاربر super-admin است (گروه Fartak یا role superadmin).
static bool isSuperTenant(String token) {
final slug = tenantSlugOf(token);
if (slug == 'fartak') return true;
final roles = realmRolesOf(token);
if (roles.contains('superadmin')) return true;
final groups = groupsOf(token);
for (final g in groups) {
if (g == '/Fartak' || g.startsWith('/Fartak/')) return true;
}
return false;
}
/// True اگر کاربر می‌تواند نوع محتوایی مشخص را ببیند.
/// deny role همیشه اولویت دارد؛ در غیر این صورت بخش باز است.
///
/// انواع شناخته‌شده: news، radar، trend، risk، technology، startup،
/// podcast، videocast، didvan-plus، strategy-bite، monthly، survey، story،
/// infography، hoshan، swot.
static bool canView(String token, String contentType) {
final roles = realmRolesOf(token);
if (roles.contains('deny:view:$contentType')) return false;
return true;
}
static String _padBase64(String input) {
final mod = input.length % 4;
if (mod == 0) return input;
return input + '=' * (4 - mod);
}
}