1use axum_login::AuthUser;
2use serde::{Deserialize, Serialize};
3use sqlx::{Result, SqlitePool};
4use time::OffsetDateTime;
5
6use super::Model;
7
8#[derive(Clone, Serialize, Deserialize)]
9pub struct User {
10 pub user_id: i64,
11 pub username: String,
12 pub password_hash: String,
13 pub created_at: OffsetDateTime,
14}
15
16impl std::fmt::Debug for User {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 f.debug_struct("User")
21 .field("user_id", &self.user_id)
22 .field("username", &self.username)
23 .field("password_hash", &"[redacted]")
24 .field("created_at", &self.created_at)
25 .finish()
26 }
27}
28
29impl AuthUser for User {
30 type Id = i64;
31
32 fn id(&self) -> Self::Id {
33 self.user_id
34 }
35
36 fn session_auth_hash(&self) -> &[u8] {
37 self.password_hash.as_bytes() }
42}
43
44#[allow(dead_code)]
45pub struct Default {
46 pub user_id: i64,
47}
48
49impl Default {
50 #[allow(clippy::unused_self)]
51 pub fn created_at(&self) -> OffsetDateTime {
52 OffsetDateTime::now_utc()
53 }
54}
55
56impl Model for User {
57 type Default = Default;
58 const DEFAULT: Default = Default { user_id: -1 };
59
60 async fn create_using_self(&mut self, pool: &SqlitePool) -> Result<()> {
61 let result = sqlx::query!(
62 r#"
63 INSERT INTO users (username, password_hash, created_at)
64 VALUES (?, ?, ?)
65 RETURNING user_id
66 "#,
67 self.username,
68 self.password_hash,
69 self.created_at
70 )
71 .fetch_one(pool)
72 .await?;
73
74 self.user_id = result.user_id;
75
76 Ok(())
77 }
78
79 async fn get_using_id(pool: &SqlitePool, id: i64) -> Result<Self> {
80 sqlx::query_as!(
81 User,
82 r#"
83 SELECT *
84 FROM users
85 WHERE user_id = ?
86 "#,
87 id
88 )
89 .fetch_one(pool)
90 .await
91 }
92
93 async fn update_using_self(&self, pool: &SqlitePool) -> Result<()> {
94 sqlx::query!(
95 r#"
96 UPDATE users
97 SET username = ?, password_hash = ?, created_at = ?
98 WHERE user_id = ?
99 RETURNING user_id
100 "#,
101 self.username,
102 self.password_hash,
103 self.created_at,
104 self.user_id
105 )
106 .fetch_one(pool)
107 .await?;
108
109 Ok(())
110 }
111
112 async fn delete_using_id(pool: &SqlitePool, id: i64) -> Result<()> {
113 sqlx::query!(
114 r#"
115 DELETE
116 FROM users
117 WHERE user_id = ?
118 RETURNING user_id
119 "#,
120 id
121 )
122 .fetch_one(pool)
123 .await?;
124
125 Ok(())
126 }
127}
128
129impl User {
130 pub async fn get_using_username(pool: &SqlitePool, username: &str) -> Result<Self> {
131 sqlx::query_as!(
132 User,
133 r#"
134 SELECT *
135 FROM users
136 WHERE username = ?
137 "#,
138 username
139 )
140 .fetch_one(pool)
141 .await
142 }
143
144 pub async fn get_all(pool: &SqlitePool) -> Result<Vec<Self>> {
145 sqlx::query_as!(
146 User,
147 r#"
148 SELECT *
149 FROM users
150 "#,
151 )
152 .fetch_all(pool)
153 .await
154 }
155
156 #[must_use]
157 pub fn to_redacted_clone(&self) -> Self {
158 Self {
159 user_id: self.user_id,
160 username: self.username.clone(),
161 password_hash: "[redacted]".to_string(),
162 created_at: self.created_at,
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
172 async fn create(pool: SqlitePool) -> Result<()> {
173 let mut user = User {
174 user_id: User::DEFAULT.user_id,
175 username: "test_user".to_string(),
176 password_hash: "test_hash".to_string(),
177 created_at: User::DEFAULT.created_at(),
178 };
179
180 user.create_using_self(&pool).await?;
181
182 assert_eq!(user.user_id, 5);
183
184 let returned_user = User::get_using_id(&pool, 5).await?;
185
186 assert_eq!(returned_user.username, user.username);
187 assert_eq!(returned_user.password_hash, user.password_hash);
188 assert_eq!(returned_user.created_at, user.created_at);
189
190 Ok(())
191 }
192
193 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
194 async fn create_existing(pool: SqlitePool) -> Result<()> {
195 let mut user = User {
196 user_id: User::DEFAULT.user_id,
197 username: "piotrpdev".to_string(),
198 password_hash: "test_hash".to_string(),
199 created_at: User::DEFAULT.created_at(),
200 };
201
202 let returned_user_result = user.create_using_self(&pool).await;
203
204 assert!(returned_user_result.is_err());
205
206 Ok(())
207 }
208
209 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
210 async fn get(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
211 let user_id = 2;
212 let returned_user = User::get_using_id(&pool, user_id).await?;
213
214 assert_eq!(returned_user.user_id, user_id);
215 assert_eq!(returned_user.username, "piotrpdev");
216 assert_eq!(returned_user.password_hash, "$argon2id$v=19$m=19456,t=2,p=1$VE0e3g7DalWHgDwou3nuRA$uC6TER156UQpk0lNQ5+jHM0l5poVjPA1he/Tyn9J4Zw");
217 assert_eq!(
218 returned_user.created_at,
219 OffsetDateTime::from_unix_timestamp(1_729_530_138)?
220 );
221
222 Ok(())
223 }
224
225 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
226 async fn get_using_username(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
227 let username = "piotrpdev";
228
229 let returned_user = User::get_using_username(&pool, username).await?;
230
231 assert_eq!(returned_user.user_id, 2);
232 assert_eq!(returned_user.username, username);
233 assert_eq!(returned_user.password_hash, "$argon2id$v=19$m=19456,t=2,p=1$VE0e3g7DalWHgDwou3nuRA$uC6TER156UQpk0lNQ5+jHM0l5poVjPA1he/Tyn9J4Zw");
234 assert_eq!(
235 returned_user.created_at,
236 OffsetDateTime::from_unix_timestamp(1_729_530_138)?
237 );
238
239 Ok(())
240 }
241
242 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
243 async fn get_all(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
244 let usernames = ["admin", "piotrpdev", "joedaly", "guest"];
245 let returned_users = User::get_all(&pool).await?;
246
247 assert_eq!(returned_users.len(), 4);
248
249 assert!(returned_users
250 .iter()
251 .all(|user| usernames.contains(&user.username.as_str())));
252
253 Ok(())
254 }
255
256 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
257 async fn update(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
258 let old_user = User::get_using_id(&pool, 2).await?;
259
260 let updated_user = User {
261 user_id: old_user.user_id,
262 username: "new_joedaly".to_string(),
263 password_hash: old_user.password_hash,
264 created_at: OffsetDateTime::from_unix_timestamp(1_729_530_138)?,
265 };
266
267 let updated = updated_user.update_using_self(&pool).await;
268
269 assert!(updated.is_ok());
270
271 let returned_user = User::get_using_id(&pool, old_user.user_id).await?;
272
273 assert_eq!(returned_user.user_id, updated_user.user_id);
274 assert_eq!(returned_user.username, updated_user.username);
275 assert_eq!(returned_user.password_hash, updated_user.password_hash);
276 assert_eq!(returned_user.created_at, updated_user.created_at);
277
278 Ok(())
279 }
280
281 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
282 async fn delete(pool: SqlitePool) -> Result<()> {
283 let user_id = 2;
284 let deleted = User::delete_using_id(&pool, user_id).await;
285
286 assert!(deleted.is_ok());
287
288 let returned_user = User::get_using_id(&pool, user_id).await;
289
290 assert!(returned_user.is_err());
291
292 let impossible_deleted = User::delete_using_id(&pool, user_id).await;
293
294 assert!(impossible_deleted.is_err());
295
296 Ok(())
297 }
298
299 #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
300 async fn to_redacted_clone(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
301 let user_id = 2;
302
303 let returned_user = User::get_using_id(&pool, user_id).await?;
304 let redacted_user = returned_user.to_redacted_clone();
305
306 assert_eq!(redacted_user.user_id, user_id);
307 assert_eq!(redacted_user.username, "piotrpdev");
308 assert_eq!(redacted_user.password_hash, "[redacted]");
309 assert_eq!(
310 redacted_user.created_at,
311 OffsetDateTime::from_unix_timestamp(1_729_530_138)?
312 );
313
314 Ok(())
315 }
316}