Skip to main content

headless_lms_models/
open_university_product_access_tokens.rs

1use secrecy::ExposeSecret;
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone)]
6pub struct OpenUniversityProductAccessToken {
7    pub id: Uuid,
8    pub created_at: DateTime<Utc>,
9    pub updated_at: DateTime<Utc>,
10    pub deleted_at: Option<DateTime<Utc>>,
11    pub open_university_product_id: String,
12    pub access_token: DbSecret,
13    pub state: String,
14    pub document_state: String,
15    pub suotar_token_id: Option<String>,
16    pub last_refreshed_at: DateTime<Utc>,
17    pub last_refresh_failed_at: Option<DateTime<Utc>>,
18    pub last_refresh_error: Option<String>,
19    pub consecutive_failures: i32,
20}
21
22#[derive(Debug, Clone)]
23pub struct NewOpenUniversityProductAccessToken {
24    pub open_university_product_id: String,
25    pub access_token: DbSecret,
26    pub state: String,
27    pub document_state: String,
28    pub suotar_token_id: Option<String>,
29}
30
31/// Stores a freshly fetched token, replacing whatever we held for the product.
32pub async fn upsert(
33    conn: &mut PgConnection,
34    new: &NewOpenUniversityProductAccessToken,
35) -> ModelResult<Uuid> {
36    let res = sqlx::query!(
37        r#"
38INSERT INTO open_university_product_access_tokens (
39    open_university_product_id,
40    access_token,
41    state,
42    document_state,
43    suotar_token_id
44  )
45VALUES ($1, $2, $3, $4, $5) ON CONFLICT (open_university_product_id, deleted_at) DO
46UPDATE
47SET access_token = $2,
48  state = $3,
49  document_state = $4,
50  suotar_token_id = $5,
51  last_refreshed_at = now(),
52  last_refresh_failed_at = NULL,
53  last_refresh_error = NULL,
54  consecutive_failures = 0
55RETURNING id
56        "#,
57        new.open_university_product_id,
58        new.access_token.expose_secret(),
59        new.state,
60        new.document_state,
61        new.suotar_token_id,
62    )
63    .fetch_one(conn)
64    .await?;
65    Ok(res.id)
66}
67
68pub async fn get_by_product_id(
69    conn: &mut PgConnection,
70    open_university_product_id: &str,
71) -> ModelResult<Option<OpenUniversityProductAccessToken>> {
72    let res = sqlx::query_as!(
73        OpenUniversityProductAccessToken,
74        r#"
75SELECT *
76FROM open_university_product_access_tokens
77WHERE open_university_product_id = $1
78  AND deleted_at IS NULL
79        "#,
80        open_university_product_id
81    )
82    .fetch_optional(conn)
83    .await?;
84    Ok(res)
85}
86
87pub async fn get_all(
88    conn: &mut PgConnection,
89) -> ModelResult<Vec<OpenUniversityProductAccessToken>> {
90    let res = sqlx::query_as!(
91        OpenUniversityProductAccessToken,
92        r#"
93SELECT *
94FROM open_university_product_access_tokens
95WHERE deleted_at IS NULL
96ORDER BY open_university_product_id
97        "#,
98    )
99    .fetch_all(conn)
100    .await?;
101    Ok(res)
102}
103
104/// Records a failed refresh without touching the token: a stale token still beats none.
105pub async fn record_refresh_failure(
106    conn: &mut PgConnection,
107    open_university_product_id: &str,
108    error: &str,
109) -> ModelResult<()> {
110    sqlx::query!(
111        r#"
112UPDATE open_university_product_access_tokens
113SET last_refresh_failed_at = now(),
114  last_refresh_error = $2,
115  consecutive_failures = consecutive_failures + 1
116WHERE open_university_product_id = $1
117  AND deleted_at IS NULL
118        "#,
119        open_university_product_id,
120        error,
121    )
122    .execute(conn)
123    .await?;
124    Ok(())
125}
126
127pub async fn soft_delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
128    sqlx::query!(
129        r#"
130UPDATE open_university_product_access_tokens
131SET deleted_at = now()
132WHERE id = $1
133  AND deleted_at IS NULL
134        "#,
135        id
136    )
137    .execute(conn)
138    .await?;
139    Ok(())
140}