fix(main): правильные отступы после фикса rate limiter (20.09.2026)
This commit is contained in:
@@ -20,13 +20,9 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ============ Версия ============
|
# ============ Версия ============
|
||||||
# Читается из .env (переменная APP_VERSION). Если не задана — fallback "1.0.0".
|
|
||||||
# Фронт живёт на Cloudflare Pages, там своя версия в index.html — они не обязаны совпадать.
|
|
||||||
# При деплое новой версии фронта обнови APP_VERSION в .env и перезапусти filwords-api.
|
|
||||||
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
|
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
|
||||||
|
|
||||||
# ============ Sentry (мониторинг ошибок) ============
|
# ============ Sentry ============
|
||||||
# ВАЖНО: инициализация ДО создания FastAPI-приложения
|
|
||||||
SENTRY_DSN = os.getenv("SENTRY_DSN", "")
|
SENTRY_DSN = os.getenv("SENTRY_DSN", "")
|
||||||
if SENTRY_DSN:
|
if SENTRY_DSN:
|
||||||
try:
|
try:
|
||||||
@@ -40,10 +36,10 @@ if SENTRY_DSN:
|
|||||||
StarletteIntegration(transaction_style="endpoint"),
|
StarletteIntegration(transaction_style="endpoint"),
|
||||||
FastApiIntegration(transaction_style="endpoint"),
|
FastApiIntegration(transaction_style="endpoint"),
|
||||||
],
|
],
|
||||||
traces_sample_rate=0.2, # 20% запросов для performance monitoring
|
traces_sample_rate=0.2,
|
||||||
environment=os.getenv("ENV", "production"),
|
environment=os.getenv("ENV", "production"),
|
||||||
release=f"filwords@{APP_VERSION}",
|
release=f"filwords@{APP_VERSION}",
|
||||||
send_default_pii=False, # НЕ отправляем IP/headers/cookies пользователей
|
send_default_pii=False,
|
||||||
)
|
)
|
||||||
logger.info(f"Sentry initialized (release=filwords@{APP_VERSION})")
|
logger.info(f"Sentry initialized (release=filwords@{APP_VERSION})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -53,6 +49,7 @@ else:
|
|||||||
|
|
||||||
app = FastAPI(title="Filwords PL API")
|
app = FastAPI(title="Filwords PL API")
|
||||||
|
|
||||||
|
|
||||||
# ============ Rate Limiting ============
|
# ============ Rate Limiting ============
|
||||||
class RateLimiter:
|
class RateLimiter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -76,7 +73,6 @@ class RateLimiter:
|
|||||||
"/support/send": {"max_requests": 10, "window": 60},
|
"/support/send": {"max_requests": 10, "window": 60},
|
||||||
"/support/admin-reply": {"max_requests": 30, "window": 60},
|
"/support/admin-reply": {"max_requests": 30, "window": 60},
|
||||||
"/support/translate": {"max_requests": 20, "window": 60},
|
"/support/translate": {"max_requests": 20, "window": 60},
|
||||||
# 🔧 НОВОЕ (18.09.2026): лимиты для feed
|
|
||||||
"/admin/feed/post": {"max_requests": 20, "window": 60},
|
"/admin/feed/post": {"max_requests": 20, "window": 60},
|
||||||
"/admin/feed/upload-image": {"max_requests": 10, "window": 60},
|
"/admin/feed/upload-image": {"max_requests": 10, "window": 60},
|
||||||
"/admin/feed/translate": {"max_requests": 10, "window": 60},
|
"/admin/feed/translate": {"max_requests": 10, "window": 60},
|
||||||
@@ -97,16 +93,12 @@ class RateLimiter:
|
|||||||
|
|
||||||
def is_allowed(self, key: str, path: str) -> bool:
|
def is_allowed(self, key: str, path: str) -> bool:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
limit_config = self.limits.get(path, self.limits["default"])
|
limit_config = self.limits.get(path, self.limits["default"])
|
||||||
max_requests = limit_config["max_requests"]
|
max_requests = limit_config["max_requests"]
|
||||||
window = limit_config["window"]
|
window = limit_config["window"]
|
||||||
|
|
||||||
self.requests[key] = [t for t in self.requests[key] if now - t < window]
|
self.requests[key] = [t for t in self.requests[key] if now - t < window]
|
||||||
|
|
||||||
if len(self.requests[key]) >= max_requests:
|
if len(self.requests[key]) >= max_requests:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.requests[key].append(now)
|
self.requests[key].append(now)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -118,8 +110,10 @@ class RateLimiter:
|
|||||||
oldest = min(self.requests[key])
|
oldest = min(self.requests[key])
|
||||||
return max(0, int(window - (time.time() - oldest)))
|
return max(0, int(window - (time.time() - oldest)))
|
||||||
|
|
||||||
|
|
||||||
rate_limiter = RateLimiter()
|
rate_limiter = RateLimiter()
|
||||||
|
|
||||||
|
|
||||||
# ============ Rate Limiting Middleware ============
|
# ============ Rate Limiting Middleware ============
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def rate_limit_middleware(request: Request, call_next):
|
async def rate_limit_middleware(request: Request, call_next):
|
||||||
@@ -159,8 +153,8 @@ rate_key = f"{client_ip}:{path}"
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ============ CORS ============
|
# ============ CORS ============
|
||||||
# 🔧 ОБНОВЛЕНО: добавлен PUT — нужен для редактирования feed-постов
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[FRONTEND_ORIGIN],
|
allow_origins=[FRONTEND_ORIGIN],
|
||||||
@@ -169,9 +163,7 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============ 🔧 НОВОЕ (18.09.2026): статика для картинок feed-постов ============
|
# ============ Статика для картинок feed-постов ============
|
||||||
# Файлы лежат в /root/filwordspl/backend/uploads/feed/{filename}
|
|
||||||
# URL на фронте: https://api.filwordspl.com/uploads/feed/{filename}
|
|
||||||
app.mount("/uploads", StaticFiles(directory=UPLOADS_DIR), name="uploads")
|
app.mount("/uploads", StaticFiles(directory=UPLOADS_DIR), name="uploads")
|
||||||
|
|
||||||
# ============ Роутеры ============
|
# ============ Роутеры ============
|
||||||
@@ -191,11 +183,10 @@ app.include_router(typing.router)
|
|||||||
app.include_router(teacher.router)
|
app.include_router(teacher.router)
|
||||||
app.include_router(support.router)
|
app.include_router(support.router)
|
||||||
app.include_router(admin.router)
|
app.include_router(admin.router)
|
||||||
# 🔧 НОВОЕ (18.09.2026): feed — посты карусели (публичный + админский)
|
|
||||||
app.include_router(feed.router)
|
app.include_router(feed.router)
|
||||||
# 🔧 НОВОЕ (19.09.2026): admin content — контент-менеджер (только для админов)
|
|
||||||
app.include_router(admin_content.router)
|
app.include_router(admin_content.router)
|
||||||
|
|
||||||
|
|
||||||
# ============ Фоновое обновление кэша рейтинга ============
|
# ============ Фоновое обновление кэша рейтинга ============
|
||||||
async def refresh_leaderboard_cache_loop():
|
async def refresh_leaderboard_cache_loop():
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
@@ -209,6 +200,7 @@ async def refresh_leaderboard_cache_loop():
|
|||||||
logger.error(f"Failed to refresh leaderboard cache: {e}")
|
logger.error(f"Failed to refresh leaderboard cache: {e}")
|
||||||
await asyncio.sleep(30)
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
# ============ Startup ============
|
# ============ Startup ============
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup():
|
async def startup():
|
||||||
@@ -222,7 +214,6 @@ async def startup():
|
|||||||
from database import get_pool
|
from database import get_pool
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
# Пересоздаём materialized view с новой структурой (с level)
|
|
||||||
await conn.execute("DROP MATERIALIZED VIEW IF EXISTS leaderboard_cache")
|
await conn.execute("DROP MATERIALIZED VIEW IF EXISTS leaderboard_cache")
|
||||||
await conn.execute("""
|
await conn.execute("""
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS leaderboard_cache AS
|
CREATE MATERIALIZED VIEW IF NOT EXISTS leaderboard_cache AS
|
||||||
@@ -273,11 +264,13 @@ async def startup():
|
|||||||
|
|
||||||
logger.info(f"Filwords API v{APP_VERSION} started")
|
logger.info(f"Filwords API v{APP_VERSION} started")
|
||||||
|
|
||||||
|
|
||||||
# ============ Базовые эндпоинты ============
|
# ============ Базовые эндпоинты ============
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {"status": "ok", "app": "Filwords PL", "version": APP_VERSION}
|
return {"status": "ok", "app": "Filwords PL", "version": APP_VERSION}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health():
|
async def health():
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user