US-002: 用户模型与认证模块
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
031a7aa0f2
commit
a767f81d21
16
prd.json
16
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<User>",
|
||||
@ -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": [],
|
||||
|
||||
2
src/auth/mod.rs
Normal file
2
src/auth/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod password;
|
||||
pub mod session;
|
||||
21
src/auth/password.rs
Normal file
21
src/auth/password.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
||||
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<bool, argon2::password_hash::Error> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
14
src/auth/session.rs
Normal file
14
src/auth/session.rs
Normal file
@ -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> {
|
||||
Utc::now() + Duration::days(30)
|
||||
}
|
||||
|
||||
pub fn is_expired(expires_at: DateTime<Utc>) -> bool {
|
||||
Utc::now() > expires_at
|
||||
}
|
||||
2
src/models/mod.rs
Normal file
2
src/models/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
11
src/models/session.rs
Normal file
11
src/models/session.rs
Normal file
@ -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<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
35
src/models/user.rs
Normal file
35
src/models/user.rs
Normal file
@ -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<Self> {
|
||||
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<Utc>,
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user