diff --git a/hesabixAPI/adapters/api/v1/auth.py b/hesabixAPI/adapters/api/v1/auth.py index be582c2..c9bec09 100644 --- a/hesabixAPI/adapters/api/v1/auth.py +++ b/hesabixAPI/adapters/api/v1/auth.py @@ -28,6 +28,15 @@ def generate_captcha(db: Session = Depends(get_db)) -> dict: }) +@router.get("/me", summary="Get current user info") +def get_current_user_info( + request: Request, + ctx: AuthContext = Depends(get_current_user) +) -> dict: + """دریافت اطلاعات کاربر کنونی""" + return success_response(ctx.to_dict(), request) + + @router.post("/register", summary="Register new user") def register(request: Request, payload: RegisterRequest, db: Session = Depends(get_db)) -> dict: user_id = register_user( diff --git a/hesabixUI/hesabix_ui/lib/core/auth_store.dart b/hesabixUI/hesabix_ui/lib/core/auth_store.dart index 32fc696..054a1f1 100644 --- a/hesabixUI/hesabix_ui/lib/core/auth_store.dart +++ b/hesabixUI/hesabix_ui/lib/core/auth_store.dart @@ -1,18 +1,26 @@ +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; +import 'api_client.dart'; class AuthStore with ChangeNotifier { static const _kApiKey = 'auth_api_key'; static const _kDeviceId = 'device_id'; + static const _kAppPermissions = 'app_permissions'; + static const _kIsSuperAdmin = 'is_superadmin'; final FlutterSecureStorage _secure = const FlutterSecureStorage(); String? _apiKey; String? _deviceId; + Map? _appPermissions; + bool _isSuperAdmin = false; String? get apiKey => _apiKey; String get deviceId => _deviceId ?? ''; + Map? get appPermissions => _appPermissions; + bool get isSuperAdmin => _isSuperAdmin; Future load() async { final prefs = await SharedPreferences.getInstance(); @@ -28,9 +36,56 @@ class AuthStore with ChangeNotifier { _apiKey = await _secure.read(key: _kApiKey); _apiKey ??= prefs.getString(_kApiKey); } + + // بارگذاری دسترسی‌های اپلیکیشن + await _loadAppPermissions(); + + // اگر API key موجود است اما دسترسی‌ها نیست، از سرور دریافت کن + if (_apiKey != null && _apiKey!.isNotEmpty && (_appPermissions == null || _appPermissions!.isEmpty)) { + await _fetchPermissionsFromServer(); + } + notifyListeners(); } + Future _loadAppPermissions() async { + final prefs = await SharedPreferences.getInstance(); + + if (kIsWeb) { + final permissionsJson = prefs.getString(_kAppPermissions); + + if (permissionsJson != null) { + try { + _appPermissions = Map.from( + const JsonDecoder().convert(permissionsJson) + ); + } catch (e) { + _appPermissions = null; + } + } else { + _appPermissions = null; + } + _isSuperAdmin = prefs.getBool(_kIsSuperAdmin) ?? false; + } else { + try { + final permissionsJson = await _secure.read(key: _kAppPermissions); + + if (permissionsJson != null) { + _appPermissions = Map.from( + const JsonDecoder().convert(permissionsJson) + ); + } else { + _appPermissions = null; + } + final superAdminStr = await _secure.read(key: _kIsSuperAdmin); + _isSuperAdmin = superAdminStr == 'true'; + } catch (e) { + _appPermissions = null; + _isSuperAdmin = false; + } + } + } + Future saveApiKey(String? key) async { final prefs = await SharedPreferences.getInstance(); _apiKey = key; @@ -43,6 +98,8 @@ class AuthStore with ChangeNotifier { } catch (_) {} await prefs.remove(_kApiKey); } + // پاک کردن دسترسی‌ها هنگام خروج + await _clearAppPermissions(); } else { if (kIsWeb) { await prefs.setString(_kApiKey, key); @@ -55,6 +112,87 @@ class AuthStore with ChangeNotifier { } notifyListeners(); } + + Future saveAppPermissions(Map? permissions, bool isSuperAdmin) async { + final prefs = await SharedPreferences.getInstance(); + _appPermissions = permissions; + _isSuperAdmin = isSuperAdmin; + + if (permissions == null) { + await _clearAppPermissions(); + } else { + final permissionsJson = const JsonEncoder().convert(permissions); + + if (kIsWeb) { + await prefs.setString(_kAppPermissions, permissionsJson); + await prefs.setBool(_kIsSuperAdmin, isSuperAdmin); + } else { + try { + await _secure.write(key: _kAppPermissions, value: permissionsJson); + await _secure.write(key: _kIsSuperAdmin, value: isSuperAdmin.toString()); + } catch (_) { + // Fallback to SharedPreferences + await prefs.setString(_kAppPermissions, permissionsJson); + await prefs.setBool(_kIsSuperAdmin, isSuperAdmin); + } + } + } + notifyListeners(); + } + + Future _clearAppPermissions() async { + final prefs = await SharedPreferences.getInstance(); + _appPermissions = null; + _isSuperAdmin = false; + + if (kIsWeb) { + await prefs.remove(_kAppPermissions); + await prefs.remove(_kIsSuperAdmin); + } else { + try { + await _secure.delete(key: _kAppPermissions); + await _secure.delete(key: _kIsSuperAdmin); + } catch (_) {} + await prefs.remove(_kAppPermissions); + await prefs.remove(_kIsSuperAdmin); + } + } + + Future _fetchPermissionsFromServer() async { + if (_apiKey == null || _apiKey!.isEmpty) { + return; + } + + try { + final apiClient = ApiClient(); + final response = await apiClient.get('/api/v1/auth/me'); + + if (response.statusCode == 200) { + final data = response.data; + if (data is Map) { + final user = data['user'] as Map?; + if (user != null) { + final appPermissions = user['app_permissions'] as Map?; + final isSuperAdmin = appPermissions?['superadmin'] == true; + + if (appPermissions != null) { + await saveAppPermissions(appPermissions, isSuperAdmin); + } + } + } + } + } catch (e) { + // Silent fail - permissions will be loaded from storage + } + } + + bool hasAppPermission(String permission) { + if (_isSuperAdmin) { + return true; + } + + return _appPermissions?[permission] == true; + } } diff --git a/hesabixUI/hesabix_ui/lib/core/permission_guard.dart b/hesabixUI/hesabix_ui/lib/core/permission_guard.dart new file mode 100644 index 0000000..aafb262 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/core/permission_guard.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'auth_store.dart'; + +class PermissionGuard { + static bool checkSuperAdminAccess(AuthStore authStore) { + return authStore.isSuperAdmin; + } + + static bool checkAppPermission(AuthStore authStore, String permission) { + return authStore.hasAppPermission(permission); + } + + static Widget buildAccessDeniedPage() { + return Builder( + builder: (context) => Scaffold( + appBar: AppBar( + title: const Text('دسترسی غیرمجاز'), + backgroundColor: Colors.red[50], + foregroundColor: Colors.red[800], + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.block, + size: 80, + color: Colors.red[400], + ), + const SizedBox(height: 24), + Text( + 'دسترسی غیرمجاز', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.red[800], + ), + ), + const SizedBox(height: 16), + Text( + 'شما دسترسی لازم برای مشاهده این صفحه را ندارید.', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () => context.go('/user/profile/dashboard'), + icon: const Icon(Icons.home), + label: const Text('بازگشت به داشبورد'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[600], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb index 1f93042..2d95e95 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb @@ -46,6 +46,8 @@ "logoutConfirmMessage": "Are you sure you want to sign out?", "menu": "Menu" , + "systemSettings": "System Settings", + "adminTools": "Admin Tools", "ok": "OK", "cancel": "Cancel", "columnSettings": "Column Settings", diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb index a1d9697..c3a4e0e 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb @@ -51,6 +51,8 @@ "support": "پشتیبانی", "changePassword": "تغییر کلمه عبور", "marketing": "بازاریابی", + "systemSettings": "تنظیمات سیستم", + "adminTools": "ابزارهای مدیریتی", "ok": "تایید", "cancel": "انصراف", "columnSettings": "تنظیمات ستون‌ها", diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart index 7b1963d..48f50d4 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart @@ -338,6 +338,18 @@ abstract class AppLocalizations { /// **'Menu'** String get menu; + /// No description provided for @systemSettings. + /// + /// In en, this message translates to: + /// **'System Settings'** + String get systemSettings; + + /// No description provided for @adminTools. + /// + /// In en, this message translates to: + /// **'Admin Tools'** + String get adminTools; + /// No description provided for @ok. /// /// In en, this message translates to: diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart index 2510aaf..403c84a 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart @@ -130,6 +130,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get menu => 'Menu'; + @override + String get systemSettings => 'System Settings'; + + @override + String get adminTools => 'Admin Tools'; + @override String get ok => 'OK'; diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart index 65cbee0..feb0d78 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart @@ -130,6 +130,12 @@ class AppLocalizationsFa extends AppLocalizations { @override String get menu => 'منو'; + @override + String get systemSettings => 'تنظیمات سیستم'; + + @override + String get adminTools => 'ابزارهای مدیریتی'; + @override String get ok => 'تایید'; diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart index 2045102..f1382d9 100644 --- a/hesabixUI/hesabix_ui/lib/main.dart +++ b/hesabixUI/hesabix_ui/lib/main.dart @@ -11,6 +11,7 @@ import 'pages/profile/businesses_page.dart'; import 'pages/profile/support_page.dart'; import 'pages/profile/change_password_page.dart'; import 'pages/profile/marketing_page.dart'; +import 'pages/system_settings_page.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'core/locale_controller.dart'; import 'core/calendar_controller.dart'; @@ -18,6 +19,7 @@ import 'core/api_client.dart'; import 'theme/theme_controller.dart'; import 'theme/app_theme.dart'; import 'core/auth_store.dart'; +import 'core/permission_guard.dart'; void main() { // Use path-based routing instead of hash routing @@ -217,6 +219,21 @@ class _MyAppState extends State { ), ), ), + GoRoute( + path: '/user/profile/system-settings', + builder: (context, state) => const Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading...'), + ], + ), + ), + ), + ), // Catch-all route برای هر URL دیگر GoRoute( path: '/:path(.*)', @@ -339,6 +356,21 @@ class _MyAppState extends State { name: 'profile_change_password', builder: (context, state) => const ChangePasswordPage(), ), + GoRoute( + path: '/user/profile/system-settings', + name: 'profile_system_settings', + builder: (context, state) { + // بررسی دسترسی SuperAdmin + if (_authStore == null) { + return PermissionGuard.buildAccessDeniedPage(); + } + + if (!_authStore!.isSuperAdmin) { + return PermissionGuard.buildAccessDeniedPage(); + } + return const SystemSettingsPage(); + }, + ), ], ), ], diff --git a/hesabixUI/hesabix_ui/lib/pages/login_page.dart b/hesabixUI/hesabix_ui/lib/pages/login_page.dart index 4e7cd04..368bb6c 100644 --- a/hesabixUI/hesabix_ui/lib/pages/login_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/login_page.dart @@ -239,10 +239,19 @@ class _LoginPageState extends State with SingleTickerProviderStateMix final apiKey = data != null ? data['api_key']?.toString() : null; if (apiKey != null && apiKey.isNotEmpty) { await widget.authStore.saveApiKey(apiKey); - // ذخیره کد بازاریابی کاربر برای صفحه Marketing - final user = data?['user'] as Map?; - final String? myRef = user != null ? user['referral_code']?.toString() : null; - unawaited(ReferralStore.saveUserReferralCode(myRef)); + } + + // ذخیره کد بازاریابی کاربر برای صفحه Marketing + final user = data?['user'] as Map?; + final String? myRef = user != null ? user['referral_code']?.toString() : null; + unawaited(ReferralStore.saveUserReferralCode(myRef)); + + // ذخیره دسترسی‌های اپلیکیشن + final appPermissions = user?['app_permissions'] as Map?; + final isSuperAdmin = appPermissions?['superadmin'] == true; + + if (appPermissions != null) { + await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin); } if (!mounted) return; @@ -327,10 +336,19 @@ class _LoginPageState extends State with SingleTickerProviderStateMix if (apiKey != null && apiKey.isNotEmpty) { await widget.authStore.saveApiKey(apiKey); } + // ذخیره کد بازاریابی کاربر final user = data?['user'] as Map?; final String? myRef = user != null ? user['referral_code'] as String? : null; unawaited(ReferralStore.saveUserReferralCode(myRef)); + + // ذخیره دسترسی‌های اپلیکیشن + final appPermissions = user?['app_permissions'] as Map?; + final isSuperAdmin = appPermissions?['superadmin'] == true; + + if (appPermissions != null) { + await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin); + } _showSnack(t.registerSuccess); // پاکسازی کد معرف پس از ثبت‌نام موفق unawaited(ReferralStore.clearReferrer()); diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart index 2055ec4..6819c07 100644 --- a/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart +++ b/hesabixUI/hesabix_ui/lib/pages/profile/profile_shell.dart @@ -25,6 +25,17 @@ class ProfileShell extends StatefulWidget { class _ProfileShellState extends State { int _hoverIndex = -1; + @override + void initState() { + super.initState(); + // اضافه کردن listener برای AuthStore + widget.authStore.addListener(() { + if (mounted) { + setState(() {}); + } + }); + } + @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; @@ -47,16 +58,27 @@ class _ProfileShellState extends State { _Dest(t.changePassword, Icons.password, Icons.password, '/user/profile/change-password'), ]; + // اضافه کردن منوی تنظیمات سیستم برای ادمین‌ها + final adminDestinations = <_Dest>[ + _Dest(t.systemSettings, Icons.admin_panel_settings, Icons.admin_panel_settings, '/user/profile/system-settings'), + ]; + + // ترکیب منوهای عادی و ادمین + final allDestinations = <_Dest>[ + ...destinations, + if (widget.authStore.isSuperAdmin) ...adminDestinations, + ]; + int selectedIndex = 0; - for (int i = 0; i < destinations.length; i++) { - if (location.startsWith(destinations[i].path)) { + for (int i = 0; i < allDestinations.length; i++) { + if (location.startsWith(allDestinations[i].path)) { selectedIndex = i; break; } } Future onSelect(int index) async { - final path = destinations[index].path; + final path = allDestinations[index].path; if (GoRouterState.of(context).uri.toString() != path) { context.go(path); } @@ -150,9 +172,9 @@ class _ProfileShellState extends State { color: sideBg, child: ListView.builder( padding: EdgeInsets.zero, - itemCount: destinations.length, + itemCount: allDestinations.length, itemBuilder: (ctx, i) { - final d = destinations[i]; + final d = allDestinations[i]; final bool isHovered = i == _hoverIndex; final bool isSelected = i == selectedIndex; final bool active = isSelected || isHovered; @@ -216,9 +238,9 @@ class _ProfileShellState extends State { child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: [ - for (int i = 0; i < destinations.length; i++) ...[ + for (int i = 0; i < allDestinations.length; i++) ...[ Builder(builder: (ctx) { - final d = destinations[i]; + final d = allDestinations[i]; final bool active = i == selectedIndex; return ListTile( leading: Icon(d.selectedIcon, color: active ? activeFg : sideFg), diff --git a/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart b/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart new file mode 100644 index 0000000..f70b42f --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/system_settings_page.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; +import 'package:hesabix_ui/l10n/app_localizations.dart'; + +class SystemSettingsPage extends StatelessWidget { + const SystemSettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + final t = AppLocalizations.of(context); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Scaffold( + appBar: AppBar( + title: Text(t.systemSettings), + backgroundColor: colorScheme.surface, + foregroundColor: colorScheme.onSurface, + elevation: 0, + ), + body: Container( + color: colorScheme.surface, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + Icons.admin_panel_settings, + size: 32, + color: colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.systemSettings, + style: theme.textTheme.headlineSmall?.copyWith( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'تنظیمات پیشرفته سیستم - فقط برای ادمین‌ها', + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onPrimaryContainer.withOpacity(0.8), + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Settings Cards + Expanded( + child: GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 1.5, + children: [ + _buildSettingCard( + context, + icon: Icons.people, + title: 'مدیریت کاربران', + subtitle: 'مدیریت کاربران سیستم', + color: Colors.blue, + ), + _buildSettingCard( + context, + icon: Icons.business, + title: 'مدیریت کسب و کارها', + subtitle: 'مدیریت کسب و کارهای ثبت شده', + color: Colors.green, + ), + _buildSettingCard( + context, + icon: Icons.security, + title: 'امنیت سیستم', + subtitle: 'تنظیمات امنیتی و دسترسی‌ها', + color: Colors.orange, + ), + _buildSettingCard( + context, + icon: Icons.analytics, + title: 'گزارش‌گیری', + subtitle: 'گزارش‌های سیستم و آمار', + color: Colors.purple, + ), + _buildSettingCard( + context, + icon: Icons.backup, + title: 'پشتیبان‌گیری', + subtitle: 'مدیریت پشتیبان‌ها', + color: Colors.teal, + ), + _buildSettingCard( + context, + icon: Icons.tune, + title: 'تنظیمات پیشرفته', + subtitle: 'تنظیمات تخصصی سیستم', + color: Colors.indigo, + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Warning Message + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.amber.withOpacity(0.1), + border: Border.all(color: Colors.amber.withOpacity(0.3)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.warning_amber, + color: Colors.amber[700], + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'توجه: این بخش فقط برای ادمین‌های سیستم قابل دسترسی است. تغییرات در این بخش می‌تواند بر عملکرد کل سیستم تأثیر بگذارد.', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.amber[700], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSettingCard( + BuildContext context, { + required IconData icon, + required String title, + required String subtitle, + required Color color, + }) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + // TODO: Navigate to specific setting + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$title - در حال توسعه'), + duration: const Duration(seconds: 2), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + color: color, + size: 32, + ), + const SizedBox(height: 12), + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Expanded( + child: Text( + subtitle, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurface.withOpacity(0.7), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + Icons.arrow_forward_ios, + size: 16, + color: colorScheme.onSurface.withOpacity(0.5), + ), + ], + ), + ], + ), + ), + ), + ); + } +}