提交
This commit is contained in:
1 parent
c87732fcd5
commit
058811ae77
107 files changed
+16983
-12758
No files matched your search
@@ -6,7 +6,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Optional, Tuple
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import parse_qsl, urlencode
|
||||
|
||||
import redis.asyncio as redis
|
||||
from jose import jwt
|
||||
@@ -102,10 +102,56 @@ def _json_error_response(code: int, message: str) -> Response:
|
||||
return Response(content=json.dumps(body, ensure_ascii=False), media_type="application/json", status_code=200)
|
||||
|
||||
|
||||
async def _coerce_post_method_override_to_get(request: Request, call_next) -> Response:
|
||||
"""前端 Axios 在开启加密时会把 GET 改为 POST 并带 X-HTTP-Method-Override: GET。
|
||||
|
||||
当后端未启用 API_ENCRYPT_ENABLED 时,原逻辑直接放行,路由仍按 POST 匹配,导致仅注册了 GET 的接口返回 405。
|
||||
此处在不解密的情况下,将此类请求还原为 GET(空或明文 JSON 体);若仍为 enc=1 密文则提示配置不一致。
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return await call_next(request)
|
||||
override = (request.headers.get("x-http-method-override") or "").upper()
|
||||
if override != "GET":
|
||||
return await call_next(request)
|
||||
|
||||
body_bytes = await request.body()
|
||||
req_json: dict = {}
|
||||
if body_bytes:
|
||||
try:
|
||||
raw = json.loads(body_bytes.decode("utf-8"))
|
||||
req_json = raw if isinstance(raw, dict) else {}
|
||||
except Exception:
|
||||
req_json = {}
|
||||
|
||||
if int(req_json.get("enc", 0) or 0) == 1:
|
||||
return _json_error_response(
|
||||
400,
|
||||
"客户端已开启接口加密(enc=1),但服务端未启用 API_ENCRYPT_ENABLED;"
|
||||
"请在后端开启加密并配置 API_ENCRYPT_FIXED_SECRET,或将前端 VITE_API_ENCRYPT_ENABLED 设为 false。",
|
||||
)
|
||||
|
||||
scope = dict(request.scope)
|
||||
scope["method"] = "GET"
|
||||
payload = req_json if isinstance(req_json, dict) else {}
|
||||
q0 = (request.scope.get("query_string") or b"").decode("utf-8")
|
||||
pairs = dict(parse_qsl(q0, keep_blank_values=True))
|
||||
for k, v in payload.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
continue
|
||||
pairs[k] = "" if v is None else str(v)
|
||||
scope["query_string"] = urlencode(pairs, doseq=True).encode("utf-8") if pairs else b""
|
||||
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
new_request = Request(scope, receive)
|
||||
return await call_next(new_request)
|
||||
|
||||
|
||||
class ApiCryptoMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
if not settings.API_ENCRYPT_ENABLED:
|
||||
return await call_next(request)
|
||||
return await _coerce_post_method_override_to_get(request, call_next)
|
||||
if not settings.API_ENCRYPT_FIXED_SECRET:
|
||||
if settings.API_ENCRYPT_FAIL_OPEN:
|
||||
logger.warning("api crypto bypassed: API_ENCRYPT_FIXED_SECRET is empty")
|
||||
|
||||
+17
-11
@@ -12,10 +12,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.security import verify_password
|
||||
from app.models.models import SysUser
|
||||
from app.crud.user import user_crud
|
||||
from app.core.utils import generate_uuid
|
||||
from app.services.web_auth_roles import resolve_web_role_for_user
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/public/login")
|
||||
|
||||
@@ -197,27 +196,34 @@ async def get_current_user_by_token(
|
||||
)
|
||||
|
||||
user = user_crud.get(db, user_id)
|
||||
if not user or user.is_del == 1:
|
||||
if not user or getattr(user, "del_flag", 0) != 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在或已删除",
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
if getattr(user, "status", "1") != "1":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户已被禁用",
|
||||
)
|
||||
|
||||
web_role = resolve_web_role_for_user(db, user.id)
|
||||
|
||||
phone = (getattr(user, "phone_no", None) or "").strip() or None
|
||||
email = (getattr(user, "email", None) or "").strip() or None
|
||||
avatar = getattr(user, "avatar", None)
|
||||
|
||||
return {
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"real_name": user.real_name,
|
||||
"role": user.role,
|
||||
"region_id": user.region_id,
|
||||
"phone": getattr(user, "phone", None),
|
||||
"avatar_url": getattr(user, "avatar_url", None),
|
||||
"is_active": getattr(user, "is_active", True),
|
||||
"username": user.user_no,
|
||||
"real_name": user.nick_name,
|
||||
"role": web_role,
|
||||
"region_id": user.dept_id,
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"avatar_url": avatar,
|
||||
"is_active": user.status == "1",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME: str = "中科兴皖一体化门户"
|
||||
APP_NAME: str = "zkxw 管理骨架"
|
||||
DEBUG: bool = True
|
||||
API_V1_PREFIX: str = "/api"
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/zkxw"
|
||||
DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang"
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string"
|
||||
@@ -100,6 +100,11 @@ class Settings(BaseSettings):
|
||||
WX_TEMPLATE_REFUND_RESULT: str = "" # 退款结果通知模板ID
|
||||
WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID
|
||||
|
||||
# 与 docs/sql/zkxw-requirements-v0.3-schema.sql 一致:登录按租户 + sys_user.user_no
|
||||
DEFAULT_TENANT_NO: str = "zkxw"
|
||||
# 用户未绑定 sys_user_role 时,Web 管理端默认角色(建议 super_admin 便于开发全菜单)
|
||||
WEB_AUTH_DEFAULT_ROLE: str = "super_admin"
|
||||
|
||||
# 登录 RSA:私钥 PEM(多实例/负载均衡须配置同一私钥;留空则进程启动时临时生成,仅适合单机开发)
|
||||
LOGIN_RSA_PRIVATE_KEY_PEM: str = ""
|
||||
# 是否允许明文 username/password 登录(仅建议本地排障临时开启;生产务必 false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""菜单 code 权限守卫"""
|
||||
"""菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)"""
|
||||
from typing import Callable, Set
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.auth import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.models.models import SysUserMenuScope
|
||||
from app.services.web_auth_roles import iter_user_menu_perm_codes
|
||||
|
||||
ADMIN_ROLES = {"admin", "super_admin"}
|
||||
SUPER_ADMIN_ROLE = "super_admin"
|
||||
@@ -21,18 +21,8 @@ def require_admin_or_super_admin(current_user: dict = Depends(get_current_user))
|
||||
|
||||
|
||||
def get_user_menu_codes(db: Session, user_id: str) -> Set[str]:
|
||||
"""读取用户授权的菜单 code 集合。"""
|
||||
if not user_id:
|
||||
return set()
|
||||
rows = (
|
||||
db.query(SysUserMenuScope.menu_code)
|
||||
.filter(
|
||||
SysUserMenuScope.user_id == user_id,
|
||||
SysUserMenuScope.is_del == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return {str(row[0]).strip() for row in rows if row and row[0]}
|
||||
"""读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。"""
|
||||
return iter_user_menu_perm_codes(db, user_id or "")
|
||||
|
||||
|
||||
def require_menu_code(menu_code: str) -> Callable:
|
||||
|
||||
Reference in new issue
Block a user