diff --git a/digital-avatar-app/backend/routers/authorizations.py b/digital-avatar-app/backend/routers/authorizations.py index 75c8c8b..59031d5 100644 --- a/digital-avatar-app/backend/routers/authorizations.py +++ b/digital-avatar-app/backend/routers/authorizations.py @@ -11,6 +11,9 @@ router = APIRouter(tags=["授权"]) TARGET_TYPES = {"user", "organization", "application"} PERMISSION_ORDER = ("friend", "chat", "publish", "browse", "interact", "takeover") ALLOWED_PERMISSIONS = set(PERMISSION_ORDER) +AVATAR_PERMISSION_ORDER = PERMISSION_ORDER[:-1] +AVATAR_PERMISSION_KEY = "authorizationPermissions" +DEFAULT_AVATAR_PERMISSIONS = ["friend", "chat"] LEGACY_PERMISSION_MAP = { "read": "browse", "reply": "chat", @@ -53,6 +56,44 @@ def _normalize_permissions(value) -> list[str]: 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: authorization = ( db.query(Authorization) @@ -85,6 +126,40 @@ def _duplicate_target( 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") def list_auth( avatar_id: str, diff --git a/digital-avatar-app/backend/tests/test_authorizations_api.py b/digital-avatar-app/backend/tests/test_authorizations_api.py index 968839d..e3b8742 100644 --- a/digital-avatar-app/backend/tests/test_authorizations_api.py +++ b/digital-avatar-app/backend/tests/test_authorizations_api.py @@ -92,3 +92,67 @@ def test_authorization_requires_login_and_rejects_unknown_permissions(authorizat }, ).json() 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 diff --git a/digital-avatar-app/src/App.vue b/digital-avatar-app/src/App.vue index 51e508b..c54ecb2 100644 --- a/digital-avatar-app/src/App.vue +++ b/digital-avatar-app/src/App.vue @@ -43,6 +43,7 @@ const showNav = ref(shouldShowNav(route.path)) function shouldShowNav(path: string) { return path !== '/' + && path !== '/authorization' && path !== '/avatar/create' && path !== '/login/sms' && !path.startsWith('/avatar/edit') diff --git a/digital-avatar-app/src/api/index.ts b/digital-avatar-app/src/api/index.ts index d7735f3..4ad37cd 100644 --- a/digital-avatar-app/src/api/index.ts +++ b/digital-avatar-app/src/api/index.ts @@ -145,6 +145,19 @@ export const chargeToken = (planId: string) => // ==================== 授权管理 API ==================== +export type AvatarPermission = 'friend' | 'chat' | 'publish' | 'browse' | 'interact' + +export interface AvatarPermissionSettings { + avatarId: string + permissions: AvatarPermission[] +} + +export const getAvatarPermissionSettings = (avatarId: string) => + request.get(`/avatar/${avatarId}/permission-settings`) + +export const updateAvatarPermissionSettings = (avatarId: string, permissions: AvatarPermission[]) => + request.put(`/avatar/${avatarId}/permission-settings`, { permissions }) + export interface Authorization { id: string avatarId: string diff --git a/digital-avatar-app/src/views/AuthorizationManage.vue b/digital-avatar-app/src/views/AuthorizationManage.vue index 10625e3..fa44e9d 100644 --- a/digital-avatar-app/src/views/AuthorizationManage.vue +++ b/digital-avatar-app/src/views/AuthorizationManage.vue @@ -1,1586 +1,603 @@