Skip to main content

haste_repository/pg/
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};
16
17fn hash_password(password: &str) -> Result<String, StoreError> {
18    let salt = SaltString::generate(&mut OsRng);
19    let hash = Argon2::default()
20        .hash_password(password.as_bytes(), &salt)
21        .map_err(StoreError::PasswordHashError)?
22        .to_string();
23
24    Ok(hash)
25}
26
27async fn login<'a, 'e, E>(
28    executor: E,
29    tenant: &'a TenantId,
30    method: &'a LoginMethod,
31) -> Result<LoginResult, OperationOutcomeError>
32where
33    E: PgExecutor<'e>,
34{
35    match method {
36        LoginMethod::EmailPassword { email, password } => {
37            let row = sqlx::query_as::<
38                _,
39                (
40                    String,
41                    TenantId,
42                    Option<String>,
43                    UserRole,
44                    AuthMethod,
45                    Option<String>,
46                    Option<String>,
47                ),
48            >(
49                r"
50                    SELECT id, tenant, email, role, method, provider_id, password
51                    FROM users
52                    WHERE tenant = $1 AND method = $2 AND email = $3
53                ",
54            )
55            .bind(tenant.as_ref())
56            .bind(AuthMethod::EmailPassword)
57            .bind(email)
58            .fetch_optional(executor)
59            .await
60            .map_err(StoreError::from)?;
61
62            let Some((id, tenant_id, email_val, role, method_val, provider_id, password_hash)) =
63                row
64            else {
65                return Ok(LoginResult::Failure);
66            };
67
68            let verified = password_hash
69                .as_deref()
70                .and_then(|hash| PasswordHash::new(hash).ok())
71                .is_some_and(|parsed_hash| {
72                    Argon2::default()
73                        .verify_password(password.as_bytes(), &parsed_hash)
74                        .is_ok()
75                });
76
77            if !verified {
78                return Ok(LoginResult::Failure);
79            }
80
81            Ok(LoginResult::Success {
82                user: User {
83                    id,
84                    tenant: tenant_id,
85                    email: email_val,
86                    role,
87                    method: method_val,
88                    provider_id,
89                },
90            })
91        }
92        LoginMethod::OIDC {
93            email: _,
94            provider_id: _,
95        } => Ok(LoginResult::Failure),
96    }
97}
98
99impl Login for PGConnection {
100    async fn login(
101        &self,
102        tenant: &TenantId,
103        method: &LoginMethod,
104    ) -> Result<LoginResult, haste_fhir_operation_error::OperationOutcomeError> {
105        match &self {
106            PGConnection::Pool(pool, _) => {
107                let res = login(pool, tenant, method).await?;
108                Ok(res)
109            }
110            PGConnection::Transaction(tx, _) => {
111                let mut tx = tx.lock().await;
112
113                let res = login(&mut **tx, tenant, method).await?;
114                Ok(res)
115            }
116        }
117    }
118}
119
120async fn create_user<'a, 'e, E>(
121    executor: E,
122    tenant: &'a TenantId,
123    new_user: CreateUser,
124) -> Result<User, OperationOutcomeError>
125where
126    E: PgExecutor<'e>,
127{
128    let mut query_builder = QueryBuilder::new(
129        r"
130            INSERT INTO users(tenant, id, email, role, method, provider_id, password)
131        ",
132    );
133
134    query_builder.push(" VALUES (");
135
136    let mut seperator = query_builder.separated(", ");
137
138    seperator
139        .push_bind(tenant.as_ref())
140        .push_bind(new_user.id)
141        .push_bind(new_user.email)
142        .push_bind(new_user.role)
143        .push_bind(new_user.method);
144
145    if let Some(provider_id) = new_user.provider_id {
146        seperator.push_bind(provider_id);
147    } else {
148        seperator.push_bind(None::<String>);
149    }
150
151    if let Some(password) = new_user.password {
152        let hashed_password = hash_password(&password)?;
153        seperator.push_bind(hashed_password);
154    } else {
155        seperator.push_bind(None::<String>);
156    }
157
158    query_builder.push(r") RETURNING id, tenant, provider_id, email, role, method");
159
160    let query = query_builder.build_query_as::<User>();
161
162    let user = query
163        .fetch_one(executor)
164        .await
165        .map_err(StoreError::SQLXError)?;
166
167    Ok(user)
168}
169
170async fn read_user<'a, 'e, E>(
171    executor: E,
172    tenant: &'a TenantId,
173    id: &'a str,
174) -> Result<Option<User>, OperationOutcomeError>
175where
176    E: PgExecutor<'e>,
177{
178    let user = sqlx::query_as::<_, User>(
179        r"
180            SELECT id, tenant, provider_id, email, role, method
181            FROM users
182            WHERE tenant = $1 AND id = $2
183        ",
184    )
185    .bind(tenant.as_ref())
186    .bind(id)
187    .fetch_optional(executor)
188    .await
189    .map_err(StoreError::SQLXError)?;
190
191    Ok(user)
192}
193
194async fn update_user<'a, 'e, E>(
195    executor: E,
196    tenant: &'a TenantId,
197    model: UpdateUser,
198) -> Result<User, OperationOutcomeError>
199where
200    E: PgExecutor<'e>,
201{
202    let mut query_builder = QueryBuilder::new(
203        r"
204            UPDATE users SET
205        ",
206    );
207
208    let mut update_clauses = query_builder.separated(", ");
209
210    if let Some(provider_id) = model.provider_id {
211        update_clauses
212            .push(" provider_id = ")
213            .push_bind_unseparated(provider_id);
214    }
215
216    if let Some(email) = model.email.as_ref() {
217        update_clauses
218            .push(" email = ")
219            .push_bind_unseparated(email);
220    }
221
222    if let Some(role) = model.role.as_ref() {
223        update_clauses.push(" role = ").push_bind_unseparated(role);
224    }
225
226    if let Some(method) = model.method.as_ref() {
227        update_clauses
228            .push(" method = ")
229            .push_bind_unseparated(method);
230    }
231
232    if let Some(password) = model.password {
233        let hashed_password = hash_password(&password)?;
234        update_clauses
235            .push(" password = ")
236            .push_bind_unseparated(hashed_password);
237    }
238
239    update_clauses
240        .push(" tenant = ")
241        .push_bind_unseparated(tenant.as_ref());
242
243    query_builder.push(" WHERE id = ");
244    query_builder.push_bind(model.id);
245
246    query_builder.push(r" RETURNING id, tenant, provider_id, email, role, method");
247
248    let query = query_builder.build_query_as::<User>();
249
250    let user = query
251        .fetch_one(executor)
252        .await
253        .map_err(StoreError::SQLXError)?;
254
255    Ok(user)
256}
257
258async fn delete_user<'a, 'e, E>(
259    executor: E,
260    tenant: &'a TenantId,
261    id: &'a str,
262) -> Result<(), OperationOutcomeError>
263where
264    E: PgExecutor<'e>,
265{
266    sqlx::query(
267        r"
268            DELETE FROM users
269            WHERE tenant = $1 AND id = $2
270        ",
271    )
272    .bind(tenant.as_ref())
273    .bind(id)
274    .execute(executor)
275    .await
276    .map_err(StoreError::SQLXError)?;
277
278    Ok(())
279}
280
281async fn search_user<'a, 'e, E>(
282    executor: E,
283    tenant: &'a TenantId,
284    clauses: &'a UserSearchClauses,
285) -> Result<Vec<User>, OperationOutcomeError>
286where
287    E: PgExecutor<'e>,
288{
289    let mut query_builder: QueryBuilder<sqlx::Postgres> =
290        QueryBuilder::new(r"SELECT id, tenant, email, role, method, provider_id FROM users WHERE ");
291
292    let mut seperator = query_builder.separated(" AND ");
293    seperator
294        .push(" tenant = ")
295        .push_bind_unseparated(tenant.as_ref());
296
297    if let Some(email) = clauses.email.as_ref() {
298        seperator.push(" email = ").push_bind_unseparated(email);
299    }
300
301    if let Some(role) = clauses.role.as_ref() {
302        seperator.push(" role = ").push_bind_unseparated(role);
303    }
304
305    if let Some(method) = clauses.method.as_ref() {
306        seperator.push(" method = ").push_bind_unseparated(method);
307    }
308
309    let query = query_builder.build_query_as::<User>();
310
311    let users: Vec<User> = query.fetch_all(executor).await.map_err(StoreError::from)?;
312
313    Ok(users)
314}
315
316impl<Key: AsRef<str> + Send + Sync>
317    TenantModelAdmin<CreateUser, User, UserSearchClauses, UpdateUser, Key> for PGConnection
318{
319    async fn create(
320        &self,
321        tenant: &TenantId,
322        new_user: CreateUser,
323    ) -> Result<User, OperationOutcomeError> {
324        match self {
325            PGConnection::Pool(pool, _) => {
326                let res = create_user(pool, tenant, new_user).await?;
327                Ok(res)
328            }
329            PGConnection::Transaction(tx, _) => {
330                let mut tx = tx.lock().await;
331                let res = create_user(&mut **tx, tenant, new_user).await?;
332                Ok(res)
333            }
334        }
335    }
336
337    async fn read(
338        &self,
339        tenant: &TenantId,
340        id: &Key,
341    ) -> Result<Option<User>, OperationOutcomeError> {
342        match self {
343            PGConnection::Pool(pool, _) => {
344                let res = read_user(pool, tenant, id.as_ref()).await?;
345                Ok(res)
346            }
347            PGConnection::Transaction(tx, _) => {
348                let mut tx = tx.lock().await;
349                let res = read_user(&mut **tx, tenant, id.as_ref()).await?;
350                Ok(res)
351            }
352        }
353    }
354
355    async fn update(
356        &self,
357        tenant: &TenantId,
358        user: UpdateUser,
359    ) -> Result<User, OperationOutcomeError> {
360        match self {
361            PGConnection::Pool(pool, _) => update_user(pool, tenant, user).await,
362            PGConnection::Transaction(tx, _) => {
363                let mut tx = tx.lock().await;
364                update_user(&mut **tx, tenant, user).await
365            }
366        }
367    }
368
369    async fn delete(&self, tenant: &TenantId, id: &Key) -> Result<(), OperationOutcomeError> {
370        match self {
371            PGConnection::Pool(pool, _) => delete_user(pool, tenant, id.as_ref()).await,
372            PGConnection::Transaction(tx, _) => {
373                let mut tx = tx.lock().await;
374                delete_user(&mut **tx, tenant, id.as_ref()).await
375            }
376        }
377    }
378
379    async fn search(
380        &self,
381        tenant: &TenantId,
382        clauses: &UserSearchClauses,
383    ) -> Result<Vec<User>, OperationOutcomeError> {
384        match self {
385            PGConnection::Pool(pool, _) => search_user(pool, tenant, clauses).await,
386            PGConnection::Transaction(tx, _) => {
387                let mut tx = tx.lock().await;
388                search_user(&mut **tx, tenant, clauses).await
389            }
390        }
391    }
392}