Skip to main content

shared/models/
server_subuser.rs

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