Вход админа: защита от перебора (throttle)

admin_login проходит через LoginThrottle (пара IP+логин, IP и сам аккаунт),
как вход игрока; исчерпание лимита -> 429. Неудачные попытки пишутся в аудит
(без пароля). Пароль админа — прямой путь к полному контролю, лимиты строже. #56

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
This commit is contained in:
2026-09-13 17:12:40 +03:00
co-authored by Claude Opus 4.8
parent 3f8667b561
commit 706eeb0af1
3 changed files with 105 additions and 2 deletions
+18 -2
View File
@@ -5,10 +5,11 @@ from fastapi import APIRouter, Depends, File, Query, Request, Response, UploadFi
from fastapi.responses import FileResponse
from sqlmodel import Session
from app.auth.admin_login import login_admin
from app.auth.deps import get_current_admin
from app.core import security
from app.core.security import client_ip
from app.core.errors import NotFoundError
from app.core.errors import InvalidCredentialsError, NotFoundError
from app.core.timeutil import iso_utc
from app.db.session import get_session
from app.models import User
@@ -40,7 +41,22 @@ def admin_login(
response: Response,
session: Session = Depends(get_session),
) -> s.AdminMe:
admin = admin_service.authenticate_admin(session, body.username, body.password)
try:
admin = login_admin(session, body.username, body.password, client_ip(request))
except InvalidCredentialsError:
# Неудачную попытку фиксируем в аудите (перебор пароля админа — прямой путь к
# полному контролю). Серию таких попыток ограничивает throttle в login_admin (#56).
audit_service.record(
session,
actor_id=None,
action="login_failed",
entity_type="admin",
payload={"username": (body.username or "")[:64]},
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
raise
security.set_admin_session(response, admin.id, admin.token_version) # type: ignore[arg-type]
audit_service.record(
session,