from datetime import datetime, timedelta
|
|
from fastapi import HTTPException
|
from sqlalchemy.orm import Session
|
|
from app.config import settings
|
from app.models import LoginLock
|
|
|
def _lock_message(locked_until: datetime) -> str:
|
remain = int((locked_until - datetime.utcnow()).total_seconds())
|
if remain < 60:
|
return f"登录尝试过多,请 {max(remain, 1)} 秒后重试"
|
minutes = max(remain // 60, 1)
|
return f"登录尝试过多,请 {minutes} 分钟后重试"
|
|
|
def ensure_login_allowed(db: Session, username: str) -> None:
|
key = username.strip().lower()
|
if not key:
|
return
|
row = db.get(LoginLock, key)
|
if not row or not row.locked_until:
|
return
|
if row.locked_until <= datetime.utcnow():
|
row.fail_count = 0
|
row.locked_until = None
|
db.commit()
|
return
|
raise HTTPException(429, detail=_lock_message(row.locked_until))
|
|
|
def record_login_failure(db: Session, username: str) -> None:
|
key = username.strip().lower()
|
if not key:
|
return
|
row = db.get(LoginLock, key)
|
if not row:
|
row = LoginLock(username=key, fail_count=0)
|
db.add(row)
|
row.fail_count += 1
|
row.updated_at = datetime.utcnow()
|
if row.fail_count >= settings.login_max_failures:
|
row.locked_until = datetime.utcnow() + timedelta(minutes=settings.login_lock_minutes)
|
db.commit()
|
|
|
def clear_login_lock(db: Session, username: str) -> None:
|
key = username.strip().lower()
|
if not key:
|
return
|
row = db.get(LoginLock, key)
|
if row:
|
db.delete(row)
|
db.commit()
|