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