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(|| 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}