shared/models/
server_subuser.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4    storage::StorageUrlRetriever,
5};
6use garde::Validate;
7use rand::distr::SampleString;
8use serde::{Deserialize, Serialize};
9use sqlx::{Row, postgres::PgRow};
10use std::{
11    collections::BTreeMap,
12    sync::{Arc, LazyLock},
13};
14use utoipa::ToSchema;
15
16#[derive(Serialize, Deserialize)]
17pub struct ServerSubuser {
18    pub user: super::user::User,
19    pub server: Fetchable<super::server::Server>,
20
21    pub permissions: Vec<compact_str::CompactString>,
22    pub ignored_files: Vec<compact_str::CompactString>,
23
24    pub created: chrono::NaiveDateTime,
25}
26
27impl BaseModel for ServerSubuser {
28    const NAME: &'static str = "server_subuser";
29
30    #[inline]
31    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
32        let prefix = prefix.unwrap_or_default();
33
34        let mut columns = BTreeMap::from([
35            (
36                "server_subusers.server_uuid",
37                compact_str::format_compact!("{prefix}server_uuid"),
38            ),
39            (
40                "server_subusers.permissions",
41                compact_str::format_compact!("{prefix}permissions"),
42            ),
43            (
44                "server_subusers.ignored_files",
45                compact_str::format_compact!("{prefix}ignored_files"),
46            ),
47            (
48                "server_subusers.created",
49                compact_str::format_compact!("{prefix}created"),
50            ),
51        ]);
52
53        columns.extend(super::user::User::columns(Some("user_")));
54
55        columns
56    }
57
58    #[inline]
59    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
60        let prefix = prefix.unwrap_or_default();
61
62        Ok(Self {
63            user: super::user::User::map(Some("user_"), row)?,
64            server: super::server::Server::get_fetchable(
65                row.try_get(compact_str::format_compact!("{prefix}server_uuid").as_str())?,
66            ),
67            permissions: row
68                .try_get(compact_str::format_compact!("{prefix}permissions").as_str())?,
69            ignored_files: row
70                .try_get(compact_str::format_compact!("{prefix}ignored_files").as_str())?,
71            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
72        })
73    }
74}
75
76impl ServerSubuser {
77    pub async fn by_server_uuid_username(
78        database: &crate::database::Database,
79        server_uuid: uuid::Uuid,
80        username: &str,
81    ) -> Result<Option<Self>, crate::database::DatabaseError> {
82        let row = sqlx::query(&format!(
83            r#"
84            SELECT {}
85            FROM server_subusers
86            JOIN users ON users.uuid = server_subusers.user_uuid
87            LEFT JOIN roles ON roles.uuid = users.role_uuid
88            WHERE server_subusers.server_uuid = $1 AND users.username = $2
89            "#,
90            Self::columns_sql(None)
91        ))
92        .bind(server_uuid)
93        .bind(username)
94        .fetch_optional(database.read())
95        .await?;
96
97        row.try_map(|row| Self::map(None, &row))
98    }
99
100    pub async fn by_server_uuid_with_pagination(
101        database: &crate::database::Database,
102        server_uuid: uuid::Uuid,
103        page: i64,
104        per_page: i64,
105        search: Option<&str>,
106    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
107        let offset = (page - 1) * per_page;
108
109        let rows = sqlx::query(&format!(
110            r#"
111            SELECT {}, COUNT(*) OVER() AS total_count
112            FROM server_subusers
113            JOIN users ON users.uuid = server_subusers.user_uuid
114            LEFT JOIN roles ON roles.uuid = users.role_uuid
115            WHERE server_subusers.server_uuid = $1 AND ($2 IS NULL OR users.username ILIKE '%' || $2 || '%')
116            ORDER BY server_subusers.created
117            LIMIT $3 OFFSET $4
118            "#,
119            Self::columns_sql(None)
120        ))
121        .bind(server_uuid)
122        .bind(search)
123        .bind(per_page)
124        .bind(offset)
125        .fetch_all(database.read())
126        .await?;
127
128        Ok(super::Pagination {
129            total: rows
130                .first()
131                .map_or(Ok(0), |row| row.try_get("total_count"))?,
132            per_page,
133            page,
134            data: rows
135                .into_iter()
136                .map(|row| Self::map(None, &row))
137                .try_collect_vec()?,
138        })
139    }
140
141    pub async fn delete_by_uuids(
142        database: &crate::database::Database,
143        server_uuid: uuid::Uuid,
144        user_uuid: uuid::Uuid,
145    ) -> Result<(), crate::database::DatabaseError> {
146        sqlx::query(
147            r#"
148            DELETE FROM server_subusers
149            WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
150            "#,
151        )
152        .bind(server_uuid)
153        .bind(user_uuid)
154        .execute(database.write())
155        .await?;
156
157        Ok(())
158    }
159
160    #[inline]
161    pub fn into_api_object(
162        self,
163        storage_url_retriever: &StorageUrlRetriever<'_>,
164    ) -> ApiServerSubuser {
165        ApiServerSubuser {
166            user: self.user.into_api_object(storage_url_retriever),
167            permissions: self.permissions,
168            ignored_files: self.ignored_files,
169            created: self.created.and_utc(),
170        }
171    }
172}
173
174#[derive(Validate)]
175pub struct CreateServerSubuserOptions<'a> {
176    #[garde(skip)]
177    pub server: &'a super::server::Server,
178
179    #[garde(email)]
180    pub email: compact_str::CompactString,
181    #[garde(custom(crate::permissions::validate_server_permissions))]
182    pub permissions: Vec<compact_str::CompactString>,
183    #[garde(skip)]
184    pub ignored_files: Vec<compact_str::CompactString>,
185}
186
187#[async_trait::async_trait]
188impl CreatableModel for ServerSubuser {
189    type CreateOptions<'a> = CreateServerSubuserOptions<'a>;
190    type CreateResult = Self;
191
192    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
193        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerSubuser>> =
194            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
195
196        &CREATE_LISTENERS
197    }
198
199    async fn create(
200        state: &crate::State,
201        mut options: Self::CreateOptions<'_>,
202    ) -> Result<Self, crate::database::DatabaseError> {
203        options.validate()?;
204
205        let user = match super::user::User::by_email(&state.database, &options.email).await? {
206            Some(user) => user,
207            None => {
208                let username = options
209                    .email
210                    .split('@')
211                    .next()
212                    .unwrap_or("unknown")
213                    .chars()
214                    .filter(|c| c.is_alphanumeric() || *c == '_')
215                    .take(10)
216                    .collect::<compact_str::CompactString>();
217                let username = compact_str::format_compact!(
218                    "{username}_{}",
219                    rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 4)
220                );
221
222                let app_settings = state.settings.get().await?;
223
224                let create_options = super::user::CreateUserOptions {
225                    role_uuid: None,
226                    external_id: None,
227                    username: username.clone(),
228                    email: options.email.clone(),
229                    name_first: "Server".into(),
230                    name_last: "Subuser".into(),
231                    password: None,
232                    admin: false,
233                    language: app_settings.app.language.clone(),
234                };
235                drop(app_settings);
236                let user = match super::user::User::create(state, create_options).await {
237                    Ok(user) => user,
238                    Err(err) => {
239                        tracing::error!(username = %username, email = %options.email, "failed to create subuser user: {:?}", err);
240                        return Err(err);
241                    }
242                };
243
244                match super::user_password_reset::UserPasswordReset::create(
245                    &state.database,
246                    user.uuid,
247                )
248                .await
249                {
250                    Ok(token) => {
251                        let settings = state.settings.get().await?;
252
253                        super::user_activity::UserActivity::create(
254                            state,
255                            super::user_activity::CreateUserActivityOptions {
256                                user_uuid: user.uuid,
257                                impersonator_uuid: None,
258                                api_key_uuid: None,
259                                event: "email:account-created".into(),
260                                ip: None,
261                                data: serde_json::json!({}),
262                                created: None,
263                            },
264                        )
265                        .await?;
266
267                        state
268                            .mail
269                            .send(
270                                user.email.clone(),
271                                format!("{} - Account Created", settings.app.name).into(),
272                                crate::mail::MAIL_ACCOUNT_CREATED,
273                                minijinja::context! {
274                                    user => user,
275                                    reset_link => format!(
276                                        "{}/auth/reset-password?token={}",
277                                        settings.app.url,
278                                        urlencoding::encode(&token),
279                                    )
280                                },
281                            )
282                            .await;
283                    }
284                    Err(err) => {
285                        tracing::warn!(
286                            user = %user.uuid,
287                            "failed to create subuser password reset token: {:#?}",
288                            err
289                        );
290                    }
291                }
292
293                user
294            }
295        };
296
297        if options.server.owner.uuid == user.uuid {
298            return Err(sqlx::Error::InvalidArgument(
299                "cannot create subuser for server owner".into(),
300            )
301            .into());
302        }
303
304        let mut transaction = state.database.write().begin().await?;
305
306        let mut query_builder = InsertQueryBuilder::new("server_subusers");
307
308        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
309            .await?;
310
311        query_builder
312            .set("server_uuid", options.server.uuid)
313            .set("user_uuid", user.uuid)
314            .set("permissions", &options.permissions)
315            .set("ignored_files", &options.ignored_files);
316
317        query_builder.execute(&mut *transaction).await?;
318
319        transaction.commit().await?;
320
321        let subuser =
322            Self::by_server_uuid_username(&state.database, options.server.uuid, &user.username)
323                .await?
324                .ok_or_else(|| {
325                    anyhow::anyhow!(
326                        "subuser with username {} not found after creation",
327                        user.username
328                    )
329                })?;
330
331        Ok(subuser)
332    }
333}
334
335#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
336pub struct UpdateServerSubuserOptions {
337    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
338    pub permissions: Option<Vec<compact_str::CompactString>>,
339    #[garde(skip)]
340    pub ignored_files: Option<Vec<compact_str::CompactString>>,
341}
342
343#[async_trait::async_trait]
344impl UpdatableModel for ServerSubuser {
345    type UpdateOptions = UpdateServerSubuserOptions;
346
347    fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
348        static UPDATE_LISTENERS: LazyLock<UpdateListenerList<ServerSubuser>> =
349            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
350
351        &UPDATE_LISTENERS
352    }
353
354    async fn update(
355        &mut self,
356        state: &crate::State,
357        mut options: Self::UpdateOptions,
358    ) -> Result<(), crate::database::DatabaseError> {
359        options.validate()?;
360
361        let mut transaction = state.database.write().begin().await?;
362
363        let mut query_builder = UpdateQueryBuilder::new("server_subusers");
364
365        Self::run_update_handlers(
366            self,
367            &mut options,
368            &mut query_builder,
369            state,
370            &mut transaction,
371        )
372        .await?;
373
374        query_builder
375            .set("permissions", options.permissions.as_ref())
376            .set("ignored_files", options.ignored_files.as_ref())
377            .where_eq("server_uuid", self.server.uuid)
378            .where_eq("user_uuid", self.user.uuid);
379
380        query_builder.execute(&mut *transaction).await?;
381
382        if let Some(permissions) = options.permissions {
383            self.permissions = permissions;
384        }
385        if let Some(ignored_files) = options.ignored_files {
386            self.ignored_files = ignored_files;
387        }
388
389        transaction.commit().await?;
390
391        Ok(())
392    }
393}
394
395#[async_trait::async_trait]
396impl DeletableModel for ServerSubuser {
397    type DeleteOptions = ();
398
399    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
400        static DELETE_LISTENERS: LazyLock<DeleteListenerList<ServerSubuser>> =
401            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
402
403        &DELETE_LISTENERS
404    }
405
406    async fn delete(
407        &self,
408        state: &crate::State,
409        options: Self::DeleteOptions,
410    ) -> Result<(), anyhow::Error> {
411        let mut transaction = state.database.write().begin().await?;
412
413        self.run_delete_handlers(&options, state, &mut transaction)
414            .await?;
415
416        sqlx::query(
417            r#"
418            DELETE FROM server_subusers
419            WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
420            "#,
421        )
422        .bind(self.server.uuid)
423        .bind(self.user.uuid)
424        .execute(&mut *transaction)
425        .await?;
426
427        transaction.commit().await?;
428
429        Ok(())
430    }
431}
432
433#[derive(ToSchema, Serialize)]
434#[schema(title = "ServerSubuser")]
435pub struct ApiServerSubuser {
436    pub user: super::user::ApiUser,
437
438    pub permissions: Vec<compact_str::CompactString>,
439    pub ignored_files: Vec<compact_str::CompactString>,
440
441    pub created: chrono::DateTime<chrono::Utc>,
442}