feat: add puzzle clear template runtime

This commit is contained in:
2026-06-03 22:11:46 +08:00
parent 6e74cf5add
commit 1b5e098225
148 changed files with 19588 additions and 241 deletions

View File

@@ -992,6 +992,36 @@ impl InMemoryAuthStore {
.map_err(RefreshSessionError::Store)
}
fn resolve_phone_user_locked(
state: &mut InMemoryAuthStoreState,
phone_number: &str,
) -> Option<StoredPasswordUser> {
if let Some(user_id) = state.phone_to_user_id.get(phone_number).cloned() {
if let Some(stored_user) = state
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == user_id)
.cloned()
{
return Some(stored_user);
}
state.phone_to_user_id.remove(phone_number);
}
let Some(stored_user) = state
.users_by_username
.values()
.find(|stored_user| stored_user.phone_number.as_deref() == Some(phone_number))
.cloned()
else {
return None;
};
state
.phone_to_user_id
.insert(phone_number.to_string(), stored_user.user.id.clone());
Some(stored_user)
}
fn find_by_user_id(
&self,
user_id: &str,
@@ -1086,36 +1116,22 @@ impl InMemoryAuthStore {
&self,
phone_number: &str,
) -> Result<Option<StoredPasswordUser>, PhoneAuthError> {
let state = self
let mut state = self
.inner
.lock()
.map_err(|_| PhoneAuthError::Store("用户仓储锁已中毒".to_string()))?;
let Some(user_id) = state.phone_to_user_id.get(phone_number) else {
return Ok(None);
};
Ok(state
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == *user_id)
.cloned())
Ok(Self::resolve_phone_user_locked(&mut state, phone_number))
}
fn find_by_phone_number_for_password(
&self,
phone_number: &str,
) -> Result<Option<StoredPasswordUser>, PasswordEntryError> {
let state = self
let mut state = self
.inner
.lock()
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
let Some(user_id) = state.phone_to_user_id.get(phone_number) else {
return Ok(None);
};
Ok(state
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == *user_id)
.cloned())
Ok(Self::resolve_phone_user_locked(&mut state, phone_number))
}
fn update_user_profile(
@@ -1158,7 +1174,7 @@ impl InMemoryAuthStore {
.inner
.lock()
.map_err(|_| PhoneAuthError::Store("用户仓储锁已中毒".to_string()))?;
if state.phone_to_user_id.contains_key(&phone_number.e164) {
if Self::resolve_phone_user_locked(&mut state, &phone_number.e164).is_some() {
return Err(PhoneAuthError::Store(
"手机号已存在,无法重复创建账号".to_string(),
));
@@ -1212,7 +1228,7 @@ impl InMemoryAuthStore {
.inner
.lock()
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
if state.phone_to_user_id.contains_key(&phone_number.e164) {
if Self::resolve_phone_user_locked(&mut state, &phone_number.e164).is_some() {
return Err(PasswordEntryError::InvalidCredentials);
}
@@ -1662,7 +1678,9 @@ impl InMemoryAuthStore {
.lock()
.map_err(|_| PhoneAuthError::Store("用户仓储锁已中毒".to_string()))?;
let existing_phone_user_id = state.phone_to_user_id.get(&phone_number.e164).cloned();
let existing_phone_user_id =
Self::resolve_phone_user_locked(&mut state, &phone_number.e164)
.map(|stored_user| stored_user.user.id);
if let Some(target_user_id) = existing_phone_user_id
&& target_user_id != pending_user_id
{
@@ -2048,10 +2066,8 @@ impl InMemoryAuthStore {
.inner
.lock()
.map_err(|_| PhoneAuthError::Store("用户仓储锁已中毒".to_string()))?;
let user_id = state
.phone_to_user_id
.get(phone_number)
.cloned()
let user_id = Self::resolve_phone_user_locked(&mut state, phone_number)
.map(|stored_user| stored_user.user.id)
.ok_or(PhoneAuthError::UserNotFound)?;
for stored_user in state.users_by_username.values_mut() {
@@ -2572,6 +2588,90 @@ mod tests {
assert_eq!(error, PhoneAuthError::UserNotFound);
}
#[tokio::test]
async fn dev_password_registration_ignores_orphan_phone_index() {
let snapshot = PersistentAuthStoreSnapshot {
next_user_id: 7,
users_by_username: HashMap::new(),
phone_to_user_id: HashMap::from([(
"+8613800138004".to_string(),
"user_deleted".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 json should serialize");
let service = build_password_service(
InMemoryAuthStore::from_snapshot_json(&snapshot_json).expect("snapshot should restore"),
);
let created = service
.execute_with_dev_registration(PasswordEntryInput {
phone_number: "13800138004".to_string(),
password: "secret123".to_string(),
})
.await
.expect("orphan phone index should not block dev registration");
assert!(created.created);
assert_eq!(
created.user.phone_number_masked.as_deref(),
Some("138****8004")
);
}
#[tokio::test]
async fn phone_login_ignores_orphan_phone_index_after_code_verification() {
let snapshot = PersistentAuthStoreSnapshot {
next_user_id: 8,
users_by_username: HashMap::new(),
phone_to_user_id: HashMap::from([(
"+8613800138005".to_string(),
"user_deleted".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 json should serialize");
let phone_service = build_phone_service(
InMemoryAuthStore::from_snapshot_json(&snapshot_json).expect("snapshot should restore"),
);
let now = OffsetDateTime::now_utc();
phone_service
.send_code(
SendPhoneCodeInput {
phone_number: "13800138005".to_string(),
scene: PhoneAuthScene::Login,
},
now,
)
.await
.expect("phone code should send");
let created = phone_service
.login(
PhoneLoginInput {
phone_number: "13800138005".to_string(),
verify_code: "123456".to_string(),
},
now + Duration::seconds(1),
)
.await
.expect("orphan phone index should not turn login into duplicate create");
assert!(created.created);
assert_eq!(
created.user.phone_number_masked.as_deref(),
Some("138****8005")
);
}
#[tokio::test]
async fn snapshot_json_restores_user_and_refresh_session_after_roundtrip() {
let store = InMemoryAuthStore::default();