1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct User {
6 pub id: String,
7 pub username: String,
8 pub email: String,
9 pub name: String,
10 pub password_hash: String,
11 pub is_superuser: bool,
12 pub created_at: String,
13}
14
15#[derive(Debug)]
16pub enum AuthError {
17 NotFound,
18 DuplicateUsername,
19 DuplicateEmail,
20 InvalidPassword,
21 Storage(String),
22}
23
24impl std::fmt::Display for AuthError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 AuthError::NotFound => write!(f, "user not found"),
28 AuthError::DuplicateUsername => write!(f, "username already exists"),
29 AuthError::DuplicateEmail => write!(f, "email already exists"),
30 AuthError::InvalidPassword => write!(f, "invalid password"),
31 AuthError::Storage(msg) => write!(f, "storage error: {}", msg),
32 }
33 }
34}
35
36impl std::error::Error for AuthError {}
37
38#[async_trait]
39pub trait UserStore: Send + Sync + 'static {
40 async fn find_by_username(&self, username: &str) -> Result<Option<User>, AuthError>;
41
42 async fn find_by_email(&self, _email: &str) -> Result<Option<User>, AuthError> {
43 Err(AuthError::Storage("not implemented".to_string()))
45 }
46
47 async fn create_user(
48 &self,
49 username: &str,
50 email: &str,
51 name: &str,
52 password: &str,
53 is_superuser: bool,
54 ) -> Result<User, AuthError>;
55
56 async fn delete_user(&self, username: &str) -> Result<(), AuthError>;
57}
58
59pub fn hash_password(password: &str) -> Result<String, AuthError> {
60 use argon2::password_hash::{rand_core::OsRng, SaltString};
61 use argon2::{Argon2, PasswordHasher};
62
63 let salt = SaltString::generate(&mut OsRng);
64 let hash = Argon2::default()
65 .hash_password(password.as_bytes(), &salt)
66 .map_err(|e| AuthError::Storage(e.to_string()))?
67 .to_string();
68 Ok(hash)
69}
70
71pub fn verify_password(password: &str, hash: &str) -> Result<bool, AuthError> {
72 use argon2::password_hash::PasswordHash;
73 use argon2::{Argon2, PasswordVerifier};
74
75 let parsed_hash =
76 PasswordHash::new(hash).map_err(|e| AuthError::Storage(e.to_string()))?;
77 Ok(Argon2::default()
78 .verify_password(password.as_bytes(), &parsed_hash)
79 .is_ok())
80}
81
82pub fn find_by_username_or_email<'a>(
83 users: &'a [User],
84 login: &str,
85) -> Option<&'a User> {
86 if login.contains('@') {
87 users.iter().find(|u| u.email == login)
88 } else {
89 users.iter().find(|u| u.username == login)
90 }
91}
92
93pub fn generate_id() -> String {
94 use std::sync::atomic::{AtomicU64, Ordering};
95 use std::time::{SystemTime, UNIX_EPOCH};
96 static COUNTER: AtomicU64 = AtomicU64::new(0);
97 let nanos = SystemTime::now()
98 .duration_since(UNIX_EPOCH)
99 .unwrap_or_default()
100 .as_nanos();
101 let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
102 format!("{:x}{:04x}", nanos, counter & 0xFFFF)
103}
104
105pub fn timestamp() -> String {
106 chrono::Utc::now().to_rfc3339()
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_hash_and_verify() {
115 let hash = hash_password("secret123").unwrap();
116 assert!(verify_password("secret123", &hash).unwrap());
117 assert!(!verify_password("wrong", &hash).unwrap());
118 }
119
120 #[test]
121 fn test_hash_unique_salts() {
122 let h1 = hash_password("same").unwrap();
123 let h2 = hash_password("same").unwrap();
124 assert_ne!(h1, h2, "each hash should have a unique salt");
125 }
126
127 #[test]
128 fn test_generate_id_unique() {
129 let ids: Vec<String> = (0..10).map(|_| generate_id()).collect();
130 let mut sorted = ids.clone();
131 sorted.sort();
132 sorted.dedup();
133 assert_eq!(ids.len(), sorted.len(), "all generated IDs should be unique");
134 }
135
136 #[test]
137 fn test_auth_error_display() {
138 assert_eq!(AuthError::NotFound.to_string(), "user not found");
139 assert_eq!(AuthError::DuplicateUsername.to_string(), "username already exists");
140 assert_eq!(AuthError::DuplicateEmail.to_string(), "email already exists");
141 assert_eq!(AuthError::InvalidPassword.to_string(), "invalid password");
142 assert_eq!(
143 AuthError::Storage("disk full".to_string()).to_string(),
144 "storage error: disk full"
145 );
146 }
147
148 #[test]
149 fn test_find_by_username_or_email() {
150 let users = vec![
151 User {
152 id: "1".into(),
153 username: "alice".into(),
154 email: "alice@example.com".into(),
155 name: "Alice".into(),
156 password_hash: "".into(),
157 is_superuser: false,
158 created_at: "".into(),
159 },
160 User {
161 id: "2".into(),
162 username: "bob".into(),
163 email: "bob@test.com".into(),
164 name: "Bob".into(),
165 password_hash: "".into(),
166 is_superuser: false,
167 created_at: "".into(),
168 },
169 ];
170
171 assert_eq!(find_by_username_or_email(&users, "alice").unwrap().id, "1");
172 assert_eq!(find_by_username_or_email(&users, "bob@test.com").unwrap().id, "2");
173 assert!(find_by_username_or_email(&users, "unknown").is_none());
174 }
175
176 #[test]
177 fn test_timestamp_format() {
178 let ts = timestamp();
179 assert!(ts.contains('T'), "expected ISO 8601 format, got: {}", ts);
180 }
181}