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}