31 KiB
Executable file
سناریوی پیادهسازی: مدیریت اپراتورهای پشتیبانی
📋 خلاصه اجرایی
این سند سناریوی کامل پیادهسازی بخش مدیریت App-Level Permissions (با تمرکز روی اپراتورهای پشتیبانی) را شامل میشود.
🎯 اهداف
- مدیریت مرکزی: ایجاد یک پنل مدیریتی برای تخصیص و لغو permissions در سطح اپلیکیشن
- امنیت: فقط SuperAdmin بتواند App Permissions را مدیریت کند
- ساده و کاربرپسند: UI ساده و واضح برای مدیریت نقشها
- قابل توسعه: طراحی به گونهای که بتوان permissions جدید اضافه کرد
🏗️ معماری کلی
┌─────────────────────────────────────────────────────────────┐
│ SuperAdmin Panel (UI) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ User Management │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ • List Users │ │ │
│ │ │ • View User Details │ │ │
│ │ │ • Manage App Permissions ← NEW! │ │ │
│ │ │ - Support Operator │ │ │
│ │ │ - System Settings │ │ │
│ │ │ - User Management │ │ │
│ │ │ - Business Management │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ API Calls
┌─────────────────────────────────────────────────────────────┐
│ Backend API │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ /api/v1/admin/users/{user_id}/app-permissions │ │
│ │ • GET: دریافت permissions کاربر │ │
│ │ • PUT: بهروزرسانی permissions کاربر ← NEW! │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ /api/v1/admin/operators ← NEW! │ │
│ │ • GET: لیست اپراتورهای پشتیبانی │ │
│ │ • POST: اضافه کردن اپراتور │ │
│ │ • DELETE: حذف اپراتور │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
📍 فاز 1: Backend API (اولویت بالا)
1.1 ایجاد Endpoint های جدید
فایل: hesabixAPI/adapters/api/v1/admin/users_permissions.py (جدید)
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from typing import Dict, Any, List
from pydantic import BaseModel, Field
from adapters.db.session import get_db
from adapters.db.models.user import User
from adapters.db.repositories.user_repo import UserRepository
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_app_permission
from app.core.responses import success_response
router = APIRouter(
prefix="/admin/users",
tags=["مدیریت کاربران", "مدیریت سیستم"]
)
# ========== Schemas ==========
class AppPermissionsResponse(BaseModel):
"""پاسخ دریافت App Permissions"""
user_id: int
email: str
app_permissions: Dict[str, bool] = Field(default_factory=dict)
class Config:
from_attributes = True
class UpdateAppPermissionsRequest(BaseModel):
"""درخواست بهروزرسانی App Permissions"""
permissions: Dict[str, bool] = Field(
...,
description="دسترسیهای سطح اپلیکیشن (مثال: {'support_operator': true, 'system_settings': false})"
)
class Config:
json_schema_extra = {
"example": {
"permissions": {
"support_operator": True,
"system_settings": False,
"user_management": False,
"business_management": False
}
}
}
class OperatorSummary(BaseModel):
"""خلاصه اطلاعات اپراتور"""
id: int
email: str
first_name: str | None
last_name: str | None
full_name: str | None
telegram_chat_id: str | None
is_active: bool
created_at: str
class Config:
from_attributes = True
# ========== Endpoints ==========
@router.get(
"/{user_id}/app-permissions",
summary="دریافت App Permissions کاربر",
description="دریافت دسترسیهای سطح اپلیکیشن یک کاربر. نیاز به مجوز SuperAdmin دارد.",
response_model=AppPermissionsResponse
)
@require_app_permission("superadmin")
async def get_user_app_permissions(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""دریافت App Permissions کاربر"""
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
return success_response(
data={
"user_id": user.id,
"email": user.email,
"app_permissions": user.app_permissions or {}
},
request=request
)
@router.put(
"/{user_id}/app-permissions",
summary="بهروزرسانی App Permissions کاربر",
description="بهروزرسانی دسترسیهای سطح اپلیکیشن یک کاربر. فقط SuperAdmin.",
)
@require_app_permission("superadmin")
async def update_user_app_permissions(
user_id: int,
permissions_request: UpdateAppPermissionsRequest,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""بهروزرسانی App Permissions کاربر"""
# بررسی وجود کاربر
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
# جلوگیری از تغییر permissions خودش
if user.id == ctx.get_user_id():
raise HTTPException(
status_code=403,
detail="شما نمیتوانید دسترسیهای خود را تغییر دهید"
)
# فیلتر کردن فقط permissions معتبر
valid_permissions = {
"superadmin",
"support_operator",
"system_settings",
"user_management",
"business_management"
}
# فقط permissions که true هستند را نگه میداریم
new_permissions = {
k: v for k, v in permissions_request.permissions.items()
if k in valid_permissions and v is True
}
# بهروزرسانی
user.app_permissions = new_permissions
db.commit()
db.refresh(user)
return success_response(
data={
"user_id": user.id,
"email": user.email,
"app_permissions": user.app_permissions or {}
},
request=request,
message="دسترسیها با موفقیت بهروزرسانی شد"
)
# ========== Operator Management Endpoints ==========
@router.get(
"/operators",
summary="لیست اپراتورهای پشتیبانی",
description="دریافت لیست تمام اپراتورهای پشتیبانی فعال",
response_model=List[OperatorSummary]
)
@require_app_permission("superadmin")
async def list_support_operators(
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""لیست اپراتورهای پشتیبانی"""
user_repo = UserRepository(db)
operators = user_repo.get_support_operators()
operators_data = []
for op in operators:
full_name = None
if op.first_name or op.last_name:
parts = [p for p in [op.first_name, op.last_name] if p]
full_name = " ".join(parts) if parts else None
operators_data.append({
"id": op.id,
"email": op.email,
"first_name": op.first_name,
"last_name": op.last_name,
"full_name": full_name,
"telegram_chat_id": op.telegram_chat_id,
"is_active": op.is_active,
"created_at": op.created_at.isoformat()
})
return success_response(
data={"items": operators_data, "total": len(operators_data)},
request=request
)
@router.post(
"/operators/{user_id}",
summary="اضافه کردن اپراتور پشتیبانی",
description="افزودن مجوز اپراتور پشتیبانی به یک کاربر"
)
@require_app_permission("superadmin")
async def add_support_operator(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""اضافه کردن اپراتور پشتیبانی"""
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
# اضافه کردن permission
permissions = user.app_permissions or {}
permissions['support_operator'] = True
user.app_permissions = permissions
db.commit()
db.refresh(user)
return success_response(
data={"user_id": user.id, "email": user.email},
request=request,
message="کاربر به عنوان اپراتور پشتیبانی اضافه شد"
)
@router.delete(
"/operators/{user_id}",
summary="حذف اپراتور پشتیبانی",
description="لغو مجوز اپراتور پشتیبانی از یک کاربر"
)
@require_app_permission("superadmin")
async def remove_support_operator(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""حذف اپراتور پشتیبانی"""
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
# حذف permission
if user.app_permissions and 'support_operator' in user.app_permissions:
permissions = user.app_permissions.copy()
del permissions['support_operator']
user.app_permissions = permissions if permissions else {}
db.commit()
db.refresh(user)
return success_response(
data={"user_id": user.id, "email": user.email},
request=request,
message="مجوز اپراتور پشتیبانی لغو شد"
)
1.2 ثبت Router جدید
فایل: hesabixAPI/app/main.py
# اضافه کردن import
from adapters.api.v1.admin import users_permissions
# در تابع create_app():
app.include_router(
users_permissions.router,
prefix="/api/v1",
)
1.3 تست API
# دریافت App Permissions کاربر
curl -X GET "http://localhost:8000/api/v1/admin/users/1/app-permissions" \
-H "Authorization: Bearer YOUR_TOKEN"
# بهروزرسانی App Permissions
curl -X PUT "http://localhost:8000/api/v1/admin/users/1/app-permissions" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"permissions": {
"support_operator": true,
"system_settings": false
}
}'
# لیست اپراتورها
curl -X GET "http://localhost:8000/api/v1/admin/users/operators" \
-H "Authorization: Bearer YOUR_TOKEN"
# اضافه کردن اپراتور
curl -X POST "http://localhost:8000/api/v1/admin/users/operators/1" \
-H "Authorization: Bearer YOUR_TOKEN"
# حذف اپراتور
curl -X DELETE "http://localhost:8000/api/v1/admin/users/operators/1" \
-H "Authorization: Bearer YOUR_TOKEN"
📍 فاز 2: Frontend UI (اولویت متوسط)
2.1 سرویس API در Flutter
فایل: hesabixUI/hesabix_ui/lib/services/admin_users_service.dart (جدید)
import '../core/api_client.dart';
class AdminUsersService {
final ApiClient _api;
AdminUsersService(this._api);
/// دریافت App Permissions کاربر
Future<Map<String, dynamic>> getUserAppPermissions(int userId) async {
final res = await _api.get<Map<String, dynamic>>(
'/api/v1/admin/users/$userId/app-permissions'
);
return Map<String, dynamic>.from(res.data?['data'] as Map? ?? const {});
}
/// بهروزرسانی App Permissions کاربر
Future<Map<String, dynamic>> updateUserAppPermissions(
int userId,
Map<String, bool> permissions
) async {
final res = await _api.put<Map<String, dynamic>>(
'/api/v1/admin/users/$userId/app-permissions',
data: {'permissions': permissions}
);
return Map<String, dynamic>.from(res.data?['data'] as Map? ?? const {});
}
/// لیست اپراتورهای پشتیبانی
Future<List<Map<String, dynamic>>> listSupportOperators() async {
final res = await _api.get<Map<String, dynamic>>(
'/api/v1/admin/users/operators'
);
final data = res.data?['data'] as Map? ?? {};
final items = data['items'] as List? ?? [];
return items.map((item) => Map<String, dynamic>.from(item as Map)).toList();
}
/// اضافه کردن اپراتور
Future<void> addSupportOperator(int userId) async {
await _api.post(
'/api/v1/admin/users/operators/$userId',
);
}
/// حذف اپراتور
Future<void> removeSupportOperator(int userId) async {
await _api.delete(
'/api/v1/admin/users/operators/$userId',
);
}
}
2.2 صفحه مدیریت دسترسیها
فایل: hesabixUI/hesabix_ui/lib/pages/admin/user_app_permissions_page.dart (جدید)
import 'package:flutter/material.dart';
import 'package:hesabix_ui/core/api_client.dart';
import 'package:hesabix_ui/services/admin_users_service.dart';
class UserAppPermissionsPage extends StatefulWidget {
final int userId;
final String userEmail;
const UserAppPermissionsPage({
super.key,
required this.userId,
required this.userEmail,
});
@override
State<UserAppPermissionsPage> createState() => _UserAppPermissionsPageState();
}
class _UserAppPermissionsPageState extends State<UserAppPermissionsPage> {
final _service = AdminUsersService(ApiClient());
bool _isLoading = true;
Map<String, bool> _permissions = {};
final Map<String, String> _permissionLabels = {
'support_operator': 'اپراتور پشتیبانی',
'system_settings': 'تنظیمات سیستم',
'user_management': 'مدیریت کاربران',
'business_management': 'مدیریت کسبوکارها',
};
final Map<String, String> _permissionDescriptions = {
'support_operator': 'دسترسی به پنل پشتیبانی و مدیریت تیکتها',
'system_settings': 'دسترسی به تنظیمات سیستم',
'user_management': 'مدیریت کاربران در سطح اپلیکیشن',
'business_management': 'مدیریت کسبوکارها',
};
@override
void initState() {
super.initState();
_loadPermissions();
}
Future<void> _loadPermissions() async {
setState(() => _isLoading = true);
try {
final data = await _service.getUserAppPermissions(widget.userId);
final appPerms = data['app_permissions'] as Map? ?? {};
// Initialize all permissions
final permissions = <String, bool>{};
for (var key in _permissionLabels.keys) {
permissions[key] = appPerms[key] == true;
}
setState(() {
_permissions = permissions;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در بارگذاری دسترسیها: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _savePermissions() async {
try {
await _service.updateUserAppPermissions(widget.userId, _permissions);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('دسترسیها با موفقیت ذخیره شد'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop(true); // Return true to indicate success
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در ذخیره دسترسیها: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('مدیریت دسترسیها'),
actions: [
TextButton.icon(
onPressed: _isLoading ? null : _savePermissions,
icon: const Icon(Icons.save, color: Colors.white),
label: const Text('ذخیره', style: TextStyle(color: Colors.white)),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// User Info Card
Card(
child: ListTile(
leading: CircleAvatar(
child: Text(widget.userEmail[0].toUpperCase()),
),
title: Text(widget.userEmail),
subtitle: const Text('مدیریت دسترسیهای سطح اپلیکیشن'),
),
),
const SizedBox(height: 24),
// Permissions List
const Text(
'دسترسیهای سطح اپلیکیشن',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
..._permissionLabels.entries.map((entry) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: SwitchListTile(
title: Text(entry.value),
subtitle: Text(_permissionDescriptions[entry.key] ?? ''),
value: _permissions[entry.key] ?? false,
onChanged: (value) {
setState(() {
_permissions[entry.key] = value;
});
},
secondary: Icon(
_getIconForPermission(entry.key),
color: Theme.of(context).primaryColor,
),
),
);
}),
],
),
),
);
}
IconData _getIconForPermission(String permission) {
switch (permission) {
case 'support_operator':
return Icons.support_agent;
case 'system_settings':
return Icons.settings;
case 'user_management':
return Icons.people;
case 'business_management':
return Icons.business;
default:
return Icons.check_circle;
}
}
}
2.3 صفحه لیست اپراتورها
فایل: hesabixUI/hesabix_ui/lib/pages/admin/support_operators_page.dart (جدید)
import 'package:flutter/material.dart';
import 'package:hesabix_ui/core/api_client.dart';
import 'package:hesabix_ui/services/admin_users_service.dart';
class SupportOperatorsPage extends StatefulWidget {
const SupportOperatorsPage({super.key});
@override
State<SupportOperatorsPage> createState() => _SupportOperatorsPageState();
}
class _SupportOperatorsPageState extends State<SupportOperatorsPage> {
final _service = AdminUsersService(ApiClient());
bool _isLoading = true;
List<Map<String, dynamic>> _operators = [];
@override
void initState() {
super.initState();
_loadOperators();
}
Future<void> _loadOperators() async {
setState(() => _isLoading = true);
try {
final operators = await _service.listSupportOperators();
setState(() {
_operators = operators;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در بارگذاری لیست اپراتورها: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _removeOperator(int userId, String email) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('حذف اپراتور'),
content: Text('آیا مطمئن هستید که میخواهید دسترسی اپراتور را از $email لغو کنید؟'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('حذف'),
),
],
),
);
if (confirm == true) {
try {
await _service.removeSupportOperator(userId);
await _loadOperators();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('دسترسی اپراتور با موفقیت لغو شد'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در حذف اپراتور: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('اپراتورهای پشتیبانی'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadOperators,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _operators.isEmpty
? const Center(
child: Text('هیچ اپراتور پشتیبانی یافت نشد'),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _operators.length,
itemBuilder: (context, index) {
final operator = _operators[index];
final fullName = operator['full_name'] as String?;
final email = operator['email'] as String;
final telegramId = operator['telegram_chat_id'] as String?;
final isActive = operator['is_active'] as bool? ?? false;
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: isActive
? Colors.green
: Colors.grey,
child: Icon(
Icons.support_agent,
color: Colors.white,
),
),
title: Text(fullName ?? email),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (fullName != null) Text(email),
if (telegramId != null)
Row(
children: [
const Icon(Icons.telegram, size: 16, color: Colors.blue),
const SizedBox(width: 4),
Text('متصل به تلگرام'),
],
),
if (!isActive)
const Text(
'غیرفعال',
style: TextStyle(color: Colors.red),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeOperator(
operator['id'] as int,
email,
),
),
),
);
},
),
);
}
}
2.4 افزودن به منوی تنظیمات
فایل: hesabixUI/hesabix_ui/lib/pages/system_settings/services/settings_categorization_service.dart
// در بخش items دسته 'users_businesses' (خط 154)، اضافه کنید:
SettingsItem(
id: 'support_operators',
title: 'settingsSupportOperators',
description: 'settingsSupportOperatorsDescription',
icon: Icons.support_agent_outlined,
color: const Color(0xFFE91E63),
route: '/user/profile/system-settings/support-operators',
categoryId: 'users_businesses',
order: 3,
),
2.5 افزودن Route ها
فایل: hesabixUI/hesabix_ui/lib/main.dart
// در بخش system-settings routes
GoRoute(
path: 'support-operators',
name: 'system_settings_support_operators',
builder: (context, state) {
return const SupportOperatorsPage();
},
),
📍 فاز 3: اضافه کردن به صفحه لیست کاربران (اولویت پایین)
در صفحه لیست کاربران (/api/v1/users) میتوانید یک دکمه "مدیریت دسترسیها" اضافه کنید که به صفحه UserAppPermissionsPage هدایت کند.
🔒 نکات امنیتی
- فقط SuperAdmin: تمام endpoint ها باید با
@require_app_permission("superadmin")محافظت شوند - جلوگیری از خودکاری: کاربر نباید بتواند permissions خودش را تغییر دهد
- Audit Log: تغییرات permissions باید در activity_logs ثبت شوند
- Validation: فقط permissions معتبر باید پذیرفته شوند
📊 اولویتبندی پیادهسازی
اولویت 1 (ضروری - همین حالا):
- ✅ Backend API endpoints برای مدیریت App Permissions
- ✅ تست کامل API ها
اولویت 2 (مهم - هفته آینده):
- ✅ UI صفحه مدیریت دسترسیها
- ✅ UI صفحه لیست اپراتورها
- ✅ اتصال به منوی تنظیمات
اولویت 3 (خوب است داشته باشیم - ماه آینده):
- ⬜ Audit logging برای تغییرات
- ⬜ یکپارچهسازی با صفحه لیست کاربران
- ⬜ فیلتر و جستجو در لیست اپراتورها
✅ Checklist پیادهسازی
Backend:
- ایجاد فایل
users_permissions.py - اضافه کردن Schemas (Pydantic models)
- پیادهسازی endpoint های CRUD
- ثبت router در
main.py - تست با Postman/curl
- نوشتن Unit Tests
Frontend:
- ایجاد
admin_users_service.dart - پیادهسازی
UserAppPermissionsPage - پیادهسازی
SupportOperatorsPage - افزودن به
settings_categorization_service.dart - افزودن routes
- افزودن translations
- تست در محیط توسعه
Documentation:
- بهروزرسانی
PERMISSIONS_SYSTEM.md - نوشتن مستندات API
- نوشتن راهنمای کاربر
🚀 نحوه استفاده بعد از پیادهسازی
برای SuperAdmin:
-
افزودن اپراتور:
- وارد System Settings > Support Operators شوید
- روی "افزودن اپراتور" کلیک کنید
- کاربر مورد نظر را انتخاب کنید
-
مدیریت دسترسیها:
- وارد System Settings > User Management شوید
- روی کاربر مورد نظر کلیک کنید
- "مدیریت دسترسیها" را انتخاب کنید
- دسترسیهای مورد نظر را فعال/غیرفعال کنید
- ذخیره کنید
-
حذف اپراتور:
- وارد System Settings > Support Operators شوید
- روی دکمه حذف کنار نام اپراتور کلیک کنید
- تأیید کنید
📞 پشتیبانی و سوالات
در صورت بروز مشکل یا سوال:
- مستندات API:
/docs(Swagger UI) - لاگهای سیستم:
journalctl -u hesabix-api -f - لاگهای اپلیکیشن: فایلهای log در
hesabixAPI/logs/
نکته مهم: این سناریو قابل توسعه است و میتوان permissions دیگری نیز به همین روش اضافه کرد.