hesabixArc/hesabixAPI/app/core/auth_dependency.py

556 lines
20 KiB
Python

from __future__ import annotations
from typing import Optional
from fastapi import Depends, Header, Request
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from adapters.db.repositories.api_key_repo import ApiKeyRepository
from adapters.db.models.user import User
from app.core.security import hash_api_key
from app.core.responses import ApiError
from app.core.i18n import negotiate_locale, Translator
from app.core.calendar import get_calendar_type_from_header, CalendarType
class AuthContext:
"""کلاس مرکزی برای نگهداری اطلاعات کاربر کنونی و تنظیمات"""
def __init__(
self,
user: User,
api_key_id: int,
language: str = "fa",
calendar_type: CalendarType = "jalali",
timezone: Optional[str] = None,
business_id: Optional[int] = None,
fiscal_year_id: Optional[int] = None,
db: Optional[Session] = None
) -> None:
self.user = user
self.api_key_id = api_key_id
self.language = language
self.calendar_type = calendar_type
self.timezone = timezone
self.business_id = business_id
self.fiscal_year_id = fiscal_year_id
self.db = db
# دسترسی‌های اپلیکیشن
self.app_permissions = user.app_permissions or {}
# دسترسی‌های کسب و کار (در صورت وجود business_id)
self.business_permissions = self._get_business_permissions() if business_id and db else {}
# ایجاد translator برای زبان تشخیص داده شده
self._translator = Translator(language)
@staticmethod
def _normalize_permissions_value(value) -> dict:
"""نرمال‌سازی مقدار JSON دسترسی‌ها به dict برای سازگاری با داده‌های legacy"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== _normalize_permissions_value START ===")
logger.info(f"Input value type: {type(value)}")
logger.info(f"Input value: {value}")
if isinstance(value, dict):
logger.info("Value is already a dict, returning as is")
logger.info(f"=== _normalize_permissions_value END ===")
return value
if isinstance(value, list):
logger.info("Value is a list, processing...")
try:
# لیست جفت‌ها مانند [["join", true], ["sales", {..}]]
if all(isinstance(item, list) and len(item) == 2 for item in value):
logger.info("Detected list of key-value pairs")
result = {k: v for k, v in value if isinstance(k, str)}
logger.info(f"Converted to dict: {result}")
logger.info(f"=== _normalize_permissions_value END ===")
return result
# لیست دیکشنری‌ها مانند [{"join": true}, {"sales": {...}}]
if all(isinstance(item, dict) for item in value):
logger.info("Detected list of dictionaries")
merged = {}
for item in value:
merged.update({k: v for k, v in item.items()})
logger.info(f"Merged to dict: {merged}")
logger.info(f"=== _normalize_permissions_value END ===")
return merged
except Exception as e:
logger.error(f"Error processing list: {e}")
logger.info(f"=== _normalize_permissions_value END ===")
return {}
logger.info(f"Unsupported value type {type(value)}, returning empty dict")
logger.info(f"=== _normalize_permissions_value END ===")
return {}
def get_translator(self) -> Translator:
"""دریافت translator برای ترجمه"""
return self._translator
def get_calendar_type(self) -> CalendarType:
"""دریافت نوع تقویم"""
return self.calendar_type
def get_user_id(self) -> int:
"""دریافت ID کاربر"""
return self.user.id
def get_user_email(self) -> Optional[str]:
"""دریافت ایمیل کاربر"""
return self.user.email
def get_user_mobile(self) -> Optional[str]:
"""دریافت شماره موبایل کاربر"""
return self.user.mobile
def get_user_name(self) -> str:
"""دریافت نام کامل کاربر"""
first_name = self.user.first_name or ""
last_name = self.user.last_name or ""
return f"{first_name} {last_name}".strip()
def get_referral_code(self) -> Optional[str]:
"""دریافت کد معرف کاربر"""
return getattr(self.user, "referral_code", None)
def is_user_active(self) -> bool:
"""بررسی فعال بودن کاربر"""
return self.user.is_active
def _get_business_permissions(self) -> dict:
"""دریافت دسترسی‌های کسب و کار از دیتابیس"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== _get_business_permissions START ===")
logger.info(f"User ID: {self.user.id}")
logger.info(f"Business ID: {self.business_id}")
logger.info(f"DB available: {self.db is not None}")
if not self.business_id or not self.db:
logger.info("No business_id or db, returning empty permissions")
return {}
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
repo = BusinessPermissionRepository(self.db)
permission_obj = repo.get_by_user_and_business(self.user.id, self.business_id)
logger.info(f"Permission object found: {permission_obj}")
if permission_obj and permission_obj.business_permissions:
raw_permissions = permission_obj.business_permissions
logger.info(f"Raw permissions: {raw_permissions}")
normalized_permissions = AuthContext._normalize_permissions_value(raw_permissions)
logger.info(f"Normalized permissions: {normalized_permissions}")
logger.info(f"=== _get_business_permissions END ===")
return normalized_permissions
logger.info("No permissions found, returning empty dict")
logger.info(f"=== _get_business_permissions END ===")
return {}
# بررسی دسترسی‌های اپلیکیشن
def has_app_permission(self, permission: str) -> bool:
"""بررسی دسترسی در سطح اپلیکیشن"""
# SuperAdmin تمام دسترسی‌های اپلیکیشن را دارد
if self.app_permissions.get("superadmin", False):
return True
return self.app_permissions.get(permission, False)
def is_superadmin(self) -> bool:
"""بررسی superadmin بودن"""
return self.has_app_permission("superadmin")
def can_manage_users(self) -> bool:
"""بررسی دسترسی مدیریت کاربران در سطح اپلیکیشن"""
return self.has_app_permission("user_management")
def can_manage_businesses(self) -> bool:
"""بررسی دسترسی مدیریت کسب و کارها"""
return self.has_app_permission("business_management")
def can_access_system_settings(self) -> bool:
"""بررسی دسترسی به تنظیمات سیستم"""
return self.has_app_permission("system_settings")
def can_access_support_operator(self) -> bool:
"""بررسی دسترسی به پنل اپراتور پشتیبانی"""
return self.has_app_permission("support_operator")
def is_business_owner(self, business_id: int = None) -> bool:
"""بررسی اینکه آیا کاربر مالک کسب و کار است یا نه"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== is_business_owner START ===")
logger.info(f"Requested business_id: {business_id}")
logger.info(f"Context business_id: {self.business_id}")
logger.info(f"User ID: {self.user.id}")
logger.info(f"DB available: {self.db is not None}")
target_business_id = business_id or self.business_id
logger.info(f"Target business_id: {target_business_id}")
if not target_business_id or not self.db:
logger.info(f"is_business_owner: no business_id ({target_business_id}) or db ({self.db is not None})")
logger.info(f"=== is_business_owner END (no business_id or db) ===")
return False
from adapters.db.models.business import Business
business = self.db.get(Business, target_business_id)
logger.info(f"Business lookup result: {business}")
if business:
logger.info(f"Business owner_id: {business.owner_id}")
is_owner = business.owner_id == self.user.id
logger.info(f"is_owner: {is_owner}")
else:
logger.info("Business not found")
is_owner = False
logger.info(f"=== is_business_owner END (result: {is_owner}) ===")
return is_owner
# بررسی دسترسی‌های کسب و کار
def has_business_permission(self, section: str, action: str) -> bool:
"""بررسی دسترسی در سطح کسب و کار"""
if not self.business_id:
return False
# SuperAdmin تمام دسترسی‌ها را دارد
if self.is_superadmin():
return True
# مالک کسب و کار تمام دسترسی‌ها را دارد
if self.is_business_owner():
return True
# بررسی دسترسی‌های عادی
if not self.business_permissions:
return False
# بررسی وجود بخش
if section not in self.business_permissions:
return False
section_perms = self.business_permissions[section]
# اگر بخش خالی است، فقط خواندن
if not section_perms:
return action == "read"
# بررسی دسترسی خاص
return section_perms.get(action, False)
def can_read_section(self, section: str) -> bool:
"""بررسی دسترسی خواندن بخش در کسب و کار"""
if not self.business_id:
return False
# SuperAdmin و مالک کسب و کار دسترسی کامل دارند
if self.is_superadmin() or self.is_business_owner():
return True
return section in self.business_permissions
def can_write_section(self, section: str) -> bool:
"""بررسی دسترسی نوشتن در بخش"""
return self.has_business_permission(section, "write")
def can_delete_section(self, section: str) -> bool:
"""بررسی دسترسی حذف در بخش"""
return self.has_business_permission(section, "delete")
def can_approve_section(self, section: str) -> bool:
"""بررسی دسترسی تأیید در بخش"""
return self.has_business_permission(section, "approve")
def can_export_section(self, section: str) -> bool:
"""بررسی دسترسی صادرات در بخش"""
return self.has_business_permission(section, "export")
def can_manage_business_users(self, business_id: int = None) -> bool:
"""بررسی دسترسی مدیریت کاربران کسب و کار"""
import logging
logger = logging.getLogger(__name__)
# SuperAdmin دسترسی کامل دارد
if self.is_superadmin():
logger.info(f"can_manage_business_users: user {self.user.id} is superadmin")
return True
# مالک کسب و کار دسترسی کامل دارد
if self.is_business_owner(business_id):
logger.info(f"can_manage_business_users: user {self.user.id} is business owner")
return True
# بررسی دسترسی در سطح کسب و کار
has_permission = self.has_business_permission("settings", "manage_users")
logger.info(f"can_manage_business_users: user {self.user.id} has permission: {has_permission}")
return has_permission
# ترکیب دسترسی‌ها
def has_any_permission(self, section: str, action: str) -> bool:
"""بررسی دسترسی در هر دو سطح"""
# SuperAdmin دسترسی کامل دارد
if self.is_superadmin():
return True
# بررسی دسترسی کسب و کار
return self.has_business_permission(section, action)
def can_access_business(self, business_id: int) -> bool:
"""بررسی دسترسی به کسب و کار خاص"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== can_access_business START ===")
logger.info(f"User ID: {self.user.id}")
logger.info(f"Requested business ID: {business_id}")
logger.info(f"User context business_id: {self.business_id}")
logger.info(f"User app permissions: {self.app_permissions}")
# SuperAdmin دسترسی به همه کسب و کارها دارد
if self.is_superadmin():
logger.info(f"User {self.user.id} is superadmin, granting access to business {business_id}")
logger.info(f"=== can_access_business END (superadmin) ===")
return True
# بررسی مالکیت کسب و کار
if self.db:
from adapters.db.models.business import Business
business = self.db.get(Business, business_id)
logger.info(f"Business lookup result: {business}")
if business:
logger.info(f"Business owner ID: {business.owner_id}")
if business.owner_id == self.user.id:
logger.info(f"User {self.user.id} is business owner of {business_id}, granting access")
logger.info(f"=== can_access_business END (owner) ===")
return True
else:
logger.info("No database connection available for business lookup")
# بررسی عضویت در کسب و کار
if self.db:
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
permission_repo = BusinessPermissionRepository(self.db)
business_permission = permission_repo.get_by_user_and_business(self.user.id, business_id)
logger.info(f"Business permission lookup result: {business_permission}")
if business_permission:
# بررسی دسترسی join
permissions = business_permission.business_permissions or {}
logger.info(f"User permissions for business {business_id}: {permissions}")
join_permission = permissions.get('join')
logger.info(f"Join permission: {join_permission}")
if join_permission == True:
logger.info(f"User {self.user.id} is member of business {business_id}, granting access")
logger.info(f"=== can_access_business END (member) ===")
return True
else:
logger.info(f"User {self.user.id} does not have join permission for business {business_id}")
else:
logger.info(f"No business permission found for user {self.user.id} and business {business_id}")
else:
logger.info("No database connection available for permission lookup")
# اگر کسب و کار در context کاربر است، دسترسی دارد
if business_id == self.business_id:
logger.info(f"User {self.user.id} has context access to business {business_id}")
logger.info(f"=== can_access_business END (context) ===")
return True
logger.info(f"User {self.user.id} does not have access to business {business_id}")
logger.info(f"=== can_access_business END (denied) ===")
return False
def is_business_member(self, business_id: int) -> bool:
"""بررسی اینکه آیا کاربر عضو کسب و کار است یا نه (دسترسی join)"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"Checking business membership: user {self.user.id}, business {business_id}")
# SuperAdmin عضو همه کسب و کارها محسوب می‌شود
if self.is_superadmin():
logger.info(f"User {self.user.id} is superadmin, is member of all businesses")
return True
# اگر مالک کسب و کار است، عضو محسوب می‌شود
if self.is_business_owner() and business_id == self.business_id:
logger.info(f"User {self.user.id} is business owner of {business_id}, is member")
return True
# بررسی دسترسی join در business_permissions
if not self.db:
logger.info(f"No database session available")
return False
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
repo = BusinessPermissionRepository(self.db)
permission_obj = repo.get_by_user_and_business(self.user.id, business_id)
if not permission_obj:
logger.info(f"No business permission found for user {self.user.id} and business {business_id}")
return False
# بررسی دسترسی join
business_perms = AuthContext._normalize_permissions_value(permission_obj.business_permissions)
has_join_access = business_perms.get('join', False)
logger.info(f"Business membership check: user {self.user.id} join access to business {business_id}: {has_join_access}")
return has_join_access
def to_dict(self) -> dict:
"""تبدیل به dictionary برای استفاده در API"""
return {
"user": {
"id": self.user.id,
"first_name": self.user.first_name,
"last_name": self.user.last_name,
"email": self.user.email,
"mobile": self.user.mobile,
"referral_code": getattr(self.user, "referral_code", None),
"is_active": self.user.is_active,
"app_permissions": self.app_permissions,
"created_at": self.user.created_at.isoformat() if self.user.created_at else None,
"updated_at": self.user.updated_at.isoformat() if self.user.updated_at else None,
},
"api_key_id": self.api_key_id,
"permissions": {
"app_permissions": self.app_permissions,
"business_permissions": self.business_permissions,
"is_superadmin": self.is_superadmin(),
"is_business_owner": self.is_business_owner(),
},
"settings": {
"language": self.language,
"calendar_type": self.calendar_type,
"timezone": self.timezone,
"business_id": self.business_id,
"fiscal_year_id": self.fiscal_year_id,
}
}
def get_current_user(
request: Request,
db: Session = Depends(get_db)
) -> AuthContext:
"""دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست"""
import logging
logger = logging.getLogger(__name__)
# Get authorization from request headers
auth_header = request.headers.get("Authorization")
logger.info(f"Auth header: {auth_header}")
if not auth_header or not auth_header.startswith("ApiKey "):
logger.warning(f"Invalid auth header: {auth_header}")
raise ApiError("UNAUTHORIZED", "Missing or invalid API key", http_status=401)
api_key = auth_header[len("ApiKey ") :].strip()
key_hash = hash_api_key(api_key)
repo = ApiKeyRepository(db)
obj = repo.get_by_hash(key_hash)
if not obj or obj.revoked_at is not None:
raise ApiError("UNAUTHORIZED", "Invalid API key", http_status=401)
from adapters.db.models.user import User
user = db.get(User, obj.user_id)
if not user or not user.is_active:
raise ApiError("UNAUTHORIZED", "Invalid API key", http_status=401)
# تشخیص زبان از هدر Accept-Language
language = _detect_language(request)
# تشخیص نوع تقویم از هدر X-Calendar-Type
calendar_type = _detect_calendar_type(request)
# تشخیص منطقه زمانی از هدر X-Timezone (اختیاری)
timezone = _detect_timezone(request)
# تشخیص کسب و کار از هدر X-Business-ID (آینده)
business_id = _detect_business_id(request)
# تشخیص سال مالی از هدر X-Fiscal-Year-ID (آینده)
fiscal_year_id = _detect_fiscal_year_id(request)
logger.info(f"Creating AuthContext for user {user.id}:")
logger.info(f" - Language: {language}")
logger.info(f" - Calendar type: {calendar_type}")
logger.info(f" - Timezone: {timezone}")
logger.info(f" - Business ID: {business_id}")
logger.info(f" - Fiscal year ID: {fiscal_year_id}")
logger.info(f" - App permissions: {user.app_permissions}")
auth_context = AuthContext(
user=user,
api_key_id=obj.id,
language=language,
calendar_type=calendar_type,
timezone=timezone,
business_id=business_id,
fiscal_year_id=fiscal_year_id,
db=db
)
logger.info(f"AuthContext created successfully")
return auth_context
def _detect_language(request: Request) -> str:
"""تشخیص زبان از هدر Accept-Language"""
accept_language = request.headers.get("Accept-Language")
return negotiate_locale(accept_language)
def _detect_calendar_type(request: Request) -> CalendarType:
"""تشخیص نوع تقویم از هدر X-Calendar-Type"""
calendar_header = request.headers.get("X-Calendar-Type")
return get_calendar_type_from_header(calendar_header)
def _detect_timezone(request: Request) -> Optional[str]:
"""تشخیص منطقه زمانی از هدر X-Timezone"""
return request.headers.get("X-Timezone")
def _detect_business_id(request: Request) -> Optional[int]:
"""تشخیص ID کسب و کار از هدر X-Business-ID (آینده)"""
import logging
logger = logging.getLogger(__name__)
business_id_str = request.headers.get("X-Business-ID")
logger.info(f"X-Business-ID header: {business_id_str}")
if business_id_str:
try:
business_id = int(business_id_str)
logger.info(f"Detected business ID: {business_id}")
return business_id
except ValueError:
logger.warning(f"Invalid business ID format: {business_id_str}")
pass
logger.info("No business ID detected from headers")
return None
def _detect_fiscal_year_id(request: Request) -> Optional[int]:
"""تشخیص ID سال مالی از هدر X-Fiscal-Year-ID (آینده)"""
fiscal_year_id_str = request.headers.get("X-Fiscal-Year-ID")
if fiscal_year_id_str:
try:
return int(fiscal_year_id_str)
except ValueError:
pass
return None