1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9 collections::BTreeMap,
10 sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(Serialize, Deserialize)]
15pub struct UserSshKey {
16 pub uuid: uuid::Uuid,
17
18 pub name: compact_str::CompactString,
19 pub fingerprint: compact_str::CompactString,
20
21 pub created: chrono::NaiveDateTime,
22
23 extension_data: super::ModelExtensionData,
24}
25
26impl BaseModel for UserSshKey {
27 const NAME: &'static str = "user_ssh_key";
28
29 fn get_extension_list() -> &'static super::ModelExtensionList {
30 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
31 LazyLock::new(|| std::sync::RwLock::new(Vec::new()));
32
33 &EXTENSIONS
34 }
35
36 fn get_extension_data(&self) -> &super::ModelExtensionData {
37 &self.extension_data
38 }
39
40 #[inline]
41 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
42 let prefix = prefix.unwrap_or_default();
43
44 BTreeMap::from([
45 (
46 "user_ssh_keys.uuid",
47 compact_str::format_compact!("{prefix}uuid"),
48 ),
49 (
50 "user_ssh_keys.name",
51 compact_str::format_compact!("{prefix}name"),
52 ),
53 (
54 "user_ssh_keys.fingerprint",
55 compact_str::format_compact!("{prefix}fingerprint"),
56 ),
57 (
58 "user_ssh_keys.created",
59 compact_str::format_compact!("{prefix}created"),
60 ),
61 ])
62 }
63
64 #[inline]
65 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
66 let prefix = prefix.unwrap_or_default();
67
68 Ok(Self {
69 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
70 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
71 fingerprint: row
72 .try_get(compact_str::format_compact!("{prefix}fingerprint").as_str())?,
73 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
74 extension_data: Self::map_extensions(prefix, row)?,
75 })
76 }
77}
78
79impl UserSshKey {
80 pub async fn by_user_uuid_uuid(
81 database: &crate::database::Database,
82 user_uuid: uuid::Uuid,
83 uuid: uuid::Uuid,
84 ) -> Result<Option<Self>, crate::database::DatabaseError> {
85 let row = sqlx::query(&format!(
86 r#"
87 SELECT {}
88 FROM user_ssh_keys
89 WHERE user_ssh_keys.user_uuid = $1 AND user_ssh_keys.uuid = $2
90 "#,
91 Self::columns_sql(None)
92 ))
93 .bind(user_uuid)
94 .bind(uuid)
95 .fetch_optional(database.read())
96 .await?;
97
98 row.try_map(|row| Self::map(None, &row))
99 }
100
101 pub async fn by_user_uuid_with_pagination(
102 database: &crate::database::Database,
103 user_uuid: uuid::Uuid,
104 page: i64,
105 per_page: i64,
106 search: Option<&str>,
107 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
108 let offset = (page - 1) * per_page;
109
110 let rows = sqlx::query(&format!(
111 r#"
112 SELECT {}, COUNT(*) OVER() AS total_count
113 FROM user_ssh_keys
114 WHERE user_ssh_keys.user_uuid = $1 AND ($2 IS NULL OR user_ssh_keys.name ILIKE '%' || $2 || '%')
115 ORDER BY user_ssh_keys.created
116 LIMIT $3 OFFSET $4
117 "#,
118 Self::columns_sql(None)
119 ))
120 .bind(user_uuid)
121 .bind(search)
122 .bind(per_page)
123 .bind(offset)
124 .fetch_all(database.read())
125 .await?;
126
127 Ok(super::Pagination {
128 total: rows
129 .first()
130 .map_or(Ok(0), |row| row.try_get("total_count"))?,
131 per_page,
132 page,
133 data: rows
134 .into_iter()
135 .map(|row| Self::map(None, &row))
136 .try_collect_vec()?,
137 })
138 }
139
140 pub async fn count_by_user_uuid(
141 database: &crate::database::Database,
142 user_uuid: uuid::Uuid,
143 ) -> Result<i64, sqlx::Error> {
144 sqlx::query_scalar(
145 r#"
146 SELECT COUNT(*)
147 FROM user_ssh_keys
148 WHERE user_ssh_keys.user_uuid = $1
149 "#,
150 )
151 .bind(user_uuid)
152 .fetch_one(database.read())
153 .await
154 }
155}
156
157#[async_trait::async_trait]
158impl IntoApiObject for UserSshKey {
159 type ApiObject = ApiUserSshKey;
160 type ExtraArgs<'a> = ();
161
162 async fn into_api_object<'a>(
163 self,
164 state: &crate::State,
165 _args: Self::ExtraArgs<'a>,
166 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
167 let api_object = ApiUserSshKey::init_hooks(&self, state).await?;
168
169 let api_object = finish_extendible!(
170 ApiUserSshKey {
171 uuid: self.uuid,
172 name: self.name,
173 fingerprint: self.fingerprint,
174 created: self.created.and_utc(),
175 },
176 api_object,
177 state
178 )?;
179
180 Ok(api_object)
181 }
182}
183
184#[derive(ToSchema, Deserialize, Validate)]
185pub struct CreateUserSshKeyOptions {
186 #[garde(skip)]
187 pub user_uuid: uuid::Uuid,
188
189 #[garde(length(chars, min = 3, max = 31))]
190 #[schema(min_length = 3, max_length = 31)]
191 pub name: compact_str::CompactString,
192
193 #[garde(skip)]
194 #[schema(value_type = String)]
195 #[serde(deserialize_with = "crate::deserialize::deserialize_public_key")]
196 pub public_key: russh::keys::PublicKey,
197}
198
199#[async_trait::async_trait]
200impl CreatableModel for UserSshKey {
201 type CreateOptions<'a> = CreateUserSshKeyOptions;
202 type CreateResult = Self;
203
204 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
205 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserSshKey>> =
206 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
207
208 &CREATE_LISTENERS
209 }
210
211 async fn create_with_transaction(
212 state: &crate::State,
213 mut options: Self::CreateOptions<'_>,
214 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
215 ) -> Result<Self, crate::database::DatabaseError> {
216 options.validate()?;
217
218 let mut query_builder = InsertQueryBuilder::new("user_ssh_keys");
219
220 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
221
222 query_builder
223 .set("user_uuid", options.user_uuid)
224 .set("name", &options.name)
225 .set(
226 "fingerprint",
227 options
228 .public_key
229 .fingerprint(russh::keys::HashAlg::Sha256)
230 .to_string(),
231 )
232 .set(
233 "public_key",
234 options.public_key.to_bytes().map_err(anyhow::Error::from)?,
235 );
236
237 let row = query_builder
238 .returning(&Self::columns_sql(None))
239 .fetch_one(&mut **transaction)
240 .await?;
241 let mut ssh_key = Self::map(None, &row)?;
242
243 Self::run_after_create_handlers(&mut ssh_key, &options, state, transaction).await?;
244
245 Ok(ssh_key)
246 }
247}
248
249#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
250pub struct UpdateUserSshKeyOptions {
251 #[garde(length(chars, min = 3, max = 31))]
252 #[schema(min_length = 3, max_length = 31)]
253 pub name: Option<compact_str::CompactString>,
254}
255
256#[async_trait::async_trait]
257impl UpdatableModel for UserSshKey {
258 type UpdateOptions = UpdateUserSshKeyOptions;
259
260 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
261 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserSshKey>> =
262 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
263
264 &UPDATE_LISTENERS
265 }
266
267 async fn update_with_transaction(
268 &mut self,
269 state: &crate::State,
270 mut options: Self::UpdateOptions,
271 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
272 ) -> Result<(), crate::database::DatabaseError> {
273 options.validate()?;
274
275 let mut query_builder = UpdateQueryBuilder::new("user_ssh_keys");
276
277 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
278 .await?;
279
280 query_builder
281 .set("name", options.name.as_ref())
282 .where_eq("uuid", self.uuid);
283
284 query_builder.execute(&mut **transaction).await?;
285
286 if let Some(name) = options.name {
287 self.name = name;
288 }
289
290 self.run_after_update_handlers(state, transaction).await?;
291
292 Ok(())
293 }
294}
295
296#[async_trait::async_trait]
297impl DeletableModel for UserSshKey {
298 type DeleteOptions = ();
299
300 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
301 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserSshKey>> =
302 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
303
304 &DELETE_LISTENERS
305 }
306
307 async fn delete_with_transaction(
308 &self,
309 state: &crate::State,
310 options: Self::DeleteOptions,
311 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
312 ) -> Result<(), anyhow::Error> {
313 self.run_delete_handlers(&options, state, transaction)
314 .await?;
315
316 sqlx::query(
317 r#"
318 DELETE FROM user_ssh_keys
319 WHERE user_ssh_keys.uuid = $1
320 "#,
321 )
322 .bind(self.uuid)
323 .execute(&mut **transaction)
324 .await?;
325
326 self.run_after_delete_handlers(&options, state, transaction)
327 .await?;
328
329 Ok(())
330 }
331}
332
333#[schema_extension_derive::extendible]
334#[init_args(UserSshKey, crate::State)]
335#[hook_args(crate::State)]
336#[derive(ToSchema, Serialize)]
337#[schema(title = "UserSshKey")]
338pub struct ApiUserSshKey {
339 pub uuid: uuid::Uuid,
340
341 pub name: compact_str::CompactString,
342 pub fingerprint: compact_str::CompactString,
343
344 pub created: chrono::DateTime<chrono::Utc>,
345}