Allow anonymous jump-hop recommend play

This commit is contained in:
kdletters
2026-05-24 16:02:49 +08:00
parent 2ba4691bc0
commit 2c6fb2e81a
12 changed files with 209 additions and 67 deletions

View File

@@ -59,17 +59,44 @@ pub async fn require_bearer_auth(
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let Some(authenticated) = authenticate_request(&state, &request)? else {
return Err(AppError::from_status(StatusCode::UNAUTHORIZED));
};
request.extensions_mut().insert(authenticated.clone());
let mut response = next.run(request).await;
response.extensions_mut().insert(authenticated);
Ok(response)
}
pub async fn attach_optional_bearer_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
if let Some(authenticated) = authenticate_request(&state, &request)? {
request.extensions_mut().insert(authenticated.clone());
let mut response = next.run(request).await;
response.extensions_mut().insert(authenticated);
return Ok(response);
}
Ok(next.run(request).await)
}
fn authenticate_request(
state: &AppState,
request: &Request,
) -> Result<Option<AuthenticatedAccessToken>, AppError> {
if allows_internal_forwarded_auth(request.uri().path())
&& let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers())
{
request
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims.clone()));
let mut response = next.run(request).await;
response
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims));
return Ok(response);
return Ok(Some(AuthenticatedAccessToken::new(claims)));
}
if !request.headers().contains_key(AUTHORIZATION) {
return Ok(None);
}
let bearer_token = extract_bearer_token(request.headers())?;
@@ -145,16 +172,7 @@ pub async fn require_bearer_auth(
.with_message("当前登录态已失效,请重新登录"));
}
request
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims.clone()));
let mut response = next.run(request).await;
response
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims));
Ok(response)
Ok(Some(AuthenticatedAccessToken::new(claims)))
}
pub async fn inspect_auth_claims(

View File

@@ -17,8 +17,12 @@ use spacetime_client::SpacetimeClientError;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
request_context::RequestContext, state::AppState,
api_response::json_success_body,
auth::AuthenticatedAccessToken,
http_error::AppError,
request_context::RequestContext,
state::AppState,
work_play_tracking::{record_work_play_start_after_success, WorkPlayTrackingDraft},
};
const JUMP_HOP_PROVIDER: &str = "jump-hop";
@@ -26,6 +30,8 @@ const JUMP_HOP_CREATION_PROVIDER: &str = "jump-hop-creation";
const JUMP_HOP_RUNTIME_PROVIDER: &str = "jump-hop-runtime";
const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop";
const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳";
const JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID: &str = "anonymous-runtime";
const JUMP_HOP_RUNTIME_RUNS_ROUTE: &str = "/api/runtime/jump-hop/runs";
pub async fn create_jump_hop_session(
State(state): State<AppState>,
@@ -170,14 +176,18 @@ pub async fn get_jump_hop_runtime_work(
pub async fn start_jump_hop_run(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
payload: Result<Json<JumpHopStartRunRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
ensure_non_empty(&request_context, &payload.profile_id, "profileId")?;
let authenticated = maybe_authenticated.as_ref().map(|Extension(authenticated)| authenticated);
let owner_user_id = authenticated
.map(|authenticated| authenticated.claims().user_id().to_string())
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
let run = state
.spacetime_client()
.start_jump_hop_run(payload, authenticated.claims().user_id().to_string())
.start_jump_hop_run(payload, owner_user_id.clone())
.await
.map_err(|error| {
jump_hop_error_response(
@@ -187,6 +197,24 @@ pub async fn start_jump_hop_run(
)
})?;
record_work_play_start_after_success(
&state,
&request_context,
build_jump_hop_work_play_tracking_draft(
authenticated,
run.profile_id.clone(),
JUMP_HOP_RUNTIME_RUNS_ROUTE,
)
.owner_user_id(run.owner_user_id.clone())
.run_id(run.run_id.clone())
.profile_id(run.profile_id.clone())
.extra(json!({
"runStatus": run.status,
"isAnonymous": maybe_authenticated.is_none(),
})),
)
.await;
Ok(json_success_body(
Some(&request_context),
JumpHopRunResponse { run },
@@ -197,18 +225,18 @@ pub async fn jump_hop_run_jump(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
payload: Result<Json<JumpHopJumpRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &run_id, "runId")?;
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
let owner_user_id = maybe_authenticated
.as_ref()
.map(|Extension(authenticated)| authenticated.claims().user_id().to_string())
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
let run = state
.spacetime_client()
.jump_hop_run_jump(
run_id,
authenticated.claims().user_id().to_string(),
payload,
)
.jump_hop_run_jump(run_id, owner_user_id, payload)
.await
.map_err(|error| {
jump_hop_error_response(
@@ -228,18 +256,18 @@ pub async fn restart_jump_hop_run(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
payload: Result<Json<JumpHopRestartRunRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &run_id, "runId")?;
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
let owner_user_id = maybe_authenticated
.as_ref()
.map(|Extension(authenticated)| authenticated.claims().user_id().to_string())
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
let run = state
.spacetime_client()
.restart_jump_hop_run(
run_id,
authenticated.claims().user_id().to_string(),
payload,
)
.restart_jump_hop_run(run_id, owner_user_id, payload)
.await
.map_err(|error| {
jump_hop_error_response(
@@ -298,6 +326,19 @@ pub async fn get_jump_hop_gallery_detail(
))
}
fn build_jump_hop_work_play_tracking_draft(
authenticated: Option<&AuthenticatedAccessToken>,
work_id: impl Into<String>,
source_route: &'static str,
) -> WorkPlayTrackingDraft {
match authenticated {
Some(authenticated) => {
WorkPlayTrackingDraft::new("jump-hop", work_id, authenticated, source_route)
}
None => WorkPlayTrackingDraft::anonymous("jump-hop", work_id, source_route),
}
}
fn build_jump_hop_draft(payload: &JumpHopWorkspaceCreateRequest) -> JumpHopDraftResponse {
JumpHopDraftResponse {
template_id: JUMP_HOP_TEMPLATE_ID.to_string(),

View File

@@ -4,7 +4,7 @@ use axum::{
};
use crate::{
auth::require_bearer_auth,
auth::{attach_optional_bearer_auth, require_bearer_auth},
jump_hop::{
create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail,
get_jump_hop_runtime_work, get_jump_hop_session, jump_hop_run_jump, list_jump_hop_gallery,
@@ -51,21 +51,21 @@ pub fn router(state: AppState) -> Router<AppState> {
"/api/runtime/jump-hop/runs",
post(start_jump_hop_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
attach_optional_bearer_auth,
)),
)
.route(
"/api/runtime/jump-hop/runs/{run_id}/jump",
post(jump_hop_run_jump).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
attach_optional_bearer_auth,
)),
)
.route(
"/api/runtime/jump-hop/runs/{run_id}/restart",
post(restart_jump_hop_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
attach_optional_bearer_auth,
)),
)
.route("/api/runtime/jump-hop/gallery", get(list_jump_hop_gallery))

View File

@@ -13,7 +13,7 @@ pub(crate) const WORK_PLAY_START_EVENT_KEY: &str = "work_play_start";
pub(crate) struct WorkPlayTrackingDraft {
pub play_type: &'static str,
pub work_id: String,
pub user_id: String,
pub user_id: Option<String>,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub run_id: Option<String>,
@@ -28,7 +28,28 @@ impl WorkPlayTrackingDraft {
authenticated: &AuthenticatedAccessToken,
source_route: &'static str,
) -> Self {
let user_id = authenticated.claims().user_id().to_string();
Self::with_user_id(
play_type,
work_id,
Some(authenticated.claims().user_id().to_string()),
source_route,
)
}
pub(crate) fn anonymous(
play_type: &'static str,
work_id: impl Into<String>,
source_route: &'static str,
) -> Self {
Self::with_user_id(play_type, work_id, None, source_route)
}
fn with_user_id(
play_type: &'static str,
work_id: impl Into<String>,
user_id: Option<String>,
source_route: &'static str,
) -> Self {
Self {
play_type,
work_id: work_id.into(),
@@ -91,7 +112,11 @@ async fn record_work_play_start_input_after_success(
"workId": draft.work_id,
"sourceRoute": draft.source_route,
});
metadata["userId"] = json!(draft.user_id);
if let Some(user_id) = draft.user_id.as_deref() {
metadata["userId"] = json!(user_id);
} else {
metadata["userKind"] = json!("anonymous");
}
if let Some(owner_user_id) = draft.owner_user_id.as_deref() {
metadata["ownerUserId"] = json!(owner_user_id);
}
@@ -108,7 +133,7 @@ async fn record_work_play_start_input_after_success(
let mut tracking = TrackingEventDraft::new(WORK_PLAY_START_EVENT_KEY, draft.play_type);
tracking.scope_kind = RuntimeTrackingScopeKind::Work;
tracking.scope_id = draft.work_id;
tracking.user_id = Some(draft.user_id);
tracking.user_id = draft.user_id;
tracking.owner_user_id = draft.owner_user_id;
tracking.profile_id = draft.profile_id;
tracking.metadata = metadata;