Skip to main content

shared/models/
user_ssh_key.rs

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}