headless_lms_models/
oauth_user_client_scopes.rs1use crate::library::oauth::Digest;
2use crate::prelude::*;
3use chrono::{DateTime, Utc};
4use sqlx::FromRow;
5use utoipa::ToSchema;
6use uuid::Uuid;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, FromRow)]
10pub struct OAuthUserClientScopes {
11 pub id: Uuid,
12 pub user_id: Uuid,
13 pub client_id: Uuid,
14 pub scopes: Vec<String>,
15 pub granted_at: DateTime<Utc>,
16}
17
18impl OAuthUserClientScopes {
19 pub async fn insert(
20 conn: &mut PgConnection,
21 user_id: Uuid,
22 client_id: Uuid,
23 scopes: &[String],
24 ) -> ModelResult<()> {
25 let mut tx = conn.begin().await?;
26 sqlx::query!(
27 r#"
28 INSERT INTO oauth_user_client_scopes
29 (user_id, client_id, scopes)
30 VALUES ($1, $2, $3)
31 ON CONFLICT (user_id, client_id) DO UPDATE
32 SET scopes = EXCLUDED.scopes,
33 granted_at = NOW()
34 "#,
35 user_id,
36 client_id,
37 scopes
38 )
39 .execute(&mut *tx)
40 .await?;
41 tx.commit().await?;
42 Ok(())
43 }
44
45 pub async fn find_grant(
47 conn: &mut PgConnection,
48 user_id: Uuid,
49 client_id: Uuid,
50 ) -> ModelResult<Option<OAuthUserClientScopes>> {
51 let grant = sqlx::query_as!(
52 OAuthUserClientScopes,
53 r#"
54SELECT id,
55 user_id,
56 client_id,
57 scopes,
58 granted_at
59FROM oauth_user_client_scopes
60WHERE user_id = $1
61 AND client_id = $2
62 "#,
63 user_id,
64 client_id
65 )
66 .fetch_optional(conn)
67 .await?;
68 Ok(grant)
69 }
70
71 pub async fn find_scopes(
73 conn: &mut PgConnection,
74 user_id: Uuid,
75 client_id: Uuid,
76 ) -> ModelResult<Vec<String>> {
77 Ok(Self::find_grant(conn, user_id, client_id)
78 .await?
79 .map(|grant| grant.scopes)
80 .unwrap_or_default())
81 }
82
83 pub async fn revoke_user_client_everything(
88 conn: &mut PgConnection,
89 user_id: Uuid,
90 client_id: Uuid,
91 ) -> ModelResult<Vec<Digest>> {
92 let mut tx = conn.begin().await?;
93
94 sqlx::query!(
95 r#"DELETE FROM oauth_user_client_scopes WHERE user_id = $1 AND client_id = $2"#,
96 user_id,
97 client_id
98 )
99 .execute(&mut *tx)
100 .await?;
101
102 let deleted_digests =
103 crate::oauth_refresh_tokens::OAuthRefreshTokens::revoke_grant_in_transaction(
104 &mut tx, user_id, client_id,
105 )
106 .await?;
107
108 tx.commit().await?;
109 Ok(deleted_digests)
110 }
111
112 pub async fn get_grants_for_user(
114 conn: &mut PgConnection,
115 user_id: Uuid,
116 ) -> ModelResult<Vec<OAuthGrant>> {
117 let res = sqlx::query_as!(
118 OAuthGrant,
119 r#"
120SELECT ucs.id,
121 c.client_name,
122 c.client_uri,
123 (c.metadata_document_fetched_at IS NOT NULL) AS "is_metadata_document_client!",
124 ucs.scopes,
125 ucs.granted_at,
126 ucs.last_used_at
127FROM oauth_user_client_scopes ucs
128 JOIN oauth_clients c ON c.id = ucs.client_id
129WHERE ucs.user_id = $1
130 AND c.deleted_at IS NULL
131ORDER BY ucs.last_used_at DESC NULLS LAST,
132 ucs.granted_at DESC
133 "#,
134 user_id
135 )
136 .fetch_all(conn)
137 .await?;
138 Ok(res)
139 }
140
141 pub async fn mark_grant_used(
144 conn: &mut PgConnection,
145 grant_id: Uuid,
146 last_used_at: Option<DateTime<Utc>>,
147 ) -> ModelResult<()> {
148 if last_used_at.is_some_and(|used_at| Utc::now() - used_at < chrono::TimeDelta::minutes(1))
149 {
150 return Ok(());
151 }
152 sqlx::query!(
153 r#"
154UPDATE oauth_user_client_scopes
155SET last_used_at = now()
156WHERE id = $1
157 AND (
158 last_used_at IS NULL
159 OR last_used_at < now() - INTERVAL '1 minute'
160 )
161 "#,
162 grant_id
163 )
164 .execute(conn)
165 .await?;
166 Ok(())
167 }
168
169 pub async fn revoke_grant_by_id(
172 conn: &mut PgConnection,
173 user_id: Uuid,
174 grant_id: Uuid,
175 ) -> ModelResult<Vec<Digest>> {
176 let client_id = sqlx::query_scalar!(
177 "SELECT client_id FROM oauth_user_client_scopes WHERE id = $1 AND user_id = $2",
178 grant_id,
179 user_id
180 )
181 .fetch_one(&mut *conn)
182 .await?;
183 Self::revoke_user_client_everything(conn, user_id, client_id).await
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
189pub struct OAuthGrant {
190 pub id: Uuid,
191 pub client_name: String,
192 pub client_uri: Option<String>,
193 pub is_metadata_document_client: bool,
195 pub scopes: Vec<String>,
196 pub granted_at: DateTime<Utc>,
197 pub last_used_at: Option<DateTime<Utc>>,
199}