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}