Skip to main content

actix_web_admin/auth/
json_store.rs

1use 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}