1use crate::auth::AuthService;
4use crate::error::{CollabError, Result};
5use crate::models::User;
6use sqlx::{Pool, Sqlite};
7use std::sync::Arc;
8use uuid::Uuid;
9
10pub struct UserService {
12 db: Pool<Sqlite>,
13 auth: Arc<AuthService>,
14}
15
16impl UserService {
17 #[must_use]
19 pub const fn new(db: Pool<Sqlite>, auth: Arc<AuthService>) -> Self {
20 Self { db, auth }
21 }
22
23 pub async fn create_user(
25 &self,
26 username: String,
27 email: String,
28 password: String,
29 ) -> Result<User> {
30 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 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 let password_hash = self.auth.hash_password(&password)?;
52
53 let user = User::new(username, email, password_hash);
55
56 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 pub async fn authenticate(&self, username: &str, password: &str) -> Result<User> {
80 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 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 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 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 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 pub async fn change_password(
178 &self,
179 user_id: Uuid,
180 old_password: &str,
181 new_password: &str,
182 ) -> Result<()> {
183 let user = self.get_user(user_id).await?;
185
186 if !self.auth.verify_password(old_password, &user.password_hash)? {
188 return Err(CollabError::AuthenticationFailed("Invalid old password".to_string()));
189 }
190
191 let new_hash = self.auth.hash_password(new_password)?;
193
194 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 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 }