1use crate::{
2 admin::{ProjectModelAdmin, TenantModelAdmin},
3 pg::{PGConnection, StoreError},
4 types::authorization_code::{
5 AuthorizationCode, AuthorizationCodeSearchClaims, CodeErrors, CreateAuthorizationCode,
6 },
7 utilities::generate_id,
8};
9use haste_fhir_model::r4::generated::terminology::IssueType;
10use haste_fhir_operation_error::OperationOutcomeError;
11use haste_jwt::{ProjectId, TenantId};
12use sqlx::{PgExecutor, QueryBuilder};
13use sqlx_postgres::types::PgInterval;
14
15async fn create_code<'a, 'e, E>(
16 executor: E,
17 tenant: &'a TenantId,
18 project: Option<&'a ProjectId>,
19 authorization_code: CreateAuthorizationCode,
20) -> Result<AuthorizationCode, OperationOutcomeError>
21where
22 E: PgExecutor<'e>,
23{
24 let expires_in: PgInterval = authorization_code
25 .expires_in
26 .try_into()
27 .map_err(|_e| CodeErrors::InvalidDuration)?;
28
29 let code = generate_id(Some(45));
30
31 let new_authorization_code = sqlx::query_as::<_, AuthorizationCode>(
32 r"
33 INSERT INTO authorization_code (
34 tenant, project, client_id, kind, code, expires_in,
35 user_id, pkce_code_challenge, pkce_code_challenge_method, redirect_uri, meta, membership
36 )
37 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
38 RETURNING
39 tenant,
40 kind,
41 code,
42 user_id,
43 project,
44 client_id,
45 pkce_code_challenge,
46 pkce_code_challenge_method,
47 redirect_uri,
48 meta,
49 NOW() > (created_at + expires_in) as is_expired,
50 membership,
51 created_at
52 ",
53 )
54 .bind(tenant)
55 .bind(project)
56 .bind(authorization_code.client_id)
57 .bind(authorization_code.kind)
58 .bind(code)
59 .bind(expires_in)
60 .bind(authorization_code.user_id)
61 .bind(authorization_code.pkce_code_challenge)
62 .bind(authorization_code.pkce_code_challenge_method)
63 .bind(authorization_code.redirect_uri)
64 .bind(authorization_code.meta)
65 .bind(authorization_code.membership)
66 .fetch_one(executor)
67 .await
68 .map_err(StoreError::SQLXError)?;
69
70 Ok(new_authorization_code)
71}
72
73async fn read_code<'a, 'e, E>(
74 executor: E,
75 tenant: &'a TenantId,
76 project: Option<&'a ProjectId>,
77 code: &'a str,
78) -> Result<Option<AuthorizationCode>, OperationOutcomeError>
79where
80 E: PgExecutor<'e>,
81{
82 let mut query_builder: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
83 r"
84 SELECT tenant,
85 kind,
86 code,
87 user_id,
88 project,
89 client_id,
90 pkce_code_challenge,
91 pkce_code_challenge_method,
92 redirect_uri,
93 meta,
94 NOW() > (created_at + expires_in) as is_expired,
95 membership,
96 created_at
97 FROM authorization_code
98 WHERE
99 ",
100 );
101
102 query_builder.push("tenant = ").push_bind(tenant.as_ref());
103 query_builder.push(" AND code = ").push_bind(code);
104
105 if let Some(project) = project {
106 query_builder
107 .push(" AND project = ")
108 .push_bind(project.as_ref());
109 }
110
111 let query = query_builder.build_query_as::<AuthorizationCode>();
112
113 let authorization_code = query
114 .fetch_optional(executor)
115 .await
116 .map_err(StoreError::SQLXError)?;
117
118 Ok(authorization_code)
119}
120
121async fn delete_code<'a, 'e, E>(
122 executor: E,
123 tenant: &'a TenantId,
124 project: Option<&'a ProjectId>,
125 code: &'a str,
126) -> Result<(), OperationOutcomeError>
127where
128 E: PgExecutor<'e>,
129{
130 let mut query_builder: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
131 r"
132 DELETE FROM authorization_code
133 WHERE
134 ",
135 );
136
137 query_builder.push(" tenant = ").push_bind(tenant.as_ref());
138 query_builder.push(" AND code = ").push_bind(code);
139
140 if let Some(project) = project {
141 query_builder
142 .push(" AND project = ")
143 .push_bind(project.as_ref());
144 }
145
146 let query = query_builder.build();
147
148 query
149 .execute(executor)
150 .await
151 .map_err(StoreError::SQLXError)?;
152
153 Ok(())
154}
155
156async fn search_codes<'a, 'e, E>(
157 executor: E,
158 tenant: &'a TenantId,
159 project: Option<&'a ProjectId>,
160 clauses: &'a AuthorizationCodeSearchClaims,
161) -> Result<Vec<AuthorizationCode>, OperationOutcomeError>
162where
163 E: PgExecutor<'e>,
164{
165 let mut query_builder: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
166 r"
167 SELECT tenant,
168 kind,
169 code,
170 user_id,
171 project,
172 client_id,
173 pkce_code_challenge,
174 pkce_code_challenge_method,
175 redirect_uri,
176 meta,
177 NOW() > (created_at + expires_in) as is_expired,
178 membership,
179 created_at
180 FROM authorization_code
181 WHERE
182 ",
183 );
184
185 query_builder.push(" tenant = ").push_bind(tenant.as_ref());
186
187 if let Some(project) = project {
188 query_builder
189 .push(" AND project = ")
190 .push_bind(project.as_ref());
191 }
192
193 if let Some(client_id) = &clauses.client_id {
194 query_builder.push(" AND client_id = ").push_bind(client_id);
195 }
196
197 if let Some(code) = &clauses.code {
198 query_builder.push(" AND code = ").push_bind(code);
199 }
200
201 if let Some(user_id) = &clauses.user_id {
202 query_builder.push(" AND user_id = ").push_bind(user_id);
203 }
204
205 if let Some(kind) = &clauses.kind {
206 query_builder.push(" AND kind = ").push_bind(kind);
207 }
208
209 if let Some(user_agent) = &clauses.user_agent {
210 query_builder
211 .push(" AND meta->>'user_agent' = ")
212 .push_bind(user_agent);
213 }
214
215 if let Some(is_expired) = &clauses.is_expired {
216 query_builder
217 .push(" AND (NOW() > (created_at + expires_in)) = ")
218 .push_bind(is_expired);
219 }
220
221 let query = query_builder.build_query_as::<AuthorizationCode>();
222
223 let authorization_codes = query
224 .fetch_all(executor)
225 .await
226 .map_err(StoreError::SQLXError)?;
227
228 Ok(authorization_codes)
229}
230
231impl<Key: AsRef<str> + Send + Sync>
232 TenantModelAdmin<
233 CreateAuthorizationCode,
234 AuthorizationCode,
235 AuthorizationCodeSearchClaims,
236 AuthorizationCode,
237 Key,
238 > for PGConnection
239{
240 async fn create(
241 &self,
242 tenant: &TenantId,
243 authorization_code: CreateAuthorizationCode,
244 ) -> Result<AuthorizationCode, OperationOutcomeError> {
245 match &self {
246 PGConnection::Pool(pool, _) => {
247 create_code(pool, tenant, None, authorization_code).await
248 }
249 PGConnection::Transaction(tx, _) => {
250 let mut tx = tx.lock().await;
251 create_code(&mut **tx, tenant, None, authorization_code).await
252 }
253 }
254 }
255
256 async fn read(
257 &self,
258 tenant: &TenantId,
259 code: &Key,
260 ) -> Result<Option<AuthorizationCode>, OperationOutcomeError> {
261 match &self {
262 PGConnection::Pool(pool, _) => read_code(pool, tenant, None, code.as_ref()).await,
263 PGConnection::Transaction(tx, _) => {
264 let mut tx = tx.lock().await;
265 read_code(&mut **tx, tenant, None, code.as_ref()).await
266 }
267 }
268 }
269
270 async fn update(
271 &self,
272 _tenant: &TenantId,
273 _model: AuthorizationCode,
274 ) -> Result<AuthorizationCode, OperationOutcomeError> {
275 Err(OperationOutcomeError::fatal(
276 IssueType::exception(),
277 "Update operation for AuthorizationCode is not implemented.".to_string(),
278 ))
279 }
280
281 async fn delete(&self, tenant: &TenantId, code: &Key) -> Result<(), OperationOutcomeError> {
282 match &self {
283 PGConnection::Pool(pool, _) => delete_code(pool, tenant, None, code.as_ref()).await,
284 PGConnection::Transaction(tx, _) => {
285 let mut tx = tx.lock().await;
286 delete_code(&mut **tx, tenant, None, code.as_ref()).await
287 }
288 }
289 }
290
291 async fn search(
292 &self,
293 tenant: &TenantId,
294 clauses: &AuthorizationCodeSearchClaims,
295 ) -> Result<Vec<AuthorizationCode>, OperationOutcomeError> {
296 match &self {
297 PGConnection::Pool(pool, _) => search_codes(pool, tenant, None, clauses).await,
298 PGConnection::Transaction(tx, _) => {
299 let mut tx = tx.lock().await;
300 search_codes(&mut **tx, tenant, None, clauses).await
301 }
302 }
303 }
304}
305
306impl<Key: AsRef<str> + Send + Sync>
307 ProjectModelAdmin<
308 CreateAuthorizationCode,
309 AuthorizationCode,
310 AuthorizationCodeSearchClaims,
311 AuthorizationCode,
312 Key,
313 > for PGConnection
314{
315 async fn create(
316 &self,
317 tenant: &TenantId,
318 project: &ProjectId,
319 authorization_code: CreateAuthorizationCode,
320 ) -> Result<AuthorizationCode, OperationOutcomeError> {
321 match &self {
322 PGConnection::Pool(pool, _) => {
323 create_code(pool, tenant, Some(project), authorization_code).await
324 }
325 PGConnection::Transaction(tx, _) => {
326 let mut tx = tx.lock().await;
327 create_code(&mut **tx, tenant, Some(project), authorization_code).await
328 }
329 }
330 }
331
332 async fn read(
333 &self,
334 tenant: &TenantId,
335 project: &ProjectId,
336 code: &Key,
337 ) -> Result<Option<AuthorizationCode>, OperationOutcomeError> {
338 match &self {
339 PGConnection::Pool(pool, _) => {
340 read_code(pool, tenant, Some(project), code.as_ref()).await
341 }
342 PGConnection::Transaction(tx, _) => {
343 let mut tx = tx.lock().await;
344 read_code(&mut **tx, tenant, Some(project), code.as_ref()).await
345 }
346 }
347 }
348
349 async fn update(
350 &self,
351 _tenant: &TenantId,
352 _project: &ProjectId,
353 _model: AuthorizationCode,
354 ) -> Result<AuthorizationCode, OperationOutcomeError> {
355 Err(OperationOutcomeError::fatal(
356 IssueType::exception(),
357 "Update operation for AuthorizationCode is not implemented.".to_string(),
358 ))
359 }
360
361 async fn delete(
362 &self,
363 tenant: &TenantId,
364 project: &ProjectId,
365 code: &Key,
366 ) -> Result<(), OperationOutcomeError> {
367 match &self {
368 PGConnection::Pool(pool, _) => {
369 delete_code(pool, tenant, Some(project), code.as_ref()).await
370 }
371 PGConnection::Transaction(tx, _) => {
372 let mut tx = tx.lock().await;
373 delete_code(&mut **tx, tenant, Some(project), code.as_ref()).await
374 }
375 }
376 }
377
378 async fn search(
379 &self,
380 tenant: &TenantId,
381 project: &ProjectId,
382 clauses: &AuthorizationCodeSearchClaims,
383 ) -> Result<Vec<AuthorizationCode>, OperationOutcomeError> {
384 match &self {
385 PGConnection::Pool(pool, _) => search_codes(pool, tenant, Some(project), clauses).await,
386 PGConnection::Transaction(tx, _) => {
387 let mut tx = tx.lock().await;
388 search_codes(&mut **tx, tenant, Some(project), clauses).await
389 }
390 }
391 }
392}