Files
Genarrative/server-rs/crates/module-auth/src/lib.rs
2026-04-21 14:57:17 +08:00

386 lines
11 KiB
Rust

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<String>,
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<Mutex<InMemoryPasswordUserStoreState>>,
}
#[derive(Debug)]
struct InMemoryPasswordUserStoreState {
next_id: u64,
users_by_username: HashMap<String, StoredPasswordUser>,
}
#[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<PasswordEntryResult, PasswordEntryError> {
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<Option<AuthMeResult>, 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<Option<StoredPasswordUser>, 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<AuthUser, CreateUserError> {
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<Option<StoredPasswordUser>, 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<String, PasswordEntryError> {
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);
}
}