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 pub fn new(db: Pool<Sqlite>, auth: Arc<AuthService>) -> Self {
19 Self { db, auth }
20 }
21
22 pub async fn create_user(
24 &self,
25 username: String,
26 email: String,
27 password: String,
28 ) -> Result<User> {
29 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 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 let password_hash = self.auth.hash_password(&password)?;
51
52 let user = User::new(username, email, password_hash);
54
55 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 pub async fn authenticate(&self, username: &str, password: &str) -> Result<User> {
79 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 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 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 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 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 pub async fn change_password(
177 &self,
178 user_id: Uuid,
179 old_password: &str,
180 new_password: &str,
181 ) -> Result<()> {
182 let user = self.get_user(user_id).await?;
184
185 if !self.auth.verify_password(old_password, &user.password_hash)? {
187 return Err(CollabError::AuthenticationFailed("Invalid old password".to_string()));
188 }
189
190 let new_hash = self.auth.hash_password(new_password)?;
192
193 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 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 }