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 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}