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}