security: API鉴权补全 + SQL拼接隐患消除

1. materials.py
   - get_material_groups和get_material_items补全Depends(get_current_user)
   - 移除TYPE_FILTER="1=1"死代码及4处f-string SQL拼接
   - 全部SQL改为纯参数化text()查询

2. notifications.py
   - list_notifications废弃user_id查询参数(越权漏洞)
   - user_id强制从JWT Token解析,防止篡改参数偷看他人通知
   - mark_notification_read补全鉴权
This commit is contained in:
2026-08-12 12:03:12 +08:00
parent 69f3e35d14
commit 8c54a38f55
2 changed files with 23 additions and 26 deletions

View File

@ -1,14 +1,12 @@
"""物料选择器 — 读 MOM material_base按成品/半成品 category 手风琴分组""" """物料选择器 — 读 MOM material_base按成品/半成品 category 手风琴分组"""
from fastapi import APIRouter, Query, HTTPException, status from fastapi import APIRouter, Query, HTTPException, status, Depends
from pydantic import BaseModel from pydantic import BaseModel
from app.core.mom_database import MomSessionLocal from app.core.mom_database import MomSessionLocal
from app.services.auth_service import get_current_user
from sqlalchemy import text from sqlalchemy import text
router = APIRouter(prefix="/materials", tags=["物料选择"]) router = APIRouter(prefix="/materials", tags=["物料选择"])
# 全量展示全部物料类别
TYPE_FILTER = "1=1"
# ============================================================ # ============================================================
# 响应模型 # 响应模型
@ -36,6 +34,7 @@ class MaterialItem(BaseModel):
@router.get("/groups", response_model=list[MaterialGroup]) @router.get("/groups", response_model=list[MaterialGroup])
def get_material_groups( def get_material_groups(
keyword: str = Query("", description="搜索(按名称/规格)"), keyword: str = Query("", description="搜索(按名称/规格)"),
current_user: dict = Depends(get_current_user),
): ):
""" """
按 category 分组汇总,前端渲染手风琴外层。 按 category 分组汇总,前端渲染手风琴外层。
@ -44,29 +43,23 @@ def get_material_groups(
db = MomSessionLocal() db = MomSessionLocal()
try: try:
if keyword.strip(): if keyword.strip():
sql = text( sql = text("""
f"""
SELECT category, COUNT(*) AS count SELECT category, COUNT(*) AS count
FROM material_base FROM material_base
WHERE is_enabled = TRUE WHERE is_enabled = TRUE
AND ({TYPE_FILTER})
AND (name ILIKE :kw OR spec_model ILIKE :kw) AND (name ILIKE :kw OR spec_model ILIKE :kw)
GROUP BY category GROUP BY category
ORDER BY category ORDER BY category
""" """)
)
result = db.execute(sql, {"kw": f"%{keyword.strip()}%"}) result = db.execute(sql, {"kw": f"%{keyword.strip()}%"})
else: else:
sql = text( sql = text("""
f"""
SELECT category, COUNT(*) AS count SELECT category, COUNT(*) AS count
FROM material_base FROM material_base
WHERE is_enabled = TRUE WHERE is_enabled = TRUE
AND ({TYPE_FILTER})
GROUP BY category GROUP BY category
ORDER BY category ORDER BY category
""" """)
)
result = db.execute(sql) result = db.execute(sql)
rows = result.fetchall() rows = result.fetchall()
@ -85,6 +78,7 @@ def get_material_items(
category: str = Query(..., description="物料分类"), category: str = Query(..., description="物料分类"),
keyword: str = Query("", description="分组内搜索"), keyword: str = Query("", description="分组内搜索"),
limit: int = Query(500, ge=1, le=9999), limit: int = Query(500, ge=1, le=9999),
current_user: dict = Depends(get_current_user),
): ):
""" """
获取指定 category 下的物料条目,前端展开手风琴时懒加载。 获取指定 category 下的物料条目,前端展开手风琴时懒加载。
@ -92,35 +86,29 @@ def get_material_items(
db = MomSessionLocal() db = MomSessionLocal()
try: try:
if keyword.strip(): if keyword.strip():
sql = text( sql = text("""
f"""
SELECT id, name, spec_model AS spec, category, material_type AS type, SELECT id, name, spec_model AS spec, category, material_type AS type,
COALESCE(unit, '') AS unit, is_enabled COALESCE(unit, '') AS unit, is_enabled
FROM material_base FROM material_base
WHERE is_enabled = TRUE WHERE is_enabled = TRUE
AND ({TYPE_FILTER})
AND category = :cat AND category = :cat
AND (name ILIKE :kw OR spec_model ILIKE :kw) AND (name ILIKE :kw OR spec_model ILIKE :kw)
ORDER BY name ORDER BY name
LIMIT :lim LIMIT :lim
""" """)
)
result = db.execute( result = db.execute(
sql, {"cat": category, "kw": f"%{keyword.strip()}%", "lim": limit} sql, {"cat": category, "kw": f"%{keyword.strip()}%", "lim": limit}
) )
else: else:
sql = text( sql = text("""
f"""
SELECT id, name, spec_model AS spec, category, material_type AS type, SELECT id, name, spec_model AS spec, category, material_type AS type,
COALESCE(unit, '') AS unit, is_enabled COALESCE(unit, '') AS unit, is_enabled
FROM material_base FROM material_base
WHERE is_enabled = TRUE WHERE is_enabled = TRUE
AND ({TYPE_FILTER})
AND category = :cat AND category = :cat
ORDER BY name ORDER BY name
LIMIT :lim LIMIT :lim
""" """)
)
result = db.execute(sql, {"cat": category, "lim": limit}) result = db.execute(sql, {"cat": category, "lim": limit})
rows = result.fetchall() rows = result.fetchall()

View File

@ -11,18 +11,26 @@ from app.models.notification import Notification
from app.models.task import Task from app.models.task import Task
from app.models.product import Product from app.models.product import Product
from app.schemas.notification import NotificationResponse, NotificationListResponse from app.schemas.notification import NotificationResponse, NotificationListResponse
from app.services.auth_service import get_current_user
router = APIRouter(prefix="/notifications", tags=["消息通知"]) router = APIRouter(prefix="/notifications", tags=["消息通知"])
@router.get("/", response_model=NotificationListResponse) @router.get("/", response_model=NotificationListResponse)
async def list_notifications( async def list_notifications(
user_id: str = Query(..., description="当前用户ID"),
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100), limit: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
): ):
"""获取当前用户的通知列表(按时间倒序)""" """
获取当前用户的通知列表(按时间倒序)。
安全user_id 强制从 JWT Token 解析,不接受查询参数,
杜绝通过篡改 user_id 参数越权查看他人通知。
"""
user_id: str = current_user.get("username", "") or current_user.get("sub", "")
# 总数 # 总数
count_stmt = select(func.count()).select_from(Notification).where( count_stmt = select(func.count()).select_from(Notification).where(
Notification.user_id == user_id Notification.user_id == user_id
@ -80,6 +88,7 @@ async def list_notifications(
async def mark_notification_read( async def mark_notification_read(
notification_id: str, notification_id: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
): ):
"""标记单条通知为已读""" """标记单条通知为已读"""
nid = uuid.UUID(notification_id) nid = uuid.UUID(notification_id)