Merge branch 'master' into codex/puzzle-clear-template-runtime-fixes
# Conflicts: # .hermes/shared-memory/decision-log.md # .hermes/shared-memory/project-overview.md # docs/【开发运维】本地开发验证与生产运维-2026-05-15.md # scripts/dev.test.ts # server-rs/crates/api-server/src/creation_entry_config.rs # server-rs/crates/api-server/src/wooden_fish.rs # server-rs/crates/module-auth/src/lib.rs # server-rs/crates/spacetime-client/src/wooden_fish.rs # server-rs/crates/spacetime-module/src/auth/procedures.rs # src/components/custom-world-home/creationWorkShelf.ts # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/rpgEntryWorldPresentation.ts # src/services/miniGameDraftGenerationProgress.test.ts # src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
@@ -116,6 +116,7 @@ pub struct WechatIdentityProfile {
|
||||
pub provider_union_id: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub session_key: Option<String>,
|
||||
}
|
||||
|
||||
/// 已绑定微信身份快照。
|
||||
@@ -124,6 +125,7 @@ pub struct WechatIdentityRecord {
|
||||
pub user_id: String,
|
||||
pub provider_uid: String,
|
||||
pub provider_union_id: Option<String>,
|
||||
pub session_key: Option<String>,
|
||||
}
|
||||
|
||||
/// 微信授权 state 快照。
|
||||
|
||||
@@ -11,7 +11,7 @@ pub use errors::*;
|
||||
pub use events::*;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
@@ -94,6 +94,7 @@ struct StoredWechatIdentity {
|
||||
provider_union_id: Option<String>,
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
session_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -917,16 +918,47 @@ impl Default for InMemoryAuthStoreState {
|
||||
|
||||
impl InMemoryAuthStoreState {
|
||||
fn from_persistent_snapshot(snapshot: PersistentAuthStoreSnapshot) -> Self {
|
||||
let existing_user_ids = snapshot
|
||||
.users_by_username
|
||||
.values()
|
||||
.map(|stored| stored.user.id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
let phone_to_user_id = snapshot
|
||||
.phone_to_user_id
|
||||
.into_iter()
|
||||
.filter(|(_, user_id)| existing_user_ids.contains(user_id))
|
||||
.collect();
|
||||
let sessions_by_id = snapshot
|
||||
.sessions_by_id
|
||||
.into_iter()
|
||||
.filter(|(_, stored)| existing_user_ids.contains(&stored.session.user_id))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let session_id_by_refresh_token_hash = snapshot
|
||||
.session_id_by_refresh_token_hash
|
||||
.into_iter()
|
||||
.filter(|(_, session_id)| sessions_by_id.contains_key(session_id))
|
||||
.collect();
|
||||
let wechat_identity_by_provider_uid = snapshot
|
||||
.wechat_identity_by_provider_uid
|
||||
.into_iter()
|
||||
.filter(|(_, identity)| existing_user_ids.contains(&identity.user_id))
|
||||
.collect();
|
||||
let user_id_by_provider_union_id = snapshot
|
||||
.user_id_by_provider_union_id
|
||||
.into_iter()
|
||||
.filter(|(_, user_id)| existing_user_ids.contains(user_id))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
next_user_id: snapshot.next_user_id,
|
||||
users_by_username: snapshot.users_by_username,
|
||||
phone_to_user_id: snapshot.phone_to_user_id,
|
||||
sessions_by_id: snapshot.sessions_by_id,
|
||||
session_id_by_refresh_token_hash: snapshot.session_id_by_refresh_token_hash,
|
||||
phone_to_user_id,
|
||||
sessions_by_id,
|
||||
session_id_by_refresh_token_hash,
|
||||
phone_codes_by_key: HashMap::new(),
|
||||
wechat_states_by_token: HashMap::new(),
|
||||
wechat_identity_by_provider_uid: snapshot.wechat_identity_by_provider_uid,
|
||||
user_id_by_provider_union_id: snapshot.user_id_by_provider_union_id,
|
||||
wechat_identity_by_provider_uid,
|
||||
user_id_by_provider_union_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,6 +1356,7 @@ impl InMemoryAuthStore {
|
||||
provider_union_id: normalize_optional_string(profile.provider_union_id),
|
||||
display_name: normalize_optional_string(profile.display_name),
|
||||
avatar_url,
|
||||
session_key: normalize_optional_string(profile.session_key),
|
||||
};
|
||||
if let Some(provider_union_id) = identity.provider_union_id.clone() {
|
||||
state
|
||||
@@ -1393,6 +1426,7 @@ impl InMemoryAuthStore {
|
||||
user_id: identity.user_id.clone(),
|
||||
provider_uid: identity.provider_uid.clone(),
|
||||
provider_union_id: identity.provider_union_id.clone(),
|
||||
session_key: identity.session_key.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1409,6 +1443,7 @@ impl InMemoryAuthStore {
|
||||
let next_display_name = normalize_optional_string(profile.display_name);
|
||||
let next_avatar_url = normalize_optional_string(profile.avatar_url);
|
||||
let next_provider_union_id = normalize_optional_string(profile.provider_union_id);
|
||||
let next_session_key = normalize_optional_string(profile.session_key);
|
||||
let next_provider_uid =
|
||||
normalize_required_string(&profile.provider_uid).unwrap_or_default();
|
||||
{
|
||||
@@ -1430,6 +1465,9 @@ impl InMemoryAuthStore {
|
||||
identity.display_name = next_display_name.clone();
|
||||
identity.avatar_url = next_avatar_url;
|
||||
identity.provider_union_id = next_provider_union_id.clone();
|
||||
if next_session_key.is_some() {
|
||||
identity.session_key = next_session_key.clone();
|
||||
}
|
||||
state
|
||||
.wechat_identity_by_provider_uid
|
||||
.insert(next_provider_uid.clone(), identity);
|
||||
@@ -2722,6 +2760,54 @@ mod tests {
|
||||
assert_eq!(rotated.user.id, user.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_json_drops_orphan_phone_index_before_phone_login() {
|
||||
let snapshot = PersistentAuthStoreSnapshot {
|
||||
next_user_id: 9,
|
||||
users_by_username: HashMap::new(),
|
||||
phone_to_user_id: HashMap::from([(
|
||||
"+8613800138032".to_string(),
|
||||
"user_missing_phone_owner".to_string(),
|
||||
)]),
|
||||
sessions_by_id: HashMap::new(),
|
||||
session_id_by_refresh_token_hash: HashMap::new(),
|
||||
wechat_identity_by_provider_uid: HashMap::new(),
|
||||
user_id_by_provider_union_id: HashMap::new(),
|
||||
};
|
||||
let snapshot_json = serde_json::to_string(&snapshot).expect("snapshot should serialize");
|
||||
let restored_store = InMemoryAuthStore::from_snapshot_json(&snapshot_json)
|
||||
.expect("snapshot json should restore");
|
||||
let phone_service = build_phone_service(restored_store);
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
phone_service
|
||||
.send_code(
|
||||
SendPhoneCodeInput {
|
||||
phone_number: "13800138032".to_string(),
|
||||
scene: PhoneAuthScene::Login,
|
||||
},
|
||||
now,
|
||||
)
|
||||
.await
|
||||
.expect("phone code should send");
|
||||
let result = phone_service
|
||||
.login(
|
||||
PhoneLoginInput {
|
||||
phone_number: "13800138032".to_string(),
|
||||
verify_code: DEFAULT_SMS_MOCK_VERIFY_CODE.to_string(),
|
||||
},
|
||||
now + Duration::seconds(1),
|
||||
)
|
||||
.await
|
||||
.expect("orphan phone index should not block phone login");
|
||||
|
||||
assert!(result.created);
|
||||
assert_eq!(
|
||||
result.user.phone_number_masked.as_deref(),
|
||||
Some("138****8032")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_rejects_email_or_username_identifier() {
|
||||
let service = build_password_service(build_store());
|
||||
@@ -3314,6 +3400,7 @@ mod tests {
|
||||
provider_union_id: Some("wx-union-shared".to_string()),
|
||||
display_name: Some("微信旅人甲".to_string()),
|
||||
avatar_url: None,
|
||||
session_key: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
@@ -3335,6 +3422,7 @@ mod tests {
|
||||
provider_union_id: Some("wx-union-shared".to_string()),
|
||||
display_name: Some("微信旅人乙".to_string()),
|
||||
avatar_url: None,
|
||||
session_key: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
@@ -3383,6 +3471,7 @@ mod tests {
|
||||
provider_union_id: Some("wx-union-bind".to_string()),
|
||||
display_name: Some("待绑定微信用户".to_string()),
|
||||
avatar_url: None,
|
||||
session_key: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
@@ -3428,6 +3517,7 @@ mod tests {
|
||||
provider_union_id: Some("wx-union-bind".to_string()),
|
||||
display_name: Some("已归并微信用户".to_string()),
|
||||
avatar_url: None,
|
||||
session_key: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user