actix_web_admin/auth/
json_store.rs1use async_trait::async_trait;
2use std::path::PathBuf;
3use std::sync::Mutex;
4
5use crate::auth::store::{self, AuthError, User};
6
7pub struct JsonUserStore {
8 path: PathBuf,
9 users: Mutex<Vec<User>>,
10}
11
12impl JsonUserStore {
13 pub fn new(path: impl Into<PathBuf>) -> Self {
14 let path = path.into();
15 let users = if path.exists() {
16 let data = std::fs::read_to_string(&path)
17 .unwrap_or_else(|_| "[]".to_string());
18 serde_json::from_str(&data).unwrap_or_default()
19 } else {
20 Vec::new()
21 };
22 JsonUserStore {
23 path,
24 users: Mutex::new(users),
25 }
26 }
27
28 fn save(&self) -> Result<(), AuthError> {
29 let users = self.users.lock().map_err(|e| {
30 AuthError::Storage(format!("lock error: {}", e))
31 })?;
32 let data = serde_json::to_string_pretty(&*users)
33 .map_err(|e| AuthError::Storage(e.to_string()))?;
34 std::fs::write(&self.path, data)
35 .map_err(|e| AuthError::Storage(e.to_string()))?;
36 Ok(())
37 }
38
39 fn find_by_username_locked(&self, username: &str) -> Result<Option<User>, AuthError> {
40 let users = self.users.lock().map_err(|e| {
41 AuthError::Storage(format!("lock error: {}", e))
42 })?;
43 Ok(users.iter().find(|u| u.username == username).cloned())
44 }
45
46 fn find_by_email_locked(&self, email: &str) -> Result<Option<User>, AuthError> {
47 let users = self.users.lock().map_err(|e| {
48 AuthError::Storage(format!("lock error: {}", e))
49 })?;
50 Ok(users.iter().find(|u| u.email == email).cloned())
51 }
52}
53
54#[async_trait]
55impl store::UserStore for JsonUserStore {
56 async fn find_by_username(&self, username: &str) -> Result<Option<User>, AuthError> {
57 self.find_by_username_locked(username)
58 }
59
60 async fn find_by_email(&self, email: &str) -> Result<Option<User>, AuthError> {
61 self.find_by_email_locked(email)
62 }
63
64 async fn create_user(
65 &self,
66 username: &str,
67 email: &str,
68 name: &str,
69 password: &str,
70 is_superuser: bool,
71 ) -> Result<User, AuthError> {
72 {
73 let users = self.users.lock().map_err(|e| {
74 AuthError::Storage(format!("lock error: {}", e))
75 })?;
76 if users.iter().any(|u| u.username == username) {
77 return Err(AuthError::DuplicateUsername);
78 }
79 if users.iter().any(|u| u.email == email) {
80 return Err(AuthError::DuplicateEmail);
81 }
82 }
83
84 let password_hash = store::hash_password(password)?;
85 let user = User {
86 id: store::generate_id(),
87 username: username.to_string(),
88 email: email.to_string(),
89 name: name.to_string(),
90 password_hash,
91 is_superuser,
92 created_at: store::timestamp(),
93 };
94
95 {
96 let mut users = self.users.lock().map_err(|e| {
97 AuthError::Storage(format!("lock error: {}", e))
98 })?;
99 users.push(user.clone());
100 }
101 self.save()?;
102 Ok(user)
103 }
104
105 async fn delete_user(&self, username: &str) -> Result<(), AuthError> {
106 let mut users = self.users.lock().map_err(|e| {
107 AuthError::Storage(format!("lock error: {}", e))
108 })?;
109 let len_before = users.len();
110 users.retain(|u| u.username != username);
111 if users.len() == len_before {
112 return Err(AuthError::NotFound);
113 }
114 drop(users);
115 self.save()?;
116 Ok(())
117 }
118}
119
120impl JsonUserStore {
121 pub fn all_users(&self) -> Result<Vec<User>, AuthError> {
122 let users = self.users.lock().map_err(|e| {
123 AuthError::Storage(format!("lock error: {}", e))
124 })?;
125 Ok(users.clone())
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::auth::store::UserStore;
133
134 #[tokio::test]
135 async fn test_create_and_find() {
136 let dir = std::env::temp_dir();
137 let path = dir.join("test_users3.json");
138 let _ = std::fs::remove_file(&path);
139
140 let store = JsonUserStore::new(&path);
141 let user = store
142 .create_user("alice", "alice@example.com", "Alice", "p4ss", true)
143 .await
144 .unwrap();
145 assert_eq!(user.username, "alice");
146 assert_eq!(user.email, "alice@example.com");
147 assert_eq!(user.name, "Alice");
148 assert!(user.is_superuser);
149
150 let found = store.find_by_username("alice").await.unwrap();
151 assert!(found.is_some());
152 assert_eq!(found.unwrap().email, "alice@example.com");
153
154 let found = store.find_by_email("alice@example.com").await.unwrap();
155 assert!(found.is_some());
156 assert_eq!(found.unwrap().username, "alice");
157
158 let _ = std::fs::remove_file(&path);
159 }
160
161 #[tokio::test]
162 async fn test_duplicate_username() {
163 let dir = std::env::temp_dir();
164 let path = dir.join("test_users_dup3.json");
165 let _ = std::fs::remove_file(&path);
166
167 let store = JsonUserStore::new(&path);
168 store
169 .create_user("bob", "bob@test.com", "Bob", "pass1", false)
170 .await
171 .unwrap();
172 let result = store
173 .create_user("bob", "other@test.com", "Other", "pass2", false)
174 .await;
175 assert!(matches!(result, Err(AuthError::DuplicateUsername)));
176
177 let _ = std::fs::remove_file(&path);
178 }
179
180 #[tokio::test]
181 async fn test_duplicate_email() {
182 let dir = std::env::temp_dir();
183 let path = dir.join("test_users_dup_email.json");
184 let _ = std::fs::remove_file(&path);
185
186 let store = JsonUserStore::new(&path);
187 store
188 .create_user("user1", "same@test.com", "User1", "pass1", false)
189 .await
190 .unwrap();
191 let result = store
192 .create_user("user2", "same@test.com", "User2", "pass2", false)
193 .await;
194 assert!(matches!(result, Err(AuthError::DuplicateEmail)));
195
196 let _ = std::fs::remove_file(&path);
197 }
198
199 #[tokio::test]
200 async fn test_delete_user() {
201 let dir = std::env::temp_dir();
202 let path = dir.join("test_users_delete.json");
203 let _ = std::fs::remove_file(&path);
204
205 let store = JsonUserStore::new(&path);
206 store
207 .create_user("dave", "dave@test.com", "Dave", "pass", false)
208 .await
209 .unwrap();
210
211 let result = store.delete_user("dave").await;
212 assert!(result.is_ok(), "should delete existing user");
213
214 let found = store.find_by_username("dave").await.unwrap();
215 assert!(found.is_none(), "deleted user should not be found");
216
217 let result = store.delete_user("nonexistent").await;
218 assert!(matches!(result, Err(AuthError::NotFound)));
219
220 let _ = std::fs::remove_file(&path);
221 }
222
223 #[tokio::test]
224 async fn test_persistence() {
225 let dir = std::env::temp_dir();
226 let path = dir.join("test_users_persist3.json");
227 let _ = std::fs::remove_file(&path);
228
229 {
230 let store = JsonUserStore::new(&path);
231 store
232 .create_user("carol", "carol@test.com", "Carol", "secret", false)
233 .await
234 .unwrap();
235 }
236
237 {
238 let store = JsonUserStore::new(&path);
239 let found = store.find_by_username("carol").await.unwrap();
240 assert!(found.is_some(), "user should persist to disk");
241 assert_eq!(found.unwrap().email, "carol@test.com");
242 }
243
244 let _ = std::fs::remove_file(&path);
245 }
246}