feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化

Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列
测试: tests/test_phase_upgrades.py 97项全覆盖
CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式
修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
This commit is contained in:
Yuzhiran Dev
2026-05-17 06:56:53 +08:00
parent 301dc3e438
commit 9c37c9a574
45 changed files with 3707 additions and 1366 deletions
+124 -4
View File
@@ -3,16 +3,19 @@ from sqlalchemy.orm import Session
from typing import List
import bcrypt
from typing import Optional
from pydantic import BaseModel
from ..database import get_db
from ..models import User
from ..models import User, Topic, SystemConfig
from ..schemas import UserCreate, UserUpdate, UserResponse
from ..core.audit_logger import audit_log
import json
router = APIRouter(prefix="/api/admin", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token
from .auth import verify_token, org_filter
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
@@ -29,7 +32,11 @@ def list_users(
admin_user: User = Depends(get_current_admin)
):
"""获取用户列表(管理员)"""
users = db.query(User).all()
q = db.query(User)
of = org_filter(admin_user, User)
if of is not True:
q = q.filter(of)
users = q.all()
return [UserResponse.from_orm(u) for u in users]
@router.post("/users", response_model=UserResponse)
@@ -51,7 +58,8 @@ def create_user(
user = User(
username=user_data.username,
password_hash=hashed_password,
role=user_data.role or "user"
role=user_data.role or "user",
org_id=user_data.org_id or admin_user.org_id or "default"
)
db.add(user)
db.commit()
@@ -144,3 +152,115 @@ def delete_user(
db=db
)
return {"message": "删除成功"}
# ---------- Org Management ----------
class OrgCreate(BaseModel):
org_id: str
name: str
description: Optional[str] = None
class OrgUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
@router.get("/orgs")
def list_orgs(
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
distinct_orgs = db.query(User.org_id).distinct().all()
result = []
for (org_id,) in distinct_orgs:
if not org_id:
continue
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
meta = json.loads(cfg.value) if cfg and cfg.value else {}
user_count = db.query(User).filter(User.org_id == org_id).count()
topic_count = db.query(Topic).filter(Topic.org_id == org_id).count()
result.append({
"org_id": org_id,
"name": meta.get("name", org_id),
"description": meta.get("description", ""),
"user_count": user_count,
"topic_count": topic_count,
"is_default": org_id == "default"
})
return result
@router.post("/orgs")
def create_org(
data: OrgCreate,
request: Request,
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
existing = db.query(SystemConfig).filter(SystemConfig.key == f"org:{data.org_id}").first()
if existing:
raise HTTPException(status_code=400, detail="组织已存在")
cfg = SystemConfig(
key=f"org:{data.org_id}",
value=json.dumps({"name": data.name, "description": data.description or ""}, ensure_ascii=False),
description=f"Organization: {data.org_id}"
)
db.add(cfg)
db.commit()
audit_log(
action="create_org", user=admin_user,
resource_type="org", resource_id=data.org_id,
details=data.model_dump(),
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"org_id": data.org_id, "name": data.name, "description": data.description or ""}
@router.put("/orgs/{org_id}")
def update_org(
org_id: str,
data: OrgUpdate,
request: Request,
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
if not cfg:
raise HTTPException(status_code=404, detail="组织不存在")
meta = json.loads(cfg.value) if cfg.value else {}
if data.name is not None:
meta["name"] = data.name
if data.description is not None:
meta["description"] = data.description
cfg.value = json.dumps(meta, ensure_ascii=False)
db.commit()
audit_log(
action="update_org", user=admin_user,
resource_type="org", resource_id=org_id,
details=data.model_dump(exclude_unset=True),
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"org_id": org_id, **meta}
@router.delete("/orgs/{org_id}")
def delete_org(
org_id: str,
request: Request,
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
if org_id == "default":
raise HTTPException(status_code=400, detail="不能删除默认组织")
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
if cfg:
db.delete(cfg)
db.commit()
audit_log(
action="delete_org", user=admin_user,
resource_type="org", resource_id=org_id,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"message": "删除成功"}