permission

This commit is contained in:
Hesabix 2025-09-19 16:40:05 +03:30
parent 31defb7eff
commit ad5cd35f8f
12 changed files with 563 additions and 11 deletions

View file

@ -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(

View file

@ -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<String, dynamic>? _appPermissions;
bool _isSuperAdmin = false;
String? get apiKey => _apiKey;
String get deviceId => _deviceId ?? '';
Map<String, dynamic>? get appPermissions => _appPermissions;
bool get isSuperAdmin => _isSuperAdmin;
Future<void> 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<void> _loadAppPermissions() async {
final prefs = await SharedPreferences.getInstance();
if (kIsWeb) {
final permissionsJson = prefs.getString(_kAppPermissions);
if (permissionsJson != null) {
try {
_appPermissions = Map<String, dynamic>.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<String, dynamic>.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<void> 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<void> saveAppPermissions(Map<String, dynamic>? 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<void> _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<void> _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<String, dynamic>) {
final user = data['user'] as Map<String, dynamic>?;
if (user != null) {
final appPermissions = user['app_permissions'] as Map<String, dynamic>?;
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;
}
}

View file

@ -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,
),
),
),
],
),
),
),
),
);
}
}

View file

@ -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",

View file

@ -51,6 +51,8 @@
"support": "پشتیبانی",
"changePassword": "تغییر کلمه عبور",
"marketing": "بازاریابی",
"systemSettings": "تنظیمات سیستم",
"adminTools": "ابزارهای مدیریتی",
"ok": "تایید",
"cancel": "انصراف",
"columnSettings": "تنظیمات ستون‌ها",

View file

@ -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:

View file

@ -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';

View file

@ -130,6 +130,12 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get menu => 'منو';
@override
String get systemSettings => 'تنظیمات سیستم';
@override
String get adminTools => 'ابزارهای مدیریتی';
@override
String get ok => 'تایید';

View file

@ -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<MyApp> {
),
),
),
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<MyApp> {
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();
},
),
],
),
],

View file

@ -239,10 +239,19 @@ class _LoginPageState extends State<LoginPage> 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<String, dynamic>?;
final String? myRef = user != null ? user['referral_code']?.toString() : null;
unawaited(ReferralStore.saveUserReferralCode(myRef));
// ذخیره دسترسیهای اپلیکیشن
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
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<LoginPage> with SingleTickerProviderStateMix
if (apiKey != null && apiKey.isNotEmpty) {
await widget.authStore.saveApiKey(apiKey);
}
// ذخیره کد بازاریابی کاربر
final user = data?['user'] as Map<String, dynamic>?;
final String? myRef = user != null ? user['referral_code'] as String? : null;
unawaited(ReferralStore.saveUserReferralCode(myRef));
// ذخیره دسترسیهای اپلیکیشن
final appPermissions = user?['app_permissions'] as Map<String, dynamic>?;
final isSuperAdmin = appPermissions?['superadmin'] == true;
if (appPermissions != null) {
await widget.authStore.saveAppPermissions(appPermissions, isSuperAdmin);
}
_showSnack(t.registerSuccess);
// پاکسازی کد معرف پس از ثبتنام موفق
unawaited(ReferralStore.clearReferrer());

View file

@ -25,6 +25,17 @@ class ProfileShell extends StatefulWidget {
class _ProfileShellState extends State<ProfileShell> {
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<ProfileShell> {
_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<void> 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<ProfileShell> {
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<ProfileShell> {
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),

View file

@ -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),
),
],
),
],
),
),
),
);
}
}