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}