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(
29 &self,
30 username: String,
31 email: String,
32 password: String,
33 ) -> Result<User> {
34 if username.is_empty() || email.is_empty() || password.is_empty() {
36 return Err(CollabError::InvalidInput(
37 "Username, email, and password are required".to_string(),
38 ));
39 }
40
41 let existing = sqlx::query!(
43 r#"SELECT COUNT(*) as count FROM users WHERE username = ? OR email = ?"#,
44 username,
45 email
46 )
47 .fetch_one(&self.db)
48 .await?;
49
50 if existing.count > 0 {
51 return Err(CollabError::AlreadyExists("Username or email already exists".to_string()));
52 }
53
54 let password_hash = self.auth.hash_password(&password)?;
56
57 let user = User::new(username, email, password_hash);
59
60 sqlx::query!(
62 r#"
63 INSERT INTO users (id, username, email, password_hash, display_name, avatar_url, created_at, updated_at, is_active)
64 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
65 "#,
66 user.id,
67 user.username,
68 user.email,
69 user.password_hash,
70 user.display_name,
71 user.avatar_url,
72 user.created_at,
73 user.updated_at,
74 user.is_active
75 )
76 .execute(&self.db)
77 .await?;
78
79 Ok(user)
80 }
81
82 pub async fn authenticate(&self, username: &str, password: &str) -> Result<User> {
88 let user = sqlx::query_as!(
90 User,
91 r#"
92 SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
93 created_at as "created_at: chrono::DateTime<chrono::Utc>",
94 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
95 is_active as "is_active: bool"
96 FROM users
97 WHERE (username = ? OR email = ?) AND is_active = TRUE
98 "#,
99 username,
100 username
101 )
102 .fetch_optional(&self.db)
103 .await?
104 .ok_or_else(|| CollabError::AuthenticationFailed("Invalid credentials".to_string()))?;
105
106 if !self.auth.verify_password(password, &user.password_hash)? {
108 return Err(CollabError::AuthenticationFailed("Invalid credentials".to_string()));
109 }
110
111 Ok(user)
112 }
113
114 pub async fn get_user(&self, user_id: Uuid) -> Result<User> {
120 let user = sqlx::query_as!(
121 User,
122 r#"
123 SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
124 created_at as "created_at: chrono::DateTime<chrono::Utc>",
125 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
126 is_active as "is_active: bool"
127 FROM users
128 WHERE id = ?
129 "#,
130 user_id
131 )
132 .fetch_optional(&self.db)
133 .await?
134 .ok_or_else(|| CollabError::UserNotFound(user_id.to_string()))?;
135
136 Ok(user)
137 }
138
139 pub async fn get_user_by_username(&self, username: &str) -> Result<User> {
145 let user = sqlx::query_as!(
146 User,
147 r#"
148 SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
149 created_at as "created_at: chrono::DateTime<chrono::Utc>",
150 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
151 is_active as "is_active: bool"
152 FROM users
153 WHERE username = ?
154 "#,
155 username
156 )
157 .fetch_optional(&self.db)
158 .await?
159 .ok_or_else(|| CollabError::UserNotFound(username.to_string()))?;
160
161 Ok(user)
162 }
163
164 pub async fn update_user(
170 &self,
171 user_id: Uuid,
172 display_name: Option<String>,
173 avatar_url: Option<String>,
174 ) -> Result<User> {
175 let now = chrono::Utc::now();
176
177 sqlx::query!(
178 r#"
179 UPDATE users
180 SET display_name = COALESCE(?, display_name),
181 avatar_url = COALESCE(?, avatar_url),
182 updated_at = ?
183 WHERE id = ?
184 "#,
185 display_name,
186 avatar_url,
187 now,
188 user_id
189 )
190 .execute(&self.db)
191 .await?;
192
193 self.get_user(user_id).await
194 }
195
196 pub async fn change_password(
202 &self,
203 user_id: Uuid,
204 old_password: &str,
205 new_password: &str,
206 ) -> Result<()> {
207 let user = self.get_user(user_id).await?;
209
210 if !self.auth.verify_password(old_password, &user.password_hash)? {
212 return Err(CollabError::AuthenticationFailed("Invalid old password".to_string()));
213 }
214
215 let new_hash = self.auth.hash_password(new_password)?;
217
218 let now = chrono::Utc::now();
220 sqlx::query!(
221 r#"UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?"#,
222 new_hash,
223 now,
224 user_id
225 )
226 .execute(&self.db)
227 .await?;
228
229 Ok(())
230 }
231
232 pub async fn deactivate_user(&self, user_id: Uuid) -> Result<()> {
238 let now = chrono::Utc::now();
239 sqlx::query!(
240 r#"UPDATE users SET is_active = FALSE, updated_at = ? WHERE id = ?"#,
241 now,
242 user_id
243 )
244 .execute(&self.db)
245 .await?;
246
247 Ok(())
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 }