Skip to main content

haste_repository/pg/models/
user.rs

1use crate::{
2    admin::{Login, TenantModelAdmin},
3    pg::{PGConnection, StoreError},
4    types::user::{
5        AuthMethod, CreateUser, LoginMethod, LoginResult, UpdateUser, User, UserRole,
6        UserSearchClauses,
7    },
8};
9use argon2::{
10    Argon2, PasswordHasher, PasswordVerifier,
11    password_hash::{PasswordHash, SaltString, rand_core::OsRng},
12};
13use haste_fhir_operation_error::OperationOutcomeError;
14use haste_jwt::TenantId;
15use sqlx::{PgExecutor, QueryBuilder};
16use std::sync::LazyLock;
17
18/// A hash to verify against when there is no user, or the user has no
19/// password. Verifying costs the same as a real check, so a login attempt
20/// takes as long whether or not the address has an account: without this an
21/// unknown address answered in a few milliseconds and a known one only after
22/// the Argon2 work, which told a caller which addresses exist.
23static NO_PASSWORD_HASH: LazyLock<String> = LazyLock::new(|| {
24    hash_password("no such user").expect("hashing a constant password cannot fail")
25});
26
27fn hash_password(password: &str) -> Result<String, StoreError> {
28    let salt = SaltString::generate(&mut OsRng);
29    let hash = Argon2::default()
30        .hash_password(password.as_bytes(), &salt)
31        .map_err(StoreError::PasswordHashError)?
32        .to_string();
33
34    Ok(hash)
35}
36
37async fn login<'a, 'e, E>(
38    executor: E,
39    tenant: &'a TenantId,
40    method: &'a LoginMethod,
41) -> Result<LoginResult, OperationOutcomeError>
42where
43    E: PgExecutor<'e>,
44{
45    match method {
46        LoginMethod::EmailPassword { email, password } => {
47            let row = sqlx::query_as::<
48                _,
49                (
50                    String,
51                    TenantId,
52                    Option<String>,
53                    UserRole,
54                    AuthMethod,
55                    Option<String>,
56                    Option<String>,
57                    bool,
58                ),
59            >(
60                r"
61                    SELECT id, tenant, email, role, method, provider_id, password, email_verified
62                    FROM users
63                    WHERE tenant = $1 AND method = $2 AND email = $3
64                ",
65            )
66            .bind(tenant.as_ref())
67            .bind(AuthMethod::EmailPassword)
68            .bind(email)
69            .fetch_optional(executor)
70            .await
71            .map_err(StoreError::from)?;
72
73            // The stored hash when the user exists and has a password, or a
74            // stand-in that is verified (and always fails) otherwise, so both
75            // outcomes take the same time.
76            let (user, password_hash) = match row {
77                Some((
78                    id,
79                    tenant_id,
80                    email_val,
81                    role,
82                    method_val,
83                    provider_id,
84                    hash,
85                    email_verified,
86                )) => {
87                    let user = User {
88                        id,
89                        tenant: tenant_id,
90                        email: email_val,
91                        role,
92                        method: method_val,
93                        provider_id,
94                        email_verified,
95                    };
96                    match hash {
97                        Some(hash) => (Some(user), hash),
98                        None => (None, NO_PASSWORD_HASH.clone()),
99                    }
100                }
101                None => (None, NO_PASSWORD_HASH.clone()),
102            };
103
104            let password_matches = PasswordHash::new(&password_hash).is_ok_and(|parsed_hash| {
105                Argon2::default()
106                    .verify_password(password.as_bytes(), &parsed_hash)
107                    .is_ok()
108            });
109
110            // An unverified address is refused only after the hash check, so
111            // that refusal takes the same time too.
112            let Some(user) = user.filter(|user| password_matches && user.email_verified) else {
113                return Ok(LoginResult::Failure);
114            };
115
116            Ok(LoginResult::Success { user })
117        }
118        LoginMethod::OIDC {
119            email: _,
120            provider_id: _,
121        } => Ok(LoginResult::Failure),
122    }
123}
124
125impl Login for PGConnection {
126    async fn login(
127        &self,
128        tenant: &TenantId,
129        method: &LoginMethod,
130    ) -> Result<LoginResult, haste_fhir_operation_error::OperationOutcomeError> {
131        match &self {
132            PGConnection::Pool(pool, _) => {
133                let res = login(pool, tenant, method).await?;
134                Ok(res)
135            }
136            PGConnection::Transaction(tx, _, _) => {
137                let mut tx = tx.lock().await;
138
139                let res = login(&mut **tx, tenant, method).await?;
140                Ok(res)
141            }
142        }
143    }
144}
145
146async fn create_user<'a, 'e, E>(
147    executor: E,
148    tenant: &'a TenantId,
149    new_user: CreateUser,
150) -> Result<User, OperationOutcomeError>
151where
152    E: PgExecutor<'e>,
153{
154    let mut query_builder = QueryBuilder::new(
155        r"
156            INSERT INTO users(tenant, id, email, role, method, email_verified, provider_id, password)
157        ",
158    );
159
160    query_builder.push(" VALUES (");
161
162    let mut seperator = query_builder.separated(", ");
163
164    seperator
165        .push_bind(tenant.as_ref())
166        .push_bind(new_user.id)
167        .push_bind(new_user.email)
168        .push_bind(new_user.role)
169        .push_bind(new_user.method)
170        .push_bind(new_user.email_verified);
171
172    if let Some(provider_id) = new_user.provider_id {
173        seperator.push_bind(provider_id);
174    } else {
175        seperator.push_bind(None::<String>);
176    }
177
178    if let Some(password) = new_user.password {
179        let hashed_password = hash_password(&password)?;
180        seperator.push_bind(hashed_password);
181    } else {
182        seperator.push_bind(None::<String>);
183    }
184
185    query_builder.push(r") RETURNING id, tenant, provider_id, email, role, method, email_verified");
186
187    let query = query_builder.build_query_as::<User>();
188
189    let user = query
190        .fetch_one(executor)
191        .await
192        .map_err(StoreError::SQLXError)?;
193
194    Ok(user)
195}
196
197async fn read_user<'a, 'e, E>(
198    executor: E,
199    tenant: &'a TenantId,
200    id: &'a str,
201) -> Result<Option<User>, OperationOutcomeError>
202where
203    E: PgExecutor<'e>,
204{
205    let user = sqlx::query_as::<_, User>(
206        r"
207            SELECT id, tenant, provider_id, email, role, method, email_verified
208            FROM users
209            WHERE tenant = $1 AND id = $2
210        ",
211    )
212    .bind(tenant.as_ref())
213    .bind(id)
214    .fetch_optional(executor)
215    .await
216    .map_err(StoreError::SQLXError)?;
217
218    Ok(user)
219}
220
221async fn update_user<'a, 'e, E>(
222    executor: E,
223    tenant: &'a TenantId,
224    model: UpdateUser,
225) -> Result<User, OperationOutcomeError>
226where
227    E: PgExecutor<'e>,
228{
229    let mut query_builder = QueryBuilder::new(
230        r"
231            UPDATE users SET
232        ",
233    );
234
235    let mut update_clauses = query_builder.separated(", ");
236
237    if let Some(provider_id) = model.provider_id {
238        update_clauses
239            .push(" provider_id = ")
240            .push_bind_unseparated(provider_id);
241    }
242
243    if let Some(email) = model.email.as_ref() {
244        update_clauses
245            .push(" email = ")
246            .push_bind_unseparated(email);
247    }
248
249    if let Some(role) = model.role.as_ref() {
250        update_clauses.push(" role = ").push_bind_unseparated(role);
251    }
252
253    if let Some(method) = model.method.as_ref() {
254        update_clauses
255            .push(" method = ")
256            .push_bind_unseparated(method);
257    }
258
259    if let Some(password) = model.password {
260        let hashed_password = hash_password(&password)?;
261        update_clauses
262            .push(" password = ")
263            .push_bind_unseparated(hashed_password);
264    }
265
266    if let Some(email_verified) = model.email_verified {
267        update_clauses
268            .push(" email_verified = ")
269            .push_bind_unseparated(email_verified);
270    }
271
272    update_clauses
273        .push(" tenant = ")
274        .push_bind_unseparated(tenant.as_ref());
275
276    query_builder.push(" WHERE id = ");
277    query_builder.push_bind(model.id);
278
279    query_builder.push(r" RETURNING id, tenant, provider_id, email, role, method, email_verified");
280
281    let query = query_builder.build_query_as::<User>();
282
283    let user = query
284        .fetch_one(executor)
285        .await
286        .map_err(StoreError::SQLXError)?;
287
288    Ok(user)
289}
290
291async fn delete_user<'a, 'e, E>(
292    executor: E,
293    tenant: &'a TenantId,
294    id: &'a str,
295) -> Result<(), OperationOutcomeError>
296where
297    E: PgExecutor<'e>,
298{
299    sqlx::query(
300        r"
301            DELETE FROM users
302            WHERE tenant = $1 AND id = $2
303        ",
304    )
305    .bind(tenant.as_ref())
306    .bind(id)
307    .execute(executor)
308    .await
309    .map_err(StoreError::SQLXError)?;
310
311    Ok(())
312}
313
314async fn search_user<'a, 'e, E>(
315    executor: E,
316    tenant: &'a TenantId,
317    clauses: &'a UserSearchClauses,
318) -> Result<Vec<User>, OperationOutcomeError>
319where
320    E: PgExecutor<'e>,
321{
322    let mut query_builder: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
323        r"SELECT id, tenant, email, role, method, provider_id, email_verified FROM users WHERE ",
324    );
325
326    let mut seperator = query_builder.separated(" AND ");
327    seperator
328        .push(" tenant = ")
329        .push_bind_unseparated(tenant.as_ref());
330
331    if let Some(email) = clauses.email.as_ref() {
332        seperator.push(" email = ").push_bind_unseparated(email);
333    }
334
335    if let Some(role) = clauses.role.as_ref() {
336        seperator.push(" role = ").push_bind_unseparated(role);
337    }
338
339    if let Some(method) = clauses.method.as_ref() {
340        seperator.push(" method = ").push_bind_unseparated(method);
341    }
342
343    let query = query_builder.build_query_as::<User>();
344
345    let users: Vec<User> = query.fetch_all(executor).await.map_err(StoreError::from)?;
346
347    Ok(users)
348}
349
350impl<Key: AsRef<str> + Send + Sync>
351    TenantModelAdmin<CreateUser, User, UserSearchClauses, UpdateUser, Key> for PGConnection
352{
353    async fn create(
354        &self,
355        tenant: &TenantId,
356        new_user: CreateUser,
357    ) -> Result<User, OperationOutcomeError> {
358        match self {
359            PGConnection::Pool(pool, _) => {
360                let res = create_user(pool, tenant, new_user).await?;
361                Ok(res)
362            }
363            PGConnection::Transaction(tx, _, _) => {
364                let mut tx = tx.lock().await;
365                let res = create_user(&mut **tx, tenant, new_user).await?;
366                Ok(res)
367            }
368        }
369    }
370
371    async fn read(
372        &self,
373        tenant: &TenantId,
374        id: &Key,
375    ) -> Result<Option<User>, OperationOutcomeError> {
376        match self {
377            PGConnection::Pool(pool, _) => {
378                let res = read_user(pool, tenant, id.as_ref()).await?;
379                Ok(res)
380            }
381            PGConnection::Transaction(tx, _, _) => {
382                let mut tx = tx.lock().await;
383                let res = read_user(&mut **tx, tenant, id.as_ref()).await?;
384                Ok(res)
385            }
386        }
387    }
388
389    async fn update(
390        &self,
391        tenant: &TenantId,
392        user: UpdateUser,
393    ) -> Result<User, OperationOutcomeError> {
394        match self {
395            PGConnection::Pool(pool, _) => update_user(pool, tenant, user).await,
396            PGConnection::Transaction(tx, _, _) => {
397                let mut tx = tx.lock().await;
398                update_user(&mut **tx, tenant, user).await
399            }
400        }
401    }
402
403    async fn delete(&self, tenant: &TenantId, id: &Key) -> Result<(), OperationOutcomeError> {
404        match self {
405            PGConnection::Pool(pool, _) => delete_user(pool, tenant, id.as_ref()).await,
406            PGConnection::Transaction(tx, _, _) => {
407                let mut tx = tx.lock().await;
408                delete_user(&mut **tx, tenant, id.as_ref()).await
409            }
410        }
411    }
412
413    async fn search(
414        &self,
415        tenant: &TenantId,
416        clauses: &UserSearchClauses,
417    ) -> Result<Vec<User>, OperationOutcomeError> {
418        match self {
419            PGConnection::Pool(pool, _) => search_user(pool, tenant, clauses).await,
420            PGConnection::Transaction(tx, _, _) => {
421                let mut tx = tx.lock().await;
422                search_user(&mut **tx, tenant, clauses).await
423            }
424        }
425    }
426}