1use crate::models::{User, UserRole};
2use crate::Database;
3use anyhow::Result;
4use argon2::{
5 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
6 Argon2,
7};
8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
9use rand::{rngs::OsRng, RngCore};
10
11pub const MIN_PASSWORD_LENGTH: usize = 8;
12const MAX_USERNAME_LENGTH: usize = 100;
13const MAX_EMAIL_LENGTH: usize = 254;
14
15pub fn validate_username(username: &str) -> Result<()> {
16 if username.is_empty() {
17 anyhow::bail!("Username cannot be empty");
18 }
19 if username.len() > MAX_USERNAME_LENGTH {
20 anyhow::bail!(
21 "Username must be {} characters or less",
22 MAX_USERNAME_LENGTH
23 );
24 }
25 if !username
26 .chars()
27 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
28 {
29 anyhow::bail!("Username can only contain letters, numbers, underscores, and hyphens");
30 }
31 Ok(())
32}
33
34pub fn validate_email(email: &str) -> Result<()> {
35 if email.is_empty() {
36 anyhow::bail!("Email cannot be empty");
37 }
38 if email.len() > MAX_EMAIL_LENGTH {
39 anyhow::bail!("Email must be {} characters or less", MAX_EMAIL_LENGTH);
40 }
41 if !email.contains('@') || !email.contains('.') {
42 anyhow::bail!("Invalid email format");
43 }
44 Ok(())
45}
46
47pub fn validate_password(password: &str) -> Result<()> {
48 if password.len() < MIN_PASSWORD_LENGTH {
49 anyhow::bail!(
50 "Password must be at least {} characters",
51 MIN_PASSWORD_LENGTH
52 );
53 }
54 if !password.chars().any(|c| c.is_ascii_lowercase()) {
55 anyhow::bail!("Password must contain at least one lowercase letter");
56 }
57 if !password.chars().any(|c| c.is_ascii_uppercase()) {
58 anyhow::bail!("Password must contain at least one uppercase letter");
59 }
60 if !password.chars().any(|c| c.is_ascii_digit()) {
61 anyhow::bail!("Password must contain at least one number");
62 }
63 Ok(())
64}
65
66pub fn hash_password(password: &str) -> Result<String> {
67 validate_password(password)?;
68 let salt = SaltString::generate(&mut OsRng);
69 let argon2 = Argon2::default();
70 let hash = argon2
71 .hash_password(password.as_bytes(), &salt)
72 .map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
73 Ok(hash.to_string())
74}
75
76const DUMMY_HASH: &str =
77 "$argon2id$v=19$m=19456,t=2,p=1$dW5rbm93bg$0000000000000000000000000000000000000000000";
78
79pub fn verify_password(password: &str, hash: &str) -> bool {
80 let parsed_hash = match PasswordHash::new(hash) {
81 Ok(h) => h,
82 Err(_) => {
83 if let Ok(dummy) = PasswordHash::new(DUMMY_HASH) {
84 let _ = Argon2::default().verify_password(password.as_bytes(), &dummy);
85 }
86 return false;
87 }
88 };
89 Argon2::default()
90 .verify_password(password.as_bytes(), &parsed_hash)
91 .is_ok()
92}
93
94pub fn generate_session_token() -> String {
95 let mut bytes = [0u8; 32];
96 OsRng.fill_bytes(&mut bytes);
97 URL_SAFE_NO_PAD.encode(bytes)
98}
99
100pub fn create_user(
101 db: &Database,
102 username: &str,
103 email: &str,
104 password: &str,
105 role: UserRole,
106) -> Result<i64> {
107 validate_username(username)?;
108 validate_email(email)?;
109 let password_hash = hash_password(password)?;
110 let conn = db.get()?;
111 conn.execute(
112 "INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)",
113 (username, email, &password_hash, role.to_string()),
114 )?;
115 Ok(conn.last_insert_rowid())
116}
117
118pub fn update_password(db: &Database, username: &str, password: &str) -> Result<()> {
119 let password_hash = hash_password(password)?;
120 let conn = db.get()?;
121 conn.execute(
122 "UPDATE users SET password_hash = ? WHERE username = ?",
123 (&password_hash, username),
124 )?;
125 Ok(())
126}
127
128pub fn authenticate(db: &Database, username: &str, password: &str) -> Result<Option<User>> {
129 let conn = db.get()?;
130 let user: Option<User> = conn
131 .query_row(
132 "SELECT id, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = ?",
133 [username],
134 |row| {
135 Ok(User {
136 id: row.get(0)?,
137 username: row.get(1)?,
138 email: row.get(2)?,
139 password_hash: row.get(3)?,
140 role: row.get::<_, String>(4)?.parse().unwrap_or(UserRole::Viewer),
141 created_at: row.get(5)?,
142 updated_at: row.get(6)?,
143 })
144 },
145 )
146 .ok();
147
148 match user {
149 Some(u) if verify_password(password, &u.password_hash) => Ok(Some(u)),
150 _ => Ok(None),
151 }
152}
153
154pub fn create_session(db: &Database, user_id: i64, duration_days: i64) -> Result<String> {
155 let token = generate_session_token();
156 let conn = db.get()?;
157 conn.execute(
158 "INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, datetime('now', ?||' days'))",
159 (user_id, &token, duration_days),
160 )?;
161 Ok(token)
162}
163
164pub fn validate_session(db: &Database, token: &str) -> Result<Option<User>> {
165 let conn = db.get()?;
166 let user = conn
167 .query_row(
168 r#"
169 SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at, u.updated_at
170 FROM users u
171 JOIN sessions s ON s.user_id = u.id
172 WHERE s.token = ? AND s.expires_at > datetime('now')
173 "#,
174 [token],
175 |row| {
176 Ok(User {
177 id: row.get(0)?,
178 username: row.get(1)?,
179 email: row.get(2)?,
180 password_hash: row.get(3)?,
181 role: row.get::<_, String>(4)?.parse().unwrap_or(UserRole::Viewer),
182 created_at: row.get(5)?,
183 updated_at: row.get(6)?,
184 })
185 },
186 )
187 .ok();
188 Ok(user)
189}
190
191pub fn delete_session(db: &Database, token: &str) -> Result<()> {
192 let conn = db.get()?;
193 conn.execute("DELETE FROM sessions WHERE token = ?", [token])?;
194 Ok(())
195}
196
197pub fn cleanup_expired_sessions(db: &Database) -> Result<()> {
198 let conn = db.get()?;
199 conn.execute(
200 "DELETE FROM sessions WHERE expires_at <= datetime('now')",
201 [],
202 )?;
203 Ok(())
204}
205
206pub fn has_users(db: &Database) -> Result<bool> {
207 let conn = db.get()?;
208 let count: i64 = conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?;
209 Ok(count > 0)
210}
211
212pub fn list_users(db: &Database) -> Result<Vec<User>> {
213 let conn = db.get()?;
214 let mut stmt = conn.prepare(
215 "SELECT id, username, email, password_hash, role, created_at, updated_at FROM users ORDER BY created_at DESC",
216 )?;
217 let users = stmt
218 .query_map([], |row| {
219 Ok(User {
220 id: row.get(0)?,
221 username: row.get(1)?,
222 email: row.get(2)?,
223 password_hash: row.get(3)?,
224 role: row.get::<_, String>(4)?.parse().unwrap_or(UserRole::Viewer),
225 created_at: row.get(5)?,
226 updated_at: row.get(6)?,
227 })
228 })?
229 .collect::<Result<Vec<_>, _>>()?;
230 Ok(users)
231}
232
233pub fn get_user(db: &Database, id: i64) -> Result<Option<User>> {
234 let conn = db.get()?;
235 let user = conn
236 .query_row(
237 "SELECT id, username, email, password_hash, role, created_at, updated_at FROM users WHERE id = ?",
238 [id],
239 |row| {
240 Ok(User {
241 id: row.get(0)?,
242 username: row.get(1)?,
243 email: row.get(2)?,
244 password_hash: row.get(3)?,
245 role: row.get::<_, String>(4)?.parse().unwrap_or(UserRole::Viewer),
246 created_at: row.get(5)?,
247 updated_at: row.get(6)?,
248 })
249 },
250 )
251 .ok();
252 Ok(user)
253}
254
255pub fn update_user(
256 db: &Database,
257 id: i64,
258 email: Option<&str>,
259 role: Option<UserRole>,
260) -> Result<()> {
261 let conn = db.get()?;
262 if let Some(email) = email {
263 validate_email(email)?;
264 conn.execute("UPDATE users SET email = ? WHERE id = ?", (email, id))?;
265 }
266 if let Some(role) = role {
267 conn.execute(
268 "UPDATE users SET role = ? WHERE id = ?",
269 (role.to_string(), id),
270 )?;
271 }
272 Ok(())
273}
274
275pub fn delete_user(db: &Database, id: i64) -> Result<()> {
276 let conn = db.get()?;
277 conn.execute("DELETE FROM users WHERE id = ?", [id])?;
278 Ok(())
279}