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(|| std::sync::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(&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_with_pagination(
146        database: &crate::database::Database,
147        user_uuid: uuid::Uuid,
148        page: i64,
149        per_page: i64,
150        search: Option<&str>,
151    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
152        let offset = (page - 1) * per_page;
153
154        let rows = sqlx::query(&format!(
155            r#"
156            SELECT {}, COUNT(*) OVER() AS total_count
157            FROM user_api_keys
158            WHERE user_api_keys.user_uuid = $1 AND ($2 IS NULL OR user_api_keys.name ILIKE '%' || $2 || '%')
159                AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
160            ORDER BY user_api_keys.created
161            LIMIT $3 OFFSET $4
162            "#,
163            Self::columns_sql(None)
164        ))
165        .bind(user_uuid)
166        .bind(search)
167        .bind(per_page)
168        .bind(offset)
169        .fetch_all(database.read())
170        .await?;
171
172        Ok(super::Pagination {
173            total: rows
174                .first()
175                .map_or(Ok(0), |row| row.try_get("total_count"))?,
176            per_page,
177            page,
178            data: rows
179                .into_iter()
180                .map(|row| Self::map(None, &row))
181                .try_collect_vec()?,
182        })
183    }
184
185    pub async fn delete_expired(database: &crate::database::Database) -> Result<u64, sqlx::Error> {
186        Ok(sqlx::query(
187            r#"
188            DELETE FROM user_api_keys
189            WHERE user_api_keys.expires IS NOT NULL AND user_api_keys.expires < NOW()
190            "#,
191        )
192        .execute(database.write())
193        .await?
194        .rows_affected())
195    }
196
197    pub async fn update_last_used(&self, database: &Arc<crate::database::Database>) {
198        let uuid = self.uuid;
199        let now = chrono::Utc::now().naive_utc();
200
201        database
202            .batch_action("update_user_api_key", uuid, {
203                let database = database.clone();
204
205                async move {
206                    sqlx::query!(
207                        "UPDATE user_api_keys
208                        SET last_used = $2
209                        WHERE user_api_keys.uuid = $1",
210                        uuid,
211                        now
212                    )
213                    .execute(database.write())
214                    .await?;
215
216                    Ok(())
217                }
218            })
219            .await;
220    }
221
222    pub async fn recreate(
223        &mut self,
224        database: &crate::database::Database,
225    ) -> Result<String, crate::database::DatabaseError> {
226        let new_key = format!(
227            "c7sp_{}",
228            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
229        );
230
231        sqlx::query(
232            r#"
233            UPDATE user_api_keys
234            SET key_start = $1, key = crypt($2, gen_salt('bf', 12))
235            WHERE user_api_keys.uuid = $3
236            "#,
237        )
238        .bind(&new_key[0..16])
239        .bind(&new_key)
240        .bind(self.uuid)
241        .execute(database.write())
242        .await?;
243
244        self.key_start = new_key[0..16].into();
245
246        Ok(new_key)
247    }
248
249    pub async fn count_by_user_uuid(
250        database: &crate::database::Database,
251        user_uuid: uuid::Uuid,
252    ) -> Result<i64, sqlx::Error> {
253        sqlx::query_scalar(
254            r#"
255            SELECT COUNT(*)
256            FROM user_api_keys
257            WHERE user_api_keys.user_uuid = $1
258            "#,
259        )
260        .bind(user_uuid)
261        .fetch_one(database.read())
262        .await
263    }
264}
265
266#[async_trait::async_trait]
267impl IntoApiObject for UserApiKey {
268    type ApiObject = ApiUserApiKey;
269    type ExtraArgs<'a> = ();
270
271    async fn into_api_object<'a>(
272        self,
273        state: &crate::State,
274        _args: Self::ExtraArgs<'a>,
275    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
276        let api_object = ApiUserApiKey::init_hooks(&self, state).await?;
277
278        let api_object = finish_extendible!(
279            ApiUserApiKey {
280                uuid: self.uuid,
281                name: self.name,
282                key_start: self.key_start,
283                allowed_ips: self.allowed_ips,
284                user_permissions: self.user_permissions,
285                admin_permissions: self.admin_permissions,
286                server_permissions: self.server_permissions,
287                last_used: self.last_used.map(|dt| dt.and_utc()),
288                expires: self.expires.map(|dt| dt.and_utc()),
289                created: self.created.and_utc(),
290            },
291            api_object,
292            state
293        )?;
294
295        Ok(api_object)
296    }
297}
298
299#[derive(ToSchema, Deserialize, Validate)]
300pub struct CreateUserApiKeyOptions {
301    #[garde(skip)]
302    pub user_uuid: uuid::Uuid,
303
304    #[garde(length(chars, min = 3, max = 31))]
305    #[schema(min_length = 3, max_length = 31)]
306    pub name: compact_str::CompactString,
307    #[garde(skip)]
308    #[schema(value_type = Vec<String>)]
309    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
310
311    #[garde(custom(crate::permissions::validate_user_permissions))]
312    pub user_permissions: Vec<compact_str::CompactString>,
313    #[garde(custom(crate::permissions::validate_admin_permissions))]
314    pub admin_permissions: Vec<compact_str::CompactString>,
315    #[garde(custom(crate::permissions::validate_server_permissions))]
316    pub server_permissions: Vec<compact_str::CompactString>,
317
318    #[garde(inner(custom(crate::utils::validate_time_in_future)))]
319    pub expires: Option<chrono::DateTime<chrono::Utc>>,
320}
321
322#[async_trait::async_trait]
323impl CreatableModel for UserApiKey {
324    type CreateOptions<'a> = CreateUserApiKeyOptions;
325    type CreateResult = (String, Self);
326
327    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
328        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserApiKey>> =
329            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
330
331        &CREATE_LISTENERS
332    }
333
334    async fn create_with_transaction(
335        state: &crate::State,
336        mut options: Self::CreateOptions<'_>,
337        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
338    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
339        options.validate()?;
340
341        let key = format!(
342            "c7sp_{}",
343            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
344        );
345
346        let mut query_builder = InsertQueryBuilder::new("user_api_keys");
347
348        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
349
350        query_builder
351            .set("user_uuid", options.user_uuid)
352            .set("name", &options.name)
353            .set("key_start", &key[0..16])
354            .set_expr("key", "crypt($1, gen_salt('bf', 12))", vec![&key])
355            .set("allowed_ips", &options.allowed_ips)
356            .set("user_permissions", &options.user_permissions)
357            .set("admin_permissions", &options.admin_permissions)
358            .set("server_permissions", &options.server_permissions)
359            .set("expires", options.expires.map(|d| d.naive_utc()));
360
361        let row = query_builder
362            .returning(&Self::columns_sql(None))
363            .fetch_one(&mut **transaction)
364            .await?;
365        let user_api_key = Self::map(None, &row)?;
366
367        let mut result = (key, user_api_key);
368
369        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
370
371        Ok(result)
372    }
373}
374
375#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
376pub struct UpdateUserApiKeyOptions {
377    #[garde(length(chars, min = 3, max = 31))]
378    #[schema(min_length = 3, max_length = 31)]
379    pub name: Option<compact_str::CompactString>,
380    #[garde(skip)]
381    #[schema(value_type = Vec<String>)]
382    pub allowed_ips: Option<Vec<sqlx::types::ipnetwork::IpNetwork>>,
383
384    #[garde(inner(custom(crate::permissions::validate_user_permissions)))]
385    pub user_permissions: Option<Vec<compact_str::CompactString>>,
386    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
387    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
388    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
389    pub server_permissions: Option<Vec<compact_str::CompactString>>,
390
391    #[garde(inner(inner(custom(crate::utils::validate_time_in_future))))]
392    #[serde(
393        default,
394        skip_serializing_if = "Option::is_none",
395        with = "::serde_with::rust::double_option"
396    )]
397    pub expires: Option<Option<chrono::DateTime<chrono::Utc>>>,
398}
399
400#[async_trait::async_trait]
401impl UpdatableModel for UserApiKey {
402    type UpdateOptions = UpdateUserApiKeyOptions;
403
404    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
405        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserApiKey>> =
406            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
407
408        &UPDATE_LISTENERS
409    }
410
411    async fn update_with_transaction(
412        &mut self,
413        state: &crate::State,
414        mut options: Self::UpdateOptions,
415        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
416    ) -> Result<(), crate::database::DatabaseError> {
417        options.validate()?;
418
419        let mut query_builder = UpdateQueryBuilder::new("user_api_keys");
420
421        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
422            .await?;
423
424        query_builder
425            .set("name", options.name.as_ref())
426            .set("allowed_ips", options.allowed_ips.as_ref())
427            .set("user_permissions", options.user_permissions.as_ref())
428            .set("admin_permissions", options.admin_permissions.as_ref())
429            .set("server_permissions", options.server_permissions.as_ref())
430            .set(
431                "expires",
432                options
433                    .expires
434                    .as_ref()
435                    .map(|e| e.as_ref().map(|d| d.naive_utc())),
436            )
437            .where_eq("uuid", self.uuid);
438
439        query_builder.execute(&mut **transaction).await?;
440
441        if let Some(name) = options.name {
442            self.name = name;
443        }
444        if let Some(allowed_ips) = options.allowed_ips {
445            self.allowed_ips = allowed_ips;
446        }
447        if let Some(user_permissions) = options.user_permissions {
448            self.user_permissions = Arc::new(user_permissions);
449        }
450        if let Some(admin_permissions) = options.admin_permissions {
451            self.admin_permissions = Arc::new(admin_permissions);
452        }
453        if let Some(server_permissions) = options.server_permissions {
454            self.server_permissions = Arc::new(server_permissions);
455        }
456        if let Some(expires) = options.expires {
457            self.expires = expires.map(|d| d.naive_utc());
458        }
459
460        self.run_after_update_handlers(state, transaction).await?;
461
462        Ok(())
463    }
464}
465
466#[async_trait::async_trait]
467impl ByUuid for UserApiKey {
468    async fn by_uuid(
469        database: &crate::database::Database,
470        uuid: uuid::Uuid,
471    ) -> Result<Self, crate::database::DatabaseError> {
472        let row = sqlx::query(&format!(
473            r#"
474            SELECT {}
475            FROM user_api_keys
476            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
477            "#,
478            Self::columns_sql(None)
479        ))
480        .bind(uuid)
481        .fetch_one(database.read())
482        .await?;
483
484        Self::map(None, &row)
485    }
486
487    async fn by_uuid_with_transaction(
488        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
489        uuid: uuid::Uuid,
490    ) -> Result<Self, crate::database::DatabaseError> {
491        let row = sqlx::query(&format!(
492            r#"
493            SELECT {}
494            FROM user_api_keys
495            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
496            "#,
497            Self::columns_sql(None)
498        ))
499        .bind(uuid)
500        .fetch_one(&mut **transaction)
501        .await?;
502
503        Self::map(None, &row)
504    }
505}
506
507#[async_trait::async_trait]
508impl DeletableModel for UserApiKey {
509    type DeleteOptions = ();
510
511    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
512        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserApiKey>> =
513            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
514
515        &DELETE_LISTENERS
516    }
517
518    async fn delete_with_transaction(
519        &self,
520        state: &crate::State,
521        options: Self::DeleteOptions,
522        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
523    ) -> Result<(), anyhow::Error> {
524        self.run_delete_handlers(&options, state, transaction)
525            .await?;
526
527        sqlx::query(
528            r#"
529            DELETE FROM user_api_keys
530            WHERE user_api_keys.uuid = $1
531            "#,
532        )
533        .bind(self.uuid)
534        .execute(&mut **transaction)
535        .await?;
536
537        self.run_after_delete_handlers(&options, state, transaction)
538            .await?;
539
540        Ok(())
541    }
542}
543
544#[schema_extension_derive::extendible]
545#[init_args(UserApiKey, crate::State)]
546#[hook_args(crate::State)]
547#[derive(ToSchema, Serialize)]
548#[schema(title = "UserApiKey")]
549pub struct ApiUserApiKey {
550    pub uuid: uuid::Uuid,
551
552    pub name: compact_str::CompactString,
553    pub key_start: compact_str::CompactString,
554    #[schema(value_type = Vec<String>)]
555    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
556
557    pub user_permissions: Arc<Vec<compact_str::CompactString>>,
558    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
559    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
560
561    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
562    pub expires: Option<chrono::DateTime<chrono::Utc>>,
563    pub created: chrono::DateTime<chrono::Utc>,
564}