feat: add admin session rbac foundation
This commit is contained in:
1 parent
8f000811a8
commit
2cdf7f9d7a
22 files changed
+1945
-68
No files matched your search
@@ -7,10 +7,10 @@ import re
|
||||
from email.utils import formatdate
|
||||
from uuid import uuid4
|
||||
from urllib.parse import quote, urlsplit, urlunsplit
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile, status
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, Response, UploadFile, status
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from ..auth import create_token, get_actor_id, require_admin, verify_password
|
||||
from ..auth import get_actor_id, issue_admin_session, require_admin, rotate_admin_session, verify_password
|
||||
from ..api_response import success_response
|
||||
from ..config import get_settings
|
||||
from ..database import get_db
|
||||
@@ -37,6 +37,8 @@ from ..models import (
|
||||
WanfaRoute,
|
||||
utc_now,
|
||||
)
|
||||
from ..redis_session import AdminSessionStore, RedisUnavailableError, get_admin_session_store, hash_refresh_token
|
||||
from ..rbac import apply_data_scope, build_admin_permission_context, is_within_data_scope, require_admin_permission
|
||||
from ..schemas import (
|
||||
LeadQuery,
|
||||
LeadStatus,
|
||||
@@ -434,8 +436,8 @@ def wanfa_route_dict(route: WanfaRoute) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def wanfa_category_dict(category: WanfaCategory) -> dict:
|
||||
routes = sorted(category.routes or [], key=lambda item: item.sortOrder)
|
||||
def wanfa_category_dict(category: WanfaCategory, scoped_routes=None) -> dict:
|
||||
routes = sorted(category.routes if scoped_routes is None else scoped_routes or [], key=lambda item: item.sortOrder)
|
||||
return {
|
||||
"id": category.id,
|
||||
"label": category.label,
|
||||
@@ -490,6 +492,16 @@ def home_error(status_code: int, message: str, code: str, details: dict | None =
|
||||
raise HTTPException(status_code=status_code, detail={"message": message, "code": code, "details": details or {}})
|
||||
|
||||
|
||||
def assign_created_by(item, user_id: str | None) -> None:
|
||||
if user_id and hasattr(item, "createdById") and not getattr(item, "createdById", None):
|
||||
item.createdById = user_id
|
||||
|
||||
|
||||
def ensure_data_scope(item, user: AdminUser, db: Session, label: str) -> None:
|
||||
if not is_within_data_scope(item, user, db):
|
||||
raise HTTPException(status_code=404, detail=f"{label}不存在")
|
||||
|
||||
|
||||
def home_item_or_error(db: Session, model, item_id: str, label: str, code: str):
|
||||
item = db.get(model, item_id)
|
||||
if not item:
|
||||
@@ -501,6 +513,10 @@ def home_items(db: Session, model) -> list:
|
||||
return db.scalars(select(model).order_by(model.sortOrder.asc())).all()
|
||||
|
||||
|
||||
def scoped_items(db: Session, model, user: AdminUser, order_column) -> list:
|
||||
return db.scalars(apply_data_scope(select(model).order_by(order_column), model, user, db)).all()
|
||||
|
||||
|
||||
def validate_home_order(item_ids: list[str], current_ids: list[str]) -> None:
|
||||
if len(item_ids) != len(current_ids) or len(set(item_ids)) != len(item_ids) or set(item_ids) != set(current_ids):
|
||||
home_error(400, "排序项目必须完整且不能重复", "HOME_REORDER_INVALID")
|
||||
@@ -519,6 +535,7 @@ def create_home_item(db: Session, request: Request, body, model, entity: str):
|
||||
if body.sortOrder is None:
|
||||
payload["sortOrder"] = next_site_sort_order(db, model)
|
||||
item = model(**payload)
|
||||
assign_created_by(item, get_actor_id(request))
|
||||
db.add(item)
|
||||
db.flush()
|
||||
after = model_dict(item)
|
||||
@@ -752,6 +769,7 @@ def create_home_wanfa_recommendation(
|
||||
isActive=body.isActive,
|
||||
sortOrder=body.sortOrder if body.sortOrder is not None else ((next_sort + 1) if next_sort is not None else 0),
|
||||
)
|
||||
assign_created_by(recommendation, get_actor_id(request))
|
||||
db.add(recommendation)
|
||||
db.flush()
|
||||
after = home_wanfa_recommendation_dict(recommendation)
|
||||
@@ -878,8 +896,8 @@ def validate_detail_order(item_ids: list[str], current_ids: list[str]) -> None:
|
||||
|
||||
|
||||
@router.get("/details")
|
||||
def list_details(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
details = db.scalars(select(DetailRecord).order_by(DetailRecord.sortOrder.asc())).all()
|
||||
def list_details(_user: AdminUser = Depends(require_admin_permission("admin:wanfa:read")), db: Session = Depends(get_db)):
|
||||
details = db.scalars(apply_data_scope(select(DetailRecord), DetailRecord, _user, db).order_by(DetailRecord.sortOrder.asc())).all()
|
||||
return success_response({"details": [detail_record_dict(detail) for detail in details]})
|
||||
|
||||
|
||||
@@ -901,8 +919,11 @@ def reorder_details(
|
||||
|
||||
|
||||
@router.get("/details/{detail_id}")
|
||||
def get_detail(detail_id: str, _user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
return success_response(detail_record_dict(detail_or_error(db, detail_id)))
|
||||
def get_detail(detail_id: str, _user: AdminUser = Depends(require_admin_permission("admin:wanfa:read")), db: Session = Depends(get_db)):
|
||||
detail = detail_or_error(db, detail_id)
|
||||
if not is_within_data_scope(detail, _user, db):
|
||||
raise HTTPException(status_code=404, detail="详情不存在")
|
||||
return success_response(detail_record_dict(detail))
|
||||
|
||||
|
||||
@router.post("/details", status_code=status.HTTP_201_CREATED)
|
||||
@@ -918,6 +939,7 @@ def create_detail(
|
||||
**body.model_dump(exclude={"sortOrder"}),
|
||||
sortOrder=body.sortOrder if body.sortOrder is not None else next_site_sort_order(db, DetailRecord),
|
||||
)
|
||||
assign_created_by(detail, get_actor_id(request))
|
||||
db.add(detail)
|
||||
db.flush()
|
||||
after = detail_record_dict(detail)
|
||||
@@ -935,6 +957,7 @@ def update_detail(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
detail = detail_or_error(db, detail_id)
|
||||
ensure_data_scope(detail, _user, db, "详情")
|
||||
before = detail_record_dict(detail)
|
||||
if "key" in body.model_fields_set:
|
||||
detail_key_conflict(db, body.key or "", detail.id)
|
||||
@@ -957,6 +980,7 @@ def delete_detail(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
detail = detail_or_error(db, detail_id)
|
||||
ensure_data_scope(detail, _user, db, "详情")
|
||||
before = detail_record_dict(detail)
|
||||
db.delete(detail)
|
||||
remaining = db.scalars(select(DetailRecord).where(DetailRecord.id != detail.id).order_by(DetailRecord.sortOrder.asc())).all()
|
||||
@@ -968,11 +992,21 @@ def delete_detail(
|
||||
|
||||
|
||||
@router.get("/wanfa/categories")
|
||||
def list_wanfa_categories(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
def list_wanfa_categories(_user: AdminUser = Depends(require_admin_permission("admin:wanfa:read")), db: Session = Depends(get_db)):
|
||||
categories = db.scalars(
|
||||
select(WanfaCategory).options(selectinload(WanfaCategory.routes)).order_by(WanfaCategory.sortOrder.asc())
|
||||
apply_data_scope(select(WanfaCategory), WanfaCategory, _user, db).options(selectinload(WanfaCategory.routes)).order_by(WanfaCategory.sortOrder.asc())
|
||||
).all()
|
||||
return success_response({"categories": [wanfa_category_dict(category) for category in categories]})
|
||||
return success_response(
|
||||
{
|
||||
"categories": [
|
||||
wanfa_category_dict(
|
||||
category,
|
||||
[route for route in category.routes or [] if is_within_data_scope(route, _user, db)],
|
||||
)
|
||||
for category in categories
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/wanfa/categories", status_code=status.HTTP_201_CREATED)
|
||||
@@ -983,6 +1017,7 @@ def create_wanfa_category(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
category = WanfaCategory(label=body.label, sortOrder=next_site_sort_order(db, WanfaCategory))
|
||||
assign_created_by(category, get_actor_id(request))
|
||||
db.add(category)
|
||||
db.flush()
|
||||
after = wanfa_category_dict(category)
|
||||
@@ -1017,6 +1052,7 @@ def update_wanfa_category(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
category = wanfa_category_or_error(db, category_id)
|
||||
ensure_data_scope(category, _user, db, "玩法分类")
|
||||
before = wanfa_category_dict(category)
|
||||
if "label" in body.model_fields_set:
|
||||
category.label = body.label
|
||||
@@ -1035,6 +1071,7 @@ def delete_wanfa_category(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
category = wanfa_category_or_error(db, category_id)
|
||||
ensure_data_scope(category, _user, db, "玩法分类")
|
||||
if category.routes:
|
||||
site_config_error(409, "玩法分类下仍有路线,不能删除", "WANFA_CATEGORY_NOT_EMPTY", {"categoryId": category.id})
|
||||
recommendation_count = db.scalar(
|
||||
@@ -1065,6 +1102,7 @@ def create_wanfa_route(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
category = wanfa_category_or_error(db, category_id)
|
||||
ensure_data_scope(category, _user, db, "玩法分类")
|
||||
next_sort = db.scalar(
|
||||
select(func.max(WanfaRoute.sortOrder)).where(WanfaRoute.categoryId == category.id)
|
||||
)
|
||||
@@ -1077,6 +1115,7 @@ def create_wanfa_route(
|
||||
demandKeyword=body.demandKeyword,
|
||||
sortOrder=(next_sort + 1) if next_sort is not None else 0,
|
||||
)
|
||||
assign_created_by(route, get_actor_id(request))
|
||||
db.add(route)
|
||||
db.flush()
|
||||
after = wanfa_route_dict(route)
|
||||
@@ -1114,7 +1153,9 @@ def update_wanfa_route(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
category = wanfa_category_or_error(db, category_id)
|
||||
ensure_data_scope(category, _user, db, "玩法分类")
|
||||
route = wanfa_route_or_error(db, category, route_id)
|
||||
ensure_data_scope(route, _user, db, "玩法路线")
|
||||
before = wanfa_route_dict(route)
|
||||
for field in ("title", "subtitle", "image", "routeCount", "demandKeyword"):
|
||||
if field in body.model_fields_set:
|
||||
@@ -1135,7 +1176,9 @@ def delete_wanfa_route(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
category = wanfa_category_or_error(db, category_id)
|
||||
ensure_data_scope(category, _user, db, "玩法分类")
|
||||
route = wanfa_route_or_error(db, category, route_id)
|
||||
ensure_data_scope(route, _user, db, "玩法路线")
|
||||
before = wanfa_route_dict(route)
|
||||
db.delete(route)
|
||||
audit(db, get_actor_id(request), "delete", "wanfa_route", route.id, before=before)
|
||||
@@ -1144,8 +1187,8 @@ def delete_wanfa_route(
|
||||
|
||||
|
||||
@router.get("/concierge/advisors")
|
||||
def list_concierge_advisors(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
advisors = db.scalars(select(ConciergeAdvisor).order_by(ConciergeAdvisor.sortOrder.asc())).all()
|
||||
def list_concierge_advisors(_user: AdminUser = Depends(require_admin_permission("admin:concierge:read")), db: Session = Depends(get_db)):
|
||||
advisors = db.scalars(apply_data_scope(select(ConciergeAdvisor), ConciergeAdvisor, _user, db).order_by(ConciergeAdvisor.sortOrder.asc())).all()
|
||||
return success_response({"advisors": [concierge_advisor_dict(advisor) for advisor in advisors]})
|
||||
|
||||
|
||||
@@ -1161,6 +1204,7 @@ def create_concierge_advisor(
|
||||
if body.sortOrder is None:
|
||||
payload["sortOrder"] = next_site_sort_order(db, ConciergeAdvisor)
|
||||
advisor = ConciergeAdvisor(**payload)
|
||||
assign_created_by(advisor, get_actor_id(request))
|
||||
db.add(advisor)
|
||||
db.flush()
|
||||
after = concierge_advisor_dict(advisor)
|
||||
@@ -1195,6 +1239,7 @@ def update_concierge_advisor(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
advisor = concierge_advisor_or_error(db, advisor_id)
|
||||
ensure_data_scope(advisor, _user, db, "管家顾问")
|
||||
before = concierge_advisor_dict(advisor)
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
if "details" in payload:
|
||||
@@ -1216,6 +1261,7 @@ def delete_concierge_advisor(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
advisor = concierge_advisor_or_error(db, advisor_id)
|
||||
ensure_data_scope(advisor, _user, db, "管家顾问")
|
||||
before = concierge_advisor_dict(advisor)
|
||||
db.delete(advisor)
|
||||
db.flush()
|
||||
@@ -1229,66 +1275,175 @@ def delete_concierge_advisor(
|
||||
|
||||
|
||||
@router.post("/auth/login")
|
||||
def login(body: LoginIn, db: Session = Depends(get_db)):
|
||||
def login(
|
||||
body: LoginIn,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
store: AdminSessionStore = Depends(get_admin_session_store),
|
||||
):
|
||||
settings = get_settings()
|
||||
client_host = request.client.host if request.client else "unknown"
|
||||
identity = f"{client_host}:{body.email.strip().lower()}"
|
||||
try:
|
||||
if not store.allow_login_attempt(identity, settings.admin_login_rate_limit, settings.admin_login_rate_window_seconds):
|
||||
raise HTTPException(status_code=429, detail="登录尝试过于频繁,请稍后再试", headers={"Retry-After": str(settings.admin_login_rate_window_seconds)})
|
||||
except RedisUnavailableError as exc:
|
||||
raise HTTPException(status_code=503, detail="后台会话服务暂时不可用") from exc
|
||||
user = db.scalar(select(AdminUser).where(AdminUser.email == body.email))
|
||||
if not user or not user.isActive or not verify_password(body.password, user.passwordHash):
|
||||
raise HTTPException(status_code=401, detail="账号或密码错误")
|
||||
try:
|
||||
access_token, refresh_token, expires_in = issue_admin_session(user, store)
|
||||
except RedisUnavailableError as exc:
|
||||
raise HTTPException(status_code=503, detail="后台会话服务暂时不可用") from exc
|
||||
result = success_response(
|
||||
{
|
||||
"token": access_token,
|
||||
"accessToken": access_token,
|
||||
"expiresIn": expires_in,
|
||||
"user": {"id": user.id, "email": user.email, "name": user.name, "role": user.role},
|
||||
}
|
||||
)
|
||||
result.set_cookie(
|
||||
key=settings.admin_refresh_cookie_name,
|
||||
value=refresh_token,
|
||||
httponly=True,
|
||||
secure=settings.admin_refresh_cookie_secure,
|
||||
samesite="lax",
|
||||
path=settings.admin_refresh_cookie_path,
|
||||
max_age=settings.admin_refresh_expires_days * 24 * 60 * 60,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/auth/refresh")
|
||||
def refresh_admin_session(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
store: AdminSessionStore = Depends(get_admin_session_store),
|
||||
):
|
||||
settings = get_settings()
|
||||
refresh_token = request.cookies.get(settings.admin_refresh_cookie_name)
|
||||
if not refresh_token:
|
||||
raise HTTPException(status_code=401, detail="请先登录后台")
|
||||
try:
|
||||
record = store.get_by_refresh_hash(hash_refresh_token(refresh_token))
|
||||
if not record:
|
||||
raise HTTPException(status_code=401, detail="请先登录后台")
|
||||
user = db.get(AdminUser, record.user_id)
|
||||
if not user or not user.isActive:
|
||||
store.revoke(record.session_id, record.refresh_hash)
|
||||
raise HTTPException(status_code=401, detail="请先登录后台")
|
||||
tokens = rotate_admin_session(user, record, store)
|
||||
except RedisUnavailableError as exc:
|
||||
raise HTTPException(status_code=503, detail="后台会话服务暂时不可用") from exc
|
||||
if not tokens:
|
||||
raise HTTPException(status_code=401, detail="刷新令牌已失效")
|
||||
access_token, next_refresh_token, expires_in = tokens
|
||||
result = success_response(
|
||||
{
|
||||
"token": access_token,
|
||||
"accessToken": access_token,
|
||||
"expiresIn": expires_in,
|
||||
"user": {"id": user.id, "email": user.email, "name": user.name, "role": user.role},
|
||||
}
|
||||
)
|
||||
result.set_cookie(
|
||||
key=settings.admin_refresh_cookie_name,
|
||||
value=next_refresh_token,
|
||||
httponly=True,
|
||||
secure=settings.admin_refresh_cookie_secure,
|
||||
samesite="lax",
|
||||
path=settings.admin_refresh_cookie_path,
|
||||
max_age=settings.admin_refresh_expires_days * 24 * 60 * 60,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/auth/logout")
|
||||
def logout_admin_session(
|
||||
request: Request,
|
||||
store: AdminSessionStore = Depends(get_admin_session_store),
|
||||
):
|
||||
settings = get_settings()
|
||||
refresh_token = request.cookies.get(settings.admin_refresh_cookie_name)
|
||||
try:
|
||||
if refresh_token:
|
||||
record = store.get_by_refresh_hash(hash_refresh_token(refresh_token))
|
||||
if record:
|
||||
store.revoke(record.session_id, record.refresh_hash)
|
||||
except RedisUnavailableError as exc:
|
||||
raise HTTPException(status_code=503, detail="后台会话服务暂时不可用") from exc
|
||||
result = success_response({"ok": True})
|
||||
result.delete_cookie(key=settings.admin_refresh_cookie_name, path=settings.admin_refresh_cookie_path)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
def me(
|
||||
user: AdminUser = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
store: AdminSessionStore = Depends(get_admin_session_store),
|
||||
):
|
||||
return success_response(
|
||||
{
|
||||
"token": create_token(user),
|
||||
"user": {"id": user.id, "email": user.email, "name": user.name, "role": user.role},
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
**build_admin_permission_context(user, db, store),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
def me(user: AdminUser = Depends(require_admin)):
|
||||
return success_response({"id": user.id, "email": user.email, "name": user.name, "role": user.role})
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
def dashboard(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
def dashboard(_user: AdminUser = Depends(require_admin_permission("admin:dashboard")), db: Session = Depends(get_db)):
|
||||
lead_query = apply_data_scope(select(Lead), Lead, _user, db)
|
||||
stats = {
|
||||
"newLeadCount": db.scalar(select(func.count()).select_from(Lead).where(Lead.status == "new")),
|
||||
"leadCount": db.scalar(select(func.count()).select_from(Lead)),
|
||||
"newLeadCount": db.scalar(lead_query.where(Lead.status == "new").with_only_columns(func.count())),
|
||||
"leadCount": db.scalar(lead_query.with_only_columns(func.count())),
|
||||
}
|
||||
recent = db.scalars(
|
||||
select(Lead).options(selectinload(Lead.assignedUser)).order_by(Lead.createdAt.desc()).limit(5)
|
||||
apply_data_scope(select(Lead), Lead, _user, db)
|
||||
.options(selectinload(Lead.assignedUser))
|
||||
.order_by(Lead.createdAt.desc())
|
||||
.limit(5)
|
||||
).all()
|
||||
return success_response({"stats": stats, "recentLeads": [lead_dict(lead) for lead in recent]})
|
||||
|
||||
|
||||
@router.get("/site-config")
|
||||
def admin_site_config(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
def admin_site_config(_user: AdminUser = Depends(require_admin_permission("admin:site-config")), db: Session = Depends(get_db)):
|
||||
return success_response(
|
||||
{
|
||||
"heroSlides": [
|
||||
hero_slide_admin_dict(item)
|
||||
for item in db.scalars(select(HeroSlide).order_by(HeroSlide.sortOrder.asc())).all()
|
||||
for item in scoped_items(db, HeroSlide, _user, HeroSlide.sortOrder.asc())
|
||||
],
|
||||
"destinationHero": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(DestinationHero).order_by(DestinationHero.sortOrder.asc())).all()
|
||||
for item in scoped_items(db, DestinationHero, _user, DestinationHero.sortOrder.asc())
|
||||
],
|
||||
"vehicleOptions": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(VehicleOption).order_by(VehicleOption.sortOrder.asc())).all()
|
||||
for item in scoped_items(db, VehicleOption, _user, VehicleOption.sortOrder.asc())
|
||||
],
|
||||
"vehicleService": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(VehicleServiceConfig).order_by(VehicleServiceConfig.createdAt.asc())).all()
|
||||
for item in scoped_items(db, VehicleServiceConfig, _user, VehicleServiceConfig.createdAt.asc())
|
||||
],
|
||||
"demandHero": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(DemandHero).order_by(DemandHero.sortOrder.asc())).all()
|
||||
for item in scoped_items(db, DemandHero, _user, DemandHero.sortOrder.asc())
|
||||
],
|
||||
"demandFeatureCards": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(DemandFeatureCard).order_by(DemandFeatureCard.sortOrder.asc())).all()
|
||||
for item in scoped_items(db, DemandFeatureCard, _user, DemandFeatureCard.sortOrder.asc())
|
||||
],
|
||||
"demandForm": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(DemandForm).order_by(DemandForm.createdAt.asc())).all()
|
||||
for item in scoped_items(db, DemandForm, _user, DemandForm.createdAt.asc())
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -1308,6 +1463,7 @@ def create_site_config(
|
||||
if config.get("singleton") and module_items(db, config):
|
||||
site_config_error(409, "单例配置已存在", "MODULE_CONFIG_SINGLETON_EXISTS", {"module": module})
|
||||
item = config["model"](**site_create_payload(module, config, body, db))
|
||||
assign_created_by(item, get_actor_id(request))
|
||||
db.add(item)
|
||||
db.flush()
|
||||
after = site_item_dict(module, item)
|
||||
@@ -1403,7 +1559,7 @@ def list_leads(
|
||||
createdFrom: str | None = None,
|
||||
createdTo: str | None = None,
|
||||
take: int = Query(default=100, ge=1, le=200),
|
||||
_user: AdminUser = Depends(require_admin),
|
||||
_user: AdminUser = Depends(require_admin_permission("admin:leads:read")),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = LeadQuery(
|
||||
@@ -1440,14 +1596,14 @@ def list_leads(
|
||||
Lead.note.ilike(pattern),
|
||||
)
|
||||
).distinct()
|
||||
leads = db.scalars(stmt).all()
|
||||
leads = db.scalars(apply_data_scope(stmt, Lead, _user, db)).all()
|
||||
return success_response({"items": [lead_dict(lead) for lead in leads]})
|
||||
|
||||
|
||||
@router.patch("/leads/{lead_id}/status")
|
||||
def update_lead_status(lead_id: str, body: LeadStatusIn, request: Request, _user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
def update_lead_status(lead_id: str, body: LeadStatusIn, request: Request, _user: AdminUser = Depends(require_admin_permission("admin:leads:update")), db: Session = Depends(get_db)):
|
||||
lead = db.get(Lead, lead_id)
|
||||
if not lead:
|
||||
if not lead or not is_within_data_scope(lead, _user, db):
|
||||
raise HTTPException(status_code=404, detail="线索不存在")
|
||||
before = model_dict(lead)
|
||||
lead.status = body.status
|
||||
@@ -1458,8 +1614,8 @@ def update_lead_status(lead_id: str, body: LeadStatusIn, request: Request, _user
|
||||
|
||||
|
||||
@router.get("/media-assets")
|
||||
def list_media_assets(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
assets = db.scalars(select(MediaAsset).order_by(MediaAsset.createdAt.desc()).limit(200)).all()
|
||||
def list_media_assets(_user: AdminUser = Depends(require_admin_permission("admin:media:read")), db: Session = Depends(get_db)):
|
||||
assets = db.scalars(apply_data_scope(select(MediaAsset), MediaAsset, _user, db).order_by(MediaAsset.createdAt.desc()).limit(200)).all()
|
||||
return success_response({"items": [model_dict(asset) for asset in assets]})
|
||||
|
||||
|
||||
@@ -1482,6 +1638,7 @@ def upload_media_asset(
|
||||
sizeBytes=size_bytes,
|
||||
group=safe_group,
|
||||
)
|
||||
assign_created_by(asset, get_actor_id(request))
|
||||
db.add(asset)
|
||||
db.flush()
|
||||
after = model_dict(asset)
|
||||
|
||||
Reference in new issue
Block a user