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}