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}