fix: prevent reused account ownership for orphan works

This commit is contained in:
kdletters
2026-05-27 22:44:01 +08:00
parent 83f73289dc
commit 48dd96d5cd
10 changed files with 450 additions and 25 deletions

View File

@@ -235,6 +235,22 @@ pub fn build_system_username(prefix: &str, sequence: u64) -> String {
format!("{prefix}_{sequence:08}")
}
pub fn build_wechat_username(display_name: &str, provider_uid: &str) -> String {
let normalized_display_name = display_name.trim();
let normalized_provider_uid = provider_uid.trim();
let fallback_display_name = if normalized_display_name.is_empty() {
"微信旅人"
} else {
normalized_display_name
};
let fallback_provider_uid = if normalized_provider_uid.is_empty() {
"openid"
} else {
normalized_provider_uid
};
format!("{fallback_display_name}_{fallback_provider_uid}")
}
// 公开陶泥号是稳定的公开检索键,不替代内部 user_id仅用于展示、分享与搜索。
pub fn build_public_user_code(sequence: u64) -> String {
format!("SY-{sequence:08}")

View File

@@ -800,6 +800,21 @@ impl AuthUserService {
Self { store }
}
pub fn ensure_orphan_work_owner_user(
&self,
user_id: &str,
username: &str,
display_name: &str,
public_user_code: &str,
) -> Result<AuthUser, PasswordEntryError> {
self.store.ensure_orphan_work_owner_user(
user_id,
username,
display_name,
public_user_code,
)
}
pub fn get_user_by_id(&self, user_id: &str) -> Result<Option<AuthUser>, LogoutError> {
self.store
.find_by_user_id(user_id)
@@ -997,6 +1012,68 @@ impl InMemoryAuthStore {
.cloned())
}
fn ensure_orphan_work_owner_user(
&self,
user_id: &str,
username: &str,
display_name: &str,
public_user_code: &str,
) -> Result<AuthUser, PasswordEntryError> {
let user_id = normalize_required_string(user_id).ok_or_else(|| {
PasswordEntryError::Store("孤儿作品占位用户 id 不能为空".to_string())
})?;
let username = normalize_required_string(username).ok_or_else(|| {
PasswordEntryError::Store("孤儿作品占位用户名不能为空".to_string())
})?;
let display_name = normalize_required_string(display_name).ok_or_else(|| {
PasswordEntryError::Store("孤儿作品占位展示名不能为空".to_string())
})?;
let public_user_code = normalize_required_string(public_user_code).ok_or_else(|| {
PasswordEntryError::Store("孤儿作品占位陶泥号不能为空".to_string())
})?;
let mut state = self
.inner
.lock()
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
if let Some(stored) = state
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == user_id)
{
return Ok(stored.user.clone());
}
let created_at = format_rfc3339(OffsetDateTime::now_utc()).map_err(|message| {
PasswordEntryError::Store(format!("用户创建时间格式化失败:{message}"))
})?;
let user = AuthUser {
id: user_id,
public_user_code,
username: username.clone(),
display_name,
avatar_url: None,
phone_number_masked: None,
login_method: AuthLoginMethod::Password,
binding_status: AuthBindingStatus::Active,
wechat_bound: false,
token_version: 1,
created_at,
};
state.users_by_username.insert(
username,
StoredPasswordUser {
user: user.clone(),
password_hash: String::new(),
password_login_enabled: false,
phone_number: None,
},
);
self.persist_password_state(&state)?;
Ok(user)
}
fn find_by_public_user_code(
&self,
public_user_code: &str,
@@ -1099,7 +1176,7 @@ impl InMemoryAuthStore {
PhoneAuthError::Store(format!("用户创建时间格式化失败:{message}"))
})?;
let sequence = state.next_user_id;
let user_id = format!("user_{sequence:08}");
let user_id = build_prefixed_uuid_id("user_");
let public_user_code = build_public_user_code(sequence);
state.next_user_id += 1;
let username = build_system_username("phone", state.next_user_id);
@@ -1151,7 +1228,7 @@ impl InMemoryAuthStore {
PasswordEntryError::Store(format!("用户创建时间格式化失败:{message}"))
})?;
let sequence = state.next_user_id;
let user_id = format!("user_{sequence:08}");
let user_id = build_prefixed_uuid_id("user_");
let public_user_code = build_public_user_code(sequence);
state.next_user_id += 1;
let username = build_system_username("phone", state.next_user_id);
@@ -1199,10 +1276,9 @@ impl InMemoryAuthStore {
WechatAuthError::Store(format!("用户创建时间格式化失败:{message}"))
})?;
let sequence = state.next_user_id;
let user_id = format!("user_{sequence:08}");
let user_id = build_prefixed_uuid_id("user_");
let public_user_code = build_public_user_code(sequence);
state.next_user_id += 1;
let username = build_system_username("wechat", state.next_user_id);
let avatar_url = normalize_optional_string(profile.avatar_url.clone());
let display_name = profile
.display_name
@@ -1211,6 +1287,7 @@ impl InMemoryAuthStore {
.filter(|value| !value.is_empty())
.unwrap_or("微信旅人")
.to_string();
let username = build_wechat_username(&display_name, &profile.provider_uid);
let user = AuthUser {
id: user_id.clone(),
public_user_code,
@@ -2164,6 +2241,18 @@ mod tests {
use super::*;
#[test]
fn build_wechat_username_uses_display_name_and_provider_uid() {
assert_eq!(
build_wechat_username("小明", "wx-openid-123"),
"小明_wx-openid-123"
);
assert_eq!(
build_wechat_username(" ", "wx-openid-123"),
"微信旅人_wx-openid-123"
);
}
fn build_store() -> InMemoryAuthStore {
InMemoryAuthStore::default()
}
@@ -3143,6 +3232,9 @@ mod tests {
first_wechat.user.binding_status,
AuthBindingStatus::PendingBindPhone
);
assert_eq!(first_wechat.user.username, "微信旅人甲_wx-openid-first");
assert!(first_wechat.user.id.starts_with("user_"));
assert!(!first_wechat.user.id.ends_with("00000001"));
let second_wechat = wechat_service
.resolve_login(ResolveWechatLoginInput {
@@ -3160,6 +3252,7 @@ mod tests {
assert_eq!(second_wechat.user.id, first_wechat.user.id);
assert_ne!(second_wechat.user.id, phone_user.id);
assert_eq!(second_wechat.user.login_method, AuthLoginMethod::Wechat);
assert_eq!(second_wechat.user.username, first_wechat.user.username);
}
#[tokio::test]