|
@@ -1,7 +1,11 @@
|
|
|
"""管理端 API:工作台、台账办理、配置、用户/角色、审计、附件上传。"""
|
|
"""管理端 API:工作台、台账办理、配置、用户/角色、审计、附件上传。"""
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import logging
|
|
|
from typing import Annotated, Any, Dict, List, Optional
|
|
from typing import Annotated, Any, Dict, List, Optional
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
|
|
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
|
|
|
|
|
+from fastapi.responses import StreamingResponse
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
from app.core import storage
|
|
from app.core import storage
|
|
@@ -9,7 +13,8 @@ from app.core.database import get_db
|
|
|
from app.core.deps import get_current_user, require_menu
|
|
from app.core.deps import get_current_user, require_menu
|
|
|
from app.core.response import AppHTTPError, success_response
|
|
from app.core.response import AppHTTPError, success_response
|
|
|
from app.core.security import hash_password
|
|
from app.core.security import hash_password
|
|
|
-from app.core.storage import StorageError
|
|
|
|
|
|
|
+from app.core.storage import StorageError, _normalize_folder
|
|
|
|
|
+from app.core.upload_validate import validate_image_upload, validate_upload
|
|
|
from app.crud import attachment as attachment_crud
|
|
from app.crud import attachment as attachment_crud
|
|
|
from app.crud import menu as menu_crud
|
|
from app.crud import menu as menu_crud
|
|
|
from app.crud import oper_log as oper_log_crud
|
|
from app.crud import oper_log as oper_log_crud
|
|
@@ -25,6 +30,8 @@ from app.schemas.system import (
|
|
|
)
|
|
)
|
|
|
from app.services import admin_ledger_service, auth_service, config_service, dashboard_service
|
|
from app.services import admin_ledger_service, auth_service, config_service, dashboard_service
|
|
|
|
|
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
+
|
|
|
router = APIRouter(prefix="/web/admin", tags=["admin"])
|
|
router = APIRouter(prefix="/web/admin", tags=["admin"])
|
|
|
|
|
|
|
|
|
|
|
|
@@ -198,6 +205,7 @@ def put_hotline(
|
|
|
db,
|
|
db,
|
|
|
user,
|
|
user,
|
|
|
phone=body.phone,
|
|
phone=body.phone,
|
|
|
|
|
+ phones=body.phones,
|
|
|
email=body.email,
|
|
email=body.email,
|
|
|
address=body.address,
|
|
address=body.address,
|
|
|
work_time=body.work_time,
|
|
work_time=body.work_time,
|
|
@@ -239,9 +247,17 @@ async def admin_upload(
|
|
|
):
|
|
):
|
|
|
if not file or not file.filename:
|
|
if not file or not file.filename:
|
|
|
raise AppHTTPError(400, "请选择文件")
|
|
raise AppHTTPError(400, "请选择文件")
|
|
|
|
|
+ try:
|
|
|
|
|
+ folder_norm = _normalize_folder(folder or "")
|
|
|
|
|
+ except StorageError as exc:
|
|
|
|
|
+ raise AppHTTPError(400, exc.message) from exc
|
|
|
content = await file.read()
|
|
content = await file.read()
|
|
|
|
|
+ if folder_norm == "qrcode":
|
|
|
|
|
+ validate_image_upload(file.filename, content)
|
|
|
|
|
+ else:
|
|
|
|
|
+ validate_upload(file.filename, content)
|
|
|
try:
|
|
try:
|
|
|
- stored = storage.upload_file(content, file.filename, folder or "")
|
|
|
|
|
|
|
+ stored = storage.upload_file(content, file.filename, folder_norm)
|
|
|
except StorageError as exc:
|
|
except StorageError as exc:
|
|
|
raise AppHTTPError(400, exc.message) from exc
|
|
raise AppHTTPError(400, exc.message) from exc
|
|
|
row = attachment_crud.create_attachment(
|
|
row = attachment_crud.create_attachment(
|
|
@@ -261,6 +277,25 @@ async def admin_upload(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@router.get("/attachment/file")
|
|
|
|
|
+def download_attachment_file(
|
|
|
|
|
+ user: Annotated[SysUser, Depends(get_current_user)],
|
|
|
|
|
+ url: str = Query(...),
|
|
|
|
|
+):
|
|
|
|
|
+ try:
|
|
|
|
|
+ stream, content_type, filename, inline = storage.open_file(url)
|
|
|
|
|
+ except StorageError as exc:
|
|
|
|
|
+ raise AppHTTPError(400, exc.message) from exc
|
|
|
|
|
+ logger.info("attachment_proxy user_id=%s filename=%s", user.id, filename)
|
|
|
|
|
+ disposition = "inline" if inline else "attachment"
|
|
|
|
|
+ # ASCII-safe filename fallback for Content-Disposition
|
|
|
|
|
+ safe_name = filename.encode("ascii", "ignore").decode() or "download"
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ "X-Content-Type-Options": "nosniff",
|
|
|
|
|
+ "Content-Disposition": f'{disposition}; filename="{safe_name}"',
|
|
|
|
|
+ }
|
|
|
|
|
+ return StreamingResponse(stream, media_type=content_type, headers=headers)
|
|
|
|
|
+
|
|
|
@router.get("/users")
|
|
@router.get("/users")
|
|
|
def list_users(
|
|
def list_users(
|
|
|
db: Annotated[Session, Depends(get_db)],
|
|
db: Annotated[Session, Depends(get_db)],
|