Skip to main content

shared/models/
user_api_key.rs

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    /// The total len of the header,
223    ///
224    /// "Bearer " (7) + key (48)
225    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}