Codex/avatar integrated 20260819 #2
@@ -11,6 +11,9 @@ router = APIRouter(tags=["授权"])
|
|||||||
TARGET_TYPES = {"user", "organization", "application"}
|
TARGET_TYPES = {"user", "organization", "application"}
|
||||||
PERMISSION_ORDER = ("friend", "chat", "publish", "browse", "interact", "takeover")
|
PERMISSION_ORDER = ("friend", "chat", "publish", "browse", "interact", "takeover")
|
||||||
ALLOWED_PERMISSIONS = set(PERMISSION_ORDER)
|
ALLOWED_PERMISSIONS = set(PERMISSION_ORDER)
|
||||||
|
AVATAR_PERMISSION_ORDER = PERMISSION_ORDER[:-1]
|
||||||
|
AVATAR_PERMISSION_KEY = "authorizationPermissions"
|
||||||
|
DEFAULT_AVATAR_PERMISSIONS = ["friend", "chat"]
|
||||||
LEGACY_PERMISSION_MAP = {
|
LEGACY_PERMISSION_MAP = {
|
||||||
"read": "browse",
|
"read": "browse",
|
||||||
"reply": "chat",
|
"reply": "chat",
|
||||||
@@ -53,6 +56,44 @@ def _normalize_permissions(value) -> list[str]:
|
|||||||
return sorted(normalized, key=PERMISSION_ORDER.index)
|
return sorted(normalized, key=PERMISSION_ORDER.index)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_avatar_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 AVATAR_PERMISSION_ORDER:
|
||||||
|
raise ValueError(f"不支持的权限:{raw}")
|
||||||
|
if permission not in normalized:
|
||||||
|
normalized.append(permission)
|
||||||
|
return sorted(normalized, key=AVATAR_PERMISSION_ORDER.index)
|
||||||
|
|
||||||
|
|
||||||
|
def _stored_avatar_permissions(avatar) -> list[str]:
|
||||||
|
config = avatar.config or {}
|
||||||
|
if AVATAR_PERMISSION_KEY not in config:
|
||||||
|
return list(DEFAULT_AVATAR_PERMISSIONS)
|
||||||
|
|
||||||
|
stored = config.get(AVATAR_PERMISSION_KEY)
|
||||||
|
if not isinstance(stored, list):
|
||||||
|
return list(DEFAULT_AVATAR_PERMISSIONS)
|
||||||
|
|
||||||
|
permissions = []
|
||||||
|
for raw in stored:
|
||||||
|
permission = LEGACY_PERMISSION_MAP.get(str(raw).strip(), str(raw).strip())
|
||||||
|
if permission in AVATAR_PERMISSION_ORDER and permission not in permissions:
|
||||||
|
permissions.append(permission)
|
||||||
|
return sorted(permissions, key=AVATAR_PERMISSION_ORDER.index)
|
||||||
|
|
||||||
|
|
||||||
|
def _permission_settings_payload(avatar) -> dict:
|
||||||
|
return {
|
||||||
|
"avatarId": avatar.id,
|
||||||
|
"permissions": _stored_avatar_permissions(avatar),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _require_authorization(db: Session, avatar_id: str, authorization_id: str) -> Authorization:
|
def _require_authorization(db: Session, avatar_id: str, authorization_id: str) -> Authorization:
|
||||||
authorization = (
|
authorization = (
|
||||||
db.query(Authorization)
|
db.query(Authorization)
|
||||||
@@ -85,6 +126,40 @@ def _duplicate_target(
|
|||||||
return query.first()
|
return query.first()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/avatar/{avatar_id}/permission-settings")
|
||||||
|
def get_permission_settings(
|
||||||
|
avatar_id: str,
|
||||||
|
authorization: str = Header(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
avatar = _require_owned_avatar(db, avatar_id, authorization)
|
||||||
|
return ok(_permission_settings_payload(avatar))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/avatar/{avatar_id}/permission-settings")
|
||||||
|
def update_permission_settings(
|
||||||
|
avatar_id: str,
|
||||||
|
payload: dict = Body(...),
|
||||||
|
authorization: str = Header(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
avatar = _require_owned_avatar(db, avatar_id, authorization)
|
||||||
|
if "permissions" not in payload:
|
||||||
|
return fail("缺少 permissions", 400)
|
||||||
|
try:
|
||||||
|
permissions = _normalize_avatar_permissions(payload["permissions"])
|
||||||
|
except ValueError as exc:
|
||||||
|
return fail(str(exc), 400)
|
||||||
|
|
||||||
|
avatar.config = {
|
||||||
|
**(avatar.config or {}),
|
||||||
|
AVATAR_PERMISSION_KEY: permissions,
|
||||||
|
}
|
||||||
|
db.commit()
|
||||||
|
db.refresh(avatar)
|
||||||
|
return ok(_permission_settings_payload(avatar), "授权设置已保存")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/avatar/{avatar_id}/authorizations")
|
@router.get("/avatar/{avatar_id}/authorizations")
|
||||||
def list_auth(
|
def list_auth(
|
||||||
avatar_id: str,
|
avatar_id: str,
|
||||||
|
|||||||
@@ -92,3 +92,67 @@ def test_authorization_requires_login_and_rejects_unknown_permissions(authorizat
|
|||||||
},
|
},
|
||||||
).json()
|
).json()
|
||||||
assert invalid["code"] == 400
|
assert invalid["code"] == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_permission_settings_default_and_persist(authorization_context):
|
||||||
|
context = authorization_context
|
||||||
|
endpoint = f"/api/avatar/{context['avatar'].id}/permission-settings"
|
||||||
|
|
||||||
|
initial = client.get(endpoint, headers=context["owner_headers"]).json()
|
||||||
|
assert initial["code"] == 200
|
||||||
|
assert initial["data"] == {
|
||||||
|
"avatarId": context["avatar"].id,
|
||||||
|
"permissions": ["friend", "chat"],
|
||||||
|
}
|
||||||
|
|
||||||
|
updated = client.put(
|
||||||
|
endpoint,
|
||||||
|
headers=context["owner_headers"],
|
||||||
|
json={"permissions": ["interact", "publish", "friend", "friend"]},
|
||||||
|
).json()
|
||||||
|
assert updated["code"] == 200
|
||||||
|
assert updated["data"]["permissions"] == ["friend", "publish", "interact"]
|
||||||
|
|
||||||
|
reloaded = client.get(endpoint, headers=context["owner_headers"]).json()
|
||||||
|
assert reloaded["data"]["permissions"] == ["friend", "publish", "interact"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_permission_settings_allow_all_disabled(authorization_context):
|
||||||
|
context = authorization_context
|
||||||
|
endpoint = f"/api/avatar/{context['avatar'].id}/permission-settings"
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
endpoint,
|
||||||
|
headers=context["owner_headers"],
|
||||||
|
json={"permissions": []},
|
||||||
|
).json()
|
||||||
|
assert response["code"] == 200
|
||||||
|
assert response["data"]["permissions"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_avatar_permission_settings_validate_owner_and_permissions(authorization_context):
|
||||||
|
context = authorization_context
|
||||||
|
endpoint = f"/api/avatar/{context['avatar'].id}/permission-settings"
|
||||||
|
|
||||||
|
invalid = client.put(
|
||||||
|
endpoint,
|
||||||
|
headers=context["owner_headers"],
|
||||||
|
json={"permissions": ["takeover"]},
|
||||||
|
).json()
|
||||||
|
assert invalid["code"] == 400
|
||||||
|
|
||||||
|
missing = client.put(
|
||||||
|
endpoint,
|
||||||
|
headers=context["owner_headers"],
|
||||||
|
json={},
|
||||||
|
).json()
|
||||||
|
assert missing["code"] == 400
|
||||||
|
|
||||||
|
forbidden = client.get(
|
||||||
|
f"/api/avatar/{context['other_avatar'].id}/permission-settings",
|
||||||
|
headers=context["owner_headers"],
|
||||||
|
)
|
||||||
|
assert forbidden.status_code == 403
|
||||||
|
|
||||||
|
unauthenticated = client.get(endpoint)
|
||||||
|
assert unauthenticated.status_code == 401
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const showNav = ref<boolean>(shouldShowNav(route.path))
|
|||||||
|
|
||||||
function shouldShowNav(path: string) {
|
function shouldShowNav(path: string) {
|
||||||
return path !== '/'
|
return path !== '/'
|
||||||
|
&& path !== '/authorization'
|
||||||
&& path !== '/avatar/create'
|
&& path !== '/avatar/create'
|
||||||
&& path !== '/login/sms'
|
&& path !== '/login/sms'
|
||||||
&& !path.startsWith('/avatar/edit')
|
&& !path.startsWith('/avatar/edit')
|
||||||
|
|||||||
@@ -145,6 +145,19 @@ export const chargeToken = (planId: string) =>
|
|||||||
|
|
||||||
// ==================== 授权管理 API ====================
|
// ==================== 授权管理 API ====================
|
||||||
|
|
||||||
|
export type AvatarPermission = 'friend' | 'chat' | 'publish' | 'browse' | 'interact'
|
||||||
|
|
||||||
|
export interface AvatarPermissionSettings {
|
||||||
|
avatarId: string
|
||||||
|
permissions: AvatarPermission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAvatarPermissionSettings = (avatarId: string) =>
|
||||||
|
request.get<AvatarPermissionSettings>(`/avatar/${avatarId}/permission-settings`)
|
||||||
|
|
||||||
|
export const updateAvatarPermissionSettings = (avatarId: string, permissions: AvatarPermission[]) =>
|
||||||
|
request.put<AvatarPermissionSettings>(`/avatar/${avatarId}/permission-settings`, { permissions })
|
||||||
|
|
||||||
export interface Authorization {
|
export interface Authorization {
|
||||||
id: string
|
id: string
|
||||||
avatarId: string
|
avatarId: string
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user