Skip to main content

shared/models/
user_oauth_link.rs

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