1
This commit is contained in:
@@ -53,6 +53,8 @@ pub struct AuthUser {
|
||||
pub binding_status: AuthBindingStatus,
|
||||
pub wechat_bound: bool,
|
||||
pub token_version: u64,
|
||||
#[serde(default = "default_auth_user_created_at")]
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -227,6 +229,7 @@ pub struct BindWechatPhoneInput {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BindWechatPhoneResult {
|
||||
pub user: AuthUser,
|
||||
pub activated_new_user: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -966,11 +969,14 @@ impl PhoneAuthService {
|
||||
return Err(PhoneAuthError::UserStateMismatch);
|
||||
}
|
||||
|
||||
let merged_user = self
|
||||
let (merged_user, activated_new_user) = self
|
||||
.store
|
||||
.bind_wechat_phone_to_user(&input.user_id, normalized_phone)?;
|
||||
|
||||
Ok(BindWechatPhoneResult { user: merged_user })
|
||||
Ok(BindWechatPhoneResult {
|
||||
user: merged_user,
|
||||
activated_new_user,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,6 +1384,7 @@ impl InMemoryAuthStore {
|
||||
let public_user_code = build_public_user_code(sequence);
|
||||
state.next_user_id += 1;
|
||||
let username = build_system_username("phone", state.next_user_id);
|
||||
let created_at = current_auth_user_created_at();
|
||||
let user = AuthUser {
|
||||
id: user_id.clone(),
|
||||
public_user_code,
|
||||
@@ -1389,6 +1396,7 @@ impl InMemoryAuthStore {
|
||||
binding_status: AuthBindingStatus::Active,
|
||||
wechat_bound: false,
|
||||
token_version: 1,
|
||||
created_at,
|
||||
};
|
||||
state
|
||||
.phone_to_user_id
|
||||
@@ -1426,6 +1434,7 @@ impl InMemoryAuthStore {
|
||||
let public_user_code = build_public_user_code(sequence);
|
||||
state.next_user_id += 1;
|
||||
let username = build_system_username("phone", state.next_user_id);
|
||||
let created_at = current_auth_user_created_at();
|
||||
let user = AuthUser {
|
||||
id: user_id.clone(),
|
||||
public_user_code,
|
||||
@@ -1437,6 +1446,7 @@ impl InMemoryAuthStore {
|
||||
binding_status: AuthBindingStatus::Active,
|
||||
wechat_bound: false,
|
||||
token_version: 1,
|
||||
created_at,
|
||||
};
|
||||
state
|
||||
.phone_to_user_id
|
||||
@@ -1470,6 +1480,7 @@ impl InMemoryAuthStore {
|
||||
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 created_at = current_auth_user_created_at();
|
||||
let display_name = profile
|
||||
.display_name
|
||||
.as_deref()
|
||||
@@ -1488,6 +1499,7 @@ impl InMemoryAuthStore {
|
||||
binding_status: AuthBindingStatus::PendingBindPhone,
|
||||
wechat_bound: true,
|
||||
token_version: 1,
|
||||
created_at,
|
||||
};
|
||||
state.users_by_username.insert(
|
||||
username,
|
||||
@@ -1863,7 +1875,7 @@ impl InMemoryAuthStore {
|
||||
&self,
|
||||
pending_user_id: &str,
|
||||
phone_number: PhoneNumberSnapshot,
|
||||
) -> Result<AuthUser, PhoneAuthError> {
|
||||
) -> Result<(AuthUser, bool), PhoneAuthError> {
|
||||
let mut state = self
|
||||
.inner
|
||||
.lock()
|
||||
@@ -1910,7 +1922,7 @@ impl InMemoryAuthStore {
|
||||
let next_user = target_user.user.clone();
|
||||
self.persist_phone_state(&state)?;
|
||||
|
||||
return Ok(next_user);
|
||||
return Ok((next_user, false));
|
||||
}
|
||||
|
||||
state
|
||||
@@ -1929,7 +1941,7 @@ impl InMemoryAuthStore {
|
||||
let next_user = stored_user.user.clone();
|
||||
self.persist_phone_state(&state)?;
|
||||
|
||||
Ok(next_user)
|
||||
Ok((next_user, true))
|
||||
}
|
||||
|
||||
fn find_session_by_refresh_token_hash(
|
||||
@@ -2219,7 +2231,7 @@ impl fmt::Display for PasswordEntryError {
|
||||
match self {
|
||||
Self::InvalidPhoneNumber => f.write_str("手机号格式不正确"),
|
||||
Self::InvalidPasswordLength => f.write_str("密码长度需要在 6 到 128 位之间"),
|
||||
Self::InvalidPublicUserCode => f.write_str("陶泥号格式不正确"),
|
||||
Self::InvalidPublicUserCode => f.write_str("百梦号格式不正确"),
|
||||
Self::InvalidDisplayName => {
|
||||
f.write_str("昵称需要为 2 到 20 位中文、英文、数字或下划线")
|
||||
}
|
||||
@@ -2499,7 +2511,7 @@ fn build_system_username(prefix: &str, sequence: u64) -> String {
|
||||
format!("{prefix}_{sequence:08}")
|
||||
}
|
||||
|
||||
// 公开陶泥号是稳定的公开检索键,不替代内部 user_id,仅用于展示、分享与搜索。
|
||||
// 公开百梦号是稳定的公开检索键,不替代内部 user_id,仅用于展示、分享与搜索。
|
||||
fn build_public_user_code(sequence: u64) -> String {
|
||||
format!("SY-{sequence:08}")
|
||||
}
|
||||
@@ -2527,6 +2539,15 @@ fn format_rfc3339(value: OffsetDateTime) -> Result<String, String> {
|
||||
format_shared_rfc3339(value)
|
||||
}
|
||||
|
||||
fn current_auth_user_created_at() -> String {
|
||||
format_rfc3339(OffsetDateTime::now_utc())
|
||||
.unwrap_or_else(|_| default_auth_user_created_at())
|
||||
}
|
||||
|
||||
fn default_auth_user_created_at() -> String {
|
||||
"1970-01-01T00:00:00Z".to_string()
|
||||
}
|
||||
|
||||
fn parse_phone_code_time(value: &str, field_label: &str) -> Result<OffsetDateTime, PhoneAuthError> {
|
||||
parse_rfc3339(value)
|
||||
.map_err(|error| PhoneAuthError::Store(format!("短信验证码{field_label}解析失败:{error}")))
|
||||
@@ -3467,6 +3488,7 @@ mod tests {
|
||||
assert_eq!(merged.user.id, phone_user.id);
|
||||
assert_eq!(merged.user.binding_status, AuthBindingStatus::Active);
|
||||
assert!(merged.user.wechat_bound);
|
||||
assert!(!merged.activated_new_user);
|
||||
|
||||
let reused_wechat_user = wechat_service
|
||||
.resolve_login(ResolveWechatLoginInput {
|
||||
@@ -3484,4 +3506,51 @@ mod tests {
|
||||
assert_eq!(reused_wechat_user.user.id, phone_user.id);
|
||||
assert!(reused_wechat_user.user.wechat_bound);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bind_wechat_phone_activates_pending_wechat_user_for_new_phone() {
|
||||
let store = build_store();
|
||||
let wechat_service = WechatAuthService::new(store.clone());
|
||||
let phone_service = build_phone_service(store);
|
||||
let now = OffsetDateTime::from_unix_timestamp(1_713_680_000).expect("valid timestamp");
|
||||
|
||||
let wechat_user = wechat_service
|
||||
.resolve_login(ResolveWechatLoginInput {
|
||||
profile: WechatIdentityProfile {
|
||||
provider_uid: "wx-openid-new-phone".to_string(),
|
||||
provider_union_id: Some("wx-union-new-phone".to_string()),
|
||||
display_name: Some("新微信用户".to_string()),
|
||||
avatar_url: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("wechat login should create pending user")
|
||||
.user;
|
||||
|
||||
phone_service
|
||||
.send_code(
|
||||
SendPhoneCodeInput {
|
||||
phone_number: "13800138099".to_string(),
|
||||
scene: PhoneAuthScene::BindPhone,
|
||||
},
|
||||
now + Duration::seconds(1),
|
||||
)
|
||||
.await
|
||||
.expect("bind phone code should send");
|
||||
let bound = phone_service
|
||||
.bind_wechat_phone(
|
||||
BindWechatPhoneInput {
|
||||
user_id: wechat_user.id.clone(),
|
||||
phone_number: "13800138099".to_string(),
|
||||
verify_code: "123456".to_string(),
|
||||
},
|
||||
now + Duration::seconds(2),
|
||||
)
|
||||
.await
|
||||
.expect("bind phone should activate pending user");
|
||||
|
||||
assert_eq!(bound.user.id, wechat_user.id);
|
||||
assert_eq!(bound.user.binding_status, AuthBindingStatus::Active);
|
||||
assert!(bound.activated_new_user);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user