headless_lms_models/
open_university_product_access_tokens.rs1use secrecy::ExposeSecret;
2
3use crate::prelude::*;
4
5const OPEN_UNIVERSITY_ENROLMENT_BASE_URL: &str =
8 "https://www.avoin.helsinki.fi/palvelut/esittely.aspx?s=";
9
10pub fn enrolment_url(token: &OpenUniversityProductAccessToken) -> Option<String> {
14 let access_token = token.access_token.as_ref()?;
15 Some(format!(
16 "{OPEN_UNIVERSITY_ENROLMENT_BASE_URL}{}",
17 access_token.expose_secret()
18 ))
19}
20
21pub async fn enrolment_url_for_product(
28 conn: &mut PgConnection,
29 open_university_product_id: Option<&str>,
30) -> ModelResult<Option<String>> {
31 let Some(product_id) = open_university_product_id else {
32 return Ok(None);
33 };
34 Ok(get_by_product_id(conn, product_id)
35 .await?
36 .as_ref()
37 .and_then(enrolment_url))
38}
39
40#[derive(Debug, Clone)]
41pub struct OpenUniversityProductAccessToken {
42 pub id: Uuid,
43 pub created_at: DateTime<Utc>,
44 pub updated_at: DateTime<Utc>,
45 pub deleted_at: Option<DateTime<Utc>>,
46 pub open_university_product_id: String,
47 pub access_token: Option<DbSecret>,
49 pub state: Option<String>,
50 pub document_state: Option<String>,
51 pub suotar_token_id: Option<String>,
52 pub last_refreshed_at: Option<DateTime<Utc>>,
53 pub last_refresh_failed_at: Option<DateTime<Utc>>,
54 pub last_refresh_error: Option<String>,
55 pub consecutive_failures: i32,
56}
57
58#[derive(Debug, Clone)]
59pub struct NewOpenUniversityProductAccessToken {
60 pub open_university_product_id: String,
61 pub access_token: DbSecret,
62 pub state: String,
63 pub document_state: String,
64 pub suotar_token_id: Option<String>,
65}
66
67pub async fn upsert(
69 conn: &mut PgConnection,
70 new: &NewOpenUniversityProductAccessToken,
71) -> ModelResult<Uuid> {
72 let res = sqlx::query!(
73 r#"
74INSERT INTO open_university_product_access_tokens (
75 open_university_product_id,
76 access_token,
77 state,
78 document_state,
79 suotar_token_id,
80 last_refreshed_at
81 )
82VALUES ($1, $2, $3, $4, $5, now()) ON CONFLICT (open_university_product_id, deleted_at) DO
83UPDATE
84SET access_token = $2,
85 state = $3,
86 document_state = $4,
87 suotar_token_id = $5,
88 last_refreshed_at = now(),
89 last_refresh_failed_at = NULL,
90 last_refresh_error = NULL,
91 consecutive_failures = 0
92RETURNING id
93 "#,
94 new.open_university_product_id,
95 new.access_token.expose_secret(),
96 new.state,
97 new.document_state,
98 new.suotar_token_id,
99 )
100 .fetch_one(conn)
101 .await?;
102 Ok(res.id)
103}
104
105pub async fn get_by_product_id(
106 conn: &mut PgConnection,
107 open_university_product_id: &str,
108) -> ModelResult<Option<OpenUniversityProductAccessToken>> {
109 let res = sqlx::query_as!(
110 OpenUniversityProductAccessToken,
111 r#"
112SELECT *
113FROM open_university_product_access_tokens
114WHERE open_university_product_id = $1
115 AND deleted_at IS NULL
116 "#,
117 open_university_product_id
118 )
119 .fetch_optional(conn)
120 .await?;
121 Ok(res)
122}
123
124pub async fn get_all(
125 conn: &mut PgConnection,
126) -> ModelResult<Vec<OpenUniversityProductAccessToken>> {
127 let res = sqlx::query_as!(
128 OpenUniversityProductAccessToken,
129 r#"
130SELECT *
131FROM open_university_product_access_tokens
132WHERE deleted_at IS NULL
133ORDER BY open_university_product_id
134 "#,
135 )
136 .fetch_all(conn)
137 .await?;
138 Ok(res)
139}
140
141pub async fn record_refresh_failure(
147 conn: &mut PgConnection,
148 open_university_product_id: &str,
149 error: &str,
150) -> ModelResult<()> {
151 sqlx::query!(
152 r#"
153INSERT INTO open_university_product_access_tokens (
154 open_university_product_id,
155 last_refresh_failed_at,
156 last_refresh_error,
157 consecutive_failures
158 )
159VALUES ($1, now(), $2, 1) ON CONFLICT (open_university_product_id, deleted_at) DO
160UPDATE
161SET last_refresh_failed_at = now(),
162 last_refresh_error = $2,
163 consecutive_failures = open_university_product_access_tokens.consecutive_failures + 1
164 "#,
165 open_university_product_id,
166 error,
167 )
168 .execute(conn)
169 .await?;
170 Ok(())
171}
172
173pub async fn soft_delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
174 sqlx::query!(
175 r#"
176UPDATE open_university_product_access_tokens
177SET deleted_at = now()
178WHERE id = $1
179 AND deleted_at IS NULL
180 "#,
181 id
182 )
183 .execute(conn)
184 .await?;
185 Ok(())
186}