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
32impl BaseModel for UserApiKey {
33    const NAME: &'static str = "user_api_key";
34
35    #[inline]
36    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
37        let prefix = prefix.unwrap_or_default();
38
39        BTreeMap::from([
40            (
41                "user_api_keys.uuid",
42                compact_str::format_compact!("{prefix}uuid"),
43            ),
44            (
45                "user_api_keys.name",
46                compact_str::format_compact!("{prefix}name"),
47            ),
48            (
49                "user_api_keys.key_start",
50                compact_str::format_compact!("{prefix}key_start"),
51            ),
52            (
53                "user_api_keys.allowed_ips",
54                compact_str::format_compact!("{prefix}allowed_ips"),
55            ),
56            (
57                "user_api_keys.user_permissions",
58                compact_str::format_compact!("{prefix}user_permissions"),
59            ),
60            (
61                "user_api_keys.admin_permissions",
62                compact_str::format_compact!("{prefix}admin_permissions"),
63            ),
64            (
65                "user_api_keys.server_permissions",
66                compact_str::format_compact!("{prefix}server_permissions"),
67            ),
68            (
69                "user_api_keys.last_used",
70                compact_str::format_compact!("{prefix}last_used"),
71            ),
72            (
73                "user_api_keys.expires",
74                compact_str::format_compact!("{prefix}expires"),
75            ),
76            (
77                "user_api_keys.created",
78                compact_str::format_compact!("{prefix}created"),
79            ),
80        ])
81    }
82
83    #[inline]
84    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
85        let prefix = prefix.unwrap_or_default();
86
87        Ok(Self {
88            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
89            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
90            key_start: row.try_get(compact_str::format_compact!("{prefix}key_start").as_str())?,
91            allowed_ips: row
92                .try_get(compact_str::format_compact!("{prefix}allowed_ips").as_str())?,
93            user_permissions: Arc::new(
94                row.try_get(compact_str::format_compact!("{prefix}user_permissions").as_str())?,
95            ),
96            admin_permissions: Arc::new(
97                row.try_get(compact_str::format_compact!("{prefix}admin_permissions").as_str())?,
98            ),
99            server_permissions: Arc::new(
100                row.try_get(compact_str::format_compact!("{prefix}server_permissions").as_str())?,
101            ),
102            last_used: row.try_get(compact_str::format_compact!("{prefix}last_used").as_str())?,
103            expires: row.try_get(compact_str::format_compact!("{prefix}expires").as_str())?,
104            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
105        })
106    }
107}
108
109impl UserApiKey {
110    pub async fn by_user_uuid_uuid(
111        database: &crate::database::Database,
112        user_uuid: uuid::Uuid,
113        uuid: uuid::Uuid,
114    ) -> Result<Option<Self>, crate::database::DatabaseError> {
115        let row = sqlx::query(&format!(
116            r#"
117            SELECT {}
118            FROM user_api_keys
119            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())
120            "#,
121            Self::columns_sql(None)
122        ))
123        .bind(user_uuid)
124        .bind(uuid)
125        .fetch_optional(database.read())
126        .await?;
127
128        row.try_map(|row| Self::map(None, &row))
129    }
130
131    pub async fn by_user_uuid_with_pagination(
132        database: &crate::database::Database,
133        user_uuid: uuid::Uuid,
134        page: i64,
135        per_page: i64,
136        search: Option<&str>,
137    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
138        let offset = (page - 1) * per_page;
139
140        let rows = sqlx::query(&format!(
141            r#"
142            SELECT {}, COUNT(*) OVER() AS total_count
143            FROM user_api_keys
144            WHERE user_api_keys.user_uuid = $1 AND ($2 IS NULL OR user_api_keys.name ILIKE '%' || $2 || '%')
145                AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
146            ORDER BY user_api_keys.created
147            LIMIT $3 OFFSET $4
148            "#,
149            Self::columns_sql(None)
150        ))
151        .bind(user_uuid)
152        .bind(search)
153        .bind(per_page)
154        .bind(offset)
155        .fetch_all(database.read())
156        .await?;
157
158        Ok(super::Pagination {
159            total: rows
160                .first()
161                .map_or(Ok(0), |row| row.try_get("total_count"))?,
162            per_page,
163            page,
164            data: rows
165                .into_iter()
166                .map(|row| Self::map(None, &row))
167                .try_collect_vec()?,
168        })
169    }
170
171    pub async fn delete_expired(database: &crate::database::Database) -> Result<u64, sqlx::Error> {
172        Ok(sqlx::query(
173            r#"
174            DELETE FROM user_api_keys
175            WHERE user_api_keys.expires IS NOT NULL AND user_api_keys.expires < NOW()
176            "#,
177        )
178        .execute(database.write())
179        .await?
180        .rows_affected())
181    }
182
183    pub async fn update_last_used(&self, database: &Arc<crate::database::Database>) {
184        let uuid = self.uuid;
185        let now = chrono::Utc::now().naive_utc();
186
187        database
188            .batch_action("update_user_api_key", uuid, {
189                let database = database.clone();
190
191                async move {
192                    sqlx::query!(
193                        "UPDATE user_api_keys
194                        SET last_used = $2
195                        WHERE user_api_keys.uuid = $1",
196                        uuid,
197                        now
198                    )
199                    .execute(database.write())
200                    .await?;
201
202                    Ok(())
203                }
204            })
205            .await;
206    }
207
208    #[inline]
209    pub fn into_api_object(self) -> ApiUserApiKey {
210        ApiUserApiKey {
211            uuid: self.uuid,
212            name: self.name,
213            key_start: self.key_start,
214            allowed_ips: self.allowed_ips,
215            user_permissions: self.user_permissions,
216            admin_permissions: self.admin_permissions,
217            server_permissions: self.server_permissions,
218            last_used: self.last_used.map(|dt| dt.and_utc()),
219            expires: self.expires.map(|dt| dt.and_utc()),
220            created: self.created.and_utc(),
221        }
222    }
223}
224
225#[derive(ToSchema, Deserialize, Validate)]
226pub struct CreateUserApiKeyOptions {
227    #[garde(skip)]
228    pub user_uuid: uuid::Uuid,
229
230    #[garde(length(chars, min = 3, max = 31))]
231    #[schema(min_length = 3, max_length = 31)]
232    pub name: compact_str::CompactString,
233    #[garde(skip)]
234    #[schema(value_type = Vec<String>)]
235    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
236
237    #[garde(custom(crate::permissions::validate_user_permissions))]
238    pub user_permissions: Vec<compact_str::CompactString>,
239    #[garde(custom(crate::permissions::validate_admin_permissions))]
240    pub admin_permissions: Vec<compact_str::CompactString>,
241    #[garde(custom(crate::permissions::validate_server_permissions))]
242    pub server_permissions: Vec<compact_str::CompactString>,
243
244    #[garde(inner(custom(crate::utils::validate_time_in_future)))]
245    pub expires: Option<chrono::DateTime<chrono::Utc>>,
246}
247
248#[async_trait::async_trait]
249impl CreatableModel for UserApiKey {
250    type CreateOptions<'a> = CreateUserApiKeyOptions;
251    type CreateResult = (String, Self);
252
253    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
254        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserApiKey>> =
255            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
256
257        &CREATE_LISTENERS
258    }
259
260    async fn create(
261        state: &crate::State,
262        mut options: Self::CreateOptions<'_>,
263    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
264        options.validate()?;
265
266        let key = format!(
267            "c7sp_{}",
268            rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 43)
269        );
270
271        let mut transaction = state.database.write().begin().await?;
272
273        let mut query_builder = InsertQueryBuilder::new("user_api_keys");
274
275        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
276            .await?;
277
278        query_builder
279            .set("user_uuid", options.user_uuid)
280            .set("name", &options.name)
281            .set("key_start", &key[0..16])
282            .set_expr("key", "crypt($1, gen_salt('xdes', 321))", vec![&key])
283            .set("allowed_ips", &options.allowed_ips)
284            .set("user_permissions", &options.user_permissions)
285            .set("admin_permissions", &options.admin_permissions)
286            .set("server_permissions", &options.server_permissions)
287            .set("expires", options.expires.map(|d| d.naive_utc()));
288
289        let row = query_builder
290            .returning(&Self::columns_sql(None))
291            .fetch_one(&mut *transaction)
292            .await?;
293        let user_api_key = Self::map(None, &row)?;
294
295        transaction.commit().await?;
296
297        Ok((key, user_api_key))
298    }
299}
300
301#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
302pub struct UpdateUserApiKeyOptions {
303    #[garde(length(chars, min = 3, max = 31))]
304    #[schema(min_length = 3, max_length = 31)]
305    pub name: Option<compact_str::CompactString>,
306    #[garde(skip)]
307    #[schema(value_type = Vec<String>)]
308    pub allowed_ips: Option<Vec<sqlx::types::ipnetwork::IpNetwork>>,
309
310    #[garde(inner(custom(crate::permissions::validate_user_permissions)))]
311    pub user_permissions: Option<Vec<compact_str::CompactString>>,
312    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
313    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
314    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
315    pub server_permissions: Option<Vec<compact_str::CompactString>>,
316
317    #[garde(inner(inner(custom(crate::utils::validate_time_in_future))))]
318    #[serde(
319        default,
320        skip_serializing_if = "Option::is_none",
321        with = "::serde_with::rust::double_option"
322    )]
323    pub expires: Option<Option<chrono::DateTime<chrono::Utc>>>,
324}
325
326#[async_trait::async_trait]
327impl UpdatableModel for UserApiKey {
328    type UpdateOptions = UpdateUserApiKeyOptions;
329
330    fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
331        static UPDATE_LISTENERS: LazyLock<UpdateListenerList<UserApiKey>> =
332            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
333
334        &UPDATE_LISTENERS
335    }
336
337    async fn update(
338        &mut self,
339        state: &crate::State,
340        mut options: Self::UpdateOptions,
341    ) -> Result<(), crate::database::DatabaseError> {
342        options.validate()?;
343
344        let mut transaction = state.database.write().begin().await?;
345
346        let mut query_builder = UpdateQueryBuilder::new("user_api_keys");
347
348        Self::run_update_handlers(
349            self,
350            &mut options,
351            &mut query_builder,
352            state,
353            &mut transaction,
354        )
355        .await?;
356
357        query_builder
358            .set("name", options.name.as_ref())
359            .set("allowed_ips", options.allowed_ips.as_ref())
360            .set("user_permissions", options.user_permissions.as_ref())
361            .set("admin_permissions", options.admin_permissions.as_ref())
362            .set("server_permissions", options.server_permissions.as_ref())
363            .set(
364                "expires",
365                options
366                    .expires
367                    .as_ref()
368                    .map(|e| e.as_ref().map(|d| d.naive_utc())),
369            )
370            .where_eq("uuid", self.uuid);
371
372        query_builder.execute(&mut *transaction).await?;
373
374        if let Some(name) = options.name {
375            self.name = name;
376        }
377        if let Some(allowed_ips) = options.allowed_ips {
378            self.allowed_ips = allowed_ips;
379        }
380        if let Some(user_permissions) = options.user_permissions {
381            self.user_permissions = Arc::new(user_permissions);
382        }
383        if let Some(admin_permissions) = options.admin_permissions {
384            self.admin_permissions = Arc::new(admin_permissions);
385        }
386        if let Some(server_permissions) = options.server_permissions {
387            self.server_permissions = Arc::new(server_permissions);
388        }
389        if let Some(expires) = options.expires {
390            self.expires = expires.map(|d| d.naive_utc());
391        }
392
393        transaction.commit().await?;
394
395        Ok(())
396    }
397}
398
399#[async_trait::async_trait]
400impl ByUuid for UserApiKey {
401    async fn by_uuid(
402        database: &crate::database::Database,
403        uuid: uuid::Uuid,
404    ) -> Result<Self, crate::database::DatabaseError> {
405        let row = sqlx::query(&format!(
406            r#"
407            SELECT {}
408            FROM user_api_keys
409            WHERE user_api_keys.uuid = $1 AND (user_api_keys.expires IS NULL OR user_api_keys.expires > NOW())
410            "#,
411            Self::columns_sql(None)
412        ))
413        .bind(uuid)
414        .fetch_one(database.read())
415        .await?;
416
417        Self::map(None, &row)
418    }
419}
420
421#[async_trait::async_trait]
422impl DeletableModel for UserApiKey {
423    type DeleteOptions = ();
424
425    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
426        static DELETE_LISTENERS: LazyLock<DeleteListenerList<UserApiKey>> =
427            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
428
429        &DELETE_LISTENERS
430    }
431
432    async fn delete(
433        &self,
434        state: &crate::State,
435        options: Self::DeleteOptions,
436    ) -> Result<(), anyhow::Error> {
437        let mut transaction = state.database.write().begin().await?;
438
439        self.run_delete_handlers(&options, state, &mut transaction)
440            .await?;
441
442        sqlx::query(
443            r#"
444            DELETE FROM user_api_keys
445            WHERE user_api_keys.uuid = $1
446            "#,
447        )
448        .bind(self.uuid)
449        .execute(&mut *transaction)
450        .await?;
451
452        transaction.commit().await?;
453
454        Ok(())
455    }
456}
457
458#[derive(ToSchema, Serialize)]
459#[schema(title = "UserApiKey")]
460pub struct ApiUserApiKey {
461    pub uuid: uuid::Uuid,
462
463    pub name: compact_str::CompactString,
464    pub key_start: compact_str::CompactString,
465    #[schema(value_type = Vec<String>)]
466    pub allowed_ips: Vec<sqlx::types::ipnetwork::IpNetwork>,
467
468    pub user_permissions: Arc<Vec<compact_str::CompactString>>,
469    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
470    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
471
472    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
473    pub expires: Option<chrono::DateTime<chrono::Utc>>,
474    pub created: chrono::DateTime<chrono::Utc>,
475}