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