From a767f81d216a3db7252a54caa9957c9cc13ab069 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 25 May 2026 16:17:27 +0800 Subject: [PATCH] =?UTF-8?q?US-002:=20=E7=94=A8=E6=88=B7=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=B8=8E=E8=AE=A4=E8=AF=81=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/models/user.rs: User 结构体 + UserRole 枚举 (admin/blocked) - src/models/session.rs: Session 结构体 - src/auth/password.rs: argon2 密码哈希和验证 - src/auth/session.rs: UUID v4 token 生成、30天过期、过期检查 Co-Authored-By: Claude Opus 4.7 (1M context) --- prd.json | 16 ++++++++-------- src/auth/mod.rs | 2 ++ src/auth/password.rs | 21 +++++++++++++++++++++ src/auth/session.rs | 14 ++++++++++++++ src/models/mod.rs | 2 ++ src/models/session.rs | 11 +++++++++++ src/models/user.rs | 35 +++++++++++++++++++++++++++++++++++ 7 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 src/auth/mod.rs create mode 100644 src/auth/password.rs create mode 100644 src/auth/session.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/session.rs create mode 100644 src/models/user.rs diff --git a/prd.json b/prd.json index 5c9d810..4ac5af4 100644 --- a/prd.json +++ b/prd.json @@ -20,7 +20,7 @@ "src/db/pool.rs", "migrations/001_init.sql" ], - "passes": false + "passes": true }, { "id": "US-002", @@ -48,7 +48,7 @@ "title": "认证 API (Server Functions)", "description": "实现 register, login, logout, get_current_user 四个 Dioxus server function", "acceptanceCriteria": [ - "register(): 输入验证(用户名3-50字符/邮箱格式/密码≥8位),首个用户role=admin,后续返回'Registration is closed'", + "register(): 输入验证(用户名3-50字符/邮箱格式/密码>=8位),首个用户role=admin,后续返回'Registration is closed'", "login(): 验证密码,创建30天过期session,设置HttpOnly+SameSite=Lax cookie", "logout(): 删除session行,清除cookie", "get_current_user(): 从cookie读取token,返回Option", @@ -105,12 +105,12 @@ "description": "端到端验证所有功能", "acceptanceCriteria": [ "启动PostgreSQL,运行migration", - "注册首个用户 → role=admin", - "再次注册 → 收到'Registration is closed'", - "登录 → 设置cookie,跳转/admin", - "关闭浏览器重开/admin → 无需重新登录", - "登出 → cookie清除,/admin重定向到/login", - "错误密码 → 显示'Invalid credentials'", + "注册首个用户 -> role=admin", + "再次注册 -> 收到'Registration is closed'", + "登录 -> 设置cookie,跳转/admin", + "关闭浏览器重开/admin -> 无需重新登录", + "登出 -> cookie清除,/admin重定向到/login", + "错误密码 -> 显示'Invalid credentials'", "主题切换正常" ], "filesExpected": [], diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..5348a28 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod password; +pub mod session; diff --git a/src/auth/password.rs b/src/auth/password.rs new file mode 100644 index 0000000..182f274 --- /dev/null +++ b/src/auth/password.rs @@ -0,0 +1,21 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; + +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2.hash_password(password.as_bytes(), &salt)?; + Ok(password_hash.to_string()) +} + +pub fn verify_password(password: &str, hash: &str) -> Result { + let parsed_hash = PasswordHash::new(hash)?; + let argon2 = Argon2::default(); + match argon2.verify_password(password.as_bytes(), &parsed_hash) { + Ok(()) => Ok(true), + Err(argon2::password_hash::Error::Password) => Ok(false), + Err(e) => Err(e), + } +} diff --git a/src/auth/session.rs b/src/auth/session.rs new file mode 100644 index 0000000..bf0a368 --- /dev/null +++ b/src/auth/session.rs @@ -0,0 +1,14 @@ +use chrono::{DateTime, Duration, Utc}; +use uuid::Uuid; + +pub fn generate_token() -> String { + Uuid::new_v4().to_string() +} + +pub fn default_expiry() -> DateTime { + Utc::now() + Duration::days(30) +} + +pub fn is_expired(expires_at: DateTime) -> bool { + Utc::now() > expires_at +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..f950c0f --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod session; +pub mod user; diff --git a/src/models/session.rs b/src/models/session.rs new file mode 100644 index 0000000..dd0ccae --- /dev/null +++ b/src/models/session.rs @@ -0,0 +1,11 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: i32, + pub user_id: i32, + pub token: String, + pub expires_at: DateTime, + pub created_at: DateTime, +} diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..5c52c2e --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,35 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum UserRole { + Admin, + Blocked, +} + +impl UserRole { + pub fn as_str(&self) -> &'static str { + match self { + UserRole::Admin => "admin", + UserRole::Blocked => "blocked", + } + } + + pub fn from_str(s: &str) -> Option { + match s { + "admin" => Some(UserRole::Admin), + "blocked" => Some(UserRole::Blocked), + _ => None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub id: i32, + pub username: String, + pub email: String, + pub password_hash: String, + pub role: UserRole, + pub created_at: DateTime, +}