chore: initialize GSREA website project
This commit is contained in:
commit
d3c5461de5
1842 files changed
+246140
No files matched your search
@@ -0,0 +1,177 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.deps import require_staff
|
||||
from app.db import get_db
|
||||
from app.models.content import SiteSetting
|
||||
from app.models.security import AdminAuditLog, AdminIpBlacklist
|
||||
from app.services.captcha import issue_captcha
|
||||
from app.services.login_guard import (
|
||||
LOGIN_LOCK_MINUTES_KEY,
|
||||
LOGIN_MAX_FAILURES_KEY,
|
||||
read_login_policy,
|
||||
)
|
||||
|
||||
public_router = APIRouter()
|
||||
router = APIRouter(dependencies=[Depends(require_staff)])
|
||||
|
||||
|
||||
class LoginSecurityBody(BaseModel):
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
loginMaxFailures: int
|
||||
loginLockMinutes: int
|
||||
|
||||
|
||||
class IpBlacklistBody(BaseModel):
|
||||
model_config = ConfigDict(strict=True)
|
||||
|
||||
ip: str
|
||||
remark: str = ""
|
||||
|
||||
|
||||
def _naive(value: datetime) -> datetime:
|
||||
if value.tzinfo is None:
|
||||
return value
|
||||
return value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
|
||||
def _iso(value: datetime | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=timezone.utc)
|
||||
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _save_setting(db: Session, key: str, value: int) -> None:
|
||||
row = db.scalar(select(SiteSetting).where(SiteSetting.key == key))
|
||||
text = str(value)
|
||||
if row is None:
|
||||
db.add(SiteSetting(key=key, value=text))
|
||||
else:
|
||||
row.value = text
|
||||
|
||||
|
||||
def _ip_item(row: AdminIpBlacklist) -> dict:
|
||||
return {"id": row.id, "ip": row.ip, "remark": row.remark, "createdAt": _iso(row.created_at)}
|
||||
|
||||
|
||||
def _audit_item(row: AdminAuditLog) -> dict:
|
||||
return {
|
||||
"id": row.id,
|
||||
"userId": row.user_id,
|
||||
"username": row.username,
|
||||
"method": row.method,
|
||||
"path": row.path,
|
||||
"statusCode": row.status_code,
|
||||
"ip": row.ip,
|
||||
"createdAt": _iso(row.created_at),
|
||||
}
|
||||
|
||||
|
||||
@public_router.get("/auth/captcha")
|
||||
def get_captcha() -> dict:
|
||||
return issue_captcha()
|
||||
|
||||
|
||||
@router.get("/login-security")
|
||||
def get_login_security(db: Session = Depends(get_db)) -> dict:
|
||||
failures, minutes = read_login_policy(db)
|
||||
return {"loginMaxFailures": failures, "loginLockMinutes": minutes}
|
||||
|
||||
|
||||
@router.put("/login-security")
|
||||
def update_login_security(body: LoginSecurityBody, db: Session = Depends(get_db)) -> dict:
|
||||
if body.loginMaxFailures <= 0 or body.loginLockMinutes <= 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须是正整数")
|
||||
_save_setting(db, LOGIN_MAX_FAILURES_KEY, body.loginMaxFailures)
|
||||
_save_setting(db, LOGIN_LOCK_MINUTES_KEY, body.loginLockMinutes)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
_save_setting(db, LOGIN_MAX_FAILURES_KEY, body.loginMaxFailures)
|
||||
_save_setting(db, LOGIN_LOCK_MINUTES_KEY, body.loginLockMinutes)
|
||||
db.commit()
|
||||
failures, minutes = read_login_policy(db)
|
||||
return {"loginMaxFailures": failures, "loginLockMinutes": minutes}
|
||||
|
||||
|
||||
@router.get("/ip-blacklist")
|
||||
def list_ip_blacklist(db: Session = Depends(get_db)) -> list[dict]:
|
||||
rows = db.scalars(select(AdminIpBlacklist).order_by(AdminIpBlacklist.id.asc())).all()
|
||||
return [_ip_item(row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/ip-blacklist")
|
||||
def create_ip_blacklist(body: IpBlacklistBody, db: Session = Depends(get_db)) -> dict:
|
||||
ip = body.ip.strip()
|
||||
remark = body.remark.strip()
|
||||
if not ip or len(ip) > 64 or any(char.isspace() for char in ip):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="IP 无效")
|
||||
if len(remark) > 255:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="备注过长")
|
||||
if db.scalar(select(AdminIpBlacklist.id).where(AdminIpBlacklist.ip == ip)) is not None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="IP 已存在")
|
||||
row = AdminIpBlacklist(ip=ip, remark=remark)
|
||||
db.add(row)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="IP 已存在")
|
||||
db.refresh(row)
|
||||
return _ip_item(row)
|
||||
|
||||
|
||||
@router.delete("/ip-blacklist/{id}")
|
||||
def delete_ip_blacklist(id: int, db: Session = Depends(get_db)) -> dict:
|
||||
row = db.get(AdminIpBlacklist, id)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return {"id": id}
|
||||
|
||||
|
||||
@router.get("/audit-logs")
|
||||
def list_audit_logs(
|
||||
current: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=200),
|
||||
username: str | None = None,
|
||||
startTime: datetime | None = None,
|
||||
endTime: datetime | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
filters = []
|
||||
if username and username.strip():
|
||||
escaped = username.strip().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
filters.append(AdminAuditLog.username.like(f"%{escaped}%", escape="\\"))
|
||||
if startTime is not None:
|
||||
filters.append(AdminAuditLog.created_at >= _naive(startTime))
|
||||
if endTime is not None:
|
||||
filters.append(AdminAuditLog.created_at <= _naive(endTime))
|
||||
|
||||
count_stmt = select(func.count()).select_from(AdminAuditLog)
|
||||
list_stmt = select(AdminAuditLog)
|
||||
if filters:
|
||||
count_stmt = count_stmt.where(*filters)
|
||||
list_stmt = list_stmt.where(*filters)
|
||||
total = int(db.scalar(count_stmt) or 0)
|
||||
rows = db.scalars(
|
||||
list_stmt.order_by(AdminAuditLog.created_at.desc(), AdminAuditLog.id.desc())
|
||||
.offset((current - 1) * size)
|
||||
.limit(size)
|
||||
).all()
|
||||
return {
|
||||
"records": [_audit_item(row) for row in rows],
|
||||
"current": current,
|
||||
"size": size,
|
||||
"total": total,
|
||||
}
|
||||
Reference in new issue
Block a user