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
19    pub name: compact_str::CompactString,
20    pub key_start: compact_str::CompactString,
21    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
22
23    pub user_permissions: Arc<Vec<compact_str::CompactString>>,
24    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
25    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
26
27    pub last_used: Option<chrono::NaiveDateTime>,
28    pub expires: Option<chrono::NaiveDateTime>,
29    pub created: chrono::NaiveDateTime,
30
31    extension_data: super::ModelExtensionData,
32}
33
34impl BaseModel for UserApiKey {
35    const NAME: &'static str = "user_api_key";
36
37    fn get_extension_list() -> &'static super::ModelExtensionList {
38        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
39            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
40
41        &EXTENSIONS
42    }
43
44    fn get_extension_data(&self) -> &super::ModelExtensionData {
45        &self.extension_data
46    }
47
48    #[inline]
49    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
50        let prefix = prefix.unwrap_or_default();
51
52        BTreeMap::from([
53            (
54                "user_api_keys.uuid",
55                compact_str::format_compact!("{prefix}uuid"),
56            ),
57            (
58                "user_api_keys.name",
59                compact_str::format_compact!("{prefix}name"),
60            ),
61            (
62                "user_api_keys.key_start",
63                compact_str::format_compact!("{prefix}key_start"),
64            ),
65            (
66                "user_api_keys.allowed_ips",
67                compact_str::format_compact!("{prefix}allowed_ips"),
68            ),
69            (
70                "user_api_keys.user_permissions",
71                compact_str::format_compact!("{prefix}user_permissions"),
72            ),
73            (
74                "user_api_keys.admin_permissions",
75                compact_str::format_compact!("{prefix}admin_permissions"),
76            ),
77            (
78                "user_api_keys.server_permissions",
79                compact_str::format_compact!("{prefix}server_permissions"),
80            ),
81            (
82                "user_api_keys.last_used",
83                compact_str::format_compact!("{prefix}last_used"),
84            ),
85            (
86                "user_api_keys.expires",
87                compact_str::format_compact!("{prefix}expires"),
88            ),
89            (
90                "user_api_keys.created",
91                compact_str::format_compact!("{prefix}created"),
92            ),
93        ])
94    }
95
96    #[inline]
97    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
98        let prefix = prefix.unwrap_or_default();
99
100        Ok(Self {
101            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
102            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
103            key_start: row.try_get(compact_str::format_compact!("{prefix}key_start").as_str())?,
104            allowed_ips: row
105                .try_get(compact_str::format_compact!("{prefix}allowed_ips").as_str())?,
106            user_permissions: Arc::new(
107                row.try_get(compact_str::format_compact!("{prefix}user_permissions").as_str())?,
108            ),
109            admin_permissions: Arc::new(
110                row.try_get(compact_str::format_compact!("{prefix}admin_permissions").as_str())?,
111            ),
112            server_permissions: Arc::new(
113                row.try_get(compact_str::format_compact!("{prefix}server_permissions").as_str())?,
114            ),
115            last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
116            expires: row.try_get(compact_str::format_compact!("{prefix}expires").as_str())?,
117            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
118            extension_data: Self::map_extensions(prefix, row)?,
119        })
120    }
121}
122
123impl UserApiKey {
124    pub async fn by_user_uuid_uuid(
125        database: &crate::database::Database,
126        user_uuid: uuid::Uuid,
127        uuid: uuid::Uuid,
128    ) -> Result<Option<Self>, crate::database::DatabaseError> {
129        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
130            r#"
131            SELECT {}
132            FROM user_api_keys
133            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())
134            "#,
135            Self::columns_sql(None)
136        )))
137        .bind(user_uuid)
138        .bind(uuid)
139        .fetch_optional(database.read())
140        .await?;
141
142        row.try_map(|row| Self::map(None, &row))
143    }
144
145    pub async fn by_user_uuid_key_start(
146        database: &crate::database::Database,
147        user_uuid: uuid::Uuid,
148        key_start: &str,
149    ) -> Result<Option<Self>, crate::database::DatabaseError> {
150        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
151            r#"
152            SELECT {}
153            FROM user_api_keys
154            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())
155            "#,
156            Self::columns_sql(None)
157        )))
158        .bind(user_uuid)
159        .bind(key_start)
160        .fetch_optional(database.read())
161        .await?;
162
163        row.try_map(|row| Self::map(None, &row))
164    }
165
166    pub async fn by_user_uuid_with_pagination(
167        database: &crate::database::Database,
168        user_uuid: uuid::Uuid,
169        page: i64,
170        per_page: i64,
171        search: Option<&str>,
172    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
173        let offset = (page - 1) * per_page;
174
175        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
176            r#"
177            SELECT {}, COUNT(*) OVER() AS total_count
178            FROM user_api_keys
179            WHERE user_api_keys.user_uuid = $1 AND ($2 IS NULL OR user_api_keys.name ILIKE '%' || $2 || '%')
180                AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
181            ORDER BY user_api_keys.created
182            LIMIT $3 OFFSET $4
183            "#,
184            Self::columns_sql(None)
185        )))
186        .bind(user_uuid)
187        .bind(search)
188        .bind(per_page)
189        .bind(offset)
190        .fetch_all(database.read())
191        .await?;
192
193        Ok(super::Pagination {
194            total: rows
195                .first()
196                .map_or(Ok(0), |row| row.try_get("total_count"))?,
197            per_page,
198            page,
199            data: rows
200                .into_iter()
201                .map(|row| Self::map(None, &row))
202                .try_collect_vec()?,
203        })
204    }
205
206    pub async fn delete_expired(database: &crate::database::Database) -> Result<u64, sqlx::Error> {
207        Ok(sqlx::query(
208            r#"
209            DELETE FROM user_api_keys
210            WHERE user_api_keys.expires IS NOT NULL AND user_api_keys.expires < NOW()
211            "#,
212        )
213        .execute(database.write())
214        .await?
215        .rows_affected())
216    }
217
218    pub async fn update_last_used(&self, database: &Arc<crate::database::Database>) {
219        let uuid = self.uuid;
220        let now = chrono::Utc::now().naive_utc();
221
222        database
223            .batch_action("update_user_api_key", uuid, {
224                let database = database.clone();
225
226                async move {
227                    sqlx::query!(
228                        "UPDATE user_api_keys
229                        SET last_used = $2
230                        WHERE user_api_keys.uuid = $1",
231                        uuid,
232                        now
233                    )
234                    .execute(database.write())
235                    .await?;
236
237                    Ok(())
238                }
239            })
240            .await;
241    }
242
243    pub async fn recreate(
244        &mut self,
245        database: &crate::database::Database,
246    ) -> Result<String, crate::database::DatabaseError> {
247        let new_key = format!(
248            "c7sp_{}",
249            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
250        );
251
252        sqlx::query(
253            r#"
254            UPDATE user_api_keys
255            SET key_start = $1, key = crypt($2, gen_salt('bf', 12))
256            WHERE user_api_keys.uuid = $3
257            "#,
258        )
259        .bind(&new_key[0..16])
260        .bind(&new_key)
261        .bind(self.uuid)
262        .execute(database.write())
263        .await?;
264
265        self.key_start = new_key[0..16].into();
266
267        Ok(new_key)
268    }
269
270    pub async fn count_by_user_uuid(
271        database: &crate::database::Database,
272        user_uuid: uuid::Uuid,
273    ) -> Result<i64, sqlx::Error> {
274        sqlx::query_scalar(
275            r#"
276            SELECT COUNT(*)
277            FROM user_api_keys
278            WHERE user_api_keys.user_uuid = $1
279            "#,
280        )
281        .bind(user_uuid)
282        .fetch_one(database.read())
283        .await
284    }
285}
286
287#[async_trait::async_trait]
288impl IntoApiObject for UserApiKey {
289    type ApiObject = ApiUserApiKey;
290    type ExtraArgs<'a> = ();
291
292    async fn into_api_object<'a>(
293        self,
294        state: &crate::State,
295        _args: Self::ExtraArgs<'a>,
296    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
297        let api_object = ApiUserApiKey::init_hooks(&self, state).await?;
298
299        let api_object = finish_extendible!(
300            ApiUserApiKey {
301                uuid: self.uuid,
302                name: self.name,
303                key_start: self.key_start,
304                allowed_ips: self.allowed_ips,
305                user_permissions: self.user_permissions,
306                admin_permissions: self.admin_permissions,
307                server_permissions: self.server_permissions,
308                last_used: self.last_used.map(|dt| dt.and_utc()),
309                expires: self.expires.map(|dt| dt.and_utc()),
310                created: self.created.and_utc(),
311            },
312            api_object,
313            state
314        )?;
315
316        Ok(api_object)
317    }
318}
319
320#[derive(ToSchema, Deserialize, Validate)]
321pub struct CreateUserApiKeyOptions {
322    #[garde(skip)]
323    pub user_uuid: uuid::Uuid,
324
325    #[garde(length(chars, min = 3, max = 31))]
326    #[schema(min_length = 3, max_length = 31)]
327    pub name: compact_str::CompactString,
328    #[garde(skip)]
329    #[schema(value_type = Vec<String>)]
330    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
331
332    #[garde(custom(crate::permissions::validate_user_permissions))]
333    pub user_permissions: Vec<compact_str::CompactString>,
334    #[garde(custom(crate::permissions::validate_admin_permissions))]
335    pub admin_permissions: Vec<compact_str::CompactString>,
336    #[garde(custom(crate::permissions::validate_server_permissions))]
337    pub server_permissions: Vec<compact_str::CompactString>,
338
339    #[garde(inner(custom(crate::utils::validate_time_in_future)))]
340    pub expires: Option<chrono::DateTime<chrono::Utc>>,
341}
342
343#[async_trait::async_trait]
344impl CreatableModel for UserApiKey {
345    type CreateOptions<'a> = CreateUserApiKeyOptions;
346    type CreateResult = (String, Self);
347
348    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
349        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserApiKey>> =
350            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
351
352        &CREATE_LISTENERS
353    }
354
355    async fn create_with_transaction(
356        state: &crate::State,
357        mut options: Self::CreateOptions<'_>,
358        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
359    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
360        options.validate()?;
361
362        let key = format!(
363            "c7sp_{}",
364            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
365        );
366
367        let mut query_builder = InsertQueryBuilder::new("user_api_keys");
368
369        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
370
371        query_builder
372            .set("user_uuid", options.user_uuid)
373            .set("name", &options.name)
374            .set("key_start", &key[0..16])
375            .set_expr("key", "crypt($1, gen_salt('bf', 12))", vec![&key])
376            .set("allowed_ips", &options.allowed_ips)
377            .set("user_permissions", &options.user_permissions)
378            .set("admin_permissions", &options.admin_permissions)
379            .set("server_permissions", &options.server_permissions)
380            .set("expires", options.expires.map(|d| d.naive_utc()));
381
382        let row = query_builder
383            .returning(&Self::columns_sql(None))
384            .fetch_one(&mut **transaction)
385            .await?;
386        let user_api_key = Self::map(None, &row)?;
387
388        let mut result = (key, user_api_key);
389
390        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
391
392        Ok(result)
393    }
394}
395
396#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
397pub struct UpdateUserApiKeyOptions {
398    #[garde(length(chars, min = 3, max = 31))]
399    #[schema(min_length = 3, max_length = 31)]
400    pub name: Option<compact_str::CompactString>,
401    #[garde(skip)]
402    #[schema(value_type = Vec<String>)]
403    pub allowed_ips: Option<Vec<sqlx::types::ipnetwork::IpNetwork>>,
404
405    #[garde(inner(custom(crate::permissions::validate_user_permissions)))]
406    pub user_permissions: Option<Vec<compact_str::CompactString>>,
407    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
408    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
409    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
410    pub server_permissions: Option<Vec<compact_str::CompactString>>,
411
412    #[garde(inner(inner(custom(crate::utils::validate_time_in_future))))]
413    #[serde(
414        default,
415        skip_serializing_if = "Option::is_none",
416        with = "::serde_with::rust::double_option"
417    )]
418    pub expires: Option<Option<chrono::DateTime<chrono::Utc>>>,
419}
420
421#[async_trait::async_trait]
422impl UpdatableModel for UserApiKey {
423    type UpdateOptions = UpdateUserApiKeyOptions;
424
425    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
426        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserApiKey>> =
427            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
428
429        &UPDATE_LISTENERS
430    }
431
432    async fn update_with_transaction(
433        &mut self,
434        state: &crate::State,
435        mut options: Self::UpdateOptions,
436        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
437    ) -> Result<(), crate::database::DatabaseError> {
438        options.validate()?;
439
440        let mut query_builder = UpdateQueryBuilder::new("user_api_keys");
441
442        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
443            .await?;
444
445        query_builder
446            .set("name", options.name.as_ref())
447            .set("allowed_ips", options.allowed_ips.as_ref())
448            .set("user_permissions", options.user_permissions.as_ref())
449            .set("admin_permissions", options.admin_permissions.as_ref())
450            .set("server_permissions", options.server_permissions.as_ref())
451            .set(
452                "expires",
453                options
454                    .expires
455                    .as_ref()
456                    .map(|e| e.as_ref().map(|d| d.naive_utc())),
457            )
458            .where_eq("uuid", self.uuid);
459
460        query_builder.execute(&mut **transaction).await?;
461
462        if let Some(name) = options.name {
463            self.name = name;
464        }
465        if let Some(allowed_ips) = options.allowed_ips {
466            self.allowed_ips = allowed_ips;
467        }
468        if let Some(user_permissions) = options.user_permissions {
469            self.user_permissions = Arc::new(user_permissions);
470        }
471        if let Some(admin_permissions) = options.admin_permissions {
472            self.admin_permissions = Arc::new(admin_permissions);
473        }
474        if let Some(server_permissions) = options.server_permissions {
475            self.server_permissions = Arc::new(server_permissions);
476        }
477        if let Some(expires) = options.expires {
478            self.expires = expires.map(|d| d.naive_utc());
479        }
480
481        self.run_after_update_handlers(state, transaction).await?;
482
483        Ok(())
484    }
485}
486
487#[async_trait::async_trait]
488impl ByUuid for UserApiKey {
489    async fn by_uuid(
490        database: &crate::database::Database,
491        uuid: uuid::Uuid,
492    ) -> Result<Self, crate::database::DatabaseError> {
493        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
494            r#"
495            SELECT {}
496            FROM user_api_keys
497            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
498            "#,
499            Self::columns_sql(None)
500        )))
501        .bind(uuid)
502        .fetch_one(database.read())
503        .await?;
504
505        Self::map(None, &row)
506    }
507
508    async fn by_uuid_with_transaction(
509        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
510        uuid: uuid::Uuid,
511    ) -> Result<Self, crate::database::DatabaseError> {
512        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
513            r#"
514            SELECT {}
515            FROM user_api_keys
516            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
517            "#,
518            Self::columns_sql(None)
519        )))
520        .bind(uuid)
521        .fetch_one(&mut **transaction)
522        .await?;
523
524        Self::map(None, &row)
525    }
526}
527
528#[async_trait::async_trait]
529impl DeletableModel for UserApiKey {
530    type DeleteOptions = ();
531
532    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
533        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserApiKey>> =
534            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
535
536        &DELETE_LISTENERS
537    }
538
539    async fn delete_with_transaction(
540        &self,
541        state: &crate::State,
542        options: Self::DeleteOptions,
543        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
544    ) -> Result<(), anyhow::Error> {
545        self.run_delete_handlers(&options, state, transaction)
546            .await?;
547
548        sqlx::query(
549            r#"
550            DELETE FROM user_api_keys
551            WHERE user_api_keys.uuid = $1
552            "#,
553        )
554        .bind(self.uuid)
555        .execute(&mut **transaction)
556        .await?;
557
558        self.run_after_delete_handlers(&options, state, transaction)
559            .await?;
560
561        Ok(())
562    }
563}
564
565#[schema_extension_derive::extendible]
566#[init_args(UserApiKey, crate::State)]
567#[hook_args(crate::State)]
568#[derive(ToSchema, Serialize)]
569#[schema(title = "UserApiKey")]
570pub struct ApiUserApiKey {
571    pub uuid: uuid::Uuid,
572
573    pub name: compact_str::CompactString,
574    pub key_start: compact_str::CompactString,
575    #[schema(value_type = Vec<String>)]
576    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
577
578    pub user_permissions: Arc<Vec<compact_str::CompactString>>,
579    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
580    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
581
582    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
583    pub expires: Option<chrono::DateTime<chrono::Utc>>,
584    pub created: chrono::DateTime<chrono::Utc>,
585}