fix: path traversal via URL-encoded ../, Feb 29 leap year crash, missing response_model, dead code, duplicate utcnow
This commit is contained in:
@@ -151,9 +151,12 @@ if os.path.exists(WEBUI_PATH):
|
|||||||
@app.get("/{full_path:path}")
|
@app.get("/{full_path:path}")
|
||||||
async def spa_fallback(request: Request, full_path: str):
|
async def spa_fallback(request: Request, full_path: str):
|
||||||
"""SPA 回退:先尝试提供真实文件,找不到则返回 index.html"""
|
"""SPA 回退:先尝试提供真实文件,找不到则返回 index.html"""
|
||||||
file_path = os.path.join(WEBUI_PATH, full_path)
|
# 规范化路径并防止路径穿越攻击
|
||||||
if os.path.isfile(file_path):
|
safe_path = os.path.normpath(os.path.join(WEBUI_PATH, full_path))
|
||||||
return FileResponse(file_path)
|
if not safe_path.startswith(os.path.normpath(WEBUI_PATH)):
|
||||||
|
return FileResponse(os.path.join(WEBUI_PATH, "index.html"))
|
||||||
|
if os.path.isfile(safe_path):
|
||||||
|
return FileResponse(safe_path)
|
||||||
return FileResponse(os.path.join(WEBUI_PATH, "index.html"))
|
return FileResponse(os.path.join(WEBUI_PATH, "index.html"))
|
||||||
|
|
||||||
logger.info(f"SPA 静态文件服务已配置: {WEBUI_PATH}")
|
logger.info(f"SPA 静态文件服务已配置: {WEBUI_PATH}")
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Date
|
from sqlalchemy import Column, Integer, String, Text, DateTime, Date
|
||||||
from datetime import datetime, timezone, date
|
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
|
from app.utils.datetime import utcnow
|
||||||
|
|
||||||
def utcnow():
|
|
||||||
"""统一获取 UTC 时间的工厂函数"""
|
|
||||||
return datetime.now(timezone.utc)
|
|
||||||
|
|
||||||
|
|
||||||
class UserSettings(Base):
|
class UserSettings(Base):
|
||||||
"""用户设置模型(单例,始终只有一条记录 id=1)"""
|
"""用户设置模型(单例,始终只有一条记录 id=1)"""
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from app.database import get_db
|
|||||||
from app.models.account import FinancialAccount, AccountHistory, DebtInstallment
|
from app.models.account import FinancialAccount, AccountHistory, DebtInstallment
|
||||||
from app.schemas.account import (
|
from app.schemas.account import (
|
||||||
AccountCreate, AccountUpdate, AccountResponse, BalanceUpdateRequest,
|
AccountCreate, AccountUpdate, AccountResponse, BalanceUpdateRequest,
|
||||||
AccountHistoryResponse, AccountListItemResponse,
|
AccountHistoryResponse, AccountListItemResponse, PaginatedAccountHistoryResponse,
|
||||||
DebtInstallmentCreate, DebtInstallmentUpdate, DebtInstallmentResponse,
|
DebtInstallmentCreate, DebtInstallmentUpdate, DebtInstallmentResponse,
|
||||||
)
|
)
|
||||||
from app.schemas.common import DeleteResponse
|
from app.schemas.common import DeleteResponse
|
||||||
@@ -232,7 +232,7 @@ def update_balance(account_id: int, data: BalanceUpdateRequest, db: Session = De
|
|||||||
raise HTTPException(status_code=500, detail="更新余额失败")
|
raise HTTPException(status_code=500, detail="更新余额失败")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/accounts/{account_id}/history")
|
@router.get("/accounts/{account_id}/history", response_model=PaginatedAccountHistoryResponse)
|
||||||
def get_account_history(
|
def get_account_history(
|
||||||
account_id: int,
|
account_id: int,
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
@@ -252,26 +252,13 @@ def get_account_history(
|
|||||||
AccountHistory.created_at.desc()
|
AccountHistory.created_at.desc()
|
||||||
).offset((page - 1) * page_size).limit(page_size).all()
|
).offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
|
||||||
result = {
|
logger.info(f"获取账户历史成功: account_id={account_id}, total={total}")
|
||||||
|
return {
|
||||||
"total": total,
|
"total": total,
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"records": [
|
"records": records,
|
||||||
{
|
|
||||||
"id": r.id,
|
|
||||||
"account_id": r.account_id,
|
|
||||||
"change_amount": r.change_amount,
|
|
||||||
"balance_before": r.balance_before,
|
|
||||||
"balance_after": r.balance_after,
|
|
||||||
"note": r.note,
|
|
||||||
"created_at": r.created_at,
|
|
||||||
}
|
|
||||||
for r in records
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"获取账户历史成功: account_id={account_id}, total={total}")
|
|
||||||
return result
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -287,7 +274,7 @@ def get_installments(db: Session = Depends(get_db)):
|
|||||||
try:
|
try:
|
||||||
installments = db.query(DebtInstallment).order_by(
|
installments = db.query(DebtInstallment).order_by(
|
||||||
DebtInstallment.is_completed.asc(),
|
DebtInstallment.is_completed.asc(),
|
||||||
DebtInstallment.next_payment_date.asc() if hasattr(DebtInstallment, 'next_payment_date') else DebtInstallment.id.asc()
|
DebtInstallment.id.asc()
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
today = date.today()
|
today = date.today()
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ from app.utils.logger import logger
|
|||||||
router = APIRouter(prefix="/api", tags=["纪念日"])
|
router = APIRouter(prefix="/api", tags=["纪念日"])
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_date(year: int, month: int, day: int) -> date:
|
||||||
|
"""安全构造日期,对闰年 2 月 29 日回退到 2 月 28 日"""
|
||||||
|
try:
|
||||||
|
return date(year, month, day)
|
||||||
|
except ValueError:
|
||||||
|
if month == 2 and day == 29:
|
||||||
|
return date(year, 2, 28)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def compute_next_info(anniversary: Anniversary, today: date) -> tuple:
|
def compute_next_info(anniversary: Anniversary, today: date) -> tuple:
|
||||||
"""计算纪念日的下一次日期、距今天数、周年数"""
|
"""计算纪念日的下一次日期、距今天数、周年数"""
|
||||||
month, day = anniversary.date.month, anniversary.date.day
|
month, day = anniversary.date.month, anniversary.date.day
|
||||||
@@ -24,9 +34,9 @@ def compute_next_info(anniversary: Anniversary, today: date) -> tuple:
|
|||||||
if anniversary.is_recurring:
|
if anniversary.is_recurring:
|
||||||
# 计算今年和明年的日期
|
# 计算今年和明年的日期
|
||||||
this_year = today.year
|
this_year = today.year
|
||||||
next_date = date(this_year, month, day)
|
next_date = _safe_date(this_year, month, day)
|
||||||
if next_date < today:
|
if next_date < today:
|
||||||
next_date = date(this_year + 1, month, day)
|
next_date = _safe_date(this_year + 1, month, day)
|
||||||
|
|
||||||
days_until = (next_date - today).days
|
days_until = (next_date - today).days
|
||||||
|
|
||||||
@@ -38,14 +48,14 @@ def compute_next_info(anniversary: Anniversary, today: date) -> tuple:
|
|||||||
else:
|
else:
|
||||||
# 非重复:使用原始日期(加上年份)
|
# 非重复:使用原始日期(加上年份)
|
||||||
if anniversary.year:
|
if anniversary.year:
|
||||||
target = date(anniversary.year, month, day)
|
target = _safe_date(anniversary.year, month, day)
|
||||||
if target < today:
|
if target < today:
|
||||||
return None, None, None
|
return None, None, None
|
||||||
days_until = (target - today).days
|
days_until = (target - today).days
|
||||||
return target, days_until, 0
|
return target, days_until, 0
|
||||||
else:
|
else:
|
||||||
# 无年份的日期按今年算
|
# 无年份的日期按今年算
|
||||||
target = date(today.year, month, day)
|
target = _safe_date(today.year, month, day)
|
||||||
if target < today:
|
if target < today:
|
||||||
return None, None, None
|
return None, None, None
|
||||||
days_until = (target - today).days
|
days_until = (target - today).days
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ class AccountHistoryResponse(BaseModel):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedAccountHistoryResponse(BaseModel):
|
||||||
|
"""分页账户变更历史响应模型"""
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
records: List[AccountHistoryResponse] = []
|
||||||
|
|
||||||
|
|
||||||
# ============ 分期还款计划 Schema ============
|
# ============ 分期还款计划 Schema ============
|
||||||
|
|
||||||
class DebtInstallmentBase(BaseModel):
|
class DebtInstallmentBase(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user