fix: restrict password login to existing phone accounts

This commit is contained in:
2026-04-26 01:11:45 +08:00
parent c4b9b8173f
commit 0a0f3f1bd8
33 changed files with 489 additions and 778 deletions

View File

@@ -642,13 +642,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "ai_tasks_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138100", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -659,7 +655,7 @@ mod tests {
session_id: "sess_ai_tasks".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("AI 任务用户".to_string()),

View File

@@ -1031,6 +1031,7 @@ pub fn build_router(state: AppState) -> Router {
#[cfg(test)]
mod tests {
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
};
@@ -1048,6 +1049,40 @@ mod tests {
use super::build_router;
const TEST_PASSWORD: &str = "secret123";
async fn seed_phone_user_with_password(
state: &AppState,
phone_number: &str,
password: &str,
) -> module_auth::AuthUser {
state
.seed_test_phone_user_with_password(phone_number, password)
.await
}
async fn password_login_request(
app: Router,
phone_number: &str,
password: &str,
) -> axum::response::Response {
app.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"phone": phone_number,
"password": password
})
.to_string(),
))
.expect("password login request should build"),
)
.await
.expect("password login request should succeed")
}
#[tokio::test]
async fn healthz_returns_legacy_compatible_payload_and_headers() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -1162,24 +1197,17 @@ mod tests {
async fn internal_auth_claims_returns_verified_claims() {
let config = AppConfig::default();
let state = AppState::new(config.clone()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "guest_auth_debug".to_string(),
password: "secret123".to_string(),
})
.await
.expect("seed login should succeed");
let seed_user = seed_phone_user_with_password(&state, "13800138010", TEST_PASSWORD).await;
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
user_id: seed_user.id.clone(),
session_id: "sess_auth_debug".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: seed_user.token_version,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("测试用户".to_string()),
display_name: Some(seed_user.display_name.clone()),
},
state.auth_jwt_config(),
OffsetDateTime::now_utc(),
@@ -1210,17 +1238,14 @@ mod tests {
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(
payload["claims"]["sub"],
Value::String("user_00000001".to_string())
);
assert_eq!(payload["claims"]["sub"], Value::String(seed_user.id));
assert_eq!(
payload["claims"]["sid"],
Value::String("sess_auth_debug".to_string())
);
assert_eq!(
payload["claims"]["ver"],
Value::Number(serde_json::Number::from(1))
Value::Number(serde_json::Number::from(seed_user.token_version))
);
}
@@ -1293,26 +1318,21 @@ mod tests {
}
#[tokio::test]
async fn password_entry_creates_user_and_sets_refresh_cookie() {
async fn password_entry_rejects_unknown_phone_without_registration() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
let response = password_login_request(app, "13800138011", TEST_PASSWORD).await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn password_entry_logs_in_existing_phone_user_and_sets_refresh_cookie() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138012", TEST_PASSWORD).await;
let app = build_router(state);
let response = password_login_request(app, "13800138012", TEST_PASSWORD).await;
assert_eq!(response.status(), StatusCode::OK);
assert!(
@@ -1332,9 +1352,10 @@ mod tests {
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(payload["user"]["id"], Value::String(seed_user.id));
assert_eq!(
payload["user"]["username"],
Value::String("guest_001".to_string())
payload["user"]["loginMethod"],
Value::String("password".to_string())
);
assert!(payload["token"].as_str().is_some());
}
@@ -1371,7 +1392,7 @@ mod tests {
assert_eq!(
payload["availableLoginMethods"],
serde_json::json!(["phone", "wechat"])
serde_json::json!(["phone", "password", "wechat"])
);
}
@@ -2232,7 +2253,9 @@ mod tests {
#[tokio::test]
async fn auth_sessions_returns_multi_device_session_fields() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138013", TEST_PASSWORD).await;
let app = build_router(state);
let first_login_response = app
.clone()
@@ -2248,8 +2271,8 @@ mod tests {
.header("x-client-instance-id", "chrome-instance-001")
.body(Body::from(
serde_json::json!({
"username": "guest_sessions_api",
"password": "secret123"
"phone": "13800138013",
"password": TEST_PASSWORD
})
.to_string(),
))
@@ -2292,8 +2315,8 @@ mod tests {
.header("user-agent", "Mozilla/5.0 Chrome/123.0 MicroMessenger")
.body(Body::from(
serde_json::json!({
"username": "guest_sessions_api",
"password": "secret123"
"phone": "13800138013",
"password": TEST_PASSWORD
})
.to_string(),
))
@@ -2346,27 +2369,13 @@ mod tests {
}
#[tokio::test]
async fn password_entry_reuses_same_user_for_same_credentials() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
async fn password_entry_reuses_same_user_for_same_phone() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let seed_user = seed_phone_user_with_password(&state, "13800138014", TEST_PASSWORD).await;
let app = build_router(state);
let first_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("first request should succeed");
let first_response =
password_login_request(app.clone(), "13800138014", TEST_PASSWORD).await;
let first_body = first_response
.into_body()
.collect()
@@ -2376,23 +2385,7 @@ mod tests {
let first_payload: Value =
serde_json::from_slice(&first_body).expect("first payload should be json");
let second_response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("second request should succeed");
let second_response = password_login_request(app, "13800138014", TEST_PASSWORD).await;
let second_body = second_response
.into_body()
.collect()
@@ -2402,54 +2395,23 @@ mod tests {
let second_payload: Value =
serde_json::from_slice(&second_body).expect("second payload should be json");
assert_eq!(first_payload["user"]["id"], Value::String(seed_user.id));
assert_eq!(first_payload["user"]["id"], second_payload["user"]["id"]);
}
#[tokio::test]
async fn password_entry_rejects_wrong_password() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138015", TEST_PASSWORD).await;
let app = build_router(state);
app.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret123"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("seed request should succeed");
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_001",
"password": "secret999"
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
let response = password_login_request(app, "13800138015", "secret999").await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn password_entry_rejects_invalid_username() {
async fn password_entry_rejects_email_or_username_identifier() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
@@ -2460,8 +2422,8 @@ mod tests {
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "无效用户",
"password": "secret123"
"phone": "user@example.com",
"password": TEST_PASSWORD
})
.to_string(),
))
@@ -2481,24 +2443,17 @@ mod tests {
..AppConfig::default()
};
let state = AppState::new(config).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "guest_001".to_string(),
password: "secret123".to_string(),
})
.await
.expect("seed login should succeed");
let seed_user = seed_phone_user_with_password(&state, "13800138016", TEST_PASSWORD).await;
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
user_id: seed_user.id.clone(),
session_id: "sess_me_query".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: seed_user.token_version,
phone_verified: false,
binding_status: BindingStatus::Active,
display_name: Some("guest_001".to_string()),
display_name: Some(seed_user.display_name.clone()),
},
state.auth_jwt_config(),
OffsetDateTime::now_utc(),
@@ -2529,13 +2484,10 @@ mod tests {
let payload: Value =
serde_json::from_slice(&body).expect("response body should be valid json");
assert_eq!(
payload["user"]["id"],
Value::String("user_00000001".to_string())
);
assert_eq!(payload["user"]["id"], Value::String(seed_user.id));
assert_eq!(
payload["availableLoginMethods"],
serde_json::json!(["phone", "wechat"])
serde_json::json!(["phone", "password", "wechat"])
);
}
@@ -2577,26 +2529,12 @@ mod tests {
#[tokio::test]
async fn refresh_session_rotates_cookie_and_returns_new_access_token() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138017", TEST_PASSWORD).await;
let app = build_router(state);
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_refresh",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login request should succeed");
let login_response =
password_login_request(app.clone(), "13800138017", TEST_PASSWORD).await;
let first_cookie = login_response
.headers()
.get("set-cookie")
@@ -2685,26 +2623,12 @@ mod tests {
#[tokio::test]
async fn logout_clears_cookie_and_invalidates_current_access_token() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138018", TEST_PASSWORD).await;
let app = build_router(state);
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_logout_api",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login request should succeed");
let login_response =
password_login_request(app.clone(), "13800138018", TEST_PASSWORD).await;
let refresh_cookie = login_response
.headers()
.get("set-cookie")
@@ -2773,26 +2697,12 @@ mod tests {
#[tokio::test]
async fn logout_succeeds_without_refresh_cookie_when_bearer_token_is_valid() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138019", TEST_PASSWORD).await;
let app = build_router(state);
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_logout_no_cookie",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login request should succeed");
let login_response =
password_login_request(app.clone(), "13800138019", TEST_PASSWORD).await;
let login_body = login_response
.into_body()
.collect()
@@ -2830,7 +2740,9 @@ mod tests {
#[tokio::test]
async fn logout_all_clears_cookie_and_invalidates_all_sessions() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138020", TEST_PASSWORD).await;
let app = build_router(state);
let first_login_response = app
.clone()
@@ -2845,8 +2757,8 @@ mod tests {
)
.body(Body::from(
serde_json::json!({
"username": "guest_logout_all_api",
"password": "secret123"
"phone": "13800138020",
"password": TEST_PASSWORD
})
.to_string(),
))
@@ -2884,8 +2796,8 @@ mod tests {
.header("x-client-instance-id", "logout-all-instance-002")
.body(Body::from(
serde_json::json!({
"username": "guest_logout_all_api",
"password": "secret123"
"phone": "13800138020",
"password": TEST_PASSWORD
})
.to_string(),
))
@@ -2976,26 +2888,12 @@ mod tests {
#[tokio::test]
async fn logout_all_succeeds_without_refresh_cookie_when_bearer_token_is_valid() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let state = AppState::new(AppConfig::default()).expect("state should build");
seed_phone_user_with_password(&state, "13800138021", TEST_PASSWORD).await;
let app = build_router(state);
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_logout_all_nc",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login request should succeed");
let login_response =
password_login_request(app.clone(), "13800138021", TEST_PASSWORD).await;
let login_body = login_response
.into_body()
.collect()
@@ -3079,26 +2977,11 @@ mod tests {
config.admin_username = Some("root".to_string());
config.admin_password = Some("secret123".to_string());
let state = AppState::new(config).expect("state should build");
seed_phone_user_with_password(&state, "13800138022", TEST_PASSWORD).await;
let app = build_router(state.clone());
let login_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth/entry")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "guest_admin_forbidden",
"password": "secret123"
})
.to_string(),
))
.expect("login request should build"),
)
.await
.expect("login should succeed");
let login_response =
password_login_request(app.clone(), "13800138022", TEST_PASSWORD).await;
let login_body = login_response
.into_body()
.collect()

View File

@@ -34,6 +34,7 @@ pub async fn auth_me(
user: map_auth_user_payload(user.user),
available_login_methods: build_available_login_methods(
state.config.sms_auth_enabled,
true,
state.config.wechat_auth_enabled,
),
},

View File

@@ -64,7 +64,7 @@ fn map_public_user_search_error(error: module_auth::PasswordEntryError) -> AppEr
}
module_auth::PasswordEntryError::Store(_)
| module_auth::PasswordEntryError::PasswordHash(_)
| module_auth::PasswordEntryError::InvalidUsername
| module_auth::PasswordEntryError::InvalidPhoneNumber
| module_auth::PasswordEntryError::InvalidPasswordLength
| module_auth::PasswordEntryError::InvalidCredentials
| module_auth::PasswordEntryError::UserNotFound => {

View File

@@ -257,13 +257,9 @@ mod tests {
async fn seed_authenticated_state(config: AppConfig) -> AppState {
let state = AppState::new(config).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "llm_proxy_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138101", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -274,7 +270,7 @@ mod tests {
session_id: "sess_llm_proxy".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("LLM 代理用户".to_string()),

View File

@@ -15,6 +15,7 @@ pub async fn auth_login_options(
AuthLoginOptionsResponse {
available_login_methods: build_available_login_methods(
state.config.sms_auth_enabled,
true,
state.config.wechat_auth_enabled,
),
},

View File

@@ -29,7 +29,7 @@ pub async fn password_entry(
let result = state
.password_entry_service()
.execute(PasswordEntryInput {
username: payload.username,
phone_number: payload.phone,
password: payload.password,
})
.await
@@ -64,10 +64,10 @@ pub async fn password_entry(
fn map_password_entry_error(error: PasswordEntryError) -> AppError {
match error {
PasswordEntryError::InvalidUsername => AppError::from_status(StatusCode::BAD_REQUEST)
PasswordEntryError::InvalidPhoneNumber => AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("手机号格式不正确")
.with_details(json!({
"field": "username",
"field": "phone",
})),
PasswordEntryError::InvalidPasswordLength => AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("密码长度需要在 6 到 128 位之间")
@@ -77,7 +77,7 @@ fn map_password_entry_error(error: PasswordEntryError) -> AppError {
PasswordEntryError::InvalidPublicUserCode => AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("叙世号格式不正确")
.with_details(json!({
"field": "username",
"field": "phone",
})),
PasswordEntryError::InvalidCredentials => {
AppError::from_status(StatusCode::UNAUTHORIZED).with_message("手机号或密码错误")

View File

@@ -100,7 +100,7 @@ pub async fn reset_password(
fn map_password_management_error(error: PasswordEntryError) -> AppError {
match error {
PasswordEntryError::InvalidUsername | PasswordEntryError::InvalidPublicUserCode => {
PasswordEntryError::InvalidPhoneNumber | PasswordEntryError::InvalidPublicUserCode => {
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string())
}
PasswordEntryError::InvalidPasswordLength => AppError::from_status(StatusCode::BAD_REQUEST)

View File

@@ -422,13 +422,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "browse_history_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138102", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -439,7 +435,7 @@ mod tests {
session_id: "sess_runtime_browse_history".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("浏览历史用户".to_string()),

View File

@@ -164,13 +164,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "runtime_inventory_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138103", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -181,7 +177,7 @@ mod tests {
session_id: "sess_runtime_inventory".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("背包查询用户".to_string()),

View File

@@ -481,13 +481,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "runtime_profile_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138104", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -498,7 +494,7 @@ mod tests {
session_id: "sess_runtime_profile".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("资料页用户".to_string()),

View File

@@ -380,13 +380,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "runtime_save_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138105", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -397,7 +393,7 @@ mod tests {
session_id: "sess_runtime_save".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("存档用户".to_string()),

View File

@@ -340,13 +340,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "runtime_settings_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138106", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -357,7 +353,7 @@ mod tests {
session_id: "sess_runtime_settings".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("设置用户".to_string()),

View File

@@ -1986,13 +1986,9 @@ fn runtime_story_dialogue_current_story_keeps_continue_and_deferred_options() {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "runtime_story_state_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138109", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -2003,7 +1999,7 @@ fn issue_access_token(state: &AppState) -> String {
session_id: "sess_runtime_story_state".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("运行时剧情状态用户".to_string()),

View File

@@ -38,6 +38,7 @@ pub struct AppState {
admin_runtime: Option<AdminRuntime>,
refresh_cookie_config: RefreshCookieConfig,
oss_client: Option<OssClient>,
#[cfg_attr(test, allow(dead_code))]
auth_store: InMemoryAuthStore,
password_entry_service: PasswordEntryService,
refresh_session_service: RefreshSessionService,
@@ -96,6 +97,9 @@ pub enum AppStateInitError {
impl AppState {
pub fn new(config: AppConfig) -> Result<Self, AppStateInitError> {
#[cfg(test)]
let auth_store = InMemoryAuthStore::default();
#[cfg(not(test))]
let auth_store = InMemoryAuthStore::from_persistence_path(config.auth_store_path.clone())
.map_err(AppStateInitError::AuthStore)?;
Self::new_with_auth_store(config, auth_store)
@@ -206,19 +210,27 @@ impl AppState {
}
pub async fn sync_auth_store_snapshot_to_spacetime(&self) -> Result<(), SpacetimeClientError> {
#[cfg(test)]
return Ok(());
#[cfg(not(test))]
let snapshot_json = self
.auth_store
.export_snapshot_json()
.map_err(SpacetimeClientError::Runtime)?;
#[cfg(not(test))]
let updated_at_micros = i64::try_from(
OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000,
)
.map_err(|_| SpacetimeClientError::Runtime("认证快照更新时间超出 i64 范围".to_string()))?;
#[cfg(not(test))]
self.spacetime_client
.upsert_auth_store_snapshot(snapshot_json, updated_at_micros)
.await?;
// ?????????????????????????????????
#[cfg(not(test))]
self.spacetime_client.import_auth_store_snapshot().await?;
#[cfg(not(test))]
Ok(())
}
@@ -401,6 +413,47 @@ impl AppState {
#[cfg(test)]
impl AppState {
pub(crate) async fn seed_test_phone_user_with_password(
&self,
phone_number: &str,
password: &str,
) -> module_auth::AuthUser {
let now = OffsetDateTime::now_utc();
self.phone_auth_service()
.send_code(
module_auth::SendPhoneCodeInput {
phone_number: phone_number.to_string(),
scene: module_auth::PhoneAuthScene::Login,
},
now,
)
.await
.expect("test phone code should send");
let user = self
.phone_auth_service()
.login(
module_auth::PhoneLoginInput {
phone_number: phone_number.to_string(),
verify_code: "123456".to_string(),
},
now + time::Duration::seconds(1),
)
.await
.expect("test phone login should create user")
.user;
let changed = self
.password_entry_service()
.change_password(module_auth::ChangePasswordInput {
user_id: user.id.clone(),
current_password: None,
new_password: password.to_string(),
})
.await
.expect("test password should set");
changed.user
}
fn cache_test_runtime_snapshot(&self, record: RuntimeSnapshotRecord) {
self.test_runtime_snapshot_store
.lock()

View File

@@ -797,13 +797,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "story_battles_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138107", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -814,7 +810,7 @@ mod tests {
session_id: "sess_story_battles".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("战斗接口用户".to_string()),

View File

@@ -384,13 +384,9 @@ mod tests {
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "story_sessions_user".to_string(),
password: "secret123".to_string(),
})
.seed_test_phone_user_with_password("13800138108", "secret123")
.await
.expect("seed login should succeed");
.id;
state
}
@@ -401,7 +397,7 @@ mod tests {
session_id: "sess_story_sessions".to_string(),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 1,
token_version: 2,
phone_verified: true,
binding_status: BindingStatus::Active,
display_name: Some("故事会话用户".to_string()),