feat(avatar): complete authorization management
This commit is contained in:
@@ -1,32 +1,237 @@
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
from fastapi import APIRouter, Body, Depends, Header, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models import Authorization
|
||||
from responses import ok, fail
|
||||
from responses import fail, ok
|
||||
from routers.avatars import _require_owned_avatar
|
||||
|
||||
router = APIRouter(tags=["授权"])
|
||||
|
||||
TARGET_TYPES = {"user", "organization", "application"}
|
||||
PERMISSION_ORDER = ("friend", "chat", "publish", "browse", "interact", "takeover")
|
||||
ALLOWED_PERMISSIONS = set(PERMISSION_ORDER)
|
||||
LEGACY_PERMISSION_MAP = {
|
||||
"read": "browse",
|
||||
"reply": "chat",
|
||||
"write": "publish",
|
||||
"edit": "publish",
|
||||
}
|
||||
|
||||
|
||||
def _read(payload: dict, camel_key: str, snake_key: str | None = None, default=None):
|
||||
if camel_key in payload:
|
||||
return payload[camel_key]
|
||||
if snake_key and snake_key in payload:
|
||||
return payload[snake_key]
|
||||
return default
|
||||
|
||||
|
||||
def _clean_text(value, field_name: str, *, max_length: int) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
raise ValueError(f"{field_name}不能为空")
|
||||
if len(text) > max_length:
|
||||
raise ValueError(f"{field_name}不能超过 {max_length} 个字符")
|
||||
return text
|
||||
|
||||
|
||||
def _normalize_permissions(value) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
raise ValueError("权限格式不正确")
|
||||
|
||||
normalized = []
|
||||
for raw in value:
|
||||
permission = LEGACY_PERMISSION_MAP.get(str(raw).strip(), str(raw).strip())
|
||||
if permission not in ALLOWED_PERMISSIONS:
|
||||
raise ValueError(f"不支持的权限:{raw}")
|
||||
if permission not in normalized:
|
||||
normalized.append(permission)
|
||||
|
||||
if not [item for item in normalized if item != "takeover"]:
|
||||
raise ValueError("请至少选择一项权限")
|
||||
return sorted(normalized, key=PERMISSION_ORDER.index)
|
||||
|
||||
|
||||
def _require_authorization(db: Session, avatar_id: str, authorization_id: str) -> Authorization:
|
||||
authorization = (
|
||||
db.query(Authorization)
|
||||
.filter(
|
||||
Authorization.id == authorization_id,
|
||||
Authorization.avatar_id == avatar_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=404, detail="授权不存在")
|
||||
return authorization
|
||||
|
||||
|
||||
def _duplicate_target(
|
||||
db: Session,
|
||||
avatar_id: str,
|
||||
target_type: str,
|
||||
target_id: str,
|
||||
*,
|
||||
exclude_id: str | None = None,
|
||||
):
|
||||
query = db.query(Authorization).filter(
|
||||
Authorization.avatar_id == avatar_id,
|
||||
Authorization.target_type == target_type,
|
||||
Authorization.target_id == target_id,
|
||||
)
|
||||
if exclude_id:
|
||||
query = query.filter(Authorization.id != exclude_id)
|
||||
return query.first()
|
||||
|
||||
|
||||
@router.get("/avatar/{avatar_id}/authorizations")
|
||||
def list_auth(avatar_id: str, db: Session = Depends(get_db)):
|
||||
# demo:返回全部授权(忽略具体 avatar 绑定,便于联调)
|
||||
items = db.query(Authorization).order_by(Authorization.created_at.desc()).all()
|
||||
return ok([a.to_dict() for a in items])
|
||||
def list_auth(
|
||||
avatar_id: str,
|
||||
authorization: str = Header(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
_require_owned_avatar(db, avatar_id, authorization)
|
||||
items = (
|
||||
db.query(Authorization)
|
||||
.filter(Authorization.avatar_id == avatar_id)
|
||||
.order_by(Authorization.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
return ok([item.to_dict() for item in items])
|
||||
|
||||
|
||||
@router.post("/avatar/{avatar_id}/authorizations")
|
||||
def create_auth(
|
||||
avatar_id: str,
|
||||
payload: dict = Body(...),
|
||||
authorization: str = Header(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
_require_owned_avatar(db, avatar_id, authorization)
|
||||
try:
|
||||
target_type = _clean_text(
|
||||
_read(payload, "targetType", "target_type", "user"),
|
||||
"授权类型",
|
||||
max_length=24,
|
||||
)
|
||||
if target_type not in TARGET_TYPES:
|
||||
return fail("授权类型不正确", 400)
|
||||
target_id = _clean_text(
|
||||
_read(payload, "targetId", "target_id"),
|
||||
"对象标识",
|
||||
max_length=120,
|
||||
)
|
||||
target_name = _clean_text(
|
||||
_read(payload, "targetName", "target_name"),
|
||||
"对象名称",
|
||||
max_length=50,
|
||||
)
|
||||
permissions = _normalize_permissions(payload.get("permissions", []))
|
||||
except ValueError as exc:
|
||||
return fail(str(exc), 400)
|
||||
|
||||
if _duplicate_target(db, avatar_id, target_type, target_id):
|
||||
return fail("该对象已在授权列表中,可直接编辑现有授权", 409)
|
||||
|
||||
item = Authorization(
|
||||
avatar_id=avatar_id,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
target_name=target_name,
|
||||
permissions=permissions,
|
||||
status="active",
|
||||
takeover_enabled=False,
|
||||
takeover_mode="immediate",
|
||||
takeover_delay_seconds=30,
|
||||
)
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
return ok(item.to_dict(), "授权已添加")
|
||||
|
||||
|
||||
@router.put("/avatar/{avatar_id}/authorizations")
|
||||
def update_auth(avatar_id: str, payload: dict = Body(...), db: Session = Depends(get_db)):
|
||||
auth_id = payload.get("id")
|
||||
def update_auth(
|
||||
avatar_id: str,
|
||||
payload: dict = Body(...),
|
||||
authorization: str = Header(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
_require_owned_avatar(db, avatar_id, authorization)
|
||||
auth_id = payload.get("id") or _read(payload, "authorizationId", "authorization_id")
|
||||
if not auth_id:
|
||||
return fail("缺少授权 id", 400)
|
||||
a = db.query(Authorization).filter(Authorization.id == auth_id).first()
|
||||
if not a:
|
||||
return fail("授权不存在", 404)
|
||||
|
||||
item = _require_authorization(db, avatar_id, str(auth_id))
|
||||
try:
|
||||
target_type = item.target_type
|
||||
target_id = item.target_id
|
||||
if "targetType" in payload or "target_type" in payload:
|
||||
target_type = _clean_text(
|
||||
_read(payload, "targetType", "target_type"),
|
||||
"授权类型",
|
||||
max_length=24,
|
||||
)
|
||||
if target_type not in TARGET_TYPES:
|
||||
return fail("授权类型不正确", 400)
|
||||
if "targetId" in payload or "target_id" in payload:
|
||||
target_id = _clean_text(
|
||||
_read(payload, "targetId", "target_id"),
|
||||
"对象标识",
|
||||
max_length=120,
|
||||
)
|
||||
if "targetName" in payload or "target_name" in payload:
|
||||
item.target_name = _clean_text(
|
||||
_read(payload, "targetName", "target_name"),
|
||||
"对象名称",
|
||||
max_length=50,
|
||||
)
|
||||
if "permissions" in payload:
|
||||
item.permissions = _normalize_permissions(payload["permissions"])
|
||||
except ValueError as exc:
|
||||
return fail(str(exc), 400)
|
||||
|
||||
if _duplicate_target(
|
||||
db,
|
||||
avatar_id,
|
||||
target_type,
|
||||
target_id,
|
||||
exclude_id=item.id,
|
||||
):
|
||||
return fail("该对象已在授权列表中", 409)
|
||||
|
||||
if "status" in payload:
|
||||
a.status = payload["status"]
|
||||
if "permissions" in payload:
|
||||
a.permissions = payload["permissions"]
|
||||
status = str(payload["status"] or "")
|
||||
if status not in ("active", "inactive"):
|
||||
return fail("授权状态不正确", 400)
|
||||
item.status = status
|
||||
|
||||
item.target_type = target_type
|
||||
item.target_id = target_id
|
||||
|
||||
permissions = list(item.permissions or [])
|
||||
chat_allowed = "chat" in permissions or "reply" in permissions
|
||||
if item.status != "active" or item.target_type != "user" or not chat_allowed:
|
||||
item.takeover_enabled = False
|
||||
item.permissions = [permission for permission in permissions if permission != "takeover"]
|
||||
elif item.takeover_enabled and "takeover" not in permissions:
|
||||
item.permissions = permissions + ["takeover"]
|
||||
|
||||
db.commit()
|
||||
items = db.query(Authorization).order_by(Authorization.created_at.desc()).all()
|
||||
return ok([x.to_dict() for x in items])
|
||||
db.refresh(item)
|
||||
return ok(item.to_dict(), "授权已更新")
|
||||
|
||||
|
||||
@router.delete("/avatar/{avatar_id}/authorizations/{authorization_id}")
|
||||
def delete_auth(
|
||||
avatar_id: str,
|
||||
authorization_id: str,
|
||||
authorization: str = Header(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
_require_owned_avatar(db, avatar_id, authorization)
|
||||
item = _require_authorization(db, avatar_id, authorization_id)
|
||||
db.delete(item)
|
||||
db.commit()
|
||||
return ok({"id": authorization_id}, "授权已删除")
|
||||
|
||||
@@ -1,41 +1,79 @@
|
||||
"""分身接管配置 API"""
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
"""数字分身单聊接管配置 API。"""
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Header
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from models import Authorization
|
||||
from responses import ok, fail
|
||||
from responses import fail, ok
|
||||
from routers.authorizations import _require_authorization
|
||||
from routers.avatars import _require_owned_avatar
|
||||
|
||||
router = APIRouter(tags=["分身接管"])
|
||||
|
||||
|
||||
def _has(payload: dict, camel_key: str, snake_key: str) -> bool:
|
||||
return camel_key in payload or snake_key in payload
|
||||
|
||||
|
||||
def _read(payload: dict, camel_key: str, snake_key: str, default=None):
|
||||
if camel_key in payload:
|
||||
return payload[camel_key]
|
||||
if snake_key in payload:
|
||||
return payload[snake_key]
|
||||
return default
|
||||
|
||||
|
||||
@router.put("/avatar/{avatar_id}/authorizations/takeover")
|
||||
def update_takeover_config(
|
||||
avatar_id: str,
|
||||
payload: dict = Body(...),
|
||||
authorization: str = Header(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新分身接管配置"""
|
||||
auth_id = payload.get("authorizationId") or payload.get("authorization_id")
|
||||
_require_owned_avatar(db, avatar_id, authorization)
|
||||
auth_id = _read(payload, "authorizationId", "authorization_id")
|
||||
if not auth_id:
|
||||
return fail("缺少 authorization_id", 400)
|
||||
|
||||
auth = db.query(Authorization).filter(Authorization.id == auth_id).first()
|
||||
if not auth:
|
||||
return fail("授权不存在", 404)
|
||||
auth = _require_authorization(db, avatar_id, str(auth_id))
|
||||
enabled = bool(auth.takeover_enabled)
|
||||
mode = auth.takeover_mode or "immediate"
|
||||
delay = auth.takeover_delay_seconds or 30
|
||||
|
||||
if "takeover_enabled" in payload:
|
||||
auth.takeover_enabled = payload["takeover_enabled"]
|
||||
if "takeover_mode" in payload:
|
||||
mode = payload["takeover_mode"]
|
||||
if _has(payload, "takeoverEnabled", "takeover_enabled"):
|
||||
raw_enabled = _read(payload, "takeoverEnabled", "takeover_enabled")
|
||||
if not isinstance(raw_enabled, bool):
|
||||
return fail("takeover_enabled 必须是布尔值", 400)
|
||||
enabled = raw_enabled
|
||||
|
||||
if _has(payload, "takeoverMode", "takeover_mode"):
|
||||
mode = _read(payload, "takeoverMode", "takeover_mode")
|
||||
if mode not in ("immediate", "delayed"):
|
||||
return fail("takeover_mode 必须是 immediate 或 delayed", 400)
|
||||
auth.takeover_mode = mode
|
||||
if "takeover_delay_seconds" in payload:
|
||||
delay = payload["takeover_delay_seconds"]
|
||||
if not isinstance(delay, int) or delay < 5:
|
||||
return fail("takeover_delay_seconds 必须 >= 5", 400)
|
||||
auth.takeover_delay_seconds = delay
|
||||
|
||||
if _has(payload, "takeoverDelaySeconds", "takeover_delay_seconds"):
|
||||
delay = _read(payload, "takeoverDelaySeconds", "takeover_delay_seconds")
|
||||
if isinstance(delay, bool) or not isinstance(delay, int) or not 5 <= delay <= 3600:
|
||||
return fail("延迟时间需在 5 到 3600 秒之间", 400)
|
||||
|
||||
if enabled and auth.target_type != "user":
|
||||
return fail("本期仅支持对会会用户开启单聊接管", 400)
|
||||
if enabled and auth.status != "active":
|
||||
return fail("请先启用该授权,再开启聊天接管", 400)
|
||||
|
||||
permissions = list(auth.permissions or [])
|
||||
if enabled:
|
||||
if "chat" not in permissions and "reply" not in permissions:
|
||||
permissions.append("chat")
|
||||
if "takeover" not in permissions:
|
||||
permissions.append("takeover")
|
||||
else:
|
||||
permissions = [permission for permission in permissions if permission != "takeover"]
|
||||
|
||||
auth.permissions = permissions
|
||||
auth.takeover_enabled = enabled
|
||||
auth.takeover_mode = mode
|
||||
auth.takeover_delay_seconds = delay
|
||||
db.commit()
|
||||
return ok(auth.to_dict())
|
||||
db.refresh(auth)
|
||||
return ok(auth.to_dict(), "接管配置已保存")
|
||||
|
||||
Reference in New Issue
Block a user