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    pub async fn by_user_uuid_uuid(
228        database: &crate::database::Database,
229        user_uuid: uuid::Uuid,
230        uuid: uuid::Uuid,
231    ) -> Result<Option<Self>, crate::database::DatabaseError> {
232        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
233            r#"
234            SELECT {}
235            FROM user_api_keys
236            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())
237            "#,
238            Self::columns_sql(None)
239        )))
240        .bind(user_uuid)
241        .bind(uuid)
242        .fetch_optional(database.read())
243        .await?;
244
245        row.try_map(|row| Self::map(None, &row))
246    }
247
248    pub async fn by_user_uuid_key_start(
249        database: &crate::database::Database,
250        user_uuid: uuid::Uuid,
251        key_start: &str,
252    ) -> Result<Option<Self>, crate::database::DatabaseError> {
253        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
254            r#"
255            SELECT {}
256            FROM user_api_keys
257            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())
258            "#,
259            Self::columns_sql(None)
260        )))
261        .bind(user_uuid)
262        .bind(key_start)
263        .fetch_optional(database.read())
264        .await?;
265
266        row.try_map(|row| Self::map(None, &row))
267    }
268
269    pub async fn by_user_uuid_with_pagination(
270        database: &crate::database::Database,
271        user_uuid: uuid::Uuid,
272        page: i64,
273        per_page: i64,
274        search: Option<&str>,
275    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
276        let offset = (page - 1) * per_page;
277
278        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
279            r#"
280            SELECT {}, COUNT(*) OVER() AS total_count
281            FROM user_api_keys
282            WHERE user_api_keys.user_uuid = $1 AND ($2 IS NULL OR user_api_keys.name ILIKE '%' || $2 || '%')
283                AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
284            ORDER BY user_api_keys.created
285            LIMIT $3 OFFSET $4
286            "#,
287            Self::columns_sql(None)
288        )))
289        .bind(user_uuid)
290        .bind(search)
291        .bind(per_page)
292        .bind(offset)
293        .fetch_all(database.read())
294        .await?;
295
296        Ok(super::Pagination {
297            total: rows
298                .first()
299                .map_or(Ok(0), |row| row.try_get("total_count"))?,
300            per_page,
301            page,
302            data: rows
303                .into_iter()
304                .map(|row| Self::map(None, &row))
305                .try_collect_vec()?,
306        })
307    }
308
309    pub async fn by_uuids(
310        database: &crate::database::Database,
311        uuids: &[uuid::Uuid],
312    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
313        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
314            r#"
315            SELECT {}
316            FROM user_api_keys
317            WHERE user_api_keys.uuid = ANY($1)
318            "#,
319            Self::columns_sql(None)
320        )))
321        .bind(uuids)
322        .fetch_all(database.read())
323        .await?;
324
325        rows.into_iter()
326            .map(|row| Self::map(None, &row))
327            .try_collect_vec()
328    }
329
330    pub async fn delete_expired(database: &crate::database::Database) -> Result<u64, sqlx::Error> {
331        Ok(sqlx::query(
332            r#"
333            DELETE FROM user_api_keys
334            WHERE user_api_keys.expires IS NOT NULL AND user_api_keys.expires < NOW()
335            "#,
336        )
337        .execute(database.write())
338        .await?
339        .rows_affected())
340    }
341
342    pub async fn update_last_used(&self, database: &Arc<crate::database::Database>) {
343        let uuid = self.uuid;
344        let now = chrono::Utc::now().naive_utc();
345
346        database
347            .batch_action("update_user_api_key", uuid, {
348                let database = database.clone();
349
350                async move {
351                    sqlx::query!(
352                        "UPDATE user_api_keys
353                        SET last_used = $2
354                        WHERE user_api_keys.uuid = $1",
355                        uuid,
356                        now
357                    )
358                    .execute(database.write())
359                    .await?;
360
361                    Ok(())
362                }
363            })
364            .await;
365    }
366
367    pub async fn recreate(
368        &mut self,
369        database: &crate::database::Database,
370    ) -> Result<String, crate::database::DatabaseError> {
371        let new_key = format!(
372            "c7sp_{}",
373            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
374        );
375
376        sqlx::query(
377            r#"
378            UPDATE user_api_keys
379            SET key_start = $1, key = $2
380            WHERE user_api_keys.uuid = $3
381            "#,
382        )
383        .bind(&new_key[0..16])
384        .bind(crate::crypt::token_digest(&new_key))
385        .bind(self.uuid)
386        .execute(database.write())
387        .await?;
388
389        self.key_start = new_key[0..16].into();
390
391        Self::invalidate_cached(database, self.uuid).await;
392
393        Ok(new_key)
394    }
395
396    pub async fn count_by_user_uuid(
397        database: &crate::database::Database,
398        user_uuid: uuid::Uuid,
399    ) -> Result<i64, sqlx::Error> {
400        sqlx::query_scalar(
401            r#"
402            SELECT COUNT(*)
403            FROM user_api_keys
404            WHERE user_api_keys.user_uuid = $1
405            "#,
406        )
407        .bind(user_uuid)
408        .fetch_one(database.read())
409        .await
410    }
411}
412
413#[async_trait::async_trait]
414impl IntoApiObject for UserApiKey {
415    type ApiObject = ApiUserApiKey;
416    type ExtraArgs<'a> = ();
417
418    async fn into_api_object<'a>(
419        self,
420        state: &crate::State,
421        _args: Self::ExtraArgs<'a>,
422    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
423        let api_object = ApiUserApiKey::init_hooks(&self, state).await?;
424
425        let api_object = finish_extendible!(
426            ApiUserApiKey {
427                uuid: self.uuid,
428                name: self.name,
429                key_start: self.key_start,
430                allowed_ips: self.allowed_ips,
431                enabled: self.enabled,
432                user_permissions: self.user_permissions,
433                admin_permissions: self.admin_permissions,
434                server_permissions: self.server_permissions,
435                last_used: self.last_used.map(|dt| dt.and_utc()),
436                expires: self.expires.map(|dt| dt.and_utc()),
437                created: self.created.and_utc(),
438            },
439            api_object,
440            state
441        )?;
442
443        Ok(api_object)
444    }
445}
446
447#[derive(ToSchema, Deserialize, Validate)]
448pub struct CreateUserApiKeyOptions {
449    #[garde(skip)]
450    pub user_uuid: uuid::Uuid,
451
452    #[garde(length(chars, min = 3, max = 31))]
453    #[schema(min_length = 3, max_length = 31)]
454    pub name: compact_str::CompactString,
455    #[garde(skip)]
456    #[schema(value_type = Vec<String>)]
457    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
458
459    #[garde(skip)]
460    pub enabled: bool,
461
462    #[garde(custom(crate::permissions::validate_user_permissions))]
463    pub user_permissions: Vec<compact_str::CompactString>,
464    #[garde(custom(crate::permissions::validate_admin_permissions))]
465    pub admin_permissions: Vec<compact_str::CompactString>,
466    #[garde(custom(crate::permissions::validate_server_permissions))]
467    pub server_permissions: Vec<compact_str::CompactString>,
468
469    #[garde(inner(custom(crate::utils::validate_time_in_future)))]
470    pub expires: Option<chrono::DateTime<chrono::Utc>>,
471}
472
473#[async_trait::async_trait]
474impl CreatableModel for UserApiKey {
475    type CreateOptions<'a> = CreateUserApiKeyOptions;
476    type CreateResult = (String, Self);
477
478    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
479        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserApiKey>> =
480            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
481
482        &CREATE_LISTENERS
483    }
484
485    async fn create_with_transaction(
486        state: &crate::State,
487        mut options: Self::CreateOptions<'_>,
488        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
489    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
490        options.validate()?;
491
492        let key = format!(
493            "c7sp_{}",
494            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
495        );
496
497        let mut query_builder = InsertQueryBuilder::new("user_api_keys");
498
499        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
500
501        query_builder
502            .set("user_uuid", options.user_uuid)
503            .set("name", &options.name)
504            .set("key_start", &key[0..16])
505            .set("key", crate::crypt::token_digest(&key))
506            .set("allowed_ips", &options.allowed_ips)
507            .set("enabled", options.enabled)
508            .set("user_permissions", &options.user_permissions)
509            .set("admin_permissions", &options.admin_permissions)
510            .set("server_permissions", &options.server_permissions)
511            .set("expires", options.expires.map(|d| d.naive_utc()));
512
513        let row = query_builder
514            .returning(&Self::columns_sql(None))
515            .fetch_one(&mut **transaction)
516            .await?;
517        let user_api_key = Self::map(None, &row)?;
518
519        let mut result = (key, user_api_key);
520
521        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
522
523        Ok(result)
524    }
525}
526
527#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
528pub struct UpdateUserApiKeyOptions {
529    #[garde(length(chars, min = 3, max = 31))]
530    #[schema(min_length = 3, max_length = 31)]
531    pub name: Option<compact_str::CompactString>,
532    #[garde(skip)]
533    #[schema(value_type = Vec<String>)]
534    pub allowed_ips: Option<Vec<sqlx::types::ipnetwork::IpNetwork>>,
535
536    #[garde(skip)]
537    pub enabled: Option<bool>,
538
539    #[garde(inner(custom(crate::permissions::validate_user_permissions)))]
540    pub user_permissions: Option<Vec<compact_str::CompactString>>,
541    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
542    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
543    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
544    pub server_permissions: Option<Vec<compact_str::CompactString>>,
545
546    #[garde(inner(inner(custom(crate::utils::validate_time_in_future))))]
547    #[serde(
548        default,
549        skip_serializing_if = "Option::is_none",
550        with = "::serde_with::rust::double_option"
551    )]
552    pub expires: Option<Option<chrono::DateTime<chrono::Utc>>>,
553}
554
555#[async_trait::async_trait]
556impl UpdatableModel for UserApiKey {
557    type UpdateOptions = UpdateUserApiKeyOptions;
558
559    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
560        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserApiKey>> =
561            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
562
563        &UPDATE_LISTENERS
564    }
565
566    async fn update_with_transaction(
567        &mut self,
568        state: &crate::State,
569        mut options: Self::UpdateOptions,
570        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
571    ) -> Result<(), crate::database::DatabaseError> {
572        options.validate()?;
573
574        let mut query_builder = UpdateQueryBuilder::new("user_api_keys");
575
576        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
577            .await?;
578
579        query_builder
580            .set("name", options.name.as_ref())
581            .set("allowed_ips", options.allowed_ips.as_ref())
582            .set("enabled", options.enabled)
583            .set("user_permissions", options.user_permissions.as_ref())
584            .set("admin_permissions", options.admin_permissions.as_ref())
585            .set("server_permissions", options.server_permissions.as_ref())
586            .set(
587                "expires",
588                options
589                    .expires
590                    .as_ref()
591                    .map(|e| e.as_ref().map(|d| d.naive_utc())),
592            )
593            .where_eq("uuid", self.uuid);
594
595        query_builder.execute(&mut **transaction).await?;
596
597        if let Some(name) = options.name {
598            self.name = name;
599        }
600        if let Some(allowed_ips) = options.allowed_ips {
601            self.allowed_ips = allowed_ips;
602        }
603        if let Some(enabled) = options.enabled {
604            self.enabled = enabled;
605        }
606        if let Some(user_permissions) = options.user_permissions {
607            self.user_permissions = Arc::new(user_permissions);
608        }
609        if let Some(admin_permissions) = options.admin_permissions {
610            self.admin_permissions = Arc::new(admin_permissions);
611        }
612        if let Some(server_permissions) = options.server_permissions {
613            self.server_permissions = Arc::new(server_permissions);
614        }
615        if let Some(expires) = options.expires {
616            self.expires = expires.map(|d| d.naive_utc());
617        }
618
619        self.run_after_update_handlers(state, transaction).await?;
620
621        Ok(())
622    }
623}
624
625#[async_trait::async_trait]
626impl ByUuid for UserApiKey {
627    async fn by_uuid(
628        database: &crate::database::Database,
629        uuid: uuid::Uuid,
630    ) -> Result<Self, crate::database::DatabaseError> {
631        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
632            r#"
633            SELECT {}
634            FROM user_api_keys
635            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
636            "#,
637            Self::columns_sql(None)
638        )))
639        .bind(uuid)
640        .fetch_one(database.read())
641        .await?;
642
643        Self::map(None, &row)
644    }
645
646    async fn by_uuid_with_transaction(
647        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
648        uuid: uuid::Uuid,
649    ) -> Result<Self, crate::database::DatabaseError> {
650        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
651            r#"
652            SELECT {}
653            FROM user_api_keys
654            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
655            "#,
656            Self::columns_sql(None)
657        )))
658        .bind(uuid)
659        .fetch_one(&mut **transaction)
660        .await?;
661
662        Self::map(None, &row)
663    }
664}
665
666#[async_trait::async_trait]
667impl DeletableModel for UserApiKey {
668    type DeleteOptions = ();
669
670    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
671        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserApiKey>> =
672            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
673
674        &DELETE_LISTENERS
675    }
676
677    async fn delete_with_transaction(
678        &self,
679        state: &crate::State,
680        options: Self::DeleteOptions,
681        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
682    ) -> Result<(), anyhow::Error> {
683        self.run_delete_handlers(&options, state, transaction)
684            .await?;
685
686        sqlx::query(
687            r#"
688            DELETE FROM user_api_keys
689            WHERE user_api_keys.uuid = $1
690            "#,
691        )
692        .bind(self.uuid)
693        .execute(&mut **transaction)
694        .await?;
695
696        self.run_after_delete_handlers(&options, state, transaction)
697            .await?;
698
699        Ok(())
700    }
701}
702
703#[schema_extension_derive::extendible]
704#[init_args(UserApiKey, crate::State)]
705#[hook_args(crate::State)]
706#[derive(ToSchema, Serialize)]
707#[schema(title = "UserApiKey")]
708pub struct ApiUserApiKey {
709    pub uuid: uuid::Uuid,
710
711    pub name: compact_str::CompactString,
712    pub key_start: compact_str::CompactString,
713    #[schema(value_type = Vec<String>)]
714    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
715
716    pub enabled: bool,
717
718    pub user_permissions: Arc<Vec<compact_str::CompactString>>,
719    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
720    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
721
722    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
723    pub expires: Option<chrono::DateTime<chrono::Utc>>,
724    pub created: chrono::DateTime<chrono::Utc>,
725}