rust_web_server/auth_flows/
store.rs1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct AuthUser {
9 pub id: String,
11 pub email: String,
13 pub password_hash: String,
15 pub confirmed: bool,
17}
18
19pub trait UserStore: Send + Sync {
24 fn find_by_email(&self, email: &str) -> Result<Option<AuthUser>, String>;
26 fn find_by_id(&self, id: &str) -> Result<Option<AuthUser>, String>;
28 fn create(&self, email: &str, password_hash: &str) -> Result<Option<AuthUser>, String>;
30 fn set_password_hash(&self, id: &str, password_hash: &str) -> Result<(), String>;
32 fn confirm(&self, id: &str) -> Result<(), String>;
34}
35
36#[derive(Clone, Default)]
38pub struct MemoryUserStore {
39 users: Arc<Mutex<HashMap<String, AuthUser>>>,
40}
41
42impl MemoryUserStore {
43 pub fn new() -> Self {
44 MemoryUserStore::default()
45 }
46
47 pub fn users(&self) -> Vec<AuthUser> {
49 let mut users: Vec<AuthUser> = self.users.lock().unwrap_or_else(|e| e.into_inner()).values().cloned().collect();
50 users.sort_by(|a, b| a.id.cmp(&b.id));
51 users
52 }
53}
54
55impl UserStore for MemoryUserStore {
56 fn find_by_email(&self, email: &str) -> Result<Option<AuthUser>, String> {
57 Ok(self.users.lock().unwrap_or_else(|e| e.into_inner()).values().find(|u| u.email == email).cloned())
58 }
59
60 fn find_by_id(&self, id: &str) -> Result<Option<AuthUser>, String> {
61 Ok(self.users.lock().unwrap_or_else(|e| e.into_inner()).get(id).cloned())
62 }
63
64 fn create(&self, email: &str, password_hash: &str) -> Result<Option<AuthUser>, String> {
65 let mut users = self.users.lock().unwrap_or_else(|e| e.into_inner());
66 if users.values().any(|u| u.email == email) {
67 return Ok(None);
68 }
69 let id = (users.len() + 1).to_string();
70 let user = AuthUser { id: id.clone(), email: email.to_string(), password_hash: password_hash.to_string(), confirmed: false };
71 users.insert(id, user.clone());
72 Ok(Some(user))
73 }
74
75 fn set_password_hash(&self, id: &str, password_hash: &str) -> Result<(), String> {
76 match self.users.lock().unwrap_or_else(|e| e.into_inner()).get_mut(id) {
77 Some(user) => {
78 user.password_hash = password_hash.to_string();
79 Ok(())
80 }
81 None => Err(format!("no user {id}")),
82 }
83 }
84
85 fn confirm(&self, id: &str) -> Result<(), String> {
86 match self.users.lock().unwrap_or_else(|e| e.into_inner()).get_mut(id) {
87 Some(user) => {
88 user.confirmed = true;
89 Ok(())
90 }
91 None => Err(format!("no user {id}")),
92 }
93 }
94}
95
96#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
112#[derive(Clone)]
113pub struct DbUserStore {
114 pool: crate::model::DbPool,
115}
116
117#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
118impl DbUserStore {
119 pub async fn new(pool: crate::model::DbPool) -> Result<Self, crate::model::DbError> {
121 pool.execute(
122 "CREATE TABLE IF NOT EXISTS rws_users (id VARCHAR(64) PRIMARY KEY, email VARCHAR(191) NOT NULL UNIQUE, \
123 password_hash TEXT NOT NULL, confirmed INTEGER NOT NULL DEFAULT 0, created_at BIGINT NOT NULL)",
124 &[],
125 )
126 .await?;
127 Ok(DbUserStore { pool })
128 }
129
130 pub fn pool(&self) -> &crate::model::DbPool {
131 &self.pool
132 }
133
134 fn sql(&self, sql: &str) -> String {
135 #[cfg(feature = "model-postgres")]
136 if self.pool.backend() == crate::model::Backend::Postgres {
137 let mut out = String::new();
138 let mut n = 0;
139 for c in sql.chars() {
140 if c == '?' {
141 n += 1;
142 out.push_str(&format!("${n}"));
143 } else {
144 out.push(c);
145 }
146 }
147 return out;
148 }
149 sql.to_string()
150 }
151
152 fn run<T: Send + 'static>(&self, future: impl std::future::Future<Output = Result<T, crate::model::DbError>> + Send + 'static) -> Result<T, String> {
153 rws_server::async_bridge::block_on_shared(future).map_err(|e| e.to_string())
154 }
155
156 fn one(&self, sql: &str, params: Vec<crate::model::Value>) -> Result<Option<AuthUser>, String> {
157 let (pool, sql) = (self.pool.clone(), self.sql(sql));
158 let rows = self.run(async move { pool.query_rows(&sql, ¶ms).await })?;
159 let Some(row) = rows.first() else { return Ok(None) };
160 let user = (|| -> Result<AuthUser, crate::model::DbError> {
161 Ok(AuthUser { id: row.get("id")?, email: row.get("email")?, password_hash: row.get("password_hash")?, confirmed: row.get::<i64>("confirmed")? != 0 })
162 })()
163 .map_err(|e| e.to_string())?;
164 Ok(Some(user))
165 }
166
167 fn exec(&self, sql: &str, params: Vec<crate::model::Value>) -> Result<u64, String> {
168 let (pool, sql) = (self.pool.clone(), self.sql(sql));
169 self.run(async move { pool.execute(&sql, ¶ms).await })
170 }
171}
172
173#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
174impl UserStore for DbUserStore {
175 fn find_by_email(&self, email: &str) -> Result<Option<AuthUser>, String> {
176 self.one("SELECT id, email, password_hash, confirmed FROM rws_users WHERE email = ?", vec![crate::model::Value::Text(email.to_string())])
177 }
178
179 fn find_by_id(&self, id: &str) -> Result<Option<AuthUser>, String> {
180 self.one("SELECT id, email, password_hash, confirmed FROM rws_users WHERE id = ?", vec![crate::model::Value::Text(id.to_string())])
181 }
182
183 fn create(&self, email: &str, password_hash: &str) -> Result<Option<AuthUser>, String> {
184 use crate::model::Value;
185 if self.find_by_email(email)?.is_some() {
186 return Ok(None);
187 }
188 let id = crate::crypto::generate_token(16);
189 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0);
190 let inserted = self.exec(
191 "INSERT INTO rws_users (id, email, password_hash, confirmed, created_at) VALUES (?, ?, ?, 0, ?)",
192 vec![Value::Text(id.clone()), Value::Text(email.to_string()), Value::Text(password_hash.to_string()), Value::Int(now)],
193 );
194 match inserted {
195 Ok(_) => Ok(Some(AuthUser { id, email: email.to_string(), password_hash: password_hash.to_string(), confirmed: false })),
196 Err(_) if self.find_by_email(email)?.is_some() => Ok(None),
198 Err(e) => Err(e),
199 }
200 }
201
202 fn set_password_hash(&self, id: &str, password_hash: &str) -> Result<(), String> {
203 use crate::model::Value;
204 self.exec("UPDATE rws_users SET password_hash = ? WHERE id = ?", vec![Value::Text(password_hash.to_string()), Value::Text(id.to_string())]).map(|_| ())
205 }
206
207 fn confirm(&self, id: &str) -> Result<(), String> {
208 use crate::model::Value;
209 self.exec("UPDATE rws_users SET confirmed = 1 WHERE id = ?", vec![Value::Text(id.to_string())]).map(|_| ())
210 }
211}