opendev_http/
user_store.rs1use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::{Arc, RwLock};
10
11use chrono::Utc;
12use tracing::warn;
13use uuid::Uuid;
14
15use opendev_models::User;
16
17use crate::models::HttpError;
18
19#[derive(Debug)]
25pub struct UserStore {
26 users_file: PathBuf,
27 users: Arc<RwLock<HashMap<String, User>>>,
28}
29
30impl UserStore {
31 pub fn new(storage_dir: PathBuf) -> Result<Self, HttpError> {
35 let users_file = storage_dir.join("users.json");
36 let store = Self {
37 users_file,
38 users: Arc::new(RwLock::new(HashMap::new())),
39 };
40 store.load()?;
41 Ok(store)
42 }
43
44 pub fn get_by_username(&self, username: &str) -> Option<User> {
46 let users = self.users.read().expect("RwLock poisoned");
47 users.get(username).cloned()
48 }
49
50 pub fn get_by_id(&self, user_id: Uuid) -> Option<User> {
52 let users = self.users.read().expect("RwLock poisoned");
53 users.values().find(|u| u.id == user_id).cloned()
54 }
55
56 pub fn create_user(
60 &self,
61 username: &str,
62 password_hash: &str,
63 email: Option<&str>,
64 ) -> Result<User, HttpError> {
65 let mut users = self.users.write().expect("RwLock poisoned");
66 if users.contains_key(username) {
67 return Err(HttpError::Other(format!(
68 "User already exists: {}",
69 username
70 )));
71 }
72 let mut user = User::new(username.to_string(), password_hash.to_string());
73 user.email = email.map(|s| s.to_string());
74 users.insert(username.to_string(), user.clone());
75 drop(users);
76 self.persist()?;
77 Ok(user)
78 }
79
80 pub fn update_user(&self, mut user: User) -> Result<(), HttpError> {
84 let mut users = self.users.write().expect("RwLock poisoned");
85 user.updated_at = Utc::now();
86 users.insert(user.username.clone(), user);
87 drop(users);
88 self.persist()
89 }
90
91 pub fn delete_user(&self, username: &str) -> Result<bool, HttpError> {
95 let mut users = self.users.write().expect("RwLock poisoned");
96 let removed = users.remove(username).is_some();
97 drop(users);
98 if removed {
99 self.persist()?;
100 }
101 Ok(removed)
102 }
103
104 pub fn list_usernames(&self) -> Vec<String> {
106 let users = self.users.read().expect("RwLock poisoned");
107 users.keys().cloned().collect()
108 }
109
110 pub fn count(&self) -> usize {
112 let users = self.users.read().expect("RwLock poisoned");
113 users.len()
114 }
115
116 fn load(&self) -> Result<(), HttpError> {
118 if !self.users_file.exists() {
119 if let Some(parent) = self.users_file.parent() {
121 std::fs::create_dir_all(parent)?;
122 }
123 let tmp_path = self
125 .users_file
126 .with_extension(format!("tmp.{}", Uuid::new_v4()));
127
128 #[cfg(unix)]
129 {
130 use std::os::unix::fs::OpenOptionsExt;
131 let mut opts = std::fs::OpenOptions::new();
132 opts.write(true).create(true).truncate(true).mode(0o600);
133 std::io::Write::write_all(&mut opts.open(&tmp_path)?, b"{}")?;
134 }
135 #[cfg(not(unix))]
136 {
137 std::fs::write(&tmp_path, "{}")?;
138 }
139
140 std::fs::rename(&tmp_path, &self.users_file)?;
141 return Ok(());
142 }
143
144 #[cfg(unix)]
146 self.check_permissions();
147
148 match std::fs::read_to_string(&self.users_file) {
149 Ok(content) => {
150 let parsed: HashMap<String, User> =
151 serde_json::from_str(&content).unwrap_or_else(|e| {
152 warn!("Failed to parse users file {:?}: {}", self.users_file, e);
153 HashMap::new()
154 });
155 let mut users = self.users.write().expect("RwLock poisoned");
156 *users = parsed;
157 }
158 Err(e) => {
159 warn!("Failed to read users file {:?}: {}", self.users_file, e);
160 }
161 }
162 Ok(())
163 }
164
165 fn persist(&self) -> Result<(), HttpError> {
167 let users = self.users.read().expect("RwLock poisoned");
168 let json = serde_json::to_string_pretty(&*users)?;
169 drop(users);
170
171 if let Some(parent) = self.users_file.parent() {
172 std::fs::create_dir_all(parent)?;
173 }
174
175 let tmp_path = self
177 .users_file
178 .with_extension(format!("tmp.{}", Uuid::new_v4()));
179
180 #[cfg(unix)]
181 {
182 use std::os::unix::fs::OpenOptionsExt;
183 let mut opts = std::fs::OpenOptions::new();
184 opts.write(true).create(true).truncate(true).mode(0o600);
185 std::io::Write::write_all(&mut opts.open(&tmp_path)?, json.as_bytes())?;
186 }
187 #[cfg(not(unix))]
188 {
189 std::fs::write(&tmp_path, json)?;
190 }
191
192 std::fs::rename(&tmp_path, &self.users_file)?;
193 Ok(())
194 }
195
196 #[cfg(unix)]
198 fn check_permissions(&self) {
199 use std::os::unix::fs::PermissionsExt;
200 if let Ok(meta) = std::fs::metadata(&self.users_file) {
201 let mode = meta.permissions().mode() & 0o777;
202 if mode & 0o077 != 0 {
203 warn!(
204 "User store file {:?} has loose permissions ({:o}). Tightening to 0600.",
205 self.users_file, mode
206 );
207 let _ = std::fs::set_permissions(
208 &self.users_file,
209 std::fs::Permissions::from_mode(0o600),
210 );
211 }
212 }
213 }
214}
215
216#[cfg(test)]
217#[path = "user_store_tests.rs"]
218mod tests;