1use chrono::Utc;
2
3use crate::db::Db;
4use crate::error::AuthError;
5use crate::password::hash_password;
6use crate::types::{Email, User, UserId, Username};
7
8pub(crate) fn map_unique_violation(err: sqlx::Error) -> AuthError {
13 if let sqlx::Error::Database(ref db_err) = err {
14 let msg = db_err.message();
15 if msg.contains("UNIQUE constraint failed") {
16 if msg.contains("email") {
17 return AuthError::Conflict("email already exists".into());
18 }
19 if msg.contains("username") {
20 return AuthError::Conflict("username already exists".into());
21 }
22 return AuthError::Conflict(msg.to_string());
23 }
24 }
25 AuthError::Database(err)
26}
27
28impl Db {
29 pub async fn create_user(
34 &self,
35 email: Email,
36 password: &str,
37 username: Option<Username>,
38 ) -> Result<User, AuthError> {
39 let id = UserId::new();
40 let pw_hash = hash_password(password)?;
41 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
42
43 sqlx::query(
44 "INSERT INTO allowthem_users \
45 (id, email, username, password_hash, email_verified, is_active, created_at, updated_at) \
46 VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5)",
47 )
48 .bind(id)
49 .bind(&email)
50 .bind(&username)
51 .bind(&pw_hash)
52 .bind(&now)
53 .execute(self.pool())
54 .await
55 .map_err(map_unique_violation)?;
56
57 self.get_user(id).await
58 }
59
60 pub async fn get_user(&self, id: UserId) -> Result<User, AuthError> {
62 sqlx::query_as::<_, User>(
63 "SELECT id, email, username, NULL as password_hash, \
64 email_verified, is_active, created_at, updated_at \
65 FROM allowthem_users WHERE id = ?",
66 )
67 .bind(id)
68 .fetch_optional(self.pool())
69 .await?
70 .ok_or(AuthError::NotFound)
71 }
72
73 pub async fn get_user_by_email(&self, email: &Email) -> Result<User, AuthError> {
75 sqlx::query_as::<_, User>(
76 "SELECT id, email, username, NULL as password_hash, \
77 email_verified, is_active, created_at, updated_at \
78 FROM allowthem_users WHERE email = ?",
79 )
80 .bind(email)
81 .fetch_optional(self.pool())
82 .await?
83 .ok_or(AuthError::NotFound)
84 }
85
86 pub async fn get_user_by_username(&self, username: &Username) -> Result<User, AuthError> {
88 sqlx::query_as::<_, User>(
89 "SELECT id, email, username, NULL as password_hash, \
90 email_verified, is_active, created_at, updated_at \
91 FROM allowthem_users WHERE username = ?",
92 )
93 .bind(username)
94 .fetch_optional(self.pool())
95 .await?
96 .ok_or(AuthError::NotFound)
97 }
98
99 pub async fn find_for_login(&self, identifier: &str) -> Result<User, AuthError> {
104 sqlx::query_as::<_, User>(
105 "SELECT id, email, username, password_hash, \
106 email_verified, is_active, created_at, updated_at \
107 FROM allowthem_users WHERE email = ?1 OR username = ?1",
108 )
109 .bind(identifier)
110 .fetch_optional(self.pool())
111 .await?
112 .ok_or(AuthError::NotFound)
113 }
114
115 pub async fn update_user_email(&self, id: UserId, email: Email) -> Result<(), AuthError> {
117 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
118 let result =
119 sqlx::query("UPDATE allowthem_users SET email = ?1, updated_at = ?2 WHERE id = ?3")
120 .bind(&email)
121 .bind(&now)
122 .bind(id)
123 .execute(self.pool())
124 .await
125 .map_err(map_unique_violation)?;
126
127 if result.rows_affected() == 0 {
128 return Err(AuthError::NotFound);
129 }
130 Ok(())
131 }
132
133 pub async fn update_user_username(
135 &self,
136 id: UserId,
137 username: Option<Username>,
138 ) -> Result<(), AuthError> {
139 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
140 let result =
141 sqlx::query("UPDATE allowthem_users SET username = ?1, updated_at = ?2 WHERE id = ?3")
142 .bind(&username)
143 .bind(&now)
144 .bind(id)
145 .execute(self.pool())
146 .await
147 .map_err(map_unique_violation)?;
148
149 if result.rows_affected() == 0 {
150 return Err(AuthError::NotFound);
151 }
152 Ok(())
153 }
154
155 pub async fn update_user_active(&self, id: UserId, is_active: bool) -> Result<(), AuthError> {
157 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
158 let result =
159 sqlx::query("UPDATE allowthem_users SET is_active = ?1, updated_at = ?2 WHERE id = ?3")
160 .bind(is_active)
161 .bind(&now)
162 .bind(id)
163 .execute(self.pool())
164 .await?;
165
166 if result.rows_affected() == 0 {
167 return Err(AuthError::NotFound);
168 }
169 Ok(())
170 }
171
172 pub async fn delete_user(&self, id: UserId) -> Result<(), AuthError> {
174 let result = sqlx::query("DELETE FROM allowthem_users WHERE id = ?")
175 .bind(id)
176 .execute(self.pool())
177 .await?;
178
179 if result.rows_affected() == 0 {
180 return Err(AuthError::NotFound);
181 }
182 Ok(())
183 }
184
185 pub async fn list_users(&self) -> Result<Vec<User>, AuthError> {
187 sqlx::query_as::<_, User>(
188 "SELECT id, email, username, NULL as password_hash, \
189 email_verified, is_active, created_at, updated_at \
190 FROM allowthem_users ORDER BY created_at ASC",
191 )
192 .fetch_all(self.pool())
193 .await
194 .map_err(AuthError::Database)
195 }
196
197 pub async fn update_user_password(
201 &self,
202 id: UserId,
203 new_password: &str,
204 ) -> Result<(), AuthError> {
205 let pw_hash = hash_password(new_password)?;
206 let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
207 let result = sqlx::query(
208 "UPDATE allowthem_users SET password_hash = ?1, updated_at = ?2 WHERE id = ?3",
209 )
210 .bind(&pw_hash)
211 .bind(&now)
212 .bind(id)
213 .execute(self.pool())
214 .await?;
215
216 if result.rows_affected() == 0 {
217 return Err(AuthError::NotFound);
218 }
219 Ok(())
220 }
221}