Integrate unfinished server-rs refactor worklists
This commit is contained in:
@@ -51,6 +51,23 @@ pub async fn create_story_battle(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
let story_state = state
|
||||
.spacetime_client()
|
||||
.get_story_session_state(payload.story_session_id.clone())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
story_battles_error_response(&request_context, map_story_battle_client_error(error))
|
||||
})?;
|
||||
require_story_session_owner_for_battle(
|
||||
&request_context,
|
||||
&story_state.session.actor_user_id,
|
||||
&actor_user_id,
|
||||
)?;
|
||||
require_story_session_runtime_for_battle(
|
||||
&request_context,
|
||||
&story_state.session.runtime_session_id,
|
||||
&payload.runtime_session_id,
|
||||
)?;
|
||||
|
||||
let result = state
|
||||
.spacetime_client()
|
||||
@@ -89,14 +106,29 @@ pub async fn create_story_battle(
|
||||
pub async fn resolve_story_battle(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<ResolveStoryBattleRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let now_micros = current_utc_micros();
|
||||
let actor_user_id = authenticated.claims().user_id().to_string();
|
||||
let battle_state_id = payload.battle_state_id;
|
||||
let current_battle = state
|
||||
.spacetime_client()
|
||||
.get_battle_state(battle_state_id.clone())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
story_battles_error_response(&request_context, map_story_battle_client_error(error))
|
||||
})?;
|
||||
require_story_battle_owner(
|
||||
&request_context,
|
||||
¤t_battle.actor_user_id,
|
||||
&actor_user_id,
|
||||
)?;
|
||||
|
||||
let result = state
|
||||
.spacetime_client()
|
||||
.resolve_combat_action(ResolveCombatActionInput {
|
||||
battle_state_id: payload.battle_state_id,
|
||||
battle_state_id,
|
||||
function_id: payload.function_id,
|
||||
action_text: payload.action_text,
|
||||
base_damage: payload.base_damage,
|
||||
@@ -128,8 +160,9 @@ pub async fn get_story_battle_state(
|
||||
State(state): State<AppState>,
|
||||
Path(battle_state_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let actor_user_id = authenticated.claims().user_id().to_string();
|
||||
let result = state
|
||||
.spacetime_client()
|
||||
.get_battle_state(battle_state_id)
|
||||
@@ -137,6 +170,7 @@ pub async fn get_story_battle_state(
|
||||
.map_err(|error| {
|
||||
story_battles_error_response(&request_context, map_story_battle_client_error(error))
|
||||
})?;
|
||||
require_story_battle_owner(&request_context, &result.actor_user_id, &actor_user_id)?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -175,6 +209,23 @@ pub async fn create_story_npc_battle(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
let story_state = state
|
||||
.spacetime_client()
|
||||
.get_story_session_state(payload.story_session_id.clone())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
story_battles_error_response(&request_context, map_story_battle_client_error(error))
|
||||
})?;
|
||||
require_story_session_owner_for_battle(
|
||||
&request_context,
|
||||
&story_state.session.actor_user_id,
|
||||
&actor_user_id,
|
||||
)?;
|
||||
require_story_session_runtime_for_battle(
|
||||
&request_context,
|
||||
&story_state.session.runtime_session_id,
|
||||
&payload.runtime_session_id,
|
||||
)?;
|
||||
|
||||
let result = state
|
||||
.spacetime_client()
|
||||
@@ -431,6 +482,73 @@ fn story_battles_error_response(request_context: &RequestContext, error: AppErro
|
||||
error.into_response_with_context(Some(request_context))
|
||||
}
|
||||
|
||||
fn require_story_session_owner_for_battle(
|
||||
request_context: &RequestContext,
|
||||
resource_actor_user_id: &str,
|
||||
authenticated_actor_user_id: &str,
|
||||
) -> Result<(), Response> {
|
||||
require_resource_owner(
|
||||
request_context,
|
||||
resource_actor_user_id,
|
||||
authenticated_actor_user_id,
|
||||
"story-session",
|
||||
"story session 不属于当前用户,不能创建战斗",
|
||||
)
|
||||
}
|
||||
|
||||
fn require_story_battle_owner(
|
||||
request_context: &RequestContext,
|
||||
resource_actor_user_id: &str,
|
||||
authenticated_actor_user_id: &str,
|
||||
) -> Result<(), Response> {
|
||||
require_resource_owner(
|
||||
request_context,
|
||||
resource_actor_user_id,
|
||||
authenticated_actor_user_id,
|
||||
"story-battle",
|
||||
"battle state 不属于当前用户",
|
||||
)
|
||||
}
|
||||
|
||||
fn require_story_session_runtime_for_battle(
|
||||
request_context: &RequestContext,
|
||||
session_runtime_id: &str,
|
||||
requested_runtime_id: &str,
|
||||
) -> Result<(), Response> {
|
||||
if session_runtime_id == requested_runtime_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(story_battles_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "story-session",
|
||||
"message": "runtimeSessionId 与 story session 不匹配,不能创建战斗",
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
fn require_resource_owner(
|
||||
request_context: &RequestContext,
|
||||
resource_actor_user_id: &str,
|
||||
authenticated_actor_user_id: &str,
|
||||
provider: &'static str,
|
||||
message: &'static str,
|
||||
) -> Result<(), Response> {
|
||||
if resource_actor_user_id == authenticated_actor_user_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// API 层只做登录用户与资源属主的边界检查;战斗结算仍由 module-combat 与 SpacetimeDB 承担。
|
||||
Err(story_battles_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::FORBIDDEN).with_details(json!({
|
||||
"provider": provider,
|
||||
"message": message,
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
||||
fn current_utc_micros() -> i64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -442,6 +560,8 @@ fn current_utc_micros() -> i64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
@@ -454,7 +574,10 @@ mod tests {
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use crate::{app::build_router, config::AppConfig, state::AppState};
|
||||
use super::{require_story_battle_owner, require_story_session_runtime_for_battle};
|
||||
use crate::{
|
||||
app::build_router, config::AppConfig, request_context::RequestContext, state::AppState,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_story_battle_requires_authentication() {
|
||||
@@ -648,6 +771,37 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_story_battle_requires_authentication() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/story/battles/resolve")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"battleStateId": "battle_001",
|
||||
"functionId": "battle_attack_basic",
|
||||
"actionText": "普通攻击",
|
||||
"baseDamage": 10,
|
||||
"manaCost": 0,
|
||||
"heal": 0,
|
||||
"manaRestore": 0,
|
||||
"counterMultiplierBasisPoints": 10000
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_story_battle_state_returns_bad_gateway_when_spacetime_not_published() {
|
||||
let state = seed_authenticated_state().await;
|
||||
@@ -735,6 +889,63 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_battle_owner_guard_rejects_mismatched_actor() {
|
||||
let context = RequestContext::new(
|
||||
"req_story_battle_owner_guard".to_string(),
|
||||
"GET /api/story/battles/battle_001".to_string(),
|
||||
Duration::ZERO,
|
||||
true,
|
||||
);
|
||||
|
||||
let response = require_story_battle_owner(&context, "user_owner", "user_other")
|
||||
.expect_err("mismatched actor should be forbidden");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_battle_owner_guard_accepts_matching_actor() {
|
||||
let context = RequestContext::new(
|
||||
"req_story_battle_owner_guard".to_string(),
|
||||
"GET /api/story/battles/battle_001".to_string(),
|
||||
Duration::ZERO,
|
||||
true,
|
||||
);
|
||||
|
||||
require_story_battle_owner(&context, "user_owner", "user_owner")
|
||||
.expect("matching actor should pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_battle_runtime_guard_rejects_mismatched_runtime_session() {
|
||||
let context = RequestContext::new(
|
||||
"req_story_battle_runtime_guard".to_string(),
|
||||
"POST /api/story/battles".to_string(),
|
||||
Duration::ZERO,
|
||||
true,
|
||||
);
|
||||
|
||||
let response =
|
||||
require_story_session_runtime_for_battle(&context, "runtime_owner", "runtime_other")
|
||||
.expect_err("mismatched runtime session should be bad request");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_battle_runtime_guard_accepts_matching_runtime_session() {
|
||||
let context = RequestContext::new(
|
||||
"req_story_battle_runtime_guard".to_string(),
|
||||
"POST /api/story/battles".to_string(),
|
||||
Duration::ZERO,
|
||||
true,
|
||||
);
|
||||
|
||||
require_story_session_runtime_for_battle(&context, "runtime_owner", "runtime_owner")
|
||||
.expect("matching runtime session should pass");
|
||||
}
|
||||
|
||||
async fn seed_authenticated_state() -> AppState {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
state
|
||||
|
||||
Reference in New Issue
Block a user