use std::{ collections::HashMap, error::Error, fmt, sync::{Arc, Mutex}, }; use platform_auth::{hash_password, verify_password}; const USERNAME_MIN_LENGTH: usize = 3; const USERNAME_MAX_LENGTH: usize = 24; const PASSWORD_MIN_LENGTH: usize = 6; const PASSWORD_MAX_LENGTH: usize = 128; #[derive(Clone, Debug, PartialEq, Eq)] pub enum AuthLoginMethod { Password, Phone, Wechat, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum AuthBindingStatus { Active, PendingBindPhone, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct AuthUser { pub id: String, pub username: String, pub display_name: String, pub phone_number_masked: Option, pub login_method: AuthLoginMethod, pub binding_status: AuthBindingStatus, pub wechat_bound: bool, pub token_version: u64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PasswordEntryInput { pub username: String, pub password: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PasswordEntryResult { pub user: AuthUser, pub created: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct AuthMeResult { pub user: AuthUser, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum PasswordEntryError { InvalidUsername, InvalidPasswordLength, InvalidCredentials, Store(String), PasswordHash(String), } #[derive(Clone, Debug)] pub struct InMemoryPasswordUserStore { inner: Arc>, } #[derive(Debug)] struct InMemoryPasswordUserStoreState { next_id: u64, users_by_username: HashMap, } #[derive(Clone, Debug)] struct StoredPasswordUser { user: AuthUser, password_hash: String, } #[derive(Clone, Debug)] pub struct PasswordEntryService { store: InMemoryPasswordUserStore, } impl PasswordEntryService { pub fn new(store: InMemoryPasswordUserStore) -> Self { Self { store } } pub async fn execute( &self, input: PasswordEntryInput, ) -> Result { let username = normalize_username(&input.username)?; validate_password(&input.password)?; if let Some(existing_user) = self.store.find_by_username(&username)? { let is_valid = verify_password(&existing_user.password_hash, &input.password) .await .map_err(|error| PasswordEntryError::PasswordHash(error.to_string()))?; if !is_valid { return Err(PasswordEntryError::InvalidCredentials); } return Ok(PasswordEntryResult { user: existing_user.user, created: false, }); } let password_hash = hash_password(&input.password) .await .map_err(|error| PasswordEntryError::PasswordHash(error.to_string()))?; match self .store .create_user(username.clone(), password_hash.clone()) { Ok(user) => Ok(PasswordEntryResult { user, created: true, }), Err(CreateUserError::AlreadyExists) => { let existing_user = self.store.find_by_username(&username)?.ok_or_else(|| { PasswordEntryError::Store("唯一键冲突后未能重新读取账号".to_string()) })?; let is_valid = verify_password(&existing_user.password_hash, &input.password) .await .map_err(|error| PasswordEntryError::PasswordHash(error.to_string()))?; if !is_valid { return Err(PasswordEntryError::InvalidCredentials); } Ok(PasswordEntryResult { user: existing_user.user, created: false, }) } Err(CreateUserError::Store(message)) => Err(PasswordEntryError::Store(message)), } } } impl PasswordEntryService { pub fn get_user_by_id( &self, user_id: &str, ) -> Result, PasswordEntryError> { self.store .find_by_user_id(user_id) .map(|maybe_user| maybe_user.map(|stored| AuthMeResult { user: stored.user })) } } impl Default for InMemoryPasswordUserStore { fn default() -> Self { Self { inner: Arc::new(Mutex::new(InMemoryPasswordUserStoreState { next_id: 1, users_by_username: HashMap::new(), })), } } } impl InMemoryPasswordUserStore { fn find_by_username( &self, username: &str, ) -> Result, PasswordEntryError> { let state = self .inner .lock() .map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?; Ok(state.users_by_username.get(username).cloned()) } fn create_user( &self, username: String, password_hash: String, ) -> Result { let mut state = self .inner .lock() .map_err(|_| CreateUserError::Store("用户仓储锁已中毒".to_string()))?; if state.users_by_username.contains_key(&username) { return Err(CreateUserError::AlreadyExists); } let user_id = format!("user_{:08}", state.next_id); state.next_id += 1; let user = AuthUser { id: user_id, username: username.clone(), display_name: username.clone(), phone_number_masked: None, login_method: AuthLoginMethod::Password, binding_status: AuthBindingStatus::Active, wechat_bound: false, token_version: 1, }; state.users_by_username.insert( username, StoredPasswordUser { user: user.clone(), password_hash, }, ); Ok(user) } fn find_by_user_id( &self, user_id: &str, ) -> Result, PasswordEntryError> { let state = self .inner .lock() .map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?; Ok(state .users_by_username .values() .find(|stored_user| stored_user.user.id == user_id) .cloned()) } } #[derive(Debug, PartialEq, Eq)] enum CreateUserError { AlreadyExists, Store(String), } impl AuthLoginMethod { pub fn as_str(&self) -> &'static str { match self { Self::Password => "password", Self::Phone => "phone", Self::Wechat => "wechat", } } } impl AuthBindingStatus { pub fn as_str(&self) -> &'static str { match self { Self::Active => "active", Self::PendingBindPhone => "pending_bind_phone", } } } impl fmt::Display for PasswordEntryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidUsername => f.write_str("用户名只允许 3 到 24 位字母、数字、下划线"), Self::InvalidPasswordLength => f.write_str("密码长度需要在 6 到 128 位之间"), Self::InvalidCredentials => f.write_str("用户名或密码错误"), Self::Store(message) | Self::PasswordHash(message) => f.write_str(message), } } } impl Error for PasswordEntryError {} fn normalize_username(raw_username: &str) -> Result { let username = raw_username.trim().to_string(); let valid_length = (USERNAME_MIN_LENGTH..=USERNAME_MAX_LENGTH).contains(&username.chars().count()); let valid_chars = username .chars() .all(|character| character.is_ascii_alphanumeric() || character == '_'); if !valid_length || !valid_chars { return Err(PasswordEntryError::InvalidUsername); } Ok(username) } fn validate_password(password: &str) -> Result<(), PasswordEntryError> { let length = password.chars().count(); if !(PASSWORD_MIN_LENGTH..=PASSWORD_MAX_LENGTH).contains(&length) { return Err(PasswordEntryError::InvalidPasswordLength); } Ok(()) } #[cfg(test)] mod tests { use super::*; fn build_service() -> PasswordEntryService { PasswordEntryService::new(InMemoryPasswordUserStore::default()) } #[tokio::test] async fn first_password_entry_creates_user() { let service = build_service(); let result = service .execute(PasswordEntryInput { username: "guest_001".to_string(), password: "secret123".to_string(), }) .await .expect("first login should succeed"); assert!(result.created); assert_eq!(result.user.id, "user_00000001"); assert_eq!(result.user.username, "guest_001"); assert_eq!(result.user.display_name, "guest_001"); assert_eq!(result.user.login_method, AuthLoginMethod::Password); assert_eq!(result.user.binding_status, AuthBindingStatus::Active); } #[tokio::test] async fn repeated_password_entry_reuses_same_user() { let service = build_service(); let first = service .execute(PasswordEntryInput { username: "guest_001".to_string(), password: "secret123".to_string(), }) .await .expect("first login should succeed"); let second = service .execute(PasswordEntryInput { username: "guest_001".to_string(), password: "secret123".to_string(), }) .await .expect("second login should succeed"); assert!(first.created); assert!(!second.created); assert_eq!(second.user.id, first.user.id); } #[tokio::test] async fn repeated_password_entry_rejects_wrong_password() { let service = build_service(); service .execute(PasswordEntryInput { username: "guest_001".to_string(), password: "secret123".to_string(), }) .await .expect("first login should succeed"); let error = service .execute(PasswordEntryInput { username: "guest_001".to_string(), password: "secret999".to_string(), }) .await .expect_err("wrong password should fail"); assert_eq!(error, PasswordEntryError::InvalidCredentials); } #[tokio::test] async fn invalid_username_returns_bad_request_error() { let service = build_service(); let error = service .execute(PasswordEntryInput { username: "坏用户名".to_string(), password: "secret123".to_string(), }) .await .expect_err("invalid username should fail"); assert_eq!(error, PasswordEntryError::InvalidUsername); } }