Integrate unfinished server-rs refactor worklists

This commit is contained in:
2026-04-30 13:39:06 +08:00
parent 62934b0809
commit 7ab0933f6d
676 changed files with 24487 additions and 21531 deletions

View File

@@ -72,14 +72,29 @@ pub async fn begin_story_session(
pub async fn continue_story(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<ContinueStoryRequest>,
) -> Result<Json<Value>, Response> {
let now_micros = current_utc_micros();
let actor_user_id = authenticated.claims().user_id().to_string();
let story_session_id = payload.story_session_id;
let current_state = state
.spacetime_client()
.get_story_session_state(story_session_id.clone())
.await
.map_err(|error| {
story_sessions_error_response(&request_context, map_story_session_client_error(error))
})?;
require_story_session_owner(
&request_context,
&current_state.session.actor_user_id,
&actor_user_id,
)?;
let result = state
.spacetime_client()
.continue_story(
payload.story_session_id,
story_session_id,
module_story::generate_story_event_id(now_micros),
payload.narrative_text,
payload.choice_function_id,
@@ -123,8 +138,9 @@ pub async fn get_story_session_state(
State(state): State<AppState>,
Path(story_session_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_story_session_state(story_session_id)
@@ -132,6 +148,11 @@ pub async fn get_story_session_state(
.map_err(|error| {
story_sessions_error_response(&request_context, map_story_session_client_error(error))
})?;
require_story_session_owner(
&request_context,
&result.session.actor_user_id,
&actor_user_id,
)?;
Ok(json_success_body(
Some(&request_context),
@@ -204,6 +225,25 @@ fn story_sessions_error_response(request_context: &RequestContext, error: AppErr
error.into_response_with_context(Some(request_context))
}
fn require_story_session_owner(
request_context: &RequestContext,
resource_actor_user_id: &str,
authenticated_actor_user_id: &str,
) -> Result<(), Response> {
if resource_actor_user_id == authenticated_actor_user_id {
return Ok(());
}
// 这里只做 HTTP 鉴权边界判断story session 的推进规则仍由 SpacetimeDB 领域层处理。
Err(story_sessions_error_response(
request_context,
AppError::from_status(StatusCode::FORBIDDEN).with_details(json!({
"provider": "story-session",
"message": "story session 不属于当前用户",
})),
))
}
fn current_utc_micros() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
@@ -215,6 +255,8 @@ fn current_utc_micros() -> i64 {
#[cfg(test)]
mod tests {
use std::time::Duration;
use axum::{
body::Body,
http::{Request, StatusCode},
@@ -227,7 +269,10 @@ mod tests {
use time::OffsetDateTime;
use tower::ServiceExt;
use crate::{app::build_router, config::AppConfig, state::AppState};
use super::require_story_session_owner;
use crate::{
app::build_router, config::AppConfig, request_context::RequestContext, state::AppState,
};
#[tokio::test]
async fn begin_story_session_requires_authentication() {
@@ -302,6 +347,32 @@ mod tests {
);
}
#[tokio::test]
async fn continue_story_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/sessions/continue")
.header("content-type", "application/json")
.body(Body::from(
json!({
"storySessionId": "storysess_001",
"narrativeText": "你看见篝火边有人招手。",
"choiceFunctionId": "talk_to_npc"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn continue_story_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
@@ -457,6 +528,34 @@ mod tests {
);
}
#[test]
fn story_session_owner_guard_rejects_mismatched_actor() {
let context = RequestContext::new(
"req_story_owner_guard".to_string(),
"GET /api/story/sessions/storysess_001/state".to_string(),
Duration::ZERO,
true,
);
let response = require_story_session_owner(&context, "user_owner", "user_other")
.expect_err("mismatched actor should be forbidden");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn story_session_owner_guard_accepts_matching_actor() {
let context = RequestContext::new(
"req_story_owner_guard".to_string(),
"GET /api/story/sessions/storysess_001/state".to_string(),
Duration::ZERO,
true,
);
require_story_session_owner(&context, "user_owner", "user_owner")
.expect("matching actor should pass");
}
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state