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(Clone, 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(|| parking_lot::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 permissions_by_server_uuid_user_uuid(
91        database: &crate::database::Database,
92        server_uuid: uuid::Uuid,
93        user_uuid: uuid::Uuid,
94    ) -> Result<
95        Option<(
96            Vec<compact_str::CompactString>,
97            Vec<compact_str::CompactString>,
98        )>,
99        sqlx::Error,
100    > {
101        let row = sqlx::query(
102            r#"
103            SELECT server_subusers.permissions, server_subusers.ignored_files
104            FROM server_subusers
105            WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
106            "#,
107        )
108        .bind(server_uuid)
109        .bind(user_uuid)
110        .fetch_optional(database.read())
111        .await?;
112
113        row.map(|row| Ok((row.try_get("permissions")?, row.try_get("ignored_files")?)))
114            .transpose()
115    }
116
117    pub async fn by_server_uuid_user_uuid(
118        database: &crate::database::Database,
119        server_uuid: uuid::Uuid,
120        user_uuid: uuid::Uuid,
121    ) -> Result<Option<Self>, crate::database::DatabaseError> {
122        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
123            r#"
124            SELECT {}
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 server_subusers.user_uuid = $2
129            "#,
130            Self::columns_sql(None)
131        )))
132        .bind(server_uuid)
133        .bind(user_uuid)
134        .fetch_optional(database.read())
135        .await?;
136
137        row.try_map(|row| Self::map(None, &row))
138    }
139
140    pub async fn by_server_uuid_with_pagination(
141        database: &crate::database::Database,
142        server_uuid: uuid::Uuid,
143        page: i64,
144        per_page: i64,
145        search: Option<&str>,
146    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
147        let offset = (page - 1) * per_page;
148
149        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
150            r#"
151            SELECT {}, COUNT(*) OVER() AS total_count
152            FROM server_subusers
153            JOIN users ON users.uuid = server_subusers.user_uuid
154            LEFT JOIN roles ON roles.uuid = users.role_uuid
155            WHERE server_subusers.server_uuid = $1 AND ($2 IS NULL OR users.username ILIKE '%' || $2 || '%')
156            ORDER BY server_subusers.created
157            LIMIT $3 OFFSET $4
158            "#,
159            Self::columns_sql(None)
160        )))
161        .bind(server_uuid)
162        .bind(search)
163        .bind(per_page)
164        .bind(offset)
165        .fetch_all(database.read())
166        .await?;
167
168        Ok(super::Pagination {
169            total: rows
170                .first()
171                .map_or(Ok(0), |row| row.try_get("total_count"))?,
172            per_page,
173            page,
174            data: rows
175                .into_iter()
176                .map(|row| Self::map(None, &row))
177                .try_collect_vec()?,
178        })
179    }
180
181    pub async fn count_by_server_uuid(
182        database: &crate::database::Database,
183        server_uuid: uuid::Uuid,
184    ) -> Result<i64, sqlx::Error> {
185        sqlx::query_scalar(
186            r#"
187            SELECT COUNT(*)
188            FROM server_subusers
189            WHERE server_subusers.server_uuid = $1
190            "#,
191        )
192        .bind(server_uuid)
193        .fetch_one(database.read())
194        .await
195    }
196}
197
198#[async_trait::async_trait]
199impl IntoApiObject for ServerSubuser {
200    type ApiObject = ApiServerSubuser;
201    type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
202
203    async fn into_api_object<'a>(
204        self,
205        state: &crate::State,
206        storage_url_retriever: Self::ExtraArgs<'a>,
207    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
208        let api_object = ApiServerSubuser::init_hooks(&self, state).await?;
209
210        let api_object = finish_extendible!(
211            ApiServerSubuser {
212                user: self
213                    .user
214                    .into_api_object(state, storage_url_retriever)
215                    .await?,
216                permissions: self.permissions,
217                ignored_files: self.ignored_files,
218                created: self.created.and_utc(),
219            },
220            api_object,
221            state
222        )?;
223
224        Ok(api_object)
225    }
226}
227
228#[derive(Validate)]
229pub struct CreateServerSubuserOptions<'a> {
230    #[garde(skip)]
231    pub server: &'a super::server::Server,
232
233    #[garde(email)]
234    pub email: compact_str::CompactString,
235    #[garde(custom(crate::permissions::validate_server_permissions))]
236    pub permissions: Vec<compact_str::CompactString>,
237    #[garde(
238        length(max = 1024),
239        inner(length(chars, min = 1, max = 255)),
240        custom(crate::utils::validate_ignored_files)
241    )]
242    pub ignored_files: Vec<compact_str::CompactString>,
243}
244
245#[async_trait::async_trait]
246impl CreatableModel for ServerSubuser {
247    type CreateOptions<'a> = CreateServerSubuserOptions<'a>;
248    type CreateResult = Self;
249
250    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
251        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerSubuser>> =
252            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
253
254        &CREATE_LISTENERS
255    }
256
257    async fn create_with_transaction(
258        state: &crate::State,
259        mut options: Self::CreateOptions<'_>,
260        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
261    ) -> Result<Self, crate::database::DatabaseError> {
262        options.validate()?;
263
264        let user = match super::user::User::by_email(&state.database, &options.email).await? {
265            Some(user) => user,
266            None => {
267                let username = options
268                    .email
269                    .split('@')
270                    .next()
271                    .unwrap_or("unknown")
272                    .chars()
273                    .filter(|c| c.is_alphanumeric() || *c == '_')
274                    .take(10)
275                    .collect::<compact_str::CompactString>();
276                let username = compact_str::format_compact!(
277                    "{username}_{}",
278                    rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 4)
279                );
280
281                let app_settings = state.settings.get().await?;
282
283                let create_options = super::user::CreateUserOptions {
284                    role_uuid: None,
285                    external_id: None,
286                    username: username.clone(),
287                    email: options.email.clone(),
288                    name_first: None,
289                    name_last: None,
290                    password: None,
291                    admin: false,
292                    frozen: false,
293                    suspended: false,
294                    verify_email: false,
295                    send_email: true,
296                    language: app_settings.app.language.clone(),
297                };
298                drop(app_settings);
299                match super::user::User::create(state, create_options).await {
300                    Ok(user) => user,
301                    Err(err) => {
302                        tracing::error!(username = %username, email = %options.email, "failed to create subuser user: {:?}", err);
303                        return Err(err);
304                    }
305                }
306            }
307        };
308
309        if options.server.owner.uuid == user.uuid {
310            return Err(sqlx::Error::InvalidArgument(
311                "cannot create subuser for server owner".into(),
312            )
313            .into());
314        }
315
316        let server_uuid = options.server.uuid;
317        let username = user.username.clone();
318
319        let mut query_builder = InsertQueryBuilder::new("server_subusers");
320
321        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
322
323        query_builder
324            .set("server_uuid", options.server.uuid)
325            .set("user_uuid", user.uuid)
326            .set("permissions", &options.permissions)
327            .set("ignored_files", &options.ignored_files);
328
329        query_builder.execute(&mut **transaction).await?;
330
331        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
332            r#"
333            SELECT {}
334            FROM server_subusers
335            JOIN users ON users.uuid = server_subusers.user_uuid
336            LEFT JOIN roles ON roles.uuid = users.role_uuid
337            WHERE server_subusers.server_uuid = $1 AND users.username = $2
338            "#,
339            Self::columns_sql(None)
340        )))
341        .bind(server_uuid)
342        .bind(username.as_str())
343        .fetch_one(&mut **transaction)
344        .await?;
345
346        let mut result = Self::map(None, &row)?;
347
348        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
349
350        let settings = state.settings.get().await?;
351        state
352            .mail
353            .send_template(
354                state,
355                "added_to_server",
356                &user,
357                minijinja::context! {
358                    user => user,
359                    server => options.server,
360                    server_link => format!(
361                        "{}/server/{:08x}",
362                        settings.app.url,
363                        options.server.uuid_short,
364                    )
365                },
366            )
367            .await;
368
369        Ok(result)
370    }
371}
372
373#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
374pub struct UpdateServerSubuserOptions {
375    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
376    pub permissions: Option<Vec<compact_str::CompactString>>,
377    #[garde(inner(
378        length(max = 1024),
379        inner(length(chars, min = 1, max = 255)),
380        custom(crate::utils::validate_ignored_files)
381    ))]
382    pub ignored_files: Option<Vec<compact_str::CompactString>>,
383}
384
385#[async_trait::async_trait]
386impl UpdatableModel for ServerSubuser {
387    type UpdateOptions = UpdateServerSubuserOptions;
388
389    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
390        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerSubuser>> =
391            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
392
393        &UPDATE_LISTENERS
394    }
395
396    async fn update_with_transaction(
397        &mut self,
398        state: &crate::State,
399        mut options: Self::UpdateOptions,
400        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
401    ) -> Result<(), crate::database::DatabaseError> {
402        options.validate()?;
403
404        let mut query_builder = UpdateQueryBuilder::new("server_subusers");
405
406        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
407            .await?;
408
409        query_builder
410            .set("permissions", options.permissions.as_ref())
411            .set("ignored_files", options.ignored_files.as_ref())
412            .where_eq("server_uuid", self.server.uuid)
413            .where_eq("user_uuid", self.user.uuid);
414
415        query_builder.execute(&mut **transaction).await?;
416
417        if let Some(permissions) = options.permissions {
418            self.permissions = permissions;
419        }
420        if let Some(ignored_files) = options.ignored_files {
421            self.ignored_files = ignored_files;
422        }
423
424        self.run_after_update_handlers(state, transaction).await?;
425
426        Ok(())
427    }
428}
429
430#[async_trait::async_trait]
431impl DeletableModel for ServerSubuser {
432    type DeleteOptions = ();
433
434    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
435        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerSubuser>> =
436            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
437
438        &DELETE_LISTENERS
439    }
440
441    async fn delete_with_transaction(
442        &self,
443        state: &crate::State,
444        options: Self::DeleteOptions,
445        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
446    ) -> Result<(), anyhow::Error> {
447        self.run_delete_handlers(&options, state, transaction)
448            .await?;
449
450        sqlx::query(
451            r#"
452            DELETE FROM server_subusers
453            WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
454            "#,
455        )
456        .bind(self.server.uuid)
457        .bind(self.user.uuid)
458        .execute(&mut **transaction)
459        .await?;
460
461        self.run_after_delete_handlers(&options, state, transaction)
462            .await?;
463
464        state
465            .mail
466            .send_template(
467                state,
468                "removed_from_server",
469                &self.user,
470                minijinja::context! {
471                    user => self.user,
472                },
473            )
474            .await;
475
476        Ok(())
477    }
478}
479
480#[schema_extension_derive::extendible]
481#[init_args(ServerSubuser, crate::State)]
482#[hook_args(crate::State)]
483#[derive(ToSchema, Serialize)]
484#[schema(title = "ServerSubuser")]
485pub struct ApiServerSubuser {
486    pub user: super::user::ApiUser,
487
488    pub permissions: Vec<compact_str::CompactString>,
489    pub ignored_files: Vec<compact_str::CompactString>,
490
491    pub created: chrono::DateTime<chrono::Utc>,
492}