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:
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user