mod.rs (3049B)
1 use anyhow::Result; 2 use bcrypt::{hash, verify, DEFAULT_COST}; 3 use chrono::{Duration, Utc}; 4 use sqlx::PgPool; 5 6 use crate::models::{User, UserSession, RegisterRequest, LoginRequest}; 7 8 pub async fn register_user( 9 pool: &PgPool, 10 req: RegisterRequest, 11 ) -> Result<User> { 12 // Hash password 13 let password_hash = hash(&req.password, DEFAULT_COST)?; 14 15 // Insert user 16 let user = sqlx::query_as::<_, User>( 17 r#" 18 INSERT INTO users (email, password_hash, first_name, last_name, phone) 19 VALUES ($1, $2, $3, $4, $5) 20 RETURNING * 21 "# 22 ) 23 .bind(&req.email) 24 .bind(&password_hash) 25 .bind(&req.first_name) 26 .bind(&req.last_name) 27 .bind(&req.phone) 28 .fetch_one(pool) 29 .await?; 30 31 Ok(user) 32 } 33 34 pub async fn login_user( 35 pool: &PgPool, 36 req: LoginRequest, 37 ) -> Result<(User, UserSession)> { 38 // Find user by email 39 let user = sqlx::query_as::<_, User>( 40 "SELECT * FROM users WHERE email = $1 AND is_active = true" 41 ) 42 .bind(&req.email) 43 .fetch_optional(pool) 44 .await? 45 .ok_or_else(|| anyhow::anyhow!("Invalid email or password"))?; 46 47 // Verify password 48 let password_valid = verify(&req.password, &user.password_hash)?; 49 if !password_valid { 50 return Err(anyhow::anyhow!("Invalid email or password")); 51 } 52 53 // Generate session token 54 let session_token = generate_session_token(); 55 let expires_at = Utc::now() + Duration::days(7); 56 57 // Create session 58 let session = sqlx::query_as::<_, UserSession>( 59 r#" 60 INSERT INTO user_sessions (user_id, session_token, expires_at) 61 VALUES ($1, $2, $3) 62 RETURNING * 63 "# 64 ) 65 .bind(user.id) 66 .bind(&session_token) 67 .bind(expires_at) 68 .fetch_one(pool) 69 .await?; 70 71 // Update last login 72 sqlx::query("UPDATE users SET last_login_at = NOW() WHERE id = $1") 73 .bind(user.id) 74 .execute(pool) 75 .await?; 76 77 Ok((user, session)) 78 } 79 80 pub async fn logout_user(pool: &PgPool, session_token: &str) -> Result<bool> { 81 let result = sqlx::query( 82 "UPDATE user_sessions SET is_active = false WHERE session_token = $1" 83 ) 84 .bind(session_token) 85 .execute(pool) 86 .await?; 87 88 Ok(result.rows_affected() > 0) 89 } 90 91 pub async fn validate_session(pool: &PgPool, session_token: &str) -> Result<Option<User>> { 92 let session = sqlx::query_as::<_, UserSession>( 93 r#" 94 SELECT * FROM user_sessions 95 WHERE session_token = $1 96 AND is_active = true 97 AND expires_at > NOW() 98 "# 99 ) 100 .bind(session_token) 101 .fetch_optional(pool) 102 .await?; 103 104 if let Some(session) = session { 105 let user = sqlx::query_as::<_, User>( 106 "SELECT * FROM users WHERE id = $1 AND is_active = true" 107 ) 108 .bind(session.user_id) 109 .fetch_optional(pool) 110 .await?; 111 112 Ok(user) 113 } else { 114 Ok(None) 115 } 116 } 117 118 fn generate_session_token() -> String { 119 use uuid::Uuid; 120 format!("SES-{}-{}", Utc::now().timestamp(), Uuid::new_v4()) 121 }