-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
163 lines (132 loc) · 5.81 KB
/
main.py
File metadata and controls
163 lines (132 loc) · 5.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import asyncio
import os
import sys
from contextlib import asynccontextmanager
from fastapi import APIRouter, FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.admin.api import admin_router
from app.auth.api import auth_router
from app.billing.api import billing_router
from app.example.api import example_router
from app.monitoring.api import monitoring_router
from app.tenants.api import tenants_router
from app.users.api import users_router
from core.config import config
from core.database import init_db
from core.logging import logger
from core.redis.redis_client import AsyncRedisClient
# Регистрация моделей для create_all
from core.schemas import RefreshToken, Subscription, Tenant, User
from core.security import SecurityHeadersMiddleware
from core.utils.sanitize import sanitize_for_logging
async def _demo_reset_loop(interval_minutes: int):
"""Периодический сброс демо-данных в норму."""
from scripts.seed_demo import run_seed_demo
while True:
await asyncio.sleep(interval_minutes * 60)
try:
await run_seed_demo()
logger.info(f"Demo data reset (interval={interval_minutes}m)")
except Exception as e:
logger.warning(f"Demo reset failed: {sanitize_for_logging(str(e))}")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Валидация JWT secret в production
if not os.getenv("TESTING") and not config.auth.is_production_secret():
if os.getenv("ENV", "development") == "production":
logger.error("JWT_SECRET не задан или используется dev-secret в production!")
sys.exit(1)
# Инициализация таблиц БД (для dev/Docker) — в тестах пропускаем (event loop)
if not os.getenv("TESTING"):
try:
await init_db()
except Exception as e:
logger.warning(f"БД недоступна на старте: {sanitize_for_logging(str(e))}")
if not os.getenv("TESTING"):
client = await AsyncRedisClient.get_client()
try:
await client.ping()
except Exception:
logger.warning("Redis недоступен на старте")
# Автосброс демо-данных: DEMO_AUTO_RESET_MINUTES=15
reset_task = None
if not os.getenv("TESTING"):
try:
interval = int(os.getenv("DEMO_AUTO_RESET_MINUTES", "0"))
if interval > 0:
from scripts.seed_demo import run_seed_demo
try:
await run_seed_demo()
logger.info("Demo data loaded on startup")
except Exception as e:
logger.warning(f"Demo seed on startup failed: {sanitize_for_logging(str(e))}")
reset_task = asyncio.create_task(_demo_reset_loop(interval))
logger.info(f"Demo auto-reset enabled: every {interval} minutes")
except ValueError:
pass
yield
if reset_task is not None:
reset_task.cancel()
try:
await reset_task
except asyncio.CancelledError:
pass
if not os.getenv("TESTING"):
await AsyncRedisClient.close()
def _global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""Глобальный обработчик: ловит только необработанные Exception (не HTTPException/ValidationError)."""
from core.logging import logger
logger.error(
f"Unhandled exception: {sanitize_for_logging(str(exc))}",
exc_info=os.getenv("ENV") == "development",
)
return JSONResponse(
status_code=500,
content={"detail": "Внутренняя ошибка сервера. Обратитесь к администратору."},
)
app = FastAPI(
title=config.app.title or "SaaS Skeleton API",
version=config.app.version or "0.1.0",
description="Production-ready SaaS backend: auth, multi-tenant, billing, admin",
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
lifespan=lifespan,
)
app.add_exception_handler(Exception, _global_exception_handler)
# Health/metrics на корне для k8s/Docker (livenessProbe, readinessProbe)
from app.monitoring.api.monitoring_api import health_liveness, health_readiness, metrics
app.get("/health")(health_liveness)
app.get("/ready")(health_readiness)
app.get("/metrics")(metrics)
main_router = APIRouter(prefix="/api/v1")
main_router.include_router(example_router)
main_router.include_router(users_router)
main_router.include_router(tenants_router)
main_router.include_router(auth_router)
main_router.include_router(admin_router)
main_router.include_router(billing_router)
main_router.include_router(monitoring_router)
app.include_router(main_router)
# Логирование middleware — отключаем в тестах (event loop conflicts)
if not os.getenv("TESTING"):
from lynx_logger.middleware import FastAPILoggingMiddleware
middleware_logger = logger.get_logger()
if middleware_logger:
middleware = FastAPILoggingMiddleware(middleware_logger)
app.middleware("http")(middleware(app))
# Security headers (CSP, HSTS, X-Frame-Options и др.)
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=config.cors.allow_origins,
allow_credentials=config.cors.allow_credentials,
allow_methods=config.cors.allow_methods,
allow_headers=config.cors.allow_headers,
expose_headers=config.cors.expose_headers,
)
# Метрики Prometheus (RPS, latency, ошибки) — после CORS
if not os.getenv("TESTING"):
from app.monitoring.middleware import PrometheusMiddleware
app.add_middleware(PrometheusMiddleware)