headless_lms_models/
oauth_dpop_proofs.rs1use crate::library::oauth::Digest;
2use crate::prelude::*;
3use chrono::{DateTime, TimeZone, Utc};
4use sqlx::{FromRow, PgConnection};
5
6#[derive(Debug, Serialize, Deserialize, FromRow)]
19pub struct OAuthDpopProof {
20 pub jti_hash: Digest, pub seen_at: DateTime<Utc>, pub client_id: Option<String>, pub jkt: Option<String>, pub htm: Option<String>, pub htu: Option<String>, pub iat: Option<DateTime<Utc>>, }
28
29impl OAuthDpopProof {
30 pub async fn insert_once(
35 conn: &mut PgConnection,
36 jti_hash: Digest,
37 client_id: Option<&str>,
38 jkt: Option<&str>,
39 htm: Option<&str>,
40 htu: Option<&str>,
41 iat_epoch: Option<i64>,
42 ) -> ModelResult<bool> {
43 let mut tx = conn.begin().await?;
44
45 let iat_ts: Option<DateTime<Utc>> =
46 iat_epoch.and_then(|s| Utc.timestamp_opt(s, 0).single());
47
48 let rows = sqlx::query!(
49 r#"
50 INSERT INTO oauth_dpop_proofs (jti_hash, client_id, jkt, htm, htu, iat)
51 VALUES ($1, $2, $3, $4, $5, $6)
52 ON CONFLICT DO NOTHING
53 "#,
54 jti_hash.as_bytes(),
55 client_id,
56 jkt,
57 htm,
58 htu,
59 iat_ts,
60 )
61 .execute(&mut *tx)
62 .await?
63 .rows_affected();
64
65 tx.commit().await?;
66 Ok(rows == 1)
67 }
68
69 pub async fn find_by_jti_hash(
71 conn: &mut PgConnection,
72 jti_hash: Digest,
73 ) -> ModelResult<Option<OAuthDpopProof>> {
74 let mut tx = conn.begin().await?;
75 let row = sqlx::query_as!(
76 OAuthDpopProof,
77 r#"
78 SELECT
79 jti_hash AS "jti_hash: _",
80 seen_at AS "seen_at: _",
81 client_id AS "client_id?",
82 jkt AS "jkt?",
83 htm AS "htm?",
84 htu AS "htu?",
85 iat AS "iat?"
86 FROM oauth_dpop_proofs
87 WHERE jti_hash = $1
88 "#,
89 jti_hash.as_bytes(),
90 )
91 .fetch_optional(&mut *tx)
92 .await?;
93 tx.commit().await?;
94 Ok(row)
95 }
96
97 pub async fn prune_older_than(conn: &mut PgConnection, keep_seconds: i64) -> ModelResult<u64> {
100 if keep_seconds < 0 {
101 return Err(ModelError::new(
102 ModelErrorType::Generic,
103 "keep_seconds must be >= 0",
104 None,
105 ));
106 }
107 let mut tx = conn.begin().await?;
108 let res = sqlx::query!(
109 r#"
110 DELETE FROM oauth_dpop_proofs
111 WHERE seen_at < now() - ($1::bigint * interval '1 second')
112 "#,
113 keep_seconds,
114 )
115 .execute(&mut *tx)
116 .await?;
117 tx.commit().await?;
118 Ok(res.rows_affected())
119 }
120}