1use crate::error::{EngramError, Result};
4use chrono::{DateTime, Utc};
5use rusqlite::{params, Connection, OptionalExtension};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct UserId(String);
13
14impl UserId {
15 pub fn new() -> Self {
17 Self(Uuid::new_v4().to_string())
18 }
19
20 pub fn from_string(s: impl Into<String>) -> Self {
22 Self(s.into())
23 }
24
25 pub fn system() -> Self {
27 Self("system".to_string())
28 }
29
30 pub fn anonymous() -> Self {
32 Self("anonymous".to_string())
33 }
34
35 pub fn as_str(&self) -> &str {
37 &self.0
38 }
39}
40
41impl Default for UserId {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl std::fmt::Display for UserId {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "{}", self.0)
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct User {
56 pub id: UserId,
57 pub username: String,
58 pub display_name: Option<String>,
59 pub email: Option<String>,
60 pub is_active: bool,
61 pub is_admin: bool,
62 pub created_at: DateTime<Utc>,
63 pub updated_at: DateTime<Utc>,
64}
65
66impl User {
67 pub fn new(username: impl Into<String>) -> Self {
69 let now = Utc::now();
70 Self {
71 id: UserId::new(),
72 username: username.into(),
73 display_name: None,
74 email: None,
75 is_active: true,
76 is_admin: false,
77 created_at: now,
78 updated_at: now,
79 }
80 }
81
82 pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
84 self.display_name = Some(name.into());
85 self
86 }
87
88 pub fn with_email(mut self, email: impl Into<String>) -> Self {
90 self.email = Some(email.into());
91 self
92 }
93
94 pub fn with_admin(mut self, is_admin: bool) -> Self {
96 self.is_admin = is_admin;
97 self
98 }
99}
100
101pub struct UserManager<'a> {
103 conn: &'a Connection,
104}
105
106impl<'a> UserManager<'a> {
107 pub fn new(conn: &'a Connection) -> Self {
109 Self { conn }
110 }
111
112 pub fn create_user(&self, user: &User, password: Option<&str>) -> Result<()> {
114 let password_hash = password.map(hash_password);
115
116 self.conn.execute(
117 r#"
118 INSERT INTO users (id, username, display_name, email, password_hash, is_active, is_admin, created_at, updated_at)
119 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
120 "#,
121 params![
122 user.id.as_str(),
123 user.username,
124 user.display_name,
125 user.email,
126 password_hash,
127 user.is_active,
128 user.is_admin,
129 user.created_at.to_rfc3339(),
130 user.updated_at.to_rfc3339(),
131 ],
132 )?;
133 Ok(())
134 }
135
136 pub fn get_user(&self, id: &UserId) -> Result<Option<User>> {
138 self.conn
139 .query_row(
140 r#"
141 SELECT id, username, display_name, email, is_active, is_admin, created_at, updated_at
142 FROM users WHERE id = ?1
143 "#,
144 params![id.as_str()],
145 |row| {
146 Ok(User {
147 id: UserId::from_string(row.get::<_, String>(0)?),
148 username: row.get(1)?,
149 display_name: row.get(2)?,
150 email: row.get(3)?,
151 is_active: row.get(4)?,
152 is_admin: row.get(5)?,
153 created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(6)?)
154 .map(|dt| dt.with_timezone(&Utc))
155 .unwrap_or_else(|_| Utc::now()),
156 updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(7)?)
157 .map(|dt| dt.with_timezone(&Utc))
158 .unwrap_or_else(|_| Utc::now()),
159 })
160 },
161 )
162 .optional()
163 .map_err(EngramError::from)
164 }
165
166 pub fn get_user_by_username(&self, username: &str) -> Result<Option<User>> {
168 self.conn
169 .query_row(
170 r#"
171 SELECT id, username, display_name, email, is_active, is_admin, created_at, updated_at
172 FROM users WHERE username = ?1
173 "#,
174 params![username],
175 |row| {
176 Ok(User {
177 id: UserId::from_string(row.get::<_, String>(0)?),
178 username: row.get(1)?,
179 display_name: row.get(2)?,
180 email: row.get(3)?,
181 is_active: row.get(4)?,
182 is_admin: row.get(5)?,
183 created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(6)?)
184 .map(|dt| dt.with_timezone(&Utc))
185 .unwrap_or_else(|_| Utc::now()),
186 updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(7)?)
187 .map(|dt| dt.with_timezone(&Utc))
188 .unwrap_or_else(|_| Utc::now()),
189 })
190 },
191 )
192 .optional()
193 .map_err(EngramError::from)
194 }
195
196 pub fn verify_password(&self, username: &str, password: &str) -> Result<Option<User>> {
198 let result: Option<(String, Option<String>)> = self
199 .conn
200 .query_row(
201 "SELECT id, password_hash FROM users WHERE username = ?1 AND is_active = 1",
202 params![username],
203 |row| Ok((row.get(0)?, row.get(1)?)),
204 )
205 .optional()?;
206
207 if let Some((id, Some(stored_hash))) = result {
208 if verify_password(password, &stored_hash) {
209 return self.get_user(&UserId::from_string(id));
210 }
211 }
212 Ok(None)
213 }
214
215 pub fn update_user(&self, user: &User) -> Result<()> {
217 self.conn.execute(
218 r#"
219 UPDATE users SET
220 username = ?2,
221 display_name = ?3,
222 email = ?4,
223 is_active = ?5,
224 is_admin = ?6,
225 updated_at = ?7
226 WHERE id = ?1
227 "#,
228 params![
229 user.id.as_str(),
230 user.username,
231 user.display_name,
232 user.email,
233 user.is_active,
234 user.is_admin,
235 Utc::now().to_rfc3339(),
236 ],
237 )?;
238 Ok(())
239 }
240
241 pub fn delete_user(&self, id: &UserId) -> Result<bool> {
243 let deleted = self
244 .conn
245 .execute("DELETE FROM users WHERE id = ?1", params![id.as_str()])?;
246 Ok(deleted > 0)
247 }
248
249 pub fn list_users(&self, include_inactive: bool) -> Result<Vec<User>> {
251 let sql = if include_inactive {
252 "SELECT id, username, display_name, email, is_active, is_admin, created_at, updated_at FROM users ORDER BY created_at DESC"
253 } else {
254 "SELECT id, username, display_name, email, is_active, is_admin, created_at, updated_at FROM users WHERE is_active = 1 ORDER BY created_at DESC"
255 };
256
257 let mut stmt = self.conn.prepare(sql)?;
258 let users = stmt
259 .query_map([], |row| {
260 Ok(User {
261 id: UserId::from_string(row.get::<_, String>(0)?),
262 username: row.get(1)?,
263 display_name: row.get(2)?,
264 email: row.get(3)?,
265 is_active: row.get(4)?,
266 is_admin: row.get(5)?,
267 created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(6)?)
268 .map(|dt| dt.with_timezone(&Utc))
269 .unwrap_or_else(|_| Utc::now()),
270 updated_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(7)?)
271 .map(|dt| dt.with_timezone(&Utc))
272 .unwrap_or_else(|_| Utc::now()),
273 })
274 })?
275 .collect::<std::result::Result<Vec<_>, _>>()?;
276 Ok(users)
277 }
278}
279
280fn hash_password(password: &str) -> String {
282 let mut hasher = Sha256::new();
283 hasher.update(password.as_bytes());
284 hex::encode(hasher.finalize())
285}
286
287fn verify_password(password: &str, hash: &str) -> bool {
289 hash_password(password) == hash
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::auth::init_auth_tables;
296
297 fn setup_db() -> Connection {
298 let conn = Connection::open_in_memory().unwrap();
299 init_auth_tables(&conn).unwrap();
300 conn
301 }
302
303 #[test]
304 fn test_create_and_get_user() {
305 let conn = setup_db();
306 let manager = UserManager::new(&conn);
307
308 let user = User::new("testuser")
309 .with_display_name("Test User")
310 .with_email("test@example.com");
311
312 manager.create_user(&user, Some("password123")).unwrap();
313
314 let fetched = manager.get_user(&user.id).unwrap().unwrap();
315 assert_eq!(fetched.username, "testuser");
316 assert_eq!(fetched.display_name, Some("Test User".to_string()));
317 assert_eq!(fetched.email, Some("test@example.com".to_string()));
318 }
319
320 #[test]
321 fn test_get_user_by_username() {
322 let conn = setup_db();
323 let manager = UserManager::new(&conn);
324
325 let user = User::new("findme");
326 manager.create_user(&user, None).unwrap();
327
328 let fetched = manager.get_user_by_username("findme").unwrap().unwrap();
329 assert_eq!(fetched.id, user.id);
330 }
331
332 #[test]
333 fn test_verify_password() {
334 let conn = setup_db();
335 let manager = UserManager::new(&conn);
336
337 let user = User::new("authuser");
338 manager.create_user(&user, Some("secret123")).unwrap();
339
340 let verified = manager.verify_password("authuser", "secret123").unwrap();
341 assert!(verified.is_some());
342
343 let wrong = manager
344 .verify_password("authuser", "wrongpassword")
345 .unwrap();
346 assert!(wrong.is_none());
347 }
348
349 #[test]
350 fn test_update_user() {
351 let conn = setup_db();
352 let manager = UserManager::new(&conn);
353
354 let mut user = User::new("updateme");
355 manager.create_user(&user, None).unwrap();
356
357 user.display_name = Some("Updated Name".to_string());
358 manager.update_user(&user).unwrap();
359
360 let fetched = manager.get_user(&user.id).unwrap().unwrap();
361 assert_eq!(fetched.display_name, Some("Updated Name".to_string()));
362 }
363
364 #[test]
365 fn test_delete_user() {
366 let conn = setup_db();
367 let manager = UserManager::new(&conn);
368
369 let user = User::new("deleteme");
370 manager.create_user(&user, None).unwrap();
371
372 let deleted = manager.delete_user(&user.id).unwrap();
373 assert!(deleted);
374
375 let fetched = manager.get_user(&user.id).unwrap();
376 assert!(fetched.is_none());
377 }
378
379 #[test]
380 fn test_list_users() {
381 let conn = setup_db();
382 let manager = UserManager::new(&conn);
383
384 let user1 = User::new("user1");
385 let mut user2 = User::new("user2");
386 user2.is_active = false;
387
388 manager.create_user(&user1, None).unwrap();
389 manager.create_user(&user2, None).unwrap();
390
391 let active = manager.list_users(false).unwrap();
392 assert_eq!(active.len(), 1);
393
394 let all = manager.list_users(true).unwrap();
395 assert_eq!(all.len(), 2);
396 }
397}