This commit is contained in:
2026-05-01 01:53:14 +08:00
69 changed files with 7346 additions and 759 deletions

View File

@@ -13,10 +13,7 @@ use tower_http::{
use tracing::{Level, Span, error, info, info_span, warn};
use crate::{
admin::{
admin_console_page, admin_debug_http, admin_login, admin_me, admin_overview,
require_admin_auth,
},
admin::{admin_debug_http, admin_login, admin_me, admin_overview, require_admin_auth},
ai_tasks::{
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
complete_ai_task, create_ai_task, fail_ai_task, start_ai_task, start_ai_task_stage,
@@ -78,6 +75,13 @@ use crate::{
login_options::auth_login_options,
logout::logout,
logout_all::logout_all,
match3d::{
click_match3d_item, create_match3d_agent_session, delete_match3d_work,
execute_match3d_agent_action, finish_match3d_time_up, get_match3d_agent_session,
get_match3d_run, get_match3d_work_detail, get_match3d_works, list_match3d_gallery,
publish_match3d_work, put_match3d_work, restart_match3d_run, start_match3d_run,
stop_match3d_run, stream_match3d_agent_message, submit_match3d_agent_message,
},
password_entry::password_entry,
password_management::{change_password, reset_password},
phone_auth::{phone_login, send_phone_code},
@@ -105,10 +109,10 @@ use crate::{
},
runtime_inventory::get_runtime_inventory_state,
runtime_profile::{
admin_disable_profile_redeem_code, admin_upsert_profile_redeem_code,
create_profile_recharge_order, get_profile_dashboard, get_profile_play_stats,
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_wallet_ledger,
redeem_profile_referral_invite_code, redeem_profile_reward_code,
admin_disable_profile_redeem_code, admin_upsert_profile_invite_code,
admin_upsert_profile_redeem_code, create_profile_recharge_order, get_profile_dashboard,
get_profile_play_stats, get_profile_recharge_center, get_profile_referral_invite_center,
get_profile_wallet_ledger, redeem_profile_referral_invite_code, redeem_profile_reward_code,
},
runtime_save::{
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
@@ -135,7 +139,6 @@ pub fn build_router(state: AppState) -> Router {
let slow_request_threshold_ms = state.config.slow_request_threshold_ms;
Router::new()
.route("/admin", get(admin_console_page))
.route("/admin/api/login", post(admin_login))
.route(
"/admin/api/me",
@@ -172,6 +175,13 @@ pub fn build_router(state: AppState) -> Router {
require_admin_auth,
)),
)
.route(
"/admin/api/profile/invite-codes",
post(admin_upsert_profile_invite_code).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/healthz",
get(|Extension(request_context): Extension<_>| async move {
@@ -702,6 +712,116 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions",
post(create_match3d_agent_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions/{session_id}",
get(get_match3d_agent_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions/{session_id}/messages",
post(submit_match3d_agent_message).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions/{session_id}/messages/stream",
post(stream_match3d_agent_message).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions/{session_id}/actions",
post(execute_match3d_agent_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/sessions/{session_id}/compile",
post(execute_match3d_agent_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/works",
get(get_match3d_works).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/works/{profile_id}",
get(get_match3d_work_detail)
.patch(put_match3d_work)
.put(put_match3d_work)
.delete(delete_match3d_work)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/match3d/works/{profile_id}/publish",
post(publish_match3d_work).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route("/api/runtime/match3d/gallery", get(list_match3d_gallery))
.route(
"/api/runtime/match3d/works/{profile_id}/runs",
post(start_match3d_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/match3d/runs/{run_id}",
get(get_match3d_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/match3d/runs/{run_id}/click",
post(click_match3d_item).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/match3d/runs/{run_id}/stop",
post(stop_match3d_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/match3d/runs/{run_id}/restart",
post(restart_match3d_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/match3d/runs/{run_id}/time-up",
post(finish_match3d_time_up).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/puzzle/agent/sessions",
post(create_puzzle_agent_session)
@@ -1993,6 +2113,8 @@ mod tests {
payload["user"]["phoneNumberMasked"],
Value::String("138****8000".to_string())
);
assert_eq!(payload["created"], Value::Bool(true));
assert!(payload["referral"].is_null());
}
#[tokio::test]
@@ -2099,6 +2221,175 @@ mod tests {
serde_json::from_slice(&second_body).expect("second login payload should be json");
assert_eq!(first_payload["user"]["id"], second_payload["user"]["id"]);
assert_eq!(first_payload["created"], Value::Bool(true));
assert_eq!(second_payload["created"], Value::Bool(false));
assert!(second_payload["referral"].is_null());
}
#[tokio::test]
async fn phone_login_invite_code_failure_does_not_block_created_user() {
let config = AppConfig {
sms_auth_enabled: true,
..AppConfig::default()
};
let app = build_router(AppState::new(config).expect("state should build"));
let send_code_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/phone/send-code")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": "13600136000",
"scene": "login"
})
.to_string(),
))
.expect("send code request should build"),
)
.await
.expect("send code request should succeed");
assert_eq!(send_code_response.status(), StatusCode::OK);
let login_response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/phone/login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": "13600136000",
"code": "123456",
"inviteCode": "SPRING2026"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login request should succeed");
assert_eq!(login_response.status(), StatusCode::OK);
let body = login_response
.into_body()
.collect()
.await
.expect("login body should collect")
.to_bytes();
let payload: Value = serde_json::from_slice(&body).expect("login payload should be json");
assert!(payload["token"].as_str().is_some());
assert_eq!(payload["created"], Value::Bool(true));
assert_eq!(payload["referral"]["ok"], Value::Bool(false));
assert_eq!(
payload["referral"]["message"],
Value::String("邀请码无效,已继续注册".to_string())
);
}
#[tokio::test]
async fn phone_login_existing_user_ignores_invite_code() {
let config = AppConfig {
sms_auth_enabled: true,
..AppConfig::default()
};
let app = build_router(AppState::new(config).expect("state should build"));
let first_send_code_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/phone/send-code")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": "13500135000",
"scene": "login"
})
.to_string(),
))
.expect("send code request should build"),
)
.await
.expect("send code request should succeed");
assert_eq!(first_send_code_response.status(), StatusCode::OK);
let first_login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/phone/login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": "13500135000",
"code": "123456"
})
.to_string(),
))
.expect("first login request should build"),
)
.await
.expect("first login request should succeed");
assert_eq!(first_login_response.status(), StatusCode::OK);
let second_send_code_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/phone/send-code")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": "13500135000",
"scene": "login"
})
.to_string(),
))
.expect("send code request should build"),
)
.await
.expect("send code request should succeed");
assert_eq!(second_send_code_response.status(), StatusCode::OK);
let second_login_response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/phone/login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": "13500135000",
"code": "123456",
"inviteCode": "SPRING2026"
})
.to_string(),
))
.expect("second login request should build"),
)
.await
.expect("second login request should succeed");
assert_eq!(second_login_response.status(), StatusCode::OK);
let body = second_login_response
.into_body()
.collect()
.await
.expect("second login body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("second login payload should be json");
assert_eq!(payload["created"], Value::Bool(false));
assert!(payload["referral"].is_null());
}
#[tokio::test]
@@ -3314,6 +3605,23 @@ mod tests {
);
}
#[tokio::test]
async fn admin_page_route_is_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.uri("/admin")
.body(Body::empty())
.expect("admin page request should build"),
)
.await
.expect("admin page request should succeed");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn admin_login_returns_token_when_configured() {
let mut config = AppConfig::default();