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