Files
yunke/yunke-server/app/api/routers/login.py
T

62 lines
1.9 KiB
Python
Raw Normal View History

"""POST /api/auth/login — username/password sign-in for the SPA.
Exchanges admin credentials (``Settings.admin_user`` / ``admin_password``,
env-overridable) for the shared ``api_token`` so browser users never have to
handle the raw token string. The endpoint is exempt from the shared-token
middleware (see ``app.api.auth._PUBLIC_PATHS``) and rate-limited per IP by the
brute-force guard in ``app.api.auth``.
"""
from __future__ import annotations
import secrets
from fastapi import APIRouter, HTTPException, Request, status
from pydantic import BaseModel
from app.api.auth import (
_client_ip,
_login_locked_out,
_record_login_failure,
_reset_login_failures,
)
from app.config import get_settings
router = APIRouter(prefix="/api/auth", tags=["auth"])
class LoginBody(BaseModel):
username: str
password: str
@router.post("/login")
async def login(body: LoginBody, request: Request) -> dict[str, str]:
settings = get_settings()
ip = _client_ip(request)
if _login_locked_out(ip):
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="尝试次数过多,请 15 分钟后再试",
)
credentials_ok = secrets.compare_digest(
body.username.encode("utf-8"), settings.admin_user.encode("utf-8")
) and secrets.compare_digest(
body.password.encode("utf-8"), settings.admin_password.encode("utf-8")
)
if not credentials_ok:
# Same generic message for unknown user / wrong password (no
# user enumeration).
_record_login_failure(ip)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
_reset_login_failures(ip)
# Empty token when auth is disabled — the SPA stores it and the request
# interceptor simply skips the empty value.
return {"token": settings.api_token}