shared/models/
user_oauth_link.rs

1use crate::{models::InsertQueryBuilder, prelude::*, storage::StorageUrlRetriever};
2use garde::Validate;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::{
6    collections::BTreeMap,
7    sync::{Arc, LazyLock},
8};
9use utoipa::ToSchema;
10
11#[derive(Serialize, Deserialize)]
12pub struct UserOAuthLink {
13    pub uuid: uuid::Uuid,
14    pub user: Fetchable<super::user::User>,
15    pub oauth_provider: Fetchable<super::oauth_provider::OAuthProvider>,
16
17    pub identifier: compact_str::CompactString,
18
19    pub last_used: Option<chrono::NaiveDateTime>,
20    pub created: chrono::NaiveDateTime,
21}
22
23impl BaseModel for UserOAuthLink {
24    const NAME: &'static str = "user_oauth_link";
25
26    #[inline]
27    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
28        let prefix = prefix.unwrap_or_default();
29
30        BTreeMap::from([
31            (
32                "user_oauth_links.uuid",
33                compact_str::format_compact!("{prefix}uuid"),
34            ),
35            (
36                "user_oauth_links.user_uuid",
37                compact_str::format_compact!("{prefix}user_uuid"),
38            ),
39            (
40                "user_oauth_links.oauth_provider_uuid",
41                compact_str::format_compact!("{prefix}oauth_provider_uuid"),
42            ),
43            (
44                "user_oauth_links.identifier",
45                compact_str::format_compact!("{prefix}identifier"),
46            ),
47            (
48                "user_oauth_links.last_used",
49                compact_str::format_compact!("{prefix}last_used"),
50            ),
51            (
52                "user_oauth_links.created",
53                compact_str::format_compact!("{prefix}created"),
54            ),
55        ])
56    }
57
58    #[inline]
59    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
60        let prefix = prefix.unwrap_or_default();
61
62        Ok(Self {
63            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
64            user: super::user::User::get_fetchable(
65                row.try_get(compact_str::format_compact!("{prefix}user_uuid").as_str())?,
66            ),
67            oauth_provider: super::oauth_provider::OAuthProvider::get_fetchable(
68                row.try_get(compact_str::format_compact!("{prefix}oauth_provider_uuid").as_str())?,
69            ),
70            identifier: row.try_get(compact_str::format_compact!("{prefix}identifier").as_str())?,
71            last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
72            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
73        })
74    }
75}
76
77impl UserOAuthLink {
78    pub async fn by_oauth_provider_uuid_identifier(
79        database: &crate::database::Database,
80        oauth_provider_uuid: uuid::Uuid,
81        identifier: &str,
82    ) -> Result<Option<Self>, crate::database::DatabaseError> {
83        let row = sqlx::query(&format!(
84            r#"
85            SELECT {}
86            FROM user_oauth_links
87            WHERE user_oauth_links.oauth_provider_uuid = $1 AND user_oauth_links.identifier = $2
88            "#,
89            Self::columns_sql(None)
90        ))
91        .bind(oauth_provider_uuid)
92        .bind(identifier)
93        .fetch_optional(database.read())
94        .await?;
95
96        row.try_map(|row| Self::map(None, &row))
97    }
98
99    pub async fn by_oauth_provider_uuid_uuid(
100        database: &crate::database::Database,
101        oauth_provider_uuid: uuid::Uuid,
102        uuid: uuid::Uuid,
103    ) -> Result<Option<Self>, crate::database::DatabaseError> {
104        let row = sqlx::query(&format!(
105            r#"
106            SELECT {}
107            FROM user_oauth_links
108            WHERE user_oauth_links.oauth_provider_uuid = $1 AND user_oauth_links.uuid = $2
109            "#,
110            Self::columns_sql(None)
111        ))
112        .bind(oauth_provider_uuid)
113        .bind(uuid)
114        .fetch_optional(database.read())
115        .await?;
116
117        row.try_map(|row| Self::map(None, &row))
118    }
119
120    pub async fn by_user_uuid_uuid(
121        database: &crate::database::Database,
122        user_uuid: uuid::Uuid,
123        uuid: uuid::Uuid,
124    ) -> Result<Option<Self>, crate::database::DatabaseError> {
125        let row = sqlx::query(&format!(
126            r#"
127            SELECT {}
128            FROM user_oauth_links
129            WHERE user_oauth_links.user_uuid = $1 AND user_oauth_links.uuid = $2
130            "#,
131            Self::columns_sql(None)
132        ))
133        .bind(user_uuid)
134        .bind(uuid)
135        .fetch_optional(database.read())
136        .await?;
137
138        row.try_map(|row| Self::map(None, &row))
139    }
140
141    pub async fn by_user_uuid_with_pagination(
142        database: &crate::database::Database,
143        user_uuid: uuid::Uuid,
144        page: i64,
145        per_page: i64,
146        search: Option<&str>,
147    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
148        let offset = (page - 1) * per_page;
149
150        let rows = sqlx::query(&format!(
151            r#"
152            SELECT {}, COUNT(*) OVER() AS total_count
153            FROM user_oauth_links
154            WHERE user_oauth_links.user_uuid = $1 AND ($2 IS NULL OR user_oauth_links.identifier ILIKE '%' || $2 || '%')
155            ORDER BY user_oauth_links.created
156            LIMIT $3 OFFSET $4
157            "#,
158            Self::columns_sql(None)
159        ))
160        .bind(user_uuid)
161        .bind(search)
162        .bind(per_page)
163        .bind(offset)
164        .fetch_all(database.read())
165        .await?;
166
167        Ok(super::Pagination {
168            total: rows
169                .first()
170                .map_or(Ok(0), |row| row.try_get("total_count"))?,
171            per_page,
172            page,
173            data: rows
174                .into_iter()
175                .map(|row| Self::map(None, &row))
176                .try_collect_vec()?,
177        })
178    }
179
180    pub async fn filtered_by_user_uuid_with_pagination(
181        database: &crate::database::Database,
182        user_uuid: uuid::Uuid,
183        page: i64,
184        per_page: i64,
185    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
186        let offset = (page - 1) * per_page;
187
188        let rows = sqlx::query(&format!(
189            r#"
190            SELECT {}, COUNT(*) OVER() AS total_count
191            FROM user_oauth_links
192            INNER JOIN oauth_providers ON user_oauth_links.oauth_provider_uuid = oauth_providers.uuid
193            WHERE user_oauth_links.user_uuid = $1 AND oauth_providers.link_viewable = true
194            ORDER BY user_oauth_links.created
195            LIMIT $2 OFFSET $3
196            "#,
197            Self::columns_sql(None)
198        ))
199        .bind(user_uuid)
200        .bind(per_page)
201        .bind(offset)
202        .fetch_all(database.read())
203        .await?;
204
205        Ok(super::Pagination {
206            total: rows
207                .first()
208                .map_or(Ok(0), |row| row.try_get("total_count"))?,
209            per_page,
210            page,
211            data: rows
212                .into_iter()
213                .map(|row| Self::map(None, &row))
214                .try_collect_vec()?,
215        })
216    }
217
218    pub async fn by_oauth_provider_uuid_with_pagination(
219        database: &crate::database::Database,
220        oauth_provider_uuid: uuid::Uuid,
221        page: i64,
222        per_page: i64,
223        search: Option<&str>,
224    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
225        let offset = (page - 1) * per_page;
226
227        let rows = sqlx::query(&format!(
228            r#"
229            SELECT {}, COUNT(*) OVER() AS total_count
230            FROM user_oauth_links
231            WHERE user_oauth_links.oauth_provider_uuid = $1 AND ($2 IS NULL OR user_oauth_links.identifier ILIKE '%' || $2 || '%')
232            ORDER BY user_oauth_links.created
233            LIMIT $3 OFFSET $4
234            "#,
235            Self::columns_sql(None)
236        ))
237        .bind(oauth_provider_uuid)
238        .bind(search)
239        .bind(per_page)
240        .bind(offset)
241        .fetch_all(database.read())
242        .await?;
243
244        Ok(super::Pagination {
245            total: rows
246                .first()
247                .map_or(Ok(0), |row| row.try_get("total_count"))?,
248            per_page,
249            page,
250            data: rows
251                .into_iter()
252                .map(|row| Self::map(None, &row))
253                .try_collect_vec()?,
254        })
255    }
256
257    #[inline]
258    pub async fn into_admin_api_object(
259        self,
260        database: &crate::database::Database,
261        storage_url_retriever: &StorageUrlRetriever<'_>,
262    ) -> Result<AdminApiUserOAuthLink, anyhow::Error> {
263        Ok(AdminApiUserOAuthLink {
264            uuid: self.uuid,
265            user: self
266                .user
267                .fetch_cached(database)
268                .await?
269                .into_api_full_object(storage_url_retriever),
270            identifier: self.identifier,
271            last_used: self.last_used.map(|dt| dt.and_utc()),
272            created: self.created.and_utc(),
273        })
274    }
275
276    #[inline]
277    pub async fn into_api_object(
278        self,
279        database: &crate::database::Database,
280    ) -> Result<ApiUserOAuthLink, anyhow::Error> {
281        Ok(ApiUserOAuthLink {
282            uuid: self.uuid,
283            oauth_provider: self
284                .oauth_provider
285                .fetch_cached(database)
286                .await?
287                .into_api_object(),
288            identifier: self.identifier,
289            last_used: self.last_used.map(|dt| dt.and_utc()),
290            created: self.created.and_utc(),
291        })
292    }
293}
294
295#[derive(ToSchema, Deserialize, Validate)]
296pub struct CreateUserOAuthLinkOptions {
297    #[garde(skip)]
298    pub user_uuid: uuid::Uuid,
299    #[garde(skip)]
300    pub oauth_provider_uuid: uuid::Uuid,
301
302    #[garde(length(chars, min = 1, max = 255))]
303    #[schema(min_length = 1, max_length = 255)]
304    pub identifier: compact_str::CompactString,
305}
306
307#[async_trait::async_trait]
308impl CreatableModel for UserOAuthLink {
309    type CreateOptions<'a> = CreateUserOAuthLinkOptions;
310    type CreateResult = Self;
311
312    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
313        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserOAuthLink>> =
314            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
315        &CREATE_LISTENERS
316    }
317
318    async fn create(
319        state: &crate::State,
320        mut options: Self::CreateOptions<'_>,
321    ) -> Result<Self, crate::database::DatabaseError> {
322        options.validate()?;
323
324        super::oauth_provider::OAuthProvider::by_uuid_optional_cached(
325            &state.database,
326            options.oauth_provider_uuid,
327        )
328        .await?
329        .ok_or(crate::database::InvalidRelationError("oauth_provider"))?;
330
331        let mut transaction = state.database.write().begin().await?;
332
333        let mut query_builder = InsertQueryBuilder::new("user_oauth_links");
334
335        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
336            .await?;
337
338        query_builder
339            .set("user_uuid", options.user_uuid)
340            .set("oauth_provider_uuid", options.oauth_provider_uuid)
341            .set("identifier", &options.identifier);
342
343        let row = query_builder
344            .returning(&Self::columns_sql(None))
345            .fetch_one(&mut *transaction)
346            .await?;
347
348        let oauth_link = Self::map(None, &row)?;
349
350        transaction.commit().await?;
351
352        Ok(oauth_link)
353    }
354}
355
356#[async_trait::async_trait]
357impl DeletableModel for UserOAuthLink {
358    type DeleteOptions = ();
359
360    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
361        static DELETE_LISTENERS: LazyLock<DeleteListenerList<UserOAuthLink>> =
362            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
363
364        &DELETE_LISTENERS
365    }
366
367    async fn delete(
368        &self,
369        state: &crate::State,
370        options: Self::DeleteOptions,
371    ) -> Result<(), anyhow::Error> {
372        let mut transaction = state.database.write().begin().await?;
373
374        self.run_delete_handlers(&options, state, &mut transaction)
375            .await?;
376
377        sqlx::query(
378            r#"
379            DELETE FROM user_oauth_links
380            WHERE user_oauth_links.uuid = $1
381            "#,
382        )
383        .bind(self.uuid)
384        .execute(&mut *transaction)
385        .await?;
386
387        transaction.commit().await?;
388
389        Ok(())
390    }
391}
392
393#[derive(ToSchema, Serialize)]
394#[schema(title = "AdminUserOAuthLink")]
395pub struct AdminApiUserOAuthLink {
396    pub uuid: uuid::Uuid,
397    pub user: super::user::ApiFullUser,
398
399    pub identifier: compact_str::CompactString,
400
401    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
402    pub created: chrono::DateTime<chrono::Utc>,
403}
404
405#[derive(ToSchema, Serialize)]
406#[schema(title = "UserOAuthLink")]
407pub struct ApiUserOAuthLink {
408    pub uuid: uuid::Uuid,
409    pub oauth_provider: super::oauth_provider::ApiOAuthProvider,
410
411    pub identifier: compact_str::CompactString,
412
413    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
414    pub created: chrono::DateTime<chrono::Utc>,
415}