1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use rand::distr::SampleString;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10 collections::BTreeMap,
11 sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(Serialize, Deserialize, Clone)]
16pub struct UserApiKey {
17 pub uuid: uuid::Uuid,
18 pub user_uuid: uuid::Uuid,
19
20 pub name: compact_str::CompactString,
21 pub key_start: compact_str::CompactString,
22 pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
23
24 pub enabled: bool,
25
26 pub user_permissions: Arc<Vec<compact_str::CompactString>>,
27 pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
28 pub server_permissions: Arc<Vec<compact_str::CompactString>>,
29
30 pub last_used: Option<chrono::NaiveDateTime>,
31 pub expires: Option<chrono::NaiveDateTime>,
32 pub created: chrono::NaiveDateTime,
33
34 extension_data: super::ModelExtensionData,
35}
36
37impl BaseModel for UserApiKey {
38 const NAME: &'static str = "user_api_key";
39
40 fn get_extension_list() -> &'static super::ModelExtensionList {
41 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
42 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
43
44 &EXTENSIONS
45 }
46
47 fn get_extension_data(&self) -> &super::ModelExtensionData {
48 &self.extension_data
49 }
50
51 #[inline]
52 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
53 let prefix = prefix.unwrap_or_default();
54
55 BTreeMap::from([
56 (
57 "user_api_keys.uuid",
58 compact_str::format_compact!("{prefix}uuid"),
59 ),
60 (
61 "user_api_keys.user_uuid",
62 compact_str::format_compact!("{prefix}user_uuid"),
63 ),
64 (
65 "user_api_keys.name",
66 compact_str::format_compact!("{prefix}name"),
67 ),
68 (
69 "user_api_keys.key_start",
70 compact_str::format_compact!("{prefix}key_start"),
71 ),
72 (
73 "user_api_keys.allowed_ips",
74 compact_str::format_compact!("{prefix}allowed_ips"),
75 ),
76 (
77 "user_api_keys.enabled",
78 compact_str::format_compact!("{prefix}enabled"),
79 ),
80 (
81 "user_api_keys.user_permissions",
82 compact_str::format_compact!("{prefix}user_permissions"),
83 ),
84 (
85 "user_api_keys.admin_permissions",
86 compact_str::format_compact!("{prefix}admin_permissions"),
87 ),
88 (
89 "user_api_keys.server_permissions",
90 compact_str::format_compact!("{prefix}server_permissions"),
91 ),
92 (
93 "user_api_keys.last_used",
94 compact_str::format_compact!("{prefix}last_used"),
95 ),
96 (
97 "user_api_keys.expires",
98 compact_str::format_compact!("{prefix}expires"),
99 ),
100 (
101 "user_api_keys.created",
102 compact_str::format_compact!("{prefix}created"),
103 ),
104 ])
105 }
106
107 #[inline]
108 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
109 let prefix = prefix.unwrap_or_default();
110
111 Ok(Self {
112 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
113 user_uuid: row.try_get(compact_str::format_compact!("{prefix}user_uuid").as_str())?,
114 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
115 key_start: row.try_get(compact_str::format_compact!("{prefix}key_start").as_str())?,
116 allowed_ips: row
117 .try_get(compact_str::format_compact!("{prefix}allowed_ips").as_str())?,
118 enabled: row.try_get(compact_str::format_compact!("{prefix}enabled").as_str())?,
119 user_permissions: Arc::new(
120 row.try_get(compact_str::format_compact!("{prefix}user_permissions").as_str())?,
121 ),
122 admin_permissions: Arc::new(
123 row.try_get(compact_str::format_compact!("{prefix}admin_permissions").as_str())?,
124 ),
125 server_permissions: Arc::new(
126 row.try_get(compact_str::format_compact!("{prefix}server_permissions").as_str())?,
127 ),
128 last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
129 expires: row.try_get(compact_str::format_compact!("{prefix}expires").as_str())?,
130 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
131 extension_data: Self::map_extensions(prefix, row)?,
132 })
133 }
134
135 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
136 vec![compact_str::format_compact!(
137 "{}::{}",
138 Self::NAME,
139 self.uuid
140 )]
141 }
142}
143
144#[async_trait::async_trait]
145impl ResolvableModel for UserApiKey {
146 type Fingerprint = compact_str::CompactString;
147
148 fn uuid(&self) -> uuid::Uuid {
149 self.uuid
150 }
151
152 fn fingerprint(&self) -> Self::Fingerprint {
153 self.key_start.clone()
154 }
155
156 async fn resolve(
157 database: &crate::database::Database,
158 identifier: &str,
159 ) -> Result<Option<Self>, anyhow::Error> {
160 let digest = crate::crypt::token_digest(identifier);
161
162 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
163 r#"
164 SELECT {}
165 FROM user_api_keys
166 WHERE user_api_keys.key = $1
167 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
168 "#,
169 Self::columns_sql(None)
170 )))
171 .bind(&digest)
172 .fetch_optional(database.read())
173 .await?;
174
175 if let Some(row) = row {
176 return Ok(Some(Self::map(None, &row)?));
177 }
178
179 let Some(key_start) = identifier.get(0..16) else {
180 return Ok(None);
181 };
182
183 let Some(row) = sqlx::query(sqlx::AssertSqlSafe(format!(
184 r#"
185 WITH user_api_keys AS MATERIALIZED (
186 SELECT * FROM user_api_keys
187 WHERE user_api_keys.key_start = $1
188 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
189 )
190 SELECT {}, user_api_keys.key AS key_hash
191 FROM user_api_keys
192 WHERE user_api_keys.key = crypt($2, user_api_keys.key)
193 "#,
194 Self::columns_sql(None)
195 )))
196 .bind(key_start)
197 .bind(identifier)
198 .fetch_optional(database.read())
199 .await?
200 else {
201 return Ok(None);
202 };
203
204 sqlx::query(
205 r#"
206 UPDATE user_api_keys
207 SET key = $2
208 WHERE user_api_keys.uuid = $1 AND user_api_keys.key = $3
209 "#,
210 )
211 .bind(row.try_get::<uuid::Uuid, _>("uuid")?)
212 .bind(&digest)
213 .bind(row.try_get::<String, _>("key_hash")?)
214 .execute(database.write())
215 .await?;
216
217 Ok(Some(Self::map(None, &row)?))
218 }
219}
220
221impl UserApiKey {
222 pub const HEADER_LEN: usize = 7 + 48;
226
227 fn generate_key() -> String {
228 format!(
229 "c7sp_{}",
230 rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
231 )
232 }
233
234 pub async fn by_user_uuid_uuid(
235 database: &crate::database::Database,
236 user_uuid: uuid::Uuid,
237 uuid: uuid::Uuid,
238 ) -> Result<Option<Self>, crate::database::DatabaseError> {
239 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
240 r#"
241 SELECT {}
242 FROM user_api_keys
243 WHERE user_api_keys.user_uuid = $1 AND user_api_keys.uuid = $2 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
244 "#,
245 Self::columns_sql(None)
246 )))
247 .bind(user_uuid)
248 .bind(uuid)
249 .fetch_optional(database.read())
250 .await?;
251
252 row.try_map(|row| Self::map(None, &row))
253 }
254
255 pub async fn by_user_uuid_key_start(
256 database: &crate::database::Database,
257 user_uuid: uuid::Uuid,
258 key_start: &str,
259 ) -> Result<Option<Self>, crate::database::DatabaseError> {
260 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
261 r#"
262 SELECT {}
263 FROM user_api_keys
264 WHERE user_api_keys.user_uuid = $1 AND user_api_keys.key_start = $2 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
265 "#,
266 Self::columns_sql(None)
267 )))
268 .bind(user_uuid)
269 .bind(key_start)
270 .fetch_optional(database.read())
271 .await?;
272
273 row.try_map(|row| Self::map(None, &row))
274 }
275
276 pub async fn by_user_uuid_with_pagination(
277 database: &crate::database::Database,
278 user_uuid: uuid::Uuid,
279 page: i64,
280 per_page: i64,
281 search: Option<&str>,
282 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
283 super::PaginatedQueryBuilder::new(
284 Self::columns_sql(None),
285 format!(
286 r#"
287 FROM user_api_keys
288 WHERE user_api_keys.user_uuid = $1 AND {search}
289 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
290 "#,
291 search = super::search_sql(2, &["user_api_keys.name"], &["user_api_keys.uuid"]),
292 ),
293 "user_api_keys.created, user_api_keys.uuid",
294 )
295 .bind(user_uuid)
296 .bind(search)
297 .fetch(database, page, per_page)
298 .await
299 }
300
301 pub async fn by_uuids(
302 database: &crate::database::Database,
303 uuids: &[uuid::Uuid],
304 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
305 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
306 r#"
307 SELECT {}
308 FROM user_api_keys
309 WHERE user_api_keys.uuid = ANY($1)
310 "#,
311 Self::columns_sql(None)
312 )))
313 .bind(uuids)
314 .fetch_all(database.read())
315 .await?;
316
317 rows.into_iter()
318 .map(|row| Self::map(None, &row))
319 .try_collect_vec()
320 }
321
322 pub async fn delete_expired(database: &crate::database::Database) -> Result<u64, sqlx::Error> {
323 Ok(sqlx::query(
324 r#"
325 DELETE FROM user_api_keys
326 WHERE user_api_keys.expires IS NOT NULL AND user_api_keys.expires < NOW()
327 "#,
328 )
329 .execute(database.write())
330 .await?
331 .rows_affected())
332 }
333
334 pub async fn update_last_used(&self, database: &Arc<crate::database::Database>) {
335 let uuid = self.uuid;
336 let now = chrono::Utc::now().naive_utc();
337
338 database
339 .batch_action("update_user_api_key", uuid, {
340 let database = database.clone();
341
342 async move {
343 sqlx::query!(
344 "UPDATE user_api_keys
345 SET last_used = $2
346 WHERE user_api_keys.uuid = $1",
347 uuid,
348 now
349 )
350 .execute(database.write())
351 .await?;
352
353 Ok(())
354 }
355 })
356 .await;
357 }
358
359 pub async fn recreate(
360 &mut self,
361 database: &crate::database::Database,
362 ) -> Result<String, crate::database::DatabaseError> {
363 let new_key = Self::generate_key();
364
365 sqlx::query(
366 r#"
367 UPDATE user_api_keys
368 SET key_start = $1, key = $2
369 WHERE user_api_keys.uuid = $3
370 "#,
371 )
372 .bind(&new_key[0..16])
373 .bind(crate::crypt::token_digest(&new_key))
374 .bind(self.uuid)
375 .execute(database.write())
376 .await?;
377
378 self.key_start = new_key[0..16].into();
379
380 Self::invalidate_cached(database, self.uuid).await;
381
382 Ok(new_key)
383 }
384
385 pub async fn count_by_user_uuid(
386 database: &crate::database::Database,
387 user_uuid: uuid::Uuid,
388 ) -> Result<i64, sqlx::Error> {
389 sqlx::query_scalar(
390 r#"
391 SELECT COUNT(*)
392 FROM user_api_keys
393 WHERE user_api_keys.user_uuid = $1
394 "#,
395 )
396 .bind(user_uuid)
397 .fetch_one(database.read())
398 .await
399 }
400}
401
402#[async_trait::async_trait]
403impl IntoApiObject for UserApiKey {
404 type ApiObject = ApiUserApiKey;
405 type ExtraArgs<'a> = ();
406
407 async fn into_api_object<'a>(
408 self,
409 state: &crate::State,
410 _args: Self::ExtraArgs<'a>,
411 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
412 let api_object = ApiUserApiKey::init_hooks(&self, state).await?;
413
414 let api_object = finish_extendible!(
415 ApiUserApiKey {
416 uuid: self.uuid,
417 name: self.name,
418 key_start: self.key_start,
419 allowed_ips: self.allowed_ips,
420 enabled: self.enabled,
421 user_permissions: self.user_permissions,
422 admin_permissions: self.admin_permissions,
423 server_permissions: self.server_permissions,
424 last_used: self.last_used.map(|dt| dt.and_utc()),
425 expires: self.expires.map(|dt| dt.and_utc()),
426 created: self.created.and_utc(),
427 },
428 api_object,
429 state
430 )?;
431
432 Ok(api_object)
433 }
434}
435
436#[derive(ToSchema, Deserialize, Validate)]
437pub struct CreateUserApiKeyOptions {
438 #[garde(skip)]
439 pub user_uuid: uuid::Uuid,
440
441 #[garde(length(chars, min = 3, max = 31))]
442 #[schema(min_length = 3, max_length = 31)]
443 pub name: compact_str::CompactString,
444 #[garde(skip)]
445 #[schema(value_type = Vec<String>)]
446 pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
447
448 #[garde(skip)]
449 pub enabled: bool,
450
451 #[garde(custom(crate::permissions::validate_user_permissions))]
452 pub user_permissions: Vec<compact_str::CompactString>,
453 #[garde(custom(crate::permissions::validate_admin_permissions))]
454 pub admin_permissions: Vec<compact_str::CompactString>,
455 #[garde(custom(crate::permissions::validate_server_permissions))]
456 pub server_permissions: Vec<compact_str::CompactString>,
457
458 #[garde(inner(custom(crate::utils::validate_time_in_future)))]
459 pub expires: Option<chrono::DateTime<chrono::Utc>>,
460}
461
462#[async_trait::async_trait]
463impl CreatableModel for UserApiKey {
464 type CreateOptions<'a> = CreateUserApiKeyOptions;
465 type CreateResult = (String, Self);
466
467 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
468 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserApiKey>> =
469 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
470
471 &CREATE_LISTENERS
472 }
473
474 async fn create_with_transaction(
475 state: &crate::State,
476 mut options: Self::CreateOptions<'_>,
477 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
478 ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
479 options.validate()?;
480
481 let key = Self::generate_key();
482
483 let mut query_builder = InsertQueryBuilder::new("user_api_keys");
484
485 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
486
487 query_builder
488 .set("user_uuid", options.user_uuid)
489 .set("name", &options.name)
490 .set("key_start", &key[0..16])
491 .set("key", crate::crypt::token_digest(&key))
492 .set("allowed_ips", &options.allowed_ips)
493 .set("enabled", options.enabled)
494 .set("user_permissions", &options.user_permissions)
495 .set("admin_permissions", &options.admin_permissions)
496 .set("server_permissions", &options.server_permissions)
497 .set("expires", options.expires.map(|d| d.naive_utc()));
498
499 let row = query_builder
500 .returning(&Self::columns_sql(None))
501 .fetch_one(&mut **transaction)
502 .await?;
503 let user_api_key = Self::map(None, &row)?;
504
505 let mut result = (key, user_api_key);
506
507 Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
508
509 Ok(result)
510 }
511}
512
513#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
514pub struct UpdateUserApiKeyOptions {
515 #[garde(length(chars, min = 3, max = 31))]
516 #[schema(min_length = 3, max_length = 31)]
517 pub name: Option<compact_str::CompactString>,
518 #[garde(skip)]
519 #[schema(value_type = Vec<String>)]
520 pub allowed_ips: Option<Vec<sqlx::types::ipnetwork::IpNetwork>>,
521
522 #[garde(skip)]
523 pub enabled: Option<bool>,
524
525 #[garde(inner(custom(crate::permissions::validate_user_permissions)))]
526 pub user_permissions: Option<Vec<compact_str::CompactString>>,
527 #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
528 pub admin_permissions: Option<Vec<compact_str::CompactString>>,
529 #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
530 pub server_permissions: Option<Vec<compact_str::CompactString>>,
531
532 #[garde(inner(inner(custom(crate::utils::validate_time_in_future))))]
533 #[serde(
534 default,
535 skip_serializing_if = "Option::is_none",
536 with = "::serde_with::rust::double_option"
537 )]
538 pub expires: Option<Option<chrono::DateTime<chrono::Utc>>>,
539}
540
541#[async_trait::async_trait]
542impl UpdatableModel for UserApiKey {
543 type UpdateOptions = UpdateUserApiKeyOptions;
544
545 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
546 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserApiKey>> =
547 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
548
549 &UPDATE_LISTENERS
550 }
551
552 async fn update(
553 &mut self,
554 state: &crate::State,
555 options: Self::UpdateOptions,
556 ) -> Result<(), crate::database::DatabaseError> {
557 let scope_changed = options.allowed_ips.is_some()
558 || options.enabled.is_some()
559 || options.user_permissions.is_some()
560 || options.admin_permissions.is_some()
561 || options.server_permissions.is_some()
562 || options.expires.is_some();
563
564 let mut transaction = state.database.write().begin().await?;
565
566 if let Err(err) = self
567 .update_with_transaction(state, options, &mut transaction)
568 .await
569 {
570 transaction.rollback().await?;
571 return Err(err);
572 }
573
574 transaction.commit().await?;
575
576 super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
577
578 if scope_changed {
579 super::node::Node::invalidate_permissions_everywhere(
580 &state.database,
581 super::node::PermissionsInvalidation {
582 revoke_credentials: vec![self.uuid],
583 ..Default::default()
584 },
585 );
586 }
587
588 Ok(())
589 }
590
591 async fn update_with_transaction(
592 &mut self,
593 state: &crate::State,
594 mut options: Self::UpdateOptions,
595 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
596 ) -> Result<(), crate::database::DatabaseError> {
597 options.validate()?;
598
599 let mut query_builder = UpdateQueryBuilder::new("user_api_keys");
600
601 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
602 .await?;
603
604 query_builder
605 .set("name", options.name.as_ref())
606 .set("allowed_ips", options.allowed_ips.as_ref())
607 .set("enabled", options.enabled)
608 .set("user_permissions", options.user_permissions.as_ref())
609 .set("admin_permissions", options.admin_permissions.as_ref())
610 .set("server_permissions", options.server_permissions.as_ref())
611 .set(
612 "expires",
613 options
614 .expires
615 .as_ref()
616 .map(|e| e.as_ref().map(|d| d.naive_utc())),
617 )
618 .where_eq("uuid", self.uuid);
619
620 query_builder.execute(&mut **transaction).await?;
621
622 if let Some(name) = options.name {
623 self.name = name;
624 }
625 if let Some(allowed_ips) = options.allowed_ips {
626 self.allowed_ips = allowed_ips;
627 }
628 if let Some(enabled) = options.enabled {
629 self.enabled = enabled;
630 }
631 if let Some(user_permissions) = options.user_permissions {
632 self.user_permissions = Arc::new(user_permissions);
633 }
634 if let Some(admin_permissions) = options.admin_permissions {
635 self.admin_permissions = Arc::new(admin_permissions);
636 }
637 if let Some(server_permissions) = options.server_permissions {
638 self.server_permissions = Arc::new(server_permissions);
639 }
640 if let Some(expires) = options.expires {
641 self.expires = expires.map(|d| d.naive_utc());
642 }
643
644 self.run_after_update_handlers(state, transaction).await?;
645
646 Ok(())
647 }
648}
649
650#[derive(Validate)]
651pub struct DuplicateUserApiKeyOptions {
652 #[garde(skip)]
653 pub user_uuid: uuid::Uuid,
654 #[garde(length(chars, min = 3, max = 31))]
655 pub name: compact_str::CompactString,
656}
657
658#[async_trait::async_trait]
659impl DuplicableModel for UserApiKey {
660 type DuplicateOptions<'a> = DuplicateUserApiKeyOptions;
661 type DuplicateResult = (String, Self);
662
663 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
664 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<UserApiKey>> =
665 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
666
667 &DUPLICATE_LISTENERS
668 }
669
670 async fn duplicate_with_transaction(
671 &self,
672 state: &crate::State,
673 options: Self::DuplicateOptions<'_>,
674 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
675 ) -> Result<Self::DuplicateResult, crate::database::DatabaseError> {
676 options.validate()?;
677
678 self.run_duplicate_handlers(&options, state, transaction)
679 .await?;
680
681 let key = Self::generate_key();
682
683 let mut query_builder = InsertQueryBuilder::new("user_api_keys");
684
685 query_builder
686 .set("user_uuid", options.user_uuid)
687 .set("name", &options.name)
688 .set("key_start", &key[0..16])
689 .set("key", crate::crypt::token_digest(&key))
690 .set("allowed_ips", &self.allowed_ips)
691 .set("enabled", self.enabled)
692 .set("user_permissions", &*self.user_permissions)
693 .set("admin_permissions", &*self.admin_permissions)
694 .set("server_permissions", &*self.server_permissions)
695 .set("expires", self.expires);
696
697 let row = query_builder
698 .returning(&Self::columns_sql(None))
699 .fetch_one(&mut **transaction)
700 .await?;
701 let mut result = (key, Self::map(None, &row)?);
702
703 self.run_after_duplicate_handlers(&mut result, &options, state, transaction)
704 .await?;
705
706 Ok(result)
707 }
708}
709
710#[async_trait::async_trait]
711impl ByUuid for UserApiKey {
712 async fn by_uuid(
713 database: &crate::database::Database,
714 uuid: uuid::Uuid,
715 ) -> Result<Self, crate::database::DatabaseError> {
716 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
717 r#"
718 SELECT {}
719 FROM user_api_keys
720 WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
721 "#,
722 Self::columns_sql(None)
723 )))
724 .bind(uuid)
725 .fetch_one(database.read())
726 .await?;
727
728 Self::map(None, &row)
729 }
730
731 async fn by_uuid_with_transaction(
732 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
733 uuid: uuid::Uuid,
734 ) -> Result<Self, crate::database::DatabaseError> {
735 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
736 r#"
737 SELECT {}
738 FROM user_api_keys
739 WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
740 "#,
741 Self::columns_sql(None)
742 )))
743 .bind(uuid)
744 .fetch_one(&mut **transaction)
745 .await?;
746
747 Self::map(None, &row)
748 }
749}
750
751#[async_trait::async_trait]
752impl DeletableModel for UserApiKey {
753 type DeleteOptions = ();
754
755 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
756 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserApiKey>> =
757 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
758
759 &DELETE_LISTENERS
760 }
761
762 async fn delete(
763 &self,
764 state: &crate::State,
765 options: Self::DeleteOptions,
766 ) -> Result<(), anyhow::Error> {
767 let mut transaction = state.database.write().begin().await?;
768
769 if let Err(err) = self
770 .delete_with_transaction(state, options, &mut transaction)
771 .await
772 {
773 transaction.rollback().await?;
774 return Err(err);
775 }
776
777 transaction.commit().await?;
778
779 super::invalidate_cache_keys(&state.database, &self.cache_invalidation_keys()).await;
780
781 super::node::Node::invalidate_permissions_everywhere(
782 &state.database,
783 super::node::PermissionsInvalidation {
784 revoke_credentials: vec![self.uuid],
785 ..Default::default()
786 },
787 );
788
789 Ok(())
790 }
791
792 async fn delete_with_transaction(
793 &self,
794 state: &crate::State,
795 options: Self::DeleteOptions,
796 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
797 ) -> Result<(), anyhow::Error> {
798 self.run_delete_handlers(&options, state, transaction)
799 .await?;
800
801 sqlx::query(
802 r#"
803 DELETE FROM user_api_keys
804 WHERE user_api_keys.uuid = $1
805 "#,
806 )
807 .bind(self.uuid)
808 .execute(&mut **transaction)
809 .await?;
810
811 self.run_after_delete_handlers(&options, state, transaction)
812 .await?;
813
814 Ok(())
815 }
816}
817
818#[schema_extension_derive::extendible]
819#[init_args(UserApiKey, crate::State)]
820#[hook_args(crate::State)]
821#[derive(ToSchema, Serialize)]
822#[schema(title = "UserApiKey")]
823pub struct ApiUserApiKey {
824 pub uuid: uuid::Uuid,
825
826 pub name: compact_str::CompactString,
827 pub key_start: compact_str::CompactString,
828 #[schema(value_type = Vec<String>)]
829 pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
830
831 pub enabled: bool,
832
833 pub user_permissions: Arc<Vec<compact_str::CompactString>>,
834 pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
835 pub server_permissions: Arc<Vec<compact_str::CompactString>>,
836
837 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
838 pub expires: Option<chrono::DateTime<chrono::Utc>>,
839 pub created: chrono::DateTime<chrono::Utc>,
840}