1use chrono::{DateTime, Utc};
2use serde::Serialize;
3use serde_json::Value;
4
5use crate::db::Db;
6use crate::error::AuthError;
7use crate::password::hash_password;
8use crate::types::{Email, User, UserId, Username};
9
10pub(crate) fn map_unique_violation(err: sqlx::Error) -> AuthError {
15 if let sqlx::Error::Database(ref db_err) = err {
16 let msg = db_err.message();
17 if msg.contains("UNIQUE constraint failed") {
18 if msg.contains("email") {
19 return AuthError::Conflict("email already exists".into());
20 }
21 if msg.contains("username") {
22 return AuthError::Conflict("username already exists".into());
23 }
24 return AuthError::Conflict(msg.to_string());
25 }
26 }
27 AuthError::Database(err)
28}
29
30pub struct SearchUsersParams<'a> {
32 pub query: Option<&'a str>,
33 pub is_active: Option<bool>,
34 pub has_mfa: Option<bool>,
35 pub limit: u32,
36 pub offset: u32,
37}
38
39#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
41pub struct UserListEntry {
42 pub id: UserId,
43 pub email: Email,
44 pub username: Option<Username>,
45 pub is_active: bool,
46 pub has_mfa: bool,
47 pub created_at: DateTime<Utc>,
48}
49
50pub struct SearchUsersResult {
52 pub users: Vec<UserListEntry>,
53 pub total: u32,
54}
55
56impl Db {
57 pub async fn create_user(
62 &self,
63 email: Email,
64 password: &str,
65 username: Option<Username>,
66 custom_data: Option<&Value>,
67 ) -> Result<User, AuthError> {
68 let id = UserId::new();
69 let pw_hash = hash_password(password)?;
70 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
71
72 sqlx::query(
73 "INSERT INTO allowthem_users \
74 (id, email, username, password_hash, email_verified, is_active, created_at, updated_at, custom_data) \
75 VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5, ?6)",
76 )
77 .bind(id)
78 .bind(&email)
79 .bind(&username)
80 .bind(&pw_hash)
81 .bind(&now)
82 .bind(custom_data.map(sqlx::types::Json))
83 .execute(self.pool())
84 .await
85 .map_err(map_unique_violation)?;
86
87 self.get_user(id).await
88 }
89
90 pub async fn create_user_with_hash(
93 &self,
94 email: Email,
95 password_hash: &str,
96 username: Option<Username>,
97 custom_data: Option<&Value>,
98 ) -> Result<User, AuthError> {
99 let id = UserId::new();
100 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
101
102 sqlx::query(
103 "INSERT INTO allowthem_users (id, email, username, password_hash, email_verified, is_active, created_at, updated_at, custom_data)
104 VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5, ?6)",
105 )
106 .bind(id)
107 .bind(&email)
108 .bind(&username)
109 .bind(password_hash)
110 .bind(&now)
111 .bind(custom_data.map(sqlx::types::Json))
112 .execute(self.pool())
113 .await
114 .map_err(map_unique_violation)?;
115
116 self.get_user(id).await
117 }
118
119 pub async fn get_user(&self, id: UserId) -> Result<User, AuthError> {
121 sqlx::query_as::<_, User>(
122 "SELECT id, email, username, NULL as password_hash, \
123 email_verified, is_active, created_at, updated_at, custom_data \
124 FROM allowthem_users WHERE id = ?",
125 )
126 .bind(id)
127 .fetch_optional(self.pool())
128 .await?
129 .ok_or(AuthError::NotFound)
130 }
131
132 pub async fn get_user_by_email(&self, email: &Email) -> Result<User, AuthError> {
134 sqlx::query_as::<_, User>(
135 "SELECT id, email, username, NULL as password_hash, \
136 email_verified, is_active, created_at, updated_at, custom_data \
137 FROM allowthem_users WHERE email = ?",
138 )
139 .bind(email)
140 .fetch_optional(self.pool())
141 .await?
142 .ok_or(AuthError::NotFound)
143 }
144
145 pub async fn get_user_by_username(&self, username: &Username) -> Result<User, AuthError> {
147 sqlx::query_as::<_, User>(
148 "SELECT id, email, username, NULL as password_hash, \
149 email_verified, is_active, created_at, updated_at, custom_data \
150 FROM allowthem_users WHERE username = ?",
151 )
152 .bind(username)
153 .fetch_optional(self.pool())
154 .await?
155 .ok_or(AuthError::NotFound)
156 }
157
158 pub async fn find_for_login(&self, identifier: &str) -> Result<User, AuthError> {
163 sqlx::query_as::<_, User>(
164 "SELECT id, email, username, password_hash, \
165 email_verified, is_active, created_at, updated_at, custom_data \
166 FROM allowthem_users WHERE email = ?1 OR username = ?1",
167 )
168 .bind(identifier)
169 .fetch_optional(self.pool())
170 .await?
171 .ok_or(AuthError::NotFound)
172 }
173
174 pub async fn update_user_email(&self, id: UserId, email: Email) -> Result<(), AuthError> {
176 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
177 let result =
178 sqlx::query("UPDATE allowthem_users SET email = ?1, updated_at = ?2 WHERE id = ?3")
179 .bind(&email)
180 .bind(&now)
181 .bind(id)
182 .execute(self.pool())
183 .await
184 .map_err(map_unique_violation)?;
185
186 if result.rows_affected() == 0 {
187 return Err(AuthError::NotFound);
188 }
189 Ok(())
190 }
191
192 pub async fn update_user_username(
194 &self,
195 id: UserId,
196 username: Option<Username>,
197 ) -> Result<(), AuthError> {
198 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
199 let result =
200 sqlx::query("UPDATE allowthem_users SET username = ?1, updated_at = ?2 WHERE id = ?3")
201 .bind(&username)
202 .bind(&now)
203 .bind(id)
204 .execute(self.pool())
205 .await
206 .map_err(map_unique_violation)?;
207
208 if result.rows_affected() == 0 {
209 return Err(AuthError::NotFound);
210 }
211 Ok(())
212 }
213
214 pub async fn update_user_active(&self, id: UserId, is_active: bool) -> Result<(), AuthError> {
216 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
217 let result =
218 sqlx::query("UPDATE allowthem_users SET is_active = ?1, updated_at = ?2 WHERE id = ?3")
219 .bind(is_active)
220 .bind(&now)
221 .bind(id)
222 .execute(self.pool())
223 .await?;
224
225 if result.rows_affected() == 0 {
226 return Err(AuthError::NotFound);
227 }
228 Ok(())
229 }
230
231 pub async fn delete_user(&self, id: UserId) -> Result<(), AuthError> {
233 let result = sqlx::query("DELETE FROM allowthem_users WHERE id = ?")
234 .bind(id)
235 .execute(self.pool())
236 .await?;
237
238 if result.rows_affected() == 0 {
239 return Err(AuthError::NotFound);
240 }
241 Ok(())
242 }
243
244 pub async fn list_users(&self) -> Result<Vec<User>, AuthError> {
246 sqlx::query_as::<_, User>(
247 "SELECT id, email, username, NULL as password_hash, \
248 email_verified, is_active, created_at, updated_at, custom_data \
249 FROM allowthem_users ORDER BY created_at ASC",
250 )
251 .fetch_all(self.pool())
252 .await
253 .map_err(AuthError::Database)
254 }
255
256 pub async fn search_users(
262 &self,
263 params: SearchUsersParams<'_>,
264 ) -> Result<SearchUsersResult, AuthError> {
265 let mut where_clauses: Vec<String> = Vec::new();
266 let mut bind_values: Vec<String> = Vec::new();
267
268 if let Some(q) = params.query {
269 let trimmed = q.trim();
270 if !trimmed.is_empty() {
271 let escaped = trimmed
272 .replace('\\', "\\\\")
273 .replace('%', "\\%")
274 .replace('_', "\\_");
275 let pattern = format!("%{escaped}%");
276 where_clauses
277 .push("(u.email LIKE ? ESCAPE '\\' OR u.username LIKE ? ESCAPE '\\')".into());
278 bind_values.push(pattern.clone());
279 bind_values.push(pattern);
280 }
281 }
282
283 if let Some(active) = params.is_active {
284 where_clauses.push("u.is_active = ?".into());
285 bind_values.push(if active { "1".into() } else { "0".into() });
286 }
287
288 if let Some(has_mfa) = params.has_mfa {
289 let exists = if has_mfa { "EXISTS" } else { "NOT EXISTS" };
290 where_clauses.push(format!(
291 "{exists} (SELECT 1 FROM allowthem_mfa_secrets WHERE user_id = u.id AND enabled = 1)"
292 ));
293 }
294
295 let where_sql = if where_clauses.is_empty() {
296 String::new()
297 } else {
298 format!("WHERE {}", where_clauses.join(" AND "))
299 };
300
301 let count_sql: &'static str = Box::leak(
302 format!("SELECT COUNT(*) FROM allowthem_users u {where_sql}").into_boxed_str(),
303 );
304 let mut count_query = sqlx::query_scalar::<_, i64>(count_sql);
305 for val in &bind_values {
306 count_query = count_query.bind(val);
307 }
308 let total = count_query
309 .fetch_one(self.pool())
310 .await
311 .map_err(AuthError::Database)? as u32;
312
313 let data_sql: &'static str = Box::leak(
314 format!(
315 "SELECT u.id, u.email, u.username, u.is_active, \
316 EXISTS (SELECT 1 FROM allowthem_mfa_secrets \
317 WHERE user_id = u.id AND enabled = 1) as has_mfa, \
318 u.created_at \
319 FROM allowthem_users u {where_sql} \
320 ORDER BY u.created_at ASC \
321 LIMIT ? OFFSET ?"
322 )
323 .into_boxed_str(),
324 );
325 let mut data_query = sqlx::query_as::<_, UserListEntry>(data_sql);
326 for val in &bind_values {
327 data_query = data_query.bind(val);
328 }
329 data_query = data_query.bind(params.limit).bind(params.offset);
330
331 let users = data_query
332 .fetch_all(self.pool())
333 .await
334 .map_err(AuthError::Database)?;
335
336 Ok(SearchUsersResult { users, total })
337 }
338
339 pub async fn update_user_password(
343 &self,
344 id: UserId,
345 new_password: &str,
346 ) -> Result<(), AuthError> {
347 let pw_hash = hash_password(new_password)?;
348 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
349 let result = sqlx::query(
350 "UPDATE allowthem_users SET password_hash = ?1, updated_at = ?2 WHERE id = ?3",
351 )
352 .bind(&pw_hash)
353 .bind(&now)
354 .bind(id)
355 .execute(self.pool())
356 .await?;
357
358 if result.rows_affected() == 0 {
359 return Err(AuthError::NotFound);
360 }
361 Ok(())
362 }
363
364 pub async fn clear_password_hash(&self, id: UserId) -> Result<(), AuthError> {
370 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
371 let result = sqlx::query(
372 "UPDATE allowthem_users SET password_hash = NULL, updated_at = ? WHERE id = ?",
373 )
374 .bind(&now)
375 .bind(id)
376 .execute(self.pool())
377 .await?;
378
379 if result.rows_affected() == 0 {
380 return Err(AuthError::NotFound);
381 }
382 Ok(())
383 }
384
385 pub async fn get_custom_data(&self, id: &UserId) -> Result<Option<Value>, AuthError> {
390 let row: Option<(Option<Value>,)> =
391 sqlx::query_as("SELECT custom_data FROM allowthem_users WHERE id = ?")
392 .bind(id)
393 .fetch_optional(self.pool())
394 .await?;
395
396 match row {
397 None => Err(AuthError::NotFound),
398 Some((data,)) => Ok(data),
399 }
400 }
401
402 pub async fn set_custom_data(&self, id: &UserId, data: &Value) -> Result<(), AuthError> {
406 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
407 let result = sqlx::query(
408 "UPDATE allowthem_users SET custom_data = ?1, updated_at = ?2 WHERE id = ?3",
409 )
410 .bind(sqlx::types::Json(data))
411 .bind(&now)
412 .bind(id)
413 .execute(self.pool())
414 .await?;
415
416 if result.rows_affected() == 0 {
417 return Err(AuthError::NotFound);
418 }
419 Ok(())
420 }
421
422 pub async fn delete_custom_data(&self, id: &UserId) -> Result<(), AuthError> {
426 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
427 sqlx::query("UPDATE allowthem_users SET custom_data = NULL, updated_at = ?1 WHERE id = ?2")
428 .bind(&now)
429 .bind(id)
430 .execute(self.pool())
431 .await?;
432
433 Ok(())
434 }
435}