Skip to main content

headless_lms_models/
oauth_user_client_scopes.rs

1use 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)]
9pub struct OAuthUserClientScopes {
10    pub user_id: Uuid,
11    pub client_id: Uuid,
12    pub scopes: Vec<String>,
13    pub granted_at: DateTime<Utc>,
14}
15
16#[derive(Debug, Clone, PartialEq, FromRow, Serialize, Deserialize, ToSchema)]
17pub struct AuthorizedClientInfo {
18    pub client_id: Uuid,     // oauth_clients.id
19    pub client_name: String, // oauth_clients.client_id (display/name)
20    pub scopes: Vec<String>,
21}
22
23impl OAuthUserClientScopes {
24    pub async fn insert(
25        conn: &mut PgConnection,
26        user_id: Uuid,
27        client_id: Uuid,
28        scopes: &[String],
29    ) -> ModelResult<()> {
30        let mut tx = conn.begin().await?;
31        sqlx::query!(
32            r#"
33                INSERT INTO oauth_user_client_scopes
34                (user_id, client_id, scopes)
35                VALUES ($1, $2, $3)
36                                ON CONFLICT (user_id, client_id) DO UPDATE
37                  SET scopes = EXCLUDED.scopes,
38                      granted_at = NOW()
39            "#,
40            user_id,
41            client_id,
42            scopes
43        )
44        .execute(&mut *tx)
45        .await?;
46        tx.commit().await?;
47        Ok(())
48    }
49
50    pub async fn find_scopes(
51        conn: &mut PgConnection,
52        user_id: Uuid,
53        client_id: Uuid,
54    ) -> ModelResult<Vec<String>> {
55        let mut tx = conn.begin().await?;
56        let rows = sqlx::query!(
57            r#"
58            SELECT scopes
59            FROM oauth_user_client_scopes
60            WHERE user_id = $1 AND client_id = $2
61        "#,
62            user_id,
63            client_id
64        )
65        .fetch_all(&mut *tx)
66        .await?;
67        tx.commit().await?;
68        Ok(rows.into_iter().flat_map(|r| r.scopes).collect())
69    }
70
71    pub async fn find_distinct_clients(
72        conn: &mut PgConnection,
73        user_id: Uuid,
74    ) -> ModelResult<Vec<Uuid>> {
75        let mut tx = conn.begin().await?;
76        let rows = sqlx::query!(
77            r#"
78            SELECT DISTINCT client_id
79            FROM oauth_user_client_scopes
80            WHERE user_id = $1
81            "#,
82            user_id
83        )
84        .fetch_all(&mut *tx)
85        .await?;
86        tx.commit().await?;
87        Ok(rows.into_iter().map(|r| r.client_id).collect())
88    }
89
90    pub async fn delete_all_for_user_client(
91        conn: &mut PgConnection,
92        user_id: Uuid,
93        client_id: Uuid,
94    ) -> ModelResult<()> {
95        let mut tx = conn.begin().await?;
96        sqlx::query!(
97            r#"DELETE FROM oauth_user_client_scopes WHERE user_id = $1 AND client_id = $2"#,
98            user_id,
99            client_id
100        )
101        .execute(&mut *tx)
102        .await?;
103        tx.commit().await?;
104        Ok(())
105    }
106
107    pub async fn list_authorized_clients_for_user(
108        conn: &mut PgConnection,
109        user_id: Uuid,
110    ) -> ModelResult<Vec<AuthorizedClientInfo>> {
111        let mut tx = conn.begin().await?;
112        // Aggregate scopes and join to clients to fetch the human-readable name (client.client_id)
113
114        let rows = sqlx::query_as!(
115            AuthorizedClientInfo,
116            r#"
117            SELECT
118              c.id        AS client_id,
119              c.client_id AS client_name,
120              COALESCE(
121                array_agg(DISTINCT s.scope ORDER BY s.scope) FILTER (WHERE s.scope IS NOT NULL),
122                '{}'::text[]
123              ) AS "scopes!"
124            FROM oauth_user_client_scopes ucs
125            JOIN oauth_clients c ON c.id = ucs.client_id
126            LEFT JOIN LATERAL unnest(ucs.scopes) AS s(scope) ON TRUE
127            WHERE ucs.user_id = $1
128            GROUP BY c.id, c.client_id
129            ORDER BY c.client_id
130            "#,
131            user_id
132        )
133        .fetch_all(&mut *tx)
134        .await?;
135
136        tx.commit().await?;
137        Ok(rows)
138    }
139
140    /// One-shot revoke: remove all scopes and tokens for a (user, client) pair atomically.
141    ///
142    /// Returns the digests of the deleted access tokens so the caller can evict their
143    /// cached user mappings; without that they keep authenticating until the cache TTL.
144    pub async fn revoke_user_client_everything(
145        conn: &mut PgConnection,
146        user_id: Uuid,
147        client_id: Uuid,
148    ) -> ModelResult<Vec<Digest>> {
149        let mut tx = conn.begin().await?;
150
151        sqlx::query!(
152            r#"DELETE FROM oauth_user_client_scopes WHERE user_id = $1 AND client_id = $2"#,
153            user_id,
154            client_id
155        )
156        .execute(&mut *tx)
157        .await?;
158
159        let deleted_digests =
160            crate::oauth_refresh_tokens::OAuthRefreshTokens::revoke_grant_in_transaction(
161                &mut tx, user_id, client_id,
162            )
163            .await?;
164
165        tx.commit().await?;
166        Ok(deleted_digests)
167    }
168}