Files
ForbiddenStarsApp/backend/app/auth/admin_login.py
T
NotBigGhostandClaude Opus 5 ec1e94119c Чистка: устаревшие комментарии и подсказки, export-скрипт, мусорный файл
Комментарии про методы входа приведены к факту (логин/пароль и Telegram везде,
stub только в development): config.py, .env.example, telegram.py, dev_stub.py,
dev_auth.py, login.py. admin_login.py больше не обещает «строже игроцкого» —
описаны реальные лимиты; client_ip — адрес за прокси уже даёт uvicorn.

Подсказки деплоя: run.ps1/run.sh (build-push на ПК, `up -d` на Pi), compose,
.env.example (build-push.ps1), Caddyfile (туннель-контейнер вместо autossh);
ratelimit.py ссылается на core/events.py вместо CLAUDE.md.

Удалены неиспользуемые scripts/export-prod.sh (с блоком export-ignore в
.gitattributes и упоминаниями в README), db/init_db.py и случайно закоммиченный
файл «h -u origin dev…». #70

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-19 03:12:16 +03:00

51 lines
2.6 KiB
Python

"""Вход администратора под защитой от перебора (#56).
Тонкий слой поверх `admin_service.authenticate_admin`: throttle по IP, по паре «IP + логин»
и по самому аккаунту через тот же `LoginThrottle`, что и вход игрока (`core/ratelimit`).
Сервис остаётся чистым от инфраструктуры лимитов. Лимиты те же, что у игрока (5 на пару,
20 на IP, 50 на аккаунт за 15 минут); отличие — при успешном входе снимаются все счётчики,
включая IP.
"""
from __future__ import annotations
from sqlmodel import Session
from app.core.errors import InvalidCredentialsError
from app.core.ratelimit import login_throttle
from app.models import User
from app.services import admin_service
# Неудач за окно LoginThrottle (15 минут):
_PAIR_LIMIT = 5 # на пару «IP + логин» — против перебора пароля с одного адреса
_IP_LIMIT = 20 # на IP — против перебора по разным логинам с одного адреса
# На сам аккаунт (IP-независимо, #60): распределённый перебор с ротацией IP всё равно
# упирается в этот предел. Щедрее пары, чтобы случайный поток ошибок не запирал вход
# админа насовсем (лимит на аккаунт — вектор lockout-DoS, поэтому не слишком строгий).
_ACCOUNT_LIMIT = 50
def _keys(ip: str, username: str) -> dict[str, int]:
pair_key = f"admin-login:{ip}:{username.casefold()}"
return {
pair_key: _PAIR_LIMIT,
f"admin-login-ip:{ip}": _IP_LIMIT,
f"admin-login-user:{username.casefold()}": _ACCOUNT_LIMIT,
}
def login_admin(session: Session, username: str, password: str, ip: str | None) -> User:
"""authenticate_admin под защитой от перебора. Сессию открывает вызывающий."""
username = (username or "").strip()
ip = ip or "unknown"
limits = _keys(ip, username)
login_throttle.check(limits)
try:
admin = admin_service.authenticate_admin(session, username, password)
except InvalidCredentialsError:
login_throttle.fail(limits)
raise
# Успех: снимаем счётчики этого аккаунта, чтобы законный вход не копил лимит.
for key in limits:
login_throttle.reset(key)
return admin