This commit is contained in:
2026-05-01 00:33:39 +08:00
parent 61969c5116
commit fe02603ba1
68 changed files with 4586 additions and 748 deletions

View File

@@ -23,8 +23,8 @@ use shared_contracts::assets::{
use spacetime_client::SpacetimeClientError;
use crate::{
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
state::AppState,
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
request_context::RequestContext, state::AppState,
};
// 历史素材类型需要与 SpacetimeDB 侧白名单保持同一口径,避免新增素材类型时 HTTP 门面漏同步。
@@ -119,6 +119,7 @@ pub async fn get_asset_read_url(
pub async fn get_asset_history(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Query(query): Query<AssetHistoryQuery>,
) -> Result<Json<Value>, AppError> {
let asset_kind = query.kind.trim().to_string();
@@ -133,18 +134,23 @@ pub async fn get_asset_history(
let entries = state
.spacetime_client()
.list_asset_history(module_assets::AssetHistoryListInput {
asset_kind,
limit: query.limit.unwrap_or(120).clamp(1, 120),
})
.list_asset_history(build_asset_history_list_input(asset_kind, query.limit))
.await
.map_err(map_confirm_asset_object_error)?;
let owner_user_id = authenticated.claims().user_id().to_string();
Ok(json_success_body(
Some(&request_context),
AssetHistoryListResponse {
assets: entries
.into_iter()
// 中文注释Maincloud 旧 wasm 的历史素材 procedure 仍按类型返回HTTP 门面必须兜底做账号隔离。
.filter(|entry| {
is_asset_history_owned_by(
entry.owner_user_id.as_deref(),
owner_user_id.as_str(),
)
})
.map(|entry| AssetHistoryEntryPayload {
owner_label: format_asset_owner_label(entry.owner_user_id.as_deref()),
asset_object_id: entry.asset_object_id,
@@ -296,6 +302,25 @@ fn is_supported_asset_history_kind(asset_kind: &str) -> bool {
SUPPORTED_ASSET_HISTORY_KINDS.contains(&asset_kind)
}
fn is_asset_history_owned_by(entry_owner_user_id: Option<&str>, owner_user_id: &str) -> bool {
let owner_user_id = owner_user_id.trim();
!owner_user_id.is_empty()
&& entry_owner_user_id
.map(str::trim)
.filter(|value| !value.is_empty())
== Some(owner_user_id)
}
fn build_asset_history_list_input(
asset_kind: String,
limit: Option<u32>,
) -> module_assets::AssetHistoryListInput {
module_assets::AssetHistoryListInput {
asset_kind,
limit: limit.unwrap_or(120).clamp(1, 120),
}
}
fn supported_asset_history_kind_message() -> String {
format!(
"历史素材类型只支持 {}",
@@ -490,6 +515,29 @@ mod tests {
);
}
#[test]
fn asset_history_owner_filter_keeps_only_authenticated_owner_assets() {
assert!(super::is_asset_history_owned_by(
Some("user-current"),
"user-current"
));
assert!(!super::is_asset_history_owned_by(
Some("user-other"),
"user-current"
));
assert!(!super::is_asset_history_owned_by(None, "user-current"));
assert!(!super::is_asset_history_owned_by(Some("user-current"), ""));
}
#[test]
fn asset_history_input_clamps_limit_for_spacetime_query() {
let input =
super::build_asset_history_list_input("puzzle_cover_image".to_string(), Some(240));
assert_eq!(input.asset_kind, "puzzle_cover_image");
assert_eq!(input.limit, 120);
}
#[tokio::test]
async fn direct_upload_ticket_returns_service_unavailable_when_oss_missing() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));