1581 lines
78 KiB
Python
Executable file
1581 lines
78 KiB
Python
Executable file
from fastapi import FastAPI, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import HTMLResponse
|
||
from app.openapi_local_docs import get_local_swagger_ui_html
|
||
import logging
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Optional, IO
|
||
|
||
from app.core.settings import get_settings
|
||
from app.core.logging import configure_logging
|
||
from adapters.db.session import get_db
|
||
from app.services.system_settings_service import get_app_name, get_app_version, is_maintenance_mode_enabled
|
||
from app.core.responses import ApiError
|
||
from adapters.api.v1.health import router as health_router
|
||
from adapters.api.v1.auth import router as auth_router
|
||
from adapters.api.v1.users import router as users_router
|
||
from adapters.api.v1.businesses import router as businesses_router
|
||
from adapters.api.v1.currencies import router as currencies_router
|
||
from adapters.api.v1.business_dashboard import router as business_dashboard_router
|
||
from adapters.api.v1.business_data_table_settings import router as business_data_table_settings_router
|
||
from adapters.api.v1.profile_dashboard import router as profile_dashboard_router
|
||
from adapters.api.v1.business_users import router as business_users_router
|
||
from adapters.api.v1.accounts import router as accounts_router
|
||
from adapters.api.v1.categories import router as categories_router
|
||
from adapters.api.v1.product_attributes import router as product_attributes_router
|
||
from adapters.api.v1.catalog_spec_fields import router as catalog_spec_fields_router
|
||
from adapters.api.v1.products import router as products_router
|
||
from adapters.api.v1.price_lists import router as price_lists_router
|
||
from adapters.api.v1.invoices import router as invoices_router
|
||
from adapters.api.v1.persons import router as persons_router
|
||
from adapters.api.v1.person_group_routes import router as person_group_routes_router
|
||
from adapters.api.v1.customers import router as customers_router
|
||
from adapters.api.v1.bank_accounts import router as bank_accounts_router
|
||
from adapters.api.v1.cash_registers import router as cash_registers_router
|
||
from adapters.api.v1.petty_cash import router as petty_cash_router
|
||
from adapters.api.v1.received_loan_facilities import router as received_loan_facilities_router
|
||
from adapters.api.v1.tax_units import router as tax_units_router
|
||
from adapters.api.v1.tax_types import router as tax_types_router
|
||
from adapters.api.v1.tax_product_codes import (
|
||
router as tax_product_codes_router,
|
||
admin_router as admin_tax_product_codes_router,
|
||
)
|
||
from adapters.api.v1.tax_settings import router as tax_settings_router
|
||
from adapters.api.v1.tax_reports import router as tax_reports_router
|
||
from adapters.api.v1.support.tickets import router as support_tickets_router
|
||
from adapters.api.v1.support.operator import router as support_operator_router
|
||
from adapters.api.v1.support.attachments_user import router as support_attachments_user_router
|
||
from adapters.api.v1.support.attachments_operator import router as support_attachments_operator_router
|
||
from adapters.api.v1.support.categories import router as support_categories_router
|
||
from adapters.api.v1.support.priorities import router as support_priorities_router
|
||
from adapters.api.v1.support.statuses import router as support_statuses_router
|
||
from adapters.api.v1.admin.file_storage import router as admin_file_storage_router
|
||
from adapters.api.v1.admin.email_config import router as admin_email_config_router
|
||
from adapters.api.v1.admin.system_settings import router as admin_system_settings_router
|
||
from adapters.api.v1.admin.legacy_import import router as admin_legacy_import_router
|
||
from adapters.api.v1.admin.firewall import router as admin_firewall_router
|
||
from adapters.api.v1.admin.currencies import router as admin_currencies_router
|
||
from adapters.api.v1.admin.fx_providers import router as admin_fx_providers_router
|
||
from adapters.api.v1.admin.monitoring import router as admin_monitoring_router
|
||
from adapters.api.v1.admin.system_services import router as admin_system_services_router
|
||
from adapters.api.v1.admin.wallet_admin import router as admin_wallet_router
|
||
from adapters.api.v1.admin.storage_plans import router as admin_storage_plans_router
|
||
from adapters.api.v1.admin.support_billing import router as admin_support_billing_router
|
||
from adapters.api.v1.admin.businesses_admin import router as admin_businesses_router
|
||
from adapters.api.v1.admin.document_monetization import router as admin_document_monetization_router
|
||
from adapters.api.v1.admin.zohal import router as admin_zohal_router
|
||
from adapters.api.v1.admin.marketplace import router as admin_marketplace_router
|
||
from adapters.api.v1.admin.users_permissions import router as admin_users_permissions_router
|
||
from adapters.api.v1.admin.scripts import router as admin_scripts_router
|
||
from adapters.api.v1.announcements import router as announcements_router
|
||
from adapters.api.v1.admin.announcements import router as admin_announcements_router
|
||
from adapters.api.v1.receipts_payments import router as receipts_payments_router
|
||
from adapters.api.v1.transfers import router as transfers_router
|
||
from adapters.api.v1.fiscal_years import router as fiscal_years_router
|
||
from adapters.api.v1.expense_income import router as expense_income_router
|
||
from adapters.api.v1.goods_expense_income import router as goods_expense_income_router
|
||
from adapters.api.v1.hscript_reports import router as hscript_reports_router
|
||
from adapters.api.v1.documents import router as documents_router
|
||
from adapters.api.v1.kardex import router as kardex_router
|
||
from adapters.api.v1.opening_balance import router as opening_balance_router
|
||
from adapters.api.v1.business_currency_rates import router as business_currency_rates_router
|
||
from adapters.api.v1.business_fx_global_rates import router as business_fx_global_rates_router
|
||
from adapters.api.v1.business_fx_auto_sync import router as business_fx_auto_sync_router
|
||
from adapters.api.v1.period_end_fx_revaluation import router as period_end_fx_revaluation_router
|
||
from adapters.api.v1.report_templates import router as report_templates_router
|
||
from adapters.api.v1.wallet import router as wallet_router
|
||
from adapters.api.v1.zohal import router as zohal_router
|
||
from adapters.api.v1.wallet_webhook import router as wallet_webhook_router
|
||
from adapters.api.v1.credit import router as credit_router
|
||
from adapters.api.v1.business_frequent_descriptions import router as business_frequent_descriptions_router
|
||
from adapters.api.v1.document_numbering import router as document_numbering_router
|
||
from adapters.api.v1.document_code_reservations import router as document_code_reservations_router
|
||
from adapters.api.v1.marketplace import router as marketplace_router
|
||
from adapters.api.v1.warranty import router as warranty_router
|
||
from adapters.api.v1.customer_club import router as customer_club_router
|
||
from adapters.api.v1.payroll import router as payroll_router
|
||
from adapters.api.v1.barcode_labels import router as barcode_labels_router
|
||
from adapters.api.v1.repair_shop import router as repair_shop_router
|
||
from adapters.api.v1.business_notifications import router as business_notifications_router
|
||
from adapters.api.v1.ping_pong import router as ping_pong_router
|
||
from adapters.api.v1.integrations.telegram import router as telegram_integration_router
|
||
from adapters.api.v1.integrations.bale import router as bale_integration_router
|
||
from adapters.api.v1.basalam_integration import router as basalam_integration_router
|
||
from adapters.api.v1.telephony import router as telephony_router
|
||
from adapters.api.v1.telephony_softphone_ws import router as telephony_softphone_ws_router
|
||
from adapters.api.v1.woocommerce_integration import router as woocommerce_integration_router
|
||
from adapters.api.v1.notifications import router as notifications_router
|
||
from adapters.api.v1.admin.notification_templates import router as admin_notification_templates_router
|
||
from adapters.api.v1.admin.notification_event_types import router as admin_notification_event_types_router
|
||
from adapters.api.v1.admin.notification_moderation import router as admin_notification_moderation_router
|
||
from adapters.api.v1.notifications_ws import router as notifications_ws_router
|
||
from adapters.api.v1.ai.voice_ws import router as ai_voice_ws_router
|
||
from adapters.api.v1.public_share_links import router as public_share_links_router
|
||
from adapters.api.v1.public_storage_file_shares import router as public_storage_file_shares_router
|
||
from adapters.api.v1.public_product_catalog import router as public_product_catalog_router
|
||
from adapters.api.v1.business_backups import router as business_backups_router
|
||
from adapters.api.v1.business_ftp_backup import router as business_ftp_backup_router
|
||
from adapters.api.v1.business.document_monetization import router as business_document_monetization_router
|
||
from adapters.api.v1.jobs import router as jobs_router
|
||
from adapters.api.v1.activity_logs import router as activity_logs_router
|
||
from adapters.api.v1.admin.activity_logs_admin import router as admin_activity_logs_router
|
||
from adapters.api.v1.admin.hscript_admin import router as admin_hscript_router
|
||
from app.services.notification_processor import background_loop as notifications_background_loop
|
||
from app.services.storage_background_jobs import storage_cleanup_loop, storage_subscription_check_loop
|
||
from app.services.document_monetization_background_jobs import document_monetization_finalize_periods_loop
|
||
from app.services.document_monetization_jobs import document_monetization_loop
|
||
from app.services.monitoring_background_jobs import (
|
||
monitoring_metrics_collection_loop,
|
||
monitoring_service_status_check_loop,
|
||
)
|
||
from app.services.business_background_jobs import check_expired_deleted_businesses_loop
|
||
from app.core.i18n import negotiate_locale, Translator
|
||
from app.core.error_handlers import register_error_handlers
|
||
from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig
|
||
from app.core.calendar_middleware import add_calendar_type
|
||
|
||
# Import activity log hooks برای ثبت event handlers
|
||
import adapters.db.activity_log_hooks # noqa: F401
|
||
|
||
|
||
_BACKGROUND_JOBS_LOCK_FH: Optional[IO[str]] = None
|
||
|
||
|
||
def _try_acquire_background_jobs_lock() -> bool:
|
||
"""
|
||
جلوگیری از اجرای چندباره background jobs در حالت multi-worker.
|
||
با uvicorn --workers هر worker جداگانه startup event را اجرا میکند؛
|
||
این lock باعث میشود فقط یکی از process ها leader شود و jobها را اجرا کند.
|
||
"""
|
||
global _BACKGROUND_JOBS_LOCK_FH
|
||
|
||
# امکان خاموش کردن کامل background jobs از طریق env
|
||
enabled = os.getenv("HESABIX_BACKGROUND_JOBS_ENABLED", "true").strip().lower()
|
||
if enabled in {"0", "false", "no", "off"}:
|
||
return False
|
||
|
||
lock_path = os.getenv("HESABIX_BACKGROUND_JOBS_LOCKFILE", "/tmp/hesabix-background-jobs.lock")
|
||
|
||
try:
|
||
import fcntl # Linux-only
|
||
except Exception:
|
||
# در محیطهایی که fcntl ندارند (مثلاً Windows) همان رفتار قبلی را نگه میداریم
|
||
return True
|
||
|
||
try:
|
||
fh = open(lock_path, "a+", encoding="utf-8")
|
||
fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||
fh.seek(0)
|
||
fh.truncate()
|
||
fh.write(f"pid={os.getpid()}\n")
|
||
fh.flush()
|
||
_BACKGROUND_JOBS_LOCK_FH = fh # نگه داشتن handle برای حفظ lock
|
||
return True
|
||
except Exception:
|
||
try:
|
||
if _BACKGROUND_JOBS_LOCK_FH:
|
||
_BACKGROUND_JOBS_LOCK_FH.close()
|
||
except Exception:
|
||
pass
|
||
_BACKGROUND_JOBS_LOCK_FH = None
|
||
return False
|
||
|
||
|
||
def _openapi_query_filter_section() -> str:
|
||
from app.openapi_query_filter_docs import OPENAPI_QUERY_FILTER_SECTION
|
||
|
||
return OPENAPI_QUERY_FILTER_SECTION
|
||
|
||
|
||
def create_app() -> FastAPI:
|
||
settings = get_settings()
|
||
configure_logging(settings)
|
||
|
||
from app.core.production_security import validate_production_security
|
||
from html import escape
|
||
|
||
validate_production_security(settings)
|
||
is_production = (settings.environment or "").strip().lower() in {"production", "prod"}
|
||
|
||
# خواندن تنظیمات از DB در صورت امکان، در غیر این صورت از env
|
||
app_name = settings.app_name
|
||
app_version = settings.app_version
|
||
try:
|
||
# تلاش برای خواندن از DB (در startup event بهروزرسانی میشود)
|
||
# استفاده از context manager برای اطمینان از بسته شدن session
|
||
from adapters.db.session import get_db_session
|
||
with get_db_session() as db:
|
||
app_name = get_app_name(db)
|
||
app_version = get_app_version(db)
|
||
except Exception:
|
||
# در صورت خطا از env استفاده میشود
|
||
pass
|
||
|
||
safe_app_name = escape(app_name)
|
||
|
||
# تعریف tags برای دستهبندی بهتر endpoint ها در Swagger
|
||
tags_metadata = [
|
||
{
|
||
"name": "احراز هویت",
|
||
"description": """
|
||
عملیات مربوط به ثبتنام، ورود، خروج و مدیریت کلیدهای API
|
||
|
||
### امکانات:
|
||
- ثبتنام کاربر جدید با تایید ایمیل
|
||
- ورود با ایمیل/موبایل و رمز عبور
|
||
- مدیریت کلیدهای API شخصی و session
|
||
- فراموشی و بازیابی رمز عبور
|
||
- تغییر رمز عبور و اطلاعات کاربری
|
||
- سیستم کپچا برای امنیت
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای کامل احراز هویت",
|
||
"url": "https://docs.hesabix.ir/authentication"
|
||
}
|
||
},
|
||
{
|
||
"name": "کاربران",
|
||
"description": "مدیریت کاربران، پروفایلها و دسترسیها",
|
||
"externalDocs": {
|
||
"description": "مستندات مدیریت کاربران",
|
||
"url": "https://docs.hesabix.ir/users"
|
||
}
|
||
},
|
||
{
|
||
"name": "کسبوکارها",
|
||
"description": """
|
||
مدیریت کسبوکارها، تنظیمات و داشبورد
|
||
|
||
### قابلیتها:
|
||
- ایجاد و مدیریت چندین کسبوکار
|
||
- تنظیمات شخصیسازی شده
|
||
- داشبورد آماری و تحلیلی
|
||
- مدیریت کاربران و نقشها
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای کسبوکارها",
|
||
"url": "https://docs.hesabix.ir/businesses"
|
||
}
|
||
},
|
||
{
|
||
"name": "محصولات و کالاها",
|
||
"description": """
|
||
مدیریت محصولات، خدمات، دستهبندیها و ویژگیها
|
||
|
||
### امکانات:
|
||
- ثبت کالا و خدمات
|
||
- دستهبندی و ویژگیهای محصول
|
||
- لیست قیمتگذاری
|
||
- موجودی و کنترل انبار
|
||
- بارکد و QR Code
|
||
""",
|
||
"externalDocs": {
|
||
"description": "مستندات محصولات",
|
||
"url": "https://docs.hesabix.ir/products"
|
||
}
|
||
},
|
||
{
|
||
"name": "انبارداری",
|
||
"description": "مدیریت انبارها، موجودی، حوالهها و کاردکس",
|
||
"externalDocs": {
|
||
"description": "راهنمای انبارداری",
|
||
"url": "https://docs.hesabix.ir/warehouse"
|
||
}
|
||
},
|
||
{
|
||
"name": "اسناد فروش",
|
||
"description": """
|
||
فاکتورهای فروش، پیشفاکتور و اسناد مرتبط
|
||
|
||
### انواع اسناد:
|
||
- فاکتور فروش
|
||
- پیشفاکتور
|
||
- برگشت از فروش
|
||
- فروش سریع
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای فروش",
|
||
"url": "https://docs.hesabix.ir/sales"
|
||
}
|
||
},
|
||
{
|
||
"name": "اسناد خرید",
|
||
"description": "فاکتورهای خرید، سفارش خرید و اسناد مرتبط",
|
||
},
|
||
{
|
||
"name": "اسناد انتقال",
|
||
"description": """
|
||
اسناد انتقال وجه بین حسابهای بانکی، صندوق و تنخواه
|
||
|
||
### کاربردها:
|
||
- انتقال بین حسابهای بانکی
|
||
- انتقال به/از صندوق
|
||
- انتقال به/از تنخواه
|
||
- ثبت کارمزد انتقال
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای اسناد انتقال",
|
||
"url": "https://docs.hesabix.ir/transfers"
|
||
}
|
||
},
|
||
{
|
||
"name": "دریافت و پرداخت",
|
||
"description": """
|
||
اسناد دریافت و پرداخت نقدی، چک و سایر روشها
|
||
|
||
### روشهای پرداخت:
|
||
- نقدی
|
||
- چک
|
||
- کارت بانکی
|
||
- انتقال آنلاین
|
||
""",
|
||
},
|
||
{
|
||
"name": "مدیریت مالی",
|
||
"description": "حسابهای بانکی، صندوق، تنخواه، چک و سایر ابزارهای مالی",
|
||
},
|
||
{
|
||
"name": "اشخاص و مشتریان",
|
||
"description": "مدیریت اشخاص، مشتریان، تامینکنندگان و طرفحسابها",
|
||
"externalDocs": {
|
||
"description": "راهنمای مدیریت اشخاص",
|
||
"url": "https://docs.hesabix.ir/persons"
|
||
}
|
||
},
|
||
{
|
||
"name": "حسابداری",
|
||
"description": """
|
||
دفتر کل، اسناد حسابداری، حسابها و طبقات
|
||
|
||
### قابلیتها:
|
||
- دفتر کل
|
||
- اسناد حسابداری
|
||
- طرح حسابها
|
||
- تراز و میزان
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای حسابداری",
|
||
"url": "https://docs.hesabix.ir/accounting"
|
||
}
|
||
},
|
||
{
|
||
"name": "گزارشها",
|
||
"description": """
|
||
گزارشهای مالی، انبارداری، فروش و تحلیلی
|
||
|
||
### انواع گزارش:
|
||
- گزارشهای مالی
|
||
- گزارش فروش و خرید
|
||
- گزارش موجودی و کاردکس
|
||
- گزارشهای تحلیلی
|
||
- خروجی Excel و PDF
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای گزارشها",
|
||
"url": "https://docs.hesabix.ir/reports"
|
||
}
|
||
},
|
||
{
|
||
"name": "مالیات",
|
||
"description": """
|
||
تنظیمات مالیاتی، نرخها، کدها و یکپارچهسازی با سامانه مودیان
|
||
|
||
### امکانات:
|
||
- تنظیمات مالیات بر ارزش افزوده
|
||
- یکپارچهسازی با سامانه مودیان
|
||
- کدهای مالیاتی محصولات
|
||
- گزارشهای مالیاتی
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای مالیات و مودیان",
|
||
"url": "https://docs.hesabix.ir/tax"
|
||
}
|
||
},
|
||
{
|
||
"name": "سال مالی",
|
||
"description": "مدیریت سالهای مالی و دورههای حسابداری",
|
||
},
|
||
{
|
||
"name": "کیف پول",
|
||
"description": "مدیریت کیف پول، شارژ، برداشت و تراکنشها",
|
||
},
|
||
{
|
||
"name": "اعتبار",
|
||
"description": "مدیریت اعتبار و بستههای خریداری شده",
|
||
},
|
||
{
|
||
"name": "قالبهای گزارش",
|
||
"description": """
|
||
مدیریت قالبهای سفارشی برای گزارشها و چاپ
|
||
|
||
### امکانات:
|
||
- طراحی قالب سفارشی
|
||
- قالب فاکتور
|
||
- قالب گزارشها
|
||
- لوگو و مهر
|
||
""",
|
||
},
|
||
{
|
||
"name": "پشتیبانی",
|
||
"description": "سیستم تیکتها، درخواستها و ارتباط با پشتیبانی",
|
||
},
|
||
{
|
||
"name": "اطلاعرسانی",
|
||
"description": "مدیریت نوتیفیکیشنها، اعلانها و پیامها",
|
||
},
|
||
{
|
||
"name": "پشتیبانگیری",
|
||
"description": """
|
||
ایجاد، بازیابی و مدیریت نسخههای پشتیبان
|
||
|
||
### امکانات:
|
||
- پشتیبانگیری اتوماتیک
|
||
- پشتیبانگیری دستی
|
||
- بازیابی دادهها
|
||
- مدیریت فضای ذخیرهسازی
|
||
""",
|
||
},
|
||
{
|
||
"name": "فایل و ذخیرهسازی",
|
||
"description": "مدیریت فایلها، آپلود، دانلود و فضای ذخیرهسازی",
|
||
},
|
||
{
|
||
"name": "یکپارچهسازی",
|
||
"description": """
|
||
اتصال به سرویسهای خارجی (تلگرام، زوهال، مارکتپلیس)
|
||
|
||
### سرویسها:
|
||
- یکپارچهسازی با تلگرام
|
||
- اتصال به سامانه زوهال
|
||
- اتصال به مارکتپلیسها
|
||
""",
|
||
},
|
||
{
|
||
"name": "مدیریت سیستم",
|
||
"description": """
|
||
تنظیمات سیستم، مانیتورینگ، لاگها و مدیریت کلی (فقط ادمین)
|
||
|
||
⚠️ این بخش فقط برای مدیران سیستم قابل دسترسی است
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای مدیریت سیستم",
|
||
"url": "https://docs.hesabix.ir/admin"
|
||
}
|
||
},
|
||
{
|
||
"name": "admin-system-services",
|
||
"description": "لاگ/وضعیت systemd؛ استقرار Docker و مجوزها: docs/SERVICE_LOGS_ADMIN_API.md",
|
||
},
|
||
{
|
||
"name": "هوش مصنوعی",
|
||
"description": """
|
||
چت با هوش مصنوعی، تحلیل دادهها و پیشنهادات هوشمند
|
||
|
||
### امکانات:
|
||
- چت با دستیار هوشمند
|
||
- تحلیل دادههای مالی
|
||
- پیشنهادات بهینهسازی
|
||
- گزارشهای هوشمند
|
||
""",
|
||
"externalDocs": {
|
||
"description": "راهنمای هوش مصنوعی",
|
||
"url": "https://docs.hesabix.ir/ai"
|
||
}
|
||
},
|
||
]
|
||
|
||
application = FastAPI(
|
||
title=app_name,
|
||
version=app_version,
|
||
debug=settings.debug,
|
||
openapi_tags=tags_metadata,
|
||
docs_url=None, # غیرفعال کردن docs پیشفرض برای سفارسیسازی
|
||
redoc_url="/redoc",
|
||
swagger_ui_parameters={
|
||
"defaultModelsExpandDepth": -1, # بسته بودن Models به صورت پیشفرض
|
||
"docExpansion": "list", # نمایش لیستی endpoints
|
||
"filter": True, # فعالسازی جستجو
|
||
"persistAuthorization": not is_production, # ذخیره توکن فقط در محیط توسعه
|
||
"displayRequestDuration": True, # نمایش زمان پاسخ
|
||
"tryItOutEnabled": True, # فعال بودن Try it out
|
||
"syntaxHighlight.theme": "monokai", # تم Syntax Highlighting
|
||
"deepLinking": True, # Deep linking برای مستقیم رفتن به endpoint
|
||
"displayOperationId": False, # عدم نمایش Operation ID
|
||
},
|
||
description="""
|
||
# Hesabix API
|
||
|
||
REST API برای اپ وب، اپ موبایل و یکپارچهسازی با سایر سرویسها: احراز هویت، کسبوکار، اسناد مالی، انبار، اشخاص، گزارش و اعلانها.
|
||
|
||
---
|
||
|
||
## دامنه و نسخه
|
||
|
||
- مسیر پایه نسخه فعلی: `/api/v1/...`
|
||
- در این محیط، آدرس سرور را از بالای صفحه (Servers) یا از آدرس بار مرورگر بردارید؛ مثالهای `curl` فقط الگو هستند.
|
||
"""
|
||
+ _openapi_query_filter_section()
|
||
+ """
|
||
---
|
||
|
||
## احراز هویت (مهم)
|
||
|
||
سرور هدر `Authorization` را **فقط** با پیشوند **`ApiKey`** میپذیرد. فرمت **`Bearer`** با پیادهسازی فعلی کار نمیکند.
|
||
|
||
```
|
||
Authorization: ApiKey <کلید_کامل_برگشتی_از_API>
|
||
```
|
||
|
||
**انواع کلید (مطابق کد سرویس):**
|
||
|
||
| نوع | معمولاً از کجا | پیشوند نمونه در کلید |
|
||
|-----|------------------|----------------------|
|
||
| Session (ورود / ثبتنام) | پاسخ `POST /api/v1/auth/login` یا `POST /api/v1/auth/register` — فیلد `data.api_key` | `ak_live_` |
|
||
| شخصی | `POST /api/v1/auth/api-keys` (نیاز به ورود) | `hsx_` |
|
||
|
||
**Swagger UI — دکمه Authorize:** در فیلد مربوط به `Authorization`، **یک رشتهی کامل** وارد کنید: کلمهی `ApiKey`، یک فاصله، و سپس خود کلید (مثال: `ApiKey ak_live_xxxxxxxx`).
|
||
|
||
**نمونه `curl`:**
|
||
|
||
```bash
|
||
curl -s -X GET "<BASE_URL>/api/v1/auth/me" \\
|
||
-H "Authorization: ApiKey ak_live_REPLACE_ME" \\
|
||
-H "Accept: application/json"
|
||
```
|
||
|
||
---
|
||
|
||
## هدرهای پرکاربرد
|
||
|
||
| هدر | الزامی | توضیح کوتاه |
|
||
|-----|--------|-------------|
|
||
| `Authorization` | برای مسیرهای محافظتشده | `ApiKey <کلید>` |
|
||
| `Accept-Language` | خیر | `fa` یا `en` (زبان پیامها و در صورت امکان برچسبها) |
|
||
| `X-Calendar-Type` | خیر | `jalali` (پیشفرض) یا `gregorian` — قالب تاریخ در پاسخ |
|
||
| `X-Timezone` | خیر | در صورت پشتیبانی، منطقهی زمانی اختیاری |
|
||
| `X-Business-ID` | بسته به سناریو | برای زمینهی کسبوکار؛ دسترسی واقعی با عضویت/مجوز در بکاند کنترل میشود |
|
||
|
||
---
|
||
|
||
## مجوزها
|
||
|
||
علاوه بر داشتن کلید معتبر، برخی مسیرها نیاز به **مجوز اپلیکیشن** یا **عضویت در کسبوکار** دارند (مثلاً مدیریت کاربران سطح سیستم).
|
||
|
||
نمونهی فیلد `app_permissions` در مدل کاربر (مفهومی):
|
||
|
||
```json
|
||
{
|
||
"user_management": true,
|
||
"superadmin": false,
|
||
"business_management": true,
|
||
"system_settings": false
|
||
}
|
||
```
|
||
|
||
مسیرهایی مانند `/api/v1/users` معمولاً به مجوزهای مدیریتی نیاز دارند؛ جزئیات هر endpoint در همین سند OpenAPI آمده است.
|
||
|
||
---
|
||
|
||
## شکل پاسخهای موفق
|
||
|
||
بسیاری از پاسخهای موفق شبیه ساختار زیر هستند (`message` اختیاری است):
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": { },
|
||
"message": "پیام اختیاری",
|
||
"calendar_type": "jalali"
|
||
}
|
||
```
|
||
|
||
خطاها معمولاً با کدهای HTTP استاندارد و بدنهی توضیحدار برمیگردند؛ برای جزئیات، همان operation را در لیست زیر باز کنید.
|
||
|
||
---
|
||
|
||
## کدهای وضعیت HTTP (خلاصه)
|
||
|
||
| کد | معنی رایج |
|
||
|----|-----------|
|
||
| 200 | موفق |
|
||
| 400 | درخواست نامعتبر |
|
||
| 401 | کلید نامعتبر یا نبودن احراز هویت |
|
||
| 403 | ممنوع (مجوز یا محدودیت) |
|
||
| 404 | یافت نشد |
|
||
| 422 | اعتبارسنجی بدنه/پارامتر (FastAPI/Pydantic) |
|
||
| 429 | محدودیت نرخ درخواست |
|
||
| 500 | خطای داخلی سرور |
|
||
|
||
---
|
||
|
||
## امنیت
|
||
|
||
- **کپچا:** `POST /api/v1/auth/captcha` — در ورود، ثبتنام و برخی عملیات حساس استفاده میشود.
|
||
- **رمز عبور:** با الگوریتمهای هش امن (مانند Argon2، با پشتیبانی از رکوردهای قدیمی bcrypt).
|
||
- **کلید API:** فقط نسخهی هش در پایگاه داده نگه داشته میشود.
|
||
|
||
---
|
||
|
||
## جریان نمونه: ورود و فراخوانی محافظتشده
|
||
|
||
```bash
|
||
# 1) کپچا
|
||
curl -s -X POST "<BASE_URL>/api/v1/auth/captcha"
|
||
|
||
# 2) ورود — api_key را از data در پاسخ JSON بردارید
|
||
curl -s -X POST "<BASE_URL>/api/v1/auth/login" \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "Accept-Language: fa" \\
|
||
-H "X-Calendar-Type: jalali" \\
|
||
-d '{"identifier":"you@example.com","password":"***","captcha_id":"...","captcha_code":"..."}'
|
||
|
||
# 3) فراخوانی با همان کلید (پیشوند ApiKey اجباری است)
|
||
curl -s -X GET "<BASE_URL>/api/v1/auth/me" \\
|
||
-H "Authorization: ApiKey ak_live_xxxx" \\
|
||
-H "Accept-Language: fa" \\
|
||
-H "X-Calendar-Type: jalali"
|
||
```
|
||
|
||
---
|
||
|
||
## شروع سریع
|
||
|
||
1. `POST /api/v1/auth/register` — ثبتنام (در صورت فعال بودن کپچا، مرحلهی کپچا را رعایت کنید)
|
||
2. `POST /api/v1/auth/login` — ورود و دریافت `data.api_key`
|
||
3. `GET /api/v1/auth/me` — تأیید کلید با هدر `Authorization: ApiKey ...`
|
||
4. `GET /api/v1/users` — فقط با مجوزهای لازم (مثلاً `user_management`)
|
||
|
||
---
|
||
|
||
## راهنما و تماس
|
||
|
||
- **ایمیل:** support@hesabix.ir
|
||
- **Swagger UI:** همین صفحه (`/docs`)
|
||
- **ReDoc:** `/redoc`
|
||
- **اسکیمای باز:** `/openapi.json`
|
||
""",
|
||
contact={
|
||
"name": "Hesabix Team",
|
||
"email": "support@hesabix.ir",
|
||
"url": "https://hesabix.ir",
|
||
},
|
||
license_info={
|
||
"name": "GNU GPLv3 License",
|
||
"url": "https://opensource.org/licenses/GPL-3.0",
|
||
},
|
||
servers=[
|
||
{
|
||
"url": "http://localhost:8000",
|
||
"description": "Development server"
|
||
},
|
||
{
|
||
"url": "https://agent.hesabix.ir",
|
||
"description": "Production server"
|
||
}
|
||
],
|
||
)
|
||
|
||
# Swagger UI vendor 4.x فیلد openapi را فقط برای 3.0.x / 2.0 قبول میکند؛ پیشفرض FastAPI 3.1.0 است.
|
||
application.openapi_version = "3.0.3"
|
||
|
||
# Mount استاتیک؛ مسیر مطلق تا با هر WorkingDirectory سرویس systemd درست باشد.
|
||
_api_root = Path(__file__).resolve().parent.parent
|
||
_assets_dir = _api_root / "assets"
|
||
_log = logging.getLogger(__name__)
|
||
try:
|
||
if not _assets_dir.is_dir():
|
||
_log.warning("دایرکتوری assets برای مستندات یافت نشد: %s", _assets_dir)
|
||
else:
|
||
application.mount("/assets", StaticFiles(directory=str(_assets_dir)), name="assets")
|
||
except Exception as e:
|
||
_log.warning("mount کردن /assets ناموفق بود: %s", e)
|
||
|
||
# Swagger UI سفارشی با استایلهای فارسی و RTL
|
||
@application.get("/docs", include_in_schema=False)
|
||
async def custom_swagger_ui_html():
|
||
"""صفحه سفارشی Swagger UI با پشتیبانی کامل از فارسی و RTL"""
|
||
return get_local_swagger_ui_html(
|
||
openapi_url=application.openapi_url,
|
||
title=f"{safe_app_name} - مستندات API",
|
||
oauth2_redirect_url=application.swagger_ui_oauth2_redirect_url,
|
||
init_oauth=application.swagger_ui_init_oauth,
|
||
swagger_ui_parameters=application.swagger_ui_parameters,
|
||
swagger_favicon_url="/assets/logo-blue.png",
|
||
)
|
||
|
||
# اضافه کردن CSS های سفارشی به صورت دستی
|
||
@application.get("/docs-custom", include_in_schema=False, response_class=HTMLResponse)
|
||
async def swagger_ui_custom():
|
||
"""صفحه Swagger UI با استایلهای سفارشی حسابیکس"""
|
||
return f"""
|
||
<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{safe_app_name} - مستندات API</title>
|
||
<link rel="icon" type="image/png" href="/assets/logo-blue.png">
|
||
<link rel="stylesheet" type="text/css" href="/assets/swagger/vendor/swagger-ui.css">
|
||
<link rel="stylesheet" type="text/css" href="/assets/swagger/custom.css">
|
||
<link rel="stylesheet" type="text/css" href="/assets/swagger/swagger-rtl.css">
|
||
<link rel="stylesheet" type="text/css" href="/assets/swagger/dark-mode.css">
|
||
<style>
|
||
html {{
|
||
box-sizing: border-box;
|
||
overflow: -moz-scrollbars-vertical;
|
||
overflow-y: scroll;
|
||
}}
|
||
*, *:before, *:after {{
|
||
box-sizing: inherit;
|
||
}}
|
||
body {{
|
||
margin:0;
|
||
padding:0;
|
||
background: #fafafa;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="swagger-ui"></div>
|
||
|
||
<!-- دکمه Toggle برای Dark Mode -->
|
||
<button id="dark-mode-toggle" class="dark-mode-toggle" title="تغییر حالت تیره/روشن" aria-label="تغییر حالت تیره/روشن">
|
||
<span id="dark-mode-icon">🌙</span>
|
||
</button>
|
||
<script src="/assets/swagger/vendor/swagger-ui-bundle.js"></script>
|
||
<script src="/assets/swagger/vendor/swagger-ui-standalone-preset.js"></script>
|
||
<script>
|
||
window.onload = function() {{
|
||
const ui = SwaggerUIBundle({{
|
||
url: "{application.openapi_url}",
|
||
dom_id: '#swagger-ui',
|
||
deepLinking: true,
|
||
presets: [
|
||
SwaggerUIBundle.presets.apis,
|
||
SwaggerUIStandalonePreset
|
||
],
|
||
plugins: [
|
||
SwaggerUIBundle.plugins.DownloadUrl
|
||
],
|
||
layout: "StandaloneLayout",
|
||
// پارامترهای سفارشی
|
||
defaultModelsExpandDepth: -1,
|
||
docExpansion: "list",
|
||
filter: true,
|
||
persistAuthorization: {"true" if not is_production else "false"},
|
||
displayRequestDuration: true,
|
||
tryItOutEnabled: true,
|
||
syntaxHighlight: {{
|
||
activate: true,
|
||
theme: "monokai"
|
||
}},
|
||
displayOperationId: false,
|
||
// تنظیمات OAuth (در صورت نیاز)
|
||
oauth2RedirectUrl: window.location.origin + "/docs/oauth2-redirect",
|
||
// پیکربندیهای اضافی
|
||
requestInterceptor: function(req) {{
|
||
// میتوانید request ها را اینجا تغییر دهید
|
||
return req;
|
||
}},
|
||
responseInterceptor: function(res) {{
|
||
// میتوانید response ها را اینجا تغییر دهید
|
||
return res;
|
||
}}
|
||
}});
|
||
window.ui = ui;
|
||
|
||
// Dark Mode Toggle
|
||
const darkModeToggle = document.getElementById('dark-mode-toggle');
|
||
const darkModeIcon = document.getElementById('dark-mode-icon');
|
||
const swaggerContainer = document.getElementById('swagger-ui');
|
||
|
||
// بررسی تنظیمات ذخیره شده
|
||
const savedTheme = localStorage.getItem('swagger-theme');
|
||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
|
||
// تنظیم theme اولیه
|
||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {{
|
||
document.body.classList.add('dark-mode');
|
||
swaggerContainer.classList.add('dark-mode');
|
||
darkModeIcon.textContent = '☀️';
|
||
}}
|
||
|
||
// Toggle Dark Mode
|
||
darkModeToggle.addEventListener('click', function() {{
|
||
document.body.classList.toggle('dark-mode');
|
||
swaggerContainer.classList.toggle('dark-mode');
|
||
|
||
if (document.body.classList.contains('dark-mode')) {{
|
||
darkModeIcon.textContent = '☀️';
|
||
localStorage.setItem('swagger-theme', 'dark');
|
||
}} else {{
|
||
darkModeIcon.textContent = '🌙';
|
||
localStorage.setItem('swagger-theme', 'light');
|
||
}}
|
||
}});
|
||
|
||
// شناسایی تغییر تنظیمات سیستم
|
||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {{
|
||
if (!localStorage.getItem('swagger-theme')) {{
|
||
if (e.matches) {{
|
||
document.body.classList.add('dark-mode');
|
||
swaggerContainer.classList.add('dark-mode');
|
||
darkModeIcon.textContent = '☀️';
|
||
}} else {{
|
||
document.body.classList.remove('dark-mode');
|
||
swaggerContainer.classList.remove('dark-mode');
|
||
darkModeIcon.textContent = '🌙';
|
||
}}
|
||
}}
|
||
}});
|
||
}};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
# Response Cache Middleware (بعد از CORS و قبل از authentication)
|
||
from app.core.response_cache import ResponseCacheMiddleware
|
||
application.add_middleware(ResponseCacheMiddleware)
|
||
|
||
application.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=settings.cors_allowed_origins,
|
||
allow_credentials=False, # Public API - no credentials needed
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
@application.middleware("http")
|
||
async def smart_number_normalizer(request: Request, call_next):
|
||
"""Middleware هوشمند برای تبدیل اعداد فارسی/عربی به انگلیسی"""
|
||
# Streaming/SSE: BaseHTTPMiddleware اگر body را replay کند،
|
||
# listen_for_disconnect پیام http.request میبیند و ExceptionGroup میسازد.
|
||
path = request.url.path
|
||
if path.startswith("/api/v1/ai/chat/") and (
|
||
request.query_params.get("stream") == "true"
|
||
or path.endswith("/events")
|
||
):
|
||
return await call_next(request)
|
||
|
||
# فقط برای درخواستهای POST/PUT/PATCH با Content-Type JSON اعمال شود
|
||
if not SmartNormalizerConfig.ENABLED or request.method not in ["POST", "PUT", "PATCH"]:
|
||
return await call_next(request)
|
||
|
||
content_type = request.headers.get("Content-Type", "").lower()
|
||
if not content_type.startswith("application/json"):
|
||
return await call_next(request)
|
||
|
||
# استثنا برای endpoint های خاص که نباید normalize شوند
|
||
# endpoint های zohal که ممکن است JSON پیچیده یا دادههای خاص داشته باشند
|
||
|
||
# اگر path مربوط به zohal است، از normalize کردن صرف نظر کن
|
||
if "/zohal/" in path:
|
||
return await call_next(request)
|
||
|
||
# نکته: برای سازگاری با StreamingResponse باید body را *کامل* قبل از شروع response بخوانیم.
|
||
# receive-wrapper قبلی میتوانست باعث شود Starlette در زمان streaming همچنان پیامهای http.request ببیند.
|
||
original_receive = request._receive
|
||
try:
|
||
raw_body: bytes = await request.body()
|
||
except Exception as e:
|
||
# اگر نتوانستیم body را بخوانیم، بدون تغییر ادامه بده
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.warning(f"Error reading request body for normalization: {e}")
|
||
return await call_next(request)
|
||
|
||
# normalize کردن body
|
||
if raw_body:
|
||
try:
|
||
normalized_body = smart_normalize_json(raw_body)
|
||
except Exception as e:
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.warning(f"Error normalizing JSON body: {e}")
|
||
normalized_body = raw_body
|
||
else:
|
||
normalized_body = b""
|
||
|
||
# body را برای downstream بازپخش میکنیم (فقط یکبار) و بعد receive اصلی را برای disconnect پاس میدهیم
|
||
sent = False
|
||
|
||
async def receive():
|
||
nonlocal sent
|
||
if not sent:
|
||
sent = True
|
||
return {"type": "http.request", "body": normalized_body, "more_body": False}
|
||
return await original_receive()
|
||
|
||
request._receive = receive
|
||
# همچنین cache داخلی Request را هم بهروز میکنیم تا handlerها body نرمالشده را ببینند
|
||
try:
|
||
request._body = normalized_body # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
|
||
return await call_next(request)
|
||
|
||
@application.middleware("http")
|
||
async def maintenance_mode_middleware(request: Request, call_next):
|
||
"""بررسی حالت تعمیرات - باید قبل از سایر middleware ها باشد"""
|
||
# استثنا برای endpoint های health و admin system settings
|
||
if request.url.path in ["/", "/health", "/api/v1/health"] or \
|
||
request.url.path.startswith("/api/v1/admin/system-settings/configuration"):
|
||
response = await call_next(request)
|
||
return response
|
||
|
||
# بررسی maintenance mode با cache
|
||
from app.core.cache import get_cache
|
||
cache = get_cache()
|
||
cache_key = "system:maintenance_mode"
|
||
cached_value = cache.get(cache_key)
|
||
|
||
if cached_value is not None:
|
||
maintenance_enabled = cached_value
|
||
else:
|
||
# اگر در cache نبود، از دیتابیس بخوان
|
||
# استفاده از context manager برای اطمینان از بسته شدن session
|
||
from adapters.db.session import get_db_session
|
||
try:
|
||
with get_db_session() as db:
|
||
maintenance_enabled = is_maintenance_mode_enabled(db)
|
||
except Exception:
|
||
# در صورت خطا، از cache یا مقدار پیشفرض استفاده کن
|
||
maintenance_enabled = False
|
||
|
||
if maintenance_enabled:
|
||
# اجازه دسترسی به admin endpoints برای مدیریت maintenance mode
|
||
if request.url.path.startswith("/api/v1/admin/system-settings"):
|
||
response = await call_next(request)
|
||
return response
|
||
# برای سایر درخواستها خطا برگردان
|
||
from fastapi.responses import JSONResponse
|
||
return JSONResponse(
|
||
status_code=503,
|
||
content={
|
||
"success": False,
|
||
"error_code": "MAINTENANCE_MODE",
|
||
"message": "سیستم در حال تعمیرات است. لطفاً بعداً تلاش کنید."
|
||
}
|
||
)
|
||
|
||
response = await call_next(request)
|
||
return response
|
||
|
||
@application.middleware("http")
|
||
async def add_locale(request: Request, call_next):
|
||
# استفاده از default_language از DB در صورت نبود Accept-Language
|
||
accept_language = request.headers.get("Accept-Language")
|
||
lang = negotiate_locale(accept_language)
|
||
|
||
# اگر زبان تشخیص داده نشد، از تنظیمات سیستم استفاده کن (با cache)
|
||
if not accept_language:
|
||
from app.core.cache import get_cache
|
||
from app.services.system_settings_service import get_default_language
|
||
cache = get_cache()
|
||
cache_key = "system:default_language"
|
||
cached_value = cache.get(cache_key)
|
||
|
||
if cached_value is not None:
|
||
lang = cached_value
|
||
else:
|
||
# اگر در cache نبود، از دیتابیس بخوان
|
||
# استفاده از context manager برای اطمینان از بسته شدن session
|
||
from adapters.db.session import get_db_session
|
||
try:
|
||
with get_db_session() as db:
|
||
lang = get_default_language(db)
|
||
except Exception:
|
||
pass
|
||
|
||
request.state.locale = lang
|
||
request.state.translator = Translator(lang)
|
||
response = await call_next(request)
|
||
return response
|
||
|
||
@application.middleware("http")
|
||
async def add_calendar_middleware(request: Request, call_next):
|
||
return await add_calendar_type(request, call_next)
|
||
|
||
application.include_router(health_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(auth_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(users_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(currencies_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_data_table_settings_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(profile_dashboard_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_users_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(accounts_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(categories_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(product_attributes_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(catalog_spec_fields_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(products_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.product_bundles import router as product_bundles_router
|
||
application.include_router(product_bundles_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.product_instances import router as product_instances_router
|
||
application.include_router(product_instances_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.warehouse_docs import router as warehouse_docs_router
|
||
application.include_router(warehouse_docs_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.warehouse_reports import router as warehouse_reports_router
|
||
application.include_router(warehouse_reports_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.warehouses import router as warehouses_router
|
||
application.include_router(warehouses_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.warehouse_locations import router as warehouse_locations_router
|
||
application.include_router(warehouse_locations_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.boms import router as boms_router
|
||
application.include_router(boms_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(price_lists_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(invoices_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(persons_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(person_group_routes_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(customers_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.projects import router as projects_router
|
||
application.include_router(projects_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.crm import router as crm_router
|
||
application.include_router(crm_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(bank_accounts_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.checks import router as checks_router
|
||
application.include_router(checks_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(cash_registers_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(petty_cash_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(received_loan_facilities_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(tax_units_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(tax_types_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(tax_product_codes_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_tax_product_codes_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(tax_settings_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(tax_reports_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(receipts_payments_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(transfers_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(expense_income_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(goods_expense_income_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(hscript_reports_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(documents_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(fiscal_years_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(activity_logs_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_activity_logs_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_hscript_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(kardex_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.query_schema import router as query_schema_router
|
||
application.include_router(query_schema_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(opening_balance_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_currency_rates_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_fx_global_rates_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_fx_auto_sync_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(period_end_fx_revaluation_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_fx_providers_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(report_templates_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(wallet_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(zohal_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(wallet_webhook_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(credit_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_frequent_descriptions_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.quick_sales import router as quick_sales_router
|
||
application.include_router(quick_sales_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(document_numbering_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(document_code_reservations_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(marketplace_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(warranty_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(customer_club_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(payroll_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(barcode_labels_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.distribution import router as distribution_router
|
||
application.include_router(distribution_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(repair_shop_router, prefix=settings.api_v1_prefix)
|
||
# Business Notifications
|
||
application.include_router(business_notifications_router, prefix=settings.api_v1_prefix)
|
||
# Ping Pong Game
|
||
application.include_router(ping_pong_router, prefix=settings.api_v1_prefix)
|
||
# Integrations
|
||
application.include_router(telegram_integration_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(bale_integration_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(basalam_integration_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(woocommerce_integration_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(telephony_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(telephony_softphone_ws_router)
|
||
# Notifications
|
||
application.include_router(notifications_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(notifications_ws_router)
|
||
# AI Voice WS (no prefix)
|
||
application.include_router(ai_voice_ws_router)
|
||
# Business backups
|
||
application.include_router(business_backups_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_ftp_backup_router, prefix=settings.api_v1_prefix)
|
||
# Business storage
|
||
from adapters.api.v1.business.storage import router as business_storage_router
|
||
application.include_router(business_storage_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(business_document_monetization_router, prefix=settings.api_v1_prefix)
|
||
# Jobs
|
||
application.include_router(jobs_router, prefix=settings.api_v1_prefix)
|
||
# Workflows
|
||
from adapters.api.v1.workflows import router as workflows_router
|
||
application.include_router(workflows_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.workflow_marketplace import router as workflow_marketplace_router
|
||
application.include_router(workflow_marketplace_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.payment_gateways import router as payment_gateways_router
|
||
application.include_router(payment_gateways_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.payment_callbacks import router as payment_callbacks_router
|
||
application.include_router(payment_callbacks_router, prefix=settings.api_v1_prefix)
|
||
# Announcements
|
||
application.include_router(announcements_router, prefix=settings.api_v1_prefix)
|
||
# Public share links (no prefix to allow short /p/{code})
|
||
application.include_router(public_share_links_router)
|
||
application.include_router(public_storage_file_shares_router)
|
||
application.include_router(public_product_catalog_router)
|
||
|
||
# Support endpoints
|
||
from adapters.api.v1.support.billing import router as support_billing_router
|
||
from adapters.api.v1.support.payment_callbacks import router as support_payment_callbacks_router
|
||
# billing باید قبل از tickets ثبت شود تا /billing با /{ticket_id} برخورد نکند
|
||
application.include_router(support_billing_router, prefix=f"{settings.api_v1_prefix}/support")
|
||
application.include_router(support_payment_callbacks_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
||
application.include_router(support_attachments_user_router, prefix=f"{settings.api_v1_prefix}/support")
|
||
application.include_router(support_operator_router, prefix=f"{settings.api_v1_prefix}/support/operator")
|
||
application.include_router(support_attachments_operator_router, prefix=f"{settings.api_v1_prefix}/support/operator")
|
||
from adapters.api.v1.support.ai_tickets import router as support_ai_router
|
||
application.include_router(support_ai_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(support_categories_router, prefix=f"{settings.api_v1_prefix}/metadata/categories")
|
||
application.include_router(support_priorities_router, prefix=f"{settings.api_v1_prefix}/metadata/priorities")
|
||
application.include_router(support_statuses_router, prefix=f"{settings.api_v1_prefix}/metadata/statuses")
|
||
|
||
# Admin endpoints
|
||
application.include_router(admin_file_storage_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_email_config_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_system_settings_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_legacy_import_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_firewall_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_currencies_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_monitoring_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_system_services_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_wallet_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_storage_plans_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_support_billing_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_businesses_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_users_permissions_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_scripts_router, prefix=settings.api_v1_prefix)
|
||
from adapters.api.v1.admin.payment_gateways import router as admin_payment_gateways_router
|
||
application.include_router(admin_payment_gateways_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_announcements_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_notification_templates_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_notification_event_types_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_notification_moderation_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_document_monetization_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_zohal_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_marketplace_router, prefix=settings.api_v1_prefix)
|
||
# AI endpoints
|
||
from adapters.api.v1.admin.ai_settings import router as admin_ai_settings_router
|
||
from adapters.api.v1.admin.ai_plans import router as admin_ai_plans_router
|
||
from adapters.api.v1.admin.ai_prompts import router as admin_ai_prompts_router
|
||
from adapters.api.v1.admin.ai_models import router as admin_ai_models_router
|
||
from adapters.api.v1.admin.ai_eval import router as admin_ai_eval_router
|
||
from adapters.api.v1.admin.ai_provider_credentials import (
|
||
router as admin_ai_provider_credentials_router,
|
||
)
|
||
from adapters.api.v1.admin.ai_skills import router as admin_ai_skills_router
|
||
from adapters.api.v1.admin.ai_voice_models import router as admin_ai_voice_models_router
|
||
application.include_router(admin_ai_settings_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_plans_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_prompts_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_eval_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_models_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_provider_credentials_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_skills_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(admin_ai_voice_models_router, prefix=settings.api_v1_prefix)
|
||
# User AI endpoints
|
||
from adapters.api.v1.ai.chat import router as ai_chat_router
|
||
from adapters.api.v1.ai.crm_ai import router as ai_crm_router
|
||
from adapters.api.v1.ai.subscription import router as ai_subscription_router
|
||
from adapters.api.v1.ai.prompts import router as ai_prompts_router
|
||
from adapters.api.v1.ai.usage import router as ai_usage_router
|
||
from adapters.api.v1.ai.voice_feedback import router as ai_voice_feedback_router
|
||
from adapters.api.v1.ai.voice_http import router as ai_voice_http_router
|
||
from adapters.api.v1.ai.mcp import router as ai_mcp_router
|
||
from adapters.api.v1.ai.models import router as ai_models_router
|
||
from adapters.api.v1.ai.skills import router as ai_skills_router
|
||
from adapters.api.v1.ai.business_provider import router as ai_business_provider_router
|
||
application.include_router(ai_chat_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_mcp_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_models_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_skills_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_crm_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_subscription_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_prompts_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_usage_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_voice_feedback_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_voice_http_router, prefix=settings.api_v1_prefix)
|
||
application.include_router(ai_business_provider_router, prefix=settings.api_v1_prefix)
|
||
|
||
register_error_handlers(application)
|
||
|
||
# Start background notification outbox processor
|
||
import asyncio
|
||
@application.on_event("startup")
|
||
async def _start_background_jobs():
|
||
# Cache invalidation subscriber: بهتر است در همه worker ها فعال باشد
|
||
from app.services.cache_invalidation_subscriber import start_cache_invalidation_subscriber
|
||
start_cache_invalidation_subscriber()
|
||
|
||
# چت CRM: وبسوکت بین چند worker → Redis pub/sub تا تایپ/رویدادها به هر سوکتی برسد
|
||
from app.services.crm_chat_realtime_fanout import start_crm_chat_fanout_subscriber
|
||
|
||
loop = asyncio.get_running_loop()
|
||
from app.services.ai.ai_run_hub import agent_run_hub
|
||
|
||
agent_run_hub.start_supervisor()
|
||
start_crm_chat_fanout_subscriber(loop)
|
||
|
||
from app.services.support.support_realtime_fanout import start_support_fanout_subscriber
|
||
|
||
start_support_fanout_subscriber(loop)
|
||
|
||
# سایر background jobs باید فقط در یک process اجرا شوند (leader-only)
|
||
if not _try_acquire_background_jobs_lock():
|
||
logger = logging.getLogger(__name__)
|
||
logger.info("Background jobs skipped (not leader / disabled).")
|
||
return
|
||
|
||
asyncio.create_task(notifications_background_loop(30))
|
||
# Storage cleanup: هر 24 ساعت یکبار
|
||
asyncio.create_task(storage_cleanup_loop(24))
|
||
# Subscription check: هر 6 ساعت یکبار
|
||
asyncio.create_task(storage_subscription_check_loop(6))
|
||
# Document monetization processor
|
||
asyncio.create_task(document_monetization_loop(10))
|
||
# Document monetization period finalization: هر 24 ساعت یکبار
|
||
asyncio.create_task(document_monetization_finalize_periods_loop(24))
|
||
|
||
# Tax system background jobs
|
||
from app.services.tax_background_jobs import tax_auto_inquiry_loop
|
||
# Auto-inquiry برای فاکتورهای pending: هر 30 دقیقه یکبار
|
||
asyncio.create_task(tax_auto_inquiry_loop(30))
|
||
|
||
# AI background jobs
|
||
from app.services.ai_background_jobs import (
|
||
ai_quota_reset_loop,
|
||
ai_chat_cleanup_loop,
|
||
ai_subscription_check_loop,
|
||
ai_eval_schedule_loop,
|
||
)
|
||
# AI quota reset: هر 24 ساعت یکبار
|
||
asyncio.create_task(ai_quota_reset_loop(24))
|
||
# AI chat cleanup: هر 24 ساعت یکبار
|
||
asyncio.create_task(ai_chat_cleanup_loop(24))
|
||
# AI subscription check: هر 6 ساعت یکبار
|
||
asyncio.create_task(ai_subscription_check_loop(6))
|
||
# AI eval regression: هر ۶۰ ثانیه بررسی cron (پیشفرض غیرفعال تا فعالسازی در ادمین)
|
||
asyncio.create_task(ai_eval_schedule_loop(60))
|
||
|
||
# حذف/پنهان خودکار اعلانهای in-app خواندهشده (تنظیم مدیر)
|
||
from app.services.announcement_retention_jobs import announcement_read_retention_loop
|
||
asyncio.create_task(announcement_read_retention_loop(24))
|
||
|
||
# Notification moderation worker بهتر است جداگانه با systemd اجرا شود.
|
||
# اگر نیاز بود داخل API هم اجرا شود، میتوان با env فعالش کرد.
|
||
run_inline_moderation = os.getenv("HESABIX_RUN_NOTIFICATION_MODERATION_IN_API", "false").strip().lower()
|
||
if run_inline_moderation in {"1", "true", "yes", "on"}:
|
||
from app.workers.notification_moderation_worker import run_worker_loop
|
||
asyncio.create_task(run_worker_loop(60))
|
||
|
||
# Monitoring metrics collection: هر 60 ثانیه
|
||
asyncio.create_task(monitoring_metrics_collection_loop(60))
|
||
# Service status check: هر 120 ثانیه
|
||
asyncio.create_task(monitoring_service_status_check_loop(120))
|
||
# Business deletion check: هر 24 ساعت یکبار (فقط لاگ - حذف نمیکند)
|
||
asyncio.create_task(check_expired_deleted_businesses_loop(24))
|
||
|
||
# عضویت زمانی اعضای کسبوکار: هر 60 دقیقه حذف رکوردهای منقضی
|
||
from app.services.business_membership_background_jobs import revoke_expired_business_memberships_loop
|
||
asyncio.create_task(revoke_expired_business_memberships_loop(60))
|
||
|
||
# ورکفلو: cron زمانبندیشده + یادآوری سررسید چک
|
||
from app.services.workflow.workflow_background_jobs import workflow_automation_background_loop
|
||
asyncio.create_task(workflow_automation_background_loop(60))
|
||
|
||
# CRM: یادآوری پیگیری/وظایف/SLA + پردازش توالیهای خودکار
|
||
from app.services.crm_background_jobs import crm_automation_background_loop
|
||
asyncio.create_task(crm_automation_background_loop(60))
|
||
|
||
from app.services.support.support_background_jobs import (
|
||
support_sla_breach_check_loop,
|
||
support_subscription_status_loop,
|
||
)
|
||
|
||
asyncio.create_task(support_sla_breach_check_loop(300))
|
||
asyncio.create_task(support_subscription_status_loop(600))
|
||
|
||
# نرخ ارز متمرکز: بررسی هر ۶۰ثانیه؛ واکشی واقعی طبق fetch_interval هر provider (پیشفرض ۱۵دقیقه)
|
||
from app.services.fx_rate_background_jobs import fx_global_rates_fetch_loop
|
||
|
||
asyncio.create_task(fx_global_rates_fetch_loop(60))
|
||
|
||
# زمانبندی کسبوکار: ثبت خودکار نرخ تسعیر از اسنپشات مرکزی + آفست
|
||
from app.services.fx_auto_sync_background_jobs import fx_auto_sync_loop
|
||
|
||
asyncio.create_task(fx_auto_sync_loop(60))
|
||
|
||
# زمانبندی گزارشهای HScript
|
||
from app.services.hscript_schedule_background_jobs import hscript_schedule_loop
|
||
|
||
asyncio.create_task(hscript_schedule_loop(60))
|
||
|
||
@application.middleware("http")
|
||
async def global_rate_limit_middleware(request: Request, call_next):
|
||
import time
|
||
"""Rate limiting عمومی برای تمام endpoint ها"""
|
||
# استثنا برای health check و static files
|
||
if request.url.path in ["/", "/health", "/api/v1/health"] or \
|
||
request.url.path.startswith("/docs") or \
|
||
request.url.path.startswith("/redoc") or \
|
||
request.url.path.startswith("/openapi.json") or \
|
||
request.url.path.startswith("/assets"):
|
||
return await call_next(request)
|
||
# نرخ چت وب عمومی فقط از طریق firewall_rate_policies (فایروال مرکزی + دیتابیس)
|
||
if request.url.path.startswith("/api/v1/public/crm-chat"):
|
||
return await call_next(request)
|
||
|
||
from app.core.rate_limiter import get_rate_limiter, get_client_ip
|
||
|
||
# Rate limiting عمومی: 500 request در دقیقه برای هر IP
|
||
# افزایش از 100 به 500 برای پشتیبانی از SPA های Flutter که چندین درخواست همزمان میفرستند
|
||
client_ip = get_client_ip(request)
|
||
rate_limit_key = f"global:{client_ip}"
|
||
|
||
limiter = get_rate_limiter()
|
||
allowed, remaining, reset_after = limiter.check_rate_limit(
|
||
rate_limit_key,
|
||
max_requests=500,
|
||
window_seconds=60,
|
||
)
|
||
|
||
if not allowed:
|
||
from fastapi.responses import JSONResponse
|
||
return JSONResponse(
|
||
status_code=429,
|
||
content={
|
||
"success": False,
|
||
"error_code": "RATE_LIMIT_EXCEEDED",
|
||
"message": "تعداد درخواستهای شما بیش از حد مجاز است. لطفاً کمی صبر کنید."
|
||
},
|
||
headers={
|
||
"X-RateLimit-Limit": "500",
|
||
"X-RateLimit-Remaining": "0",
|
||
"X-RateLimit-Reset": str(int(time.time()) + reset_after),
|
||
"Retry-After": str(reset_after),
|
||
}
|
||
)
|
||
|
||
response = await call_next(request)
|
||
|
||
# اضافه کردن rate limit headers
|
||
if hasattr(response, 'headers'):
|
||
response.headers["X-RateLimit-Limit"] = "500"
|
||
response.headers["X-RateLimit-Remaining"] = str(remaining)
|
||
response.headers["X-RateLimit-Reset"] = str(int(time.time()) + reset_after)
|
||
|
||
return response
|
||
|
||
from app.core.firewall_middleware import internal_firewall_middleware
|
||
|
||
application.middleware("http")(internal_firewall_middleware)
|
||
|
||
@application.middleware("http")
|
||
async def track_request_context(request: Request, call_next):
|
||
"""Middleware برای ذخیره اطلاعات request در context variable برای connection leak tracking"""
|
||
from adapters.db.session import _request_context
|
||
|
||
# استخراج user_id از request state (اگر در دسترس باشد)
|
||
user_id = None
|
||
try:
|
||
if hasattr(request.state, 'user_id'):
|
||
user_id = request.state.user_id
|
||
elif hasattr(request.state, 'auth_context'):
|
||
auth_ctx = request.state.auth_context
|
||
if hasattr(auth_ctx, 'get_user_id'):
|
||
user_id = auth_ctx.get_user_id()
|
||
except Exception:
|
||
pass
|
||
|
||
# ذخیره اطلاعات request در context variable
|
||
request_info = {
|
||
'path': str(request.url.path),
|
||
'method': request.method,
|
||
'user_id': user_id,
|
||
'client_ip': request.client.host if request.client else None,
|
||
}
|
||
|
||
# تنظیم context variable
|
||
token = _request_context.set(request_info)
|
||
|
||
try:
|
||
response = await call_next(request)
|
||
return response
|
||
finally:
|
||
# پاک کردن context variable
|
||
_request_context.reset(token)
|
||
|
||
@application.middleware("http")
|
||
async def log_slow_requests(request: Request, call_next):
|
||
import time
|
||
import structlog
|
||
from app.core.monitoring import get_performance_monitor
|
||
|
||
start = time.perf_counter()
|
||
status_code = 200
|
||
user_id = None
|
||
|
||
try:
|
||
response = await call_next(request)
|
||
|
||
# تلاش برای دریافت status code از response
|
||
if hasattr(response, 'status_code'):
|
||
status_code = response.status_code
|
||
|
||
return response
|
||
except Exception as e:
|
||
status_code = getattr(e, 'http_status', 500) if hasattr(e, 'http_status') else 500
|
||
raise
|
||
finally:
|
||
duration_ms = (time.perf_counter() - start) * 1000
|
||
|
||
# ثبت در monitoring
|
||
monitor = get_performance_monitor()
|
||
monitor.record_request(
|
||
method=request.method,
|
||
path=str(request.url.path),
|
||
duration_ms=duration_ms,
|
||
status_code=status_code,
|
||
user_id=user_id,
|
||
)
|
||
|
||
# Log slow requests
|
||
if duration_ms > 2000:
|
||
logger = structlog.get_logger()
|
||
logger.warning(
|
||
"slow_request",
|
||
path=str(request.url.path),
|
||
method=request.method,
|
||
duration_ms=duration_ms,
|
||
status_code=status_code,
|
||
)
|
||
|
||
@application.get("/",
|
||
summary="اطلاعات سرویس",
|
||
description="دریافت اطلاعات کلی سرویس و نسخه",
|
||
tags=["general"]
|
||
)
|
||
def read_root() -> dict[str, str]:
|
||
# خواندن از DB در هر درخواست برای بهروز بودن
|
||
# استفاده از context manager برای اطمینان از بسته شدن session
|
||
from adapters.db.session import get_db_session
|
||
with get_db_session() as db:
|
||
current_app_name = get_app_name(db)
|
||
current_app_version = get_app_version(db)
|
||
return {"service": current_app_name, "version": current_app_version}
|
||
|
||
# اضافه کردن security schemes
|
||
from fastapi.openapi.utils import get_openapi
|
||
|
||
def custom_openapi():
|
||
if application.openapi_schema:
|
||
return application.openapi_schema
|
||
|
||
openapi_schema = get_openapi(
|
||
title=application.title,
|
||
version=application.version,
|
||
openapi_version=application.openapi_version,
|
||
summary=application.summary,
|
||
description=application.description,
|
||
terms_of_service=application.terms_of_service,
|
||
contact=application.contact,
|
||
license_info=application.license_info,
|
||
routes=application.routes,
|
||
webhooks=application.webhooks.routes,
|
||
tags=application.openapi_tags,
|
||
servers=application.servers,
|
||
separate_input_output_schemas=application.separate_input_output_schemas,
|
||
external_docs=application.openapi_external_docs,
|
||
)
|
||
|
||
# اسکیمای QueryInfo و مدلهای لیست (فاز ۱۲)
|
||
try:
|
||
from adapters.api.v1.schemas import (
|
||
DocumentListQuery,
|
||
FilterItem,
|
||
InvoiceListQuery,
|
||
KardexListQuery,
|
||
QueryInfo,
|
||
WarehouseDocListQuery,
|
||
)
|
||
from app.openapi_list_query_patch import apply_list_query_openapi_patch
|
||
|
||
ref = "#/components/schemas/{model}"
|
||
components = openapi_schema.setdefault("components", {})
|
||
schemas = components.setdefault("schemas", {})
|
||
for name, model in (
|
||
("FilterItem", FilterItem),
|
||
("QueryInfo", QueryInfo),
|
||
("DocumentListQuery", DocumentListQuery),
|
||
("InvoiceListQuery", InvoiceListQuery),
|
||
("KardexListQuery", KardexListQuery),
|
||
("WarehouseDocListQuery", WarehouseDocListQuery),
|
||
):
|
||
schemas[name] = model.model_json_schema(ref_template=ref)
|
||
apply_list_query_openapi_patch(openapi_schema)
|
||
except Exception as exc:
|
||
logging.getLogger(__name__).warning(
|
||
"Could not inject QueryInfo schemas into OpenAPI: %s", exc
|
||
)
|
||
|
||
# یک طرح امنیتی؛ مقدار کامل هدر: "ApiKey <token>" (Bearer پشتیبانی نمیشود)
|
||
openapi_schema["components"]["securitySchemes"] = {
|
||
"ApiKeyAuth": {
|
||
"type": "apiKey",
|
||
"in": "header",
|
||
"name": "Authorization",
|
||
"description": """
|
||
**فرمت اجباری (همانطور که سرور دریافت میکند):**
|
||
|
||
```
|
||
Authorization: ApiKey <کلید>
|
||
```
|
||
|
||
- **Session:** پس از `POST /api/v1/auth/login` یا `POST /api/v1/auth/register` مقدار `data.api_key` را بردارید (پیشوند رایج: `ak_live_`).
|
||
- **شخصی:** `POST /api/v1/auth/api-keys` (پیشوند رایج: `hsx_`).
|
||
|
||
**Swagger Authorize:** مقدار ورودی = `ApiKey` + فاصله + کلید کامل. مثال: `ApiKey ak_live_...`
|
||
|
||
**نمونه:** `curl -H "Authorization: ApiKey ak_live_xxxx" ...`
|
||
""",
|
||
"x-displayName": "Authorization: ApiKey"
|
||
}
|
||
}
|
||
|
||
# اضافه کردن توضیحات برای security requirements
|
||
if "security" not in openapi_schema:
|
||
openapi_schema["security"] = []
|
||
|
||
# اضافه کردن security به endpoint های محافظت شده
|
||
for path, methods in openapi_schema["paths"].items():
|
||
for method, details in methods.items():
|
||
if method in ["get", "post", "put", "delete", "patch"]:
|
||
# تمام endpoint های auth، users، support و bank-accounts نیاز به احراز هویت دارند
|
||
if "/auth/public-config" in path:
|
||
continue
|
||
if "/auth/" in path or "/users" in path or "/support" in path or "/bank-accounts" in path:
|
||
details["security"] = [{"ApiKeyAuth": []}]
|
||
|
||
application.openapi_schema = openapi_schema
|
||
return application.openapi_schema
|
||
|
||
application.openapi = custom_openapi
|
||
|
||
return application
|
||
|
||
|
||
app = create_app()
|
||
|