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(|| parking_lot::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(sqlx::AssertSqlSafe(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(sqlx::AssertSqlSafe(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(sqlx::AssertSqlSafe(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 exists_usable_by_user_uuid(
156        database: &crate::database::Database,
157        user_uuid: uuid::Uuid,
158    ) -> Result<bool, sqlx::Error> {
159        sqlx::query_scalar!(
160            r#"
161            SELECT EXISTS (
162                SELECT 1
163                FROM user_oauth_links
164                JOIN oauth_providers ON oauth_providers.uuid = user_oauth_links.oauth_provider_uuid
165                WHERE user_oauth_links.user_uuid = $1 AND oauth_providers.enabled = true
166            ) AS "exists!"
167            "#,
168            user_uuid
169        )
170        .fetch_one(database.read())
171        .await
172    }
173
174    pub async fn by_user_uuid_with_pagination(
175        database: &crate::database::Database,
176        user_uuid: uuid::Uuid,
177        page: i64,
178        per_page: i64,
179        search: Option<&str>,
180    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
181        super::PaginatedQueryBuilder::new(
182            Self::columns_sql(None),
183            format!(
184                r#"
185                FROM user_oauth_links
186                WHERE user_oauth_links.user_uuid = $1 AND {search}
187                "#,
188                search = super::search_sql(
189                    2,
190                    &["user_oauth_links.identifier"],
191                    &["user_oauth_links.uuid"]
192                ),
193            ),
194            "user_oauth_links.created, user_oauth_links.uuid",
195        )
196        .bind(user_uuid)
197        .bind(search)
198        .fetch(database, page, per_page)
199        .await
200    }
201
202    pub async fn filtered_by_user_uuid_with_pagination(
203        database: &crate::database::Database,
204        user_uuid: uuid::Uuid,
205        page: i64,
206        per_page: i64,
207    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
208        super::PaginatedQueryBuilder::new(
209            Self::columns_sql(None),
210            r#"
211            FROM user_oauth_links
212            INNER JOIN oauth_providers ON user_oauth_links.oauth_provider_uuid = oauth_providers.uuid
213            WHERE user_oauth_links.user_uuid = $1 AND oauth_providers.link_viewable = true
214            "#,
215            "user_oauth_links.created, user_oauth_links.uuid",
216        )
217        .bind(user_uuid)
218        .fetch(database, page, per_page)
219        .await
220    }
221
222    pub async fn by_oauth_provider_uuid_with_pagination(
223        database: &crate::database::Database,
224        oauth_provider_uuid: uuid::Uuid,
225        page: i64,
226        per_page: i64,
227        search: Option<&str>,
228    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
229        super::PaginatedQueryBuilder::new(
230            Self::columns_sql(None),
231            format!(
232                r#"
233                FROM user_oauth_links
234                WHERE user_oauth_links.oauth_provider_uuid = $1 AND {search}
235                "#,
236                search = super::search_sql(
237                    2,
238                    &["user_oauth_links.identifier"],
239                    &["user_oauth_links.uuid"]
240                ),
241            ),
242            "user_oauth_links.created, user_oauth_links.uuid",
243        )
244        .bind(oauth_provider_uuid)
245        .bind(search)
246        .fetch(database, page, per_page)
247        .await
248    }
249}
250
251#[async_trait::async_trait]
252impl IntoAdminApiObject for UserOAuthLink {
253    type AdminApiObject = AdminApiUserOAuthLink;
254    type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
255
256    async fn into_admin_api_object<'a>(
257        self,
258        state: &crate::State,
259        storage_url_retriever: Self::ExtraArgs<'a>,
260    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
261        let api_object = AdminApiUserOAuthLink::init_hooks(&self, state).await?;
262
263        let api_object = finish_extendible!(
264            AdminApiUserOAuthLink {
265                uuid: self.uuid,
266                user: self
267                    .user
268                    .fetch_cached(&state.database)
269                    .await?
270                    .into_admin_api_object(state, storage_url_retriever)
271                    .await?,
272                identifier: self.identifier,
273                last_used: self.last_used.map(|dt| dt.and_utc()),
274                created: self.created.and_utc(),
275            },
276            api_object,
277            state
278        )?;
279
280        Ok(api_object)
281    }
282}
283
284#[async_trait::async_trait]
285impl IntoApiObject for UserOAuthLink {
286    type ApiObject = ApiUserOAuthLink;
287    type ExtraArgs<'a> = ();
288
289    async fn into_api_object<'a>(
290        self,
291        state: &crate::State,
292        _args: Self::ExtraArgs<'a>,
293    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
294        let api_object = ApiUserOAuthLink::init_hooks(&self, state).await?;
295
296        let api_object = finish_extendible!(
297            ApiUserOAuthLink {
298                uuid: self.uuid,
299                oauth_provider: self
300                    .oauth_provider
301                    .fetch_cached(&state.database)
302                    .await?
303                    .into_api_object(state, ())
304                    .await?,
305                identifier: self.identifier,
306                last_used: self.last_used.map(|dt| dt.and_utc()),
307                created: self.created.and_utc(),
308            },
309            api_object,
310            state
311        )?;
312
313        Ok(api_object)
314    }
315}
316
317#[derive(ToSchema, Deserialize, Validate)]
318pub struct CreateUserOAuthLinkOptions {
319    #[garde(skip)]
320    pub user_uuid: uuid::Uuid,
321    #[garde(skip)]
322    pub oauth_provider_uuid: uuid::Uuid,
323
324    #[garde(length(chars, min = 1, max = 255))]
325    #[schema(min_length = 1, max_length = 255)]
326    pub identifier: compact_str::CompactString,
327}
328
329#[async_trait::async_trait]
330impl CreatableModel for UserOAuthLink {
331    type CreateOptions<'a> = CreateUserOAuthLinkOptions;
332    type CreateResult = Self;
333
334    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
335        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserOAuthLink>> =
336            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
337        &CREATE_LISTENERS
338    }
339
340    async fn create_with_transaction(
341        state: &crate::State,
342        mut options: Self::CreateOptions<'_>,
343        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
344    ) -> Result<Self, crate::database::DatabaseError> {
345        options.validate()?;
346
347        super::oauth_provider::OAuthProvider::by_uuid_optional_cached(
348            &state.database,
349            options.oauth_provider_uuid,
350        )
351        .await?
352        .ok_or(crate::database::InvalidRelationError("oauth_provider"))?;
353
354        let mut query_builder = InsertQueryBuilder::new("user_oauth_links");
355
356        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
357
358        query_builder
359            .set("user_uuid", options.user_uuid)
360            .set("oauth_provider_uuid", options.oauth_provider_uuid)
361            .set("identifier", &options.identifier);
362
363        let row = query_builder
364            .returning(&Self::columns_sql(None))
365            .fetch_one(&mut **transaction)
366            .await?;
367        let mut oauth_link = Self::map(None, &row)?;
368
369        Self::run_after_create_handlers(&mut oauth_link, &options, state, transaction).await?;
370
371        Ok(oauth_link)
372    }
373}
374
375#[async_trait::async_trait]
376impl DeletableModel for UserOAuthLink {
377    type DeleteOptions = ();
378
379    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
380        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserOAuthLink>> =
381            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
382
383        &DELETE_LISTENERS
384    }
385
386    async fn delete_with_transaction(
387        &self,
388        state: &crate::State,
389        options: Self::DeleteOptions,
390        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
391    ) -> Result<(), anyhow::Error> {
392        self.run_delete_handlers(&options, state, transaction)
393            .await?;
394
395        sqlx::query(
396            r#"
397            DELETE FROM user_oauth_links
398            WHERE user_oauth_links.uuid = $1
399            "#,
400        )
401        .bind(self.uuid)
402        .execute(&mut **transaction)
403        .await?;
404
405        self.run_after_delete_handlers(&options, state, transaction)
406            .await?;
407
408        Ok(())
409    }
410}
411
412#[schema_extension_derive::extendible]
413#[init_args(UserOAuthLink, crate::State)]
414#[hook_args(crate::State)]
415#[derive(ToSchema, Serialize)]
416#[schema(title = "AdminUserOAuthLink")]
417pub struct AdminApiUserOAuthLink {
418    pub uuid: uuid::Uuid,
419    pub user: super::user::AdminApiUser,
420
421    pub identifier: compact_str::CompactString,
422
423    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
424    pub created: chrono::DateTime<chrono::Utc>,
425}
426
427#[schema_extension_derive::extendible]
428#[init_args(UserOAuthLink, crate::State)]
429#[hook_args(crate::State)]
430#[derive(ToSchema, Serialize)]
431#[schema(title = "UserOAuthLink")]
432pub struct ApiUserOAuthLink {
433    pub uuid: uuid::Uuid,
434    pub oauth_provider: super::oauth_provider::ApiOAuthProvider,
435
436    pub identifier: compact_str::CompactString,
437
438    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
439    pub created: chrono::DateTime<chrono::Utc>,
440}