mockforge_collab/
user.rs

1//! User management service
2
3use crate::auth::AuthService;
4use crate::error::{CollabError, Result};
5use crate::models::User;
6use sqlx::{Pool, Sqlite};
7use std::sync::Arc;
8use uuid::Uuid;
9
10/// User service for managing user accounts
11pub struct UserService {
12    db: Pool<Sqlite>,
13    auth: Arc<AuthService>,
14}
15
16impl UserService {
17    /// Create a new user service
18    pub fn new(db: Pool<Sqlite>, auth: Arc<AuthService>) -> Self {
19        Self { db, auth }
20    }
21
22    /// Create a new user account
23    pub async fn create_user(
24        &self,
25        username: String,
26        email: String,
27        password: String,
28    ) -> Result<User> {
29        // Validate input
30        if username.is_empty() || email.is_empty() || password.is_empty() {
31            return Err(CollabError::InvalidInput(
32                "Username, email, and password are required".to_string(),
33            ));
34        }
35
36        // Check if username already exists
37        let existing = sqlx::query!(
38            r#"SELECT COUNT(*) as count FROM users WHERE username = ? OR email = ?"#,
39            username,
40            email
41        )
42        .fetch_one(&self.db)
43        .await?;
44
45        if existing.count > 0 {
46            return Err(CollabError::AlreadyExists("Username or email already exists".to_string()));
47        }
48
49        // Hash password
50        let password_hash = self.auth.hash_password(&password)?;
51
52        // Create user
53        let user = User::new(username, email, password_hash);
54
55        // Insert into database
56        sqlx::query!(
57            r#"
58            INSERT INTO users (id, username, email, password_hash, display_name, avatar_url, created_at, updated_at, is_active)
59            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
60            "#,
61            user.id,
62            user.username,
63            user.email,
64            user.password_hash,
65            user.display_name,
66            user.avatar_url,
67            user.created_at,
68            user.updated_at,
69            user.is_active
70        )
71        .execute(&self.db)
72        .await?;
73
74        Ok(user)
75    }
76
77    /// Authenticate a user and return user if valid
78    pub async fn authenticate(&self, username: &str, password: &str) -> Result<User> {
79        // Fetch user by username or email
80        let user = sqlx::query_as!(
81            User,
82            r#"
83            SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
84                   created_at as "created_at: chrono::DateTime<chrono::Utc>",
85                   updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
86                   is_active as "is_active: bool"
87            FROM users
88            WHERE (username = ? OR email = ?) AND is_active = TRUE
89            "#,
90            username,
91            username
92        )
93        .fetch_optional(&self.db)
94        .await?
95        .ok_or_else(|| CollabError::AuthenticationFailed("Invalid credentials".to_string()))?;
96
97        // Verify password
98        if !self.auth.verify_password(password, &user.password_hash)? {
99            return Err(CollabError::AuthenticationFailed("Invalid credentials".to_string()));
100        }
101
102        Ok(user)
103    }
104
105    /// Get user by ID
106    pub async fn get_user(&self, user_id: Uuid) -> Result<User> {
107        let user = sqlx::query_as!(
108            User,
109            r#"
110            SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
111                   created_at as "created_at: chrono::DateTime<chrono::Utc>",
112                   updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
113                   is_active as "is_active: bool"
114            FROM users
115            WHERE id = ?
116            "#,
117            user_id
118        )
119        .fetch_optional(&self.db)
120        .await?
121        .ok_or_else(|| CollabError::UserNotFound(user_id.to_string()))?;
122
123        Ok(user)
124    }
125
126    /// Get user by username
127    pub async fn get_user_by_username(&self, username: &str) -> Result<User> {
128        let user = sqlx::query_as!(
129            User,
130            r#"
131            SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
132                   created_at as "created_at: chrono::DateTime<chrono::Utc>",
133                   updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
134                   is_active as "is_active: bool"
135            FROM users
136            WHERE username = ?
137            "#,
138            username
139        )
140        .fetch_optional(&self.db)
141        .await?
142        .ok_or_else(|| CollabError::UserNotFound(username.to_string()))?;
143
144        Ok(user)
145    }
146
147    /// Update user profile
148    pub async fn update_user(
149        &self,
150        user_id: Uuid,
151        display_name: Option<String>,
152        avatar_url: Option<String>,
153    ) -> Result<User> {
154        let now = chrono::Utc::now();
155
156        sqlx::query!(
157            r#"
158            UPDATE users
159            SET display_name = COALESCE(?, display_name),
160                avatar_url = COALESCE(?, avatar_url),
161                updated_at = ?
162            WHERE id = ?
163            "#,
164            display_name,
165            avatar_url,
166            now,
167            user_id
168        )
169        .execute(&self.db)
170        .await?;
171
172        self.get_user(user_id).await
173    }
174
175    /// Change user password
176    pub async fn change_password(
177        &self,
178        user_id: Uuid,
179        old_password: &str,
180        new_password: &str,
181    ) -> Result<()> {
182        // Get user
183        let user = self.get_user(user_id).await?;
184
185        // Verify old password
186        if !self.auth.verify_password(old_password, &user.password_hash)? {
187            return Err(CollabError::AuthenticationFailed("Invalid old password".to_string()));
188        }
189
190        // Hash new password
191        let new_hash = self.auth.hash_password(new_password)?;
192
193        // Update password
194        let now = chrono::Utc::now();
195        sqlx::query!(
196            r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#,
197            new_hash,
198            now,
199            user_id
200        )
201        .execute(&self.db)
202        .await?;
203
204        Ok(())
205    }
206
207    /// Deactivate user account
208    pub async fn deactivate_user(&self, user_id: Uuid) -> Result<()> {
209        let now = chrono::Utc::now();
210        sqlx::query!(
211            r#"UPDATE users SET is_active = FALSE, updated_at = ? WHERE id = ?"#,
212            now,
213            user_id
214        )
215        .execute(&self.db)
216        .await?;
217
218        Ok(())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    // Note: These tests require a database setup
227    // They serve as documentation of the API
228}