chore: add CI, lint config, login rate limit and split large modules
- Rate-limit failed logins (8 per 15 min per IP or username, 429 + Retry-After) - Add ruff/mypy config and GitHub Actions workflow (ruff, pytest, tsc, tests, build) - Apply ruff autofixes (import order, pyupgrade) - Extract rpy_helpers.py, engine-util.ts, TranslationParts.tsx and translation-types.ts from oversized modules - Add dependency-free React UI tests - Refresh README and ARCHITECTURE (Naninovel/Tomie, status and plans) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57a4edcbfd
commit
bbbcc6a5d8
42 files changed
+678
-391
No files matched your search
@@ -1,9 +1,10 @@
|
||||
from fastapi import APIRouter, HTTPException, Response, status
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.config import get_settings
|
||||
from app.deps import DB, SessionUser
|
||||
from app.models import User
|
||||
from app.ratelimit import client_ip, login_limiter
|
||||
from app.schemas import ChangeCredentialsIn, LoginIn, MeOut
|
||||
from app.security import (
|
||||
COOKIE_NAME,
|
||||
@@ -30,10 +31,19 @@ def _set_cookie(response: Response, user: User) -> None:
|
||||
|
||||
|
||||
@router.post("/login", response_model=MeOut)
|
||||
async def login(body: LoginIn, response: Response, db: DB) -> User:
|
||||
async def login(body: LoginIn, request: Request, response: Response, db: DB) -> User:
|
||||
keys = (f"ip:{client_ip(request)}", f"user:{body.username.lower()}")
|
||||
if wait := login_limiter.retry_after(*keys):
|
||||
raise HTTPException(
|
||||
status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
f"Слишком много неудачных попыток входа. Повторите через {wait} с",
|
||||
headers={"Retry-After": str(wait)},
|
||||
)
|
||||
user = await db.scalar(select(User).where(User.username == body.username))
|
||||
if not user or not user.is_active or not verify_password(user.password_hash, body.password):
|
||||
login_limiter.fail(*keys)
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Неверный логин или пароль")
|
||||
login_limiter.reset(*keys)
|
||||
_set_cookie(response, user)
|
||||
return user
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ from sqlalchemy import and_, exists, func, or_, select, true
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.blobs import put_bytes
|
||||
from app.uploads import remove_upload
|
||||
from app.deps import DB, CurrentUser, has_perm, require
|
||||
from app.models import Game, GameVersion, User
|
||||
from app.schemas import GameDetailOut, GameIn, GameOut, GameUpdateIn, VersionOut
|
||||
from app.translation import version_languages
|
||||
from app.uploads import remove_upload
|
||||
|
||||
router = APIRouter(prefix="/api/games", tags=["games"])
|
||||
Player = Annotated[User, Depends(require("games.play"))]
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
from typing import Annotated, Any
|
||||
import asyncio
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import ClientDisconnect
|
||||
|
||||
from app import translation
|
||||
from app.deps import DB, CurrentUser, has_perm, require
|
||||
from app.models import Game, GameVersion, Revision, Save, User, VersionHistory
|
||||
from app.routers.games import get_game
|
||||
from app.schemas import (
|
||||
ForkIn, HistoryOut, PublishIn, SaveDetailOut, SaveIn, SaveOut, VersionDetailOut, VersionIn,
|
||||
ForkIn,
|
||||
HistoryOut,
|
||||
PublishIn,
|
||||
SaveDetailOut,
|
||||
SaveIn,
|
||||
SaveOut,
|
||||
VersionDetailOut,
|
||||
VersionIn,
|
||||
VersionOut,
|
||||
)
|
||||
from app import translation
|
||||
from app.uploads import remove_upload, upload_path
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["versions"])
|
||||
@@ -79,9 +86,9 @@ async def upload_version(
|
||||
await db.commit()
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Пустой файл")
|
||||
|
||||
from app.converter.preflight import probe_archive
|
||||
from app.converter.archive import ArchiveError
|
||||
from app.converter.compatibility import CompatibilityError
|
||||
from app.converter.preflight import probe_archive
|
||||
try:
|
||||
probe = await asyncio.to_thread(probe_archive, path, filename)
|
||||
except (CompatibilityError, ArchiveError, OSError, ValueError) as exc:
|
||||
|
||||
Reference in new issue
Block a user