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(|| parking_lot::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(sqlx::AssertSqlSafe(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 super::PaginatedQueryBuilder::new(
109 Self::columns_sql(None),
110 format!(
111 r#"
112 FROM user_ssh_keys
113 WHERE user_ssh_keys.user_uuid = $1 AND {search}
114 "#,
115 search = super::search_sql(2, &["user_ssh_keys.name"], &["user_ssh_keys.uuid"]),
116 ),
117 "user_ssh_keys.created, user_ssh_keys.uuid",
118 )
119 .bind(user_uuid)
120 .bind(search)
121 .fetch(database, page, per_page)
122 .await
123 }
124
125 pub async fn count_by_user_uuid(
126 database: &crate::database::Database,
127 user_uuid: uuid::Uuid,
128 ) -> Result<i64, sqlx::Error> {
129 sqlx::query_scalar(
130 r#"
131 SELECT COUNT(*)
132 FROM user_ssh_keys
133 WHERE user_ssh_keys.user_uuid = $1
134 "#,
135 )
136 .bind(user_uuid)
137 .fetch_one(database.read())
138 .await
139 }
140}
141
142#[async_trait::async_trait]
143impl IntoApiObject for UserSshKey {
144 type ApiObject = ApiUserSshKey;
145 type ExtraArgs<'a> = ();
146
147 async fn into_api_object<'a>(
148 self,
149 state: &crate::State,
150 _args: Self::ExtraArgs<'a>,
151 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
152 let api_object = ApiUserSshKey::init_hooks(&self, state).await?;
153
154 let api_object = finish_extendible!(
155 ApiUserSshKey {
156 uuid: self.uuid,
157 name: self.name,
158 fingerprint: self.fingerprint,
159 created: self.created.and_utc(),
160 },
161 api_object,
162 state
163 )?;
164
165 Ok(api_object)
166 }
167}
168
169#[derive(ToSchema, Deserialize, Validate)]
170pub struct CreateUserSshKeyOptions {
171 #[garde(skip)]
172 pub user_uuid: uuid::Uuid,
173
174 #[garde(length(chars, min = 3, max = 31))]
175 #[schema(min_length = 3, max_length = 31)]
176 pub name: compact_str::CompactString,
177
178 #[garde(skip)]
179 #[schema(value_type = String)]
180 #[serde(deserialize_with = "crate::deserialize::deserialize_public_key")]
181 pub public_key: russh::keys::PublicKey,
182}
183
184#[async_trait::async_trait]
185impl CreatableModel for UserSshKey {
186 type CreateOptions<'a> = CreateUserSshKeyOptions;
187 type CreateResult = Self;
188
189 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
190 static CREATE_LISTENERS: LazyLock<CreateListenerList<UserSshKey>> =
191 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
192
193 &CREATE_LISTENERS
194 }
195
196 async fn create_with_transaction(
197 state: &crate::State,
198 mut options: Self::CreateOptions<'_>,
199 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
200 ) -> Result<Self, crate::database::DatabaseError> {
201 options.validate()?;
202
203 let mut query_builder = InsertQueryBuilder::new("user_ssh_keys");
204
205 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
206
207 query_builder
208 .set("user_uuid", options.user_uuid)
209 .set("name", &options.name)
210 .set(
211 "fingerprint",
212 options
213 .public_key
214 .fingerprint(russh::keys::HashAlg::Sha256)
215 .to_string(),
216 )
217 .set(
218 "public_key",
219 options.public_key.to_bytes().map_err(anyhow::Error::from)?,
220 );
221
222 let row = query_builder
223 .returning(&Self::columns_sql(None))
224 .fetch_one(&mut **transaction)
225 .await?;
226 let mut ssh_key = Self::map(None, &row)?;
227
228 Self::run_after_create_handlers(&mut ssh_key, &options, state, transaction).await?;
229
230 Ok(ssh_key)
231 }
232}
233
234#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
235pub struct UpdateUserSshKeyOptions {
236 #[garde(length(chars, min = 3, max = 31))]
237 #[schema(min_length = 3, max_length = 31)]
238 pub name: Option<compact_str::CompactString>,
239}
240
241#[async_trait::async_trait]
242impl UpdatableModel for UserSshKey {
243 type UpdateOptions = UpdateUserSshKeyOptions;
244
245 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
246 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserSshKey>> =
247 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
248
249 &UPDATE_LISTENERS
250 }
251
252 async fn update_with_transaction(
253 &mut self,
254 state: &crate::State,
255 mut options: Self::UpdateOptions,
256 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
257 ) -> Result<(), crate::database::DatabaseError> {
258 options.validate()?;
259
260 let mut query_builder = UpdateQueryBuilder::new("user_ssh_keys");
261
262 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
263 .await?;
264
265 query_builder
266 .set("name", options.name.as_ref())
267 .where_eq("uuid", self.uuid);
268
269 query_builder.execute(&mut **transaction).await?;
270
271 if let Some(name) = options.name {
272 self.name = name;
273 }
274
275 self.run_after_update_handlers(state, transaction).await?;
276
277 Ok(())
278 }
279}
280
281#[async_trait::async_trait]
282impl DeletableModel for UserSshKey {
283 type DeleteOptions = ();
284
285 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
286 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserSshKey>> =
287 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
288
289 &DELETE_LISTENERS
290 }
291
292 async fn delete_with_transaction(
293 &self,
294 state: &crate::State,
295 options: Self::DeleteOptions,
296 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
297 ) -> Result<(), anyhow::Error> {
298 self.run_delete_handlers(&options, state, transaction)
299 .await?;
300
301 sqlx::query(
302 r#"
303 DELETE FROM user_ssh_keys
304 WHERE user_ssh_keys.uuid = $1
305 "#,
306 )
307 .bind(self.uuid)
308 .execute(&mut **transaction)
309 .await?;
310
311 self.run_after_delete_handlers(&options, state, transaction)
312 .await?;
313
314 Ok(())
315 }
316}
317
318#[schema_extension_derive::extendible]
319#[init_args(UserSshKey, crate::State)]
320#[hook_args(crate::State)]
321#[derive(ToSchema, Serialize)]
322#[schema(title = "UserSshKey")]
323pub struct ApiUserSshKey {
324 pub uuid: uuid::Uuid,
325
326 pub name: compact_str::CompactString,
327 pub fingerprint: compact_str::CompactString,
328
329 pub created: chrono::DateTime<chrono::Utc>,
330}