1use argon2::password_hash::SaltString;
7use argon2::password_hash::rand_core::OsRng;
8use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
9use rusqlite::{Connection, params};
10use serde::{Deserialize, Serialize};
11
12use crate::db::DbPool;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct User {
17 pub id: i64,
19 pub uuid: String,
21 pub login: String,
23 pub name: String,
25 pub email: String,
27 pub apikey: String,
29 pub is_active: bool,
31 pub is_admin: bool,
33 pub created_at: String,
35 pub last_seen: Option<String>,
37}
38
39pub fn hash_password(password: &str) -> Result<String, String> {
45 let salt = SaltString::generate(&mut OsRng);
46 let argon2 = Argon2::default();
47 argon2
48 .hash_password(password.as_bytes(), &salt)
49 .map(|h| h.to_string())
50 .map_err(|e| format!("Password hashing failed: {e}"))
51}
52
53pub fn verify_password(password: &str, hash: &str) -> Result<bool, String> {
59 let parsed_hash = PasswordHash::new(hash).map_err(|e| format!("Invalid password hash: {e}"))?;
60 Ok(Argon2::default()
61 .verify_password(password.as_bytes(), &parsed_hash)
62 .is_ok())
63}
64
65#[allow(clippy::too_many_arguments)]
71pub fn create_user(
72 conn: &Connection,
73 login: &str,
74 name: &str,
75 email: &str,
76 password: &str,
77 is_admin: bool,
78) -> Result<User, String> {
79 let uuid = uuid::Uuid::new_v4().to_string();
80 let apikey = uuid::Uuid::new_v4().to_string();
81 let pwdhash = hash_password(password)?;
82
83 conn.execute(
84 "INSERT INTO users (uuid, login, name, email, pwdhash, apikey, is_admin) \
85 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
86 params![
87 uuid,
88 login,
89 name,
90 email,
91 pwdhash,
92 apikey,
93 i32::from(is_admin)
94 ],
95 )
96 .map_err(|e| format!("Failed to create user: {e}"))?;
97
98 let id = conn.last_insert_rowid();
99
100 Ok(User {
101 id,
102 uuid,
103 login: login.to_owned(),
104 name: name.to_owned(),
105 email: email.to_owned(),
106 apikey,
107 is_active: true,
108 is_admin,
109 created_at: chrono::Utc::now().to_rfc3339(),
110 last_seen: None,
111 })
112}
113
114pub fn find_by_login(pool: &DbPool, login: &str) -> Result<Option<User>, String> {
120 pool.with_conn(|conn| {
121 let mut stmt = conn.prepare(
122 "SELECT id, uuid, login, name, email, apikey, is_active, is_admin, \
123 created_at, last_seen FROM users WHERE login = ?1",
124 )?;
125
126 let user = stmt
127 .query_row(params![login], |row| {
128 Ok(User {
129 id: row.get("id")?,
130 uuid: row.get("uuid")?,
131 login: row.get("login")?,
132 name: row.get("name")?,
133 email: row.get("email")?,
134 apikey: row.get("apikey")?,
135 is_active: row.get::<_, i32>("is_active")? != 0,
136 is_admin: row.get::<_, i32>("is_admin")? != 0,
137 created_at: row.get("created_at")?,
138 last_seen: row.get("last_seen")?,
139 })
140 })
141 .optional()?;
142
143 Ok(user)
144 })
145 .map_err(|e| format!("Database query failed: {e}"))
146}
147
148use rusqlite::OptionalExtension;
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_hash_and_verify_password() {
156 let test_input = "secure_p@ssw0rd_2026";
158 let hash = hash_password(test_input).expect("hashing failed");
159 assert!(verify_password(test_input, &hash).expect("verify failed"));
160 assert!(!verify_password("wrong_input", &hash).expect("verify failed"));
161 }
162
163 #[test]
164 fn test_create_user() {
165 let pool = DbPool::open_in_memory().expect("DB open failed");
166 pool.with_conn(|conn| {
167 let user = create_user(
168 conn,
169 "testuser",
170 "Test User",
171 "test@example.com",
172 "password123",
173 false,
174 )
175 .expect("user creation failed");
176 assert_eq!(user.login, "testuser");
177 assert!(!user.is_admin);
178 assert!(user.is_active);
179 Ok(())
180 })
181 .expect("with_conn failed");
182 }
183
184 #[test]
185 fn test_find_by_login() {
186 let pool = DbPool::open_in_memory().expect("DB open failed");
187 pool.with_conn(|conn| {
188 create_user(conn, "admin", "Admin", "admin@test.com", "admin123", true)
189 .expect("user creation failed");
190 Ok(())
191 })
192 .expect("with_conn failed");
193
194 let user = find_by_login(&pool, "admin")
195 .expect("query failed")
196 .expect("user not found");
197 assert_eq!(user.login, "admin");
198 assert!(user.is_admin);
199
200 let missing = find_by_login(&pool, "nonexistent").expect("query failed");
201 assert!(missing.is_none());
202 }
203}