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 by_server_uuid_user_uuid(
91        database: &crate::database::Database,
92        server_uuid: uuid::Uuid,
93        user_uuid: uuid::Uuid,
94    ) -> Result<Option<Self>, crate::database::DatabaseError> {
95        let row = sqlx::query(sqlx::AssertSqlSafe(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 server_subusers.user_uuid = $2
102            "#,
103            Self::columns_sql(None)
104        )))
105        .bind(server_uuid)
106        .bind(user_uuid)
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(sqlx::AssertSqlSafe(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                    frozen: false,
262                    suspended: false,
263                    send_email: true,
264                    language: app_settings.app.language.clone(),
265                };
266                drop(app_settings);
267                match super::user::User::create(state, create_options).await {
268                    Ok(user) => user,
269                    Err(err) => {
270                        tracing::error!(username = %username, email = %options.email, "failed to create subuser user: {:?}", err);
271                        return Err(err);
272                    }
273                }
274            }
275        };
276
277        if options.server.owner.uuid == user.uuid {
278            return Err(sqlx::Error::InvalidArgument(
279                "cannot create subuser for server owner".into(),
280            )
281            .into());
282        }
283
284        let server_uuid = options.server.uuid;
285        let username = user.username.clone();
286
287        let mut query_builder = InsertQueryBuilder::new("server_subusers");
288
289        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
290
291        query_builder
292            .set("server_uuid", options.server.uuid)
293            .set("user_uuid", user.uuid)
294            .set("permissions", &options.permissions)
295            .set("ignored_files", &options.ignored_files);
296
297        query_builder.execute(&mut **transaction).await?;
298
299        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
300            r#"
301            SELECT {}
302            FROM server_subusers
303            JOIN users ON users.uuid = server_subusers.user_uuid
304            LEFT JOIN roles ON roles.uuid = users.role_uuid
305            WHERE server_subusers.server_uuid = $1 AND users.username = $2
306            "#,
307            Self::columns_sql(None)
308        )))
309        .bind(server_uuid)
310        .bind(username.as_str())
311        .fetch_one(&mut **transaction)
312        .await?;
313
314        let mut result = Self::map(None, &row)?;
315
316        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
317
318        let settings = state.settings.get().await?;
319        state
320            .mail
321            .send_template(
322                state,
323                "added_to_server",
324                user.email.clone(),
325                minijinja::context! {
326                    user => user,
327                    server => options.server,
328                    server_link => format!(
329                        "{}/server/{:08x}",
330                        settings.app.url,
331                        options.server.uuid_short,
332                    )
333                },
334            )
335            .await;
336
337        Ok(result)
338    }
339}
340
341#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
342pub struct UpdateServerSubuserOptions {
343    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
344    pub permissions: Option<Vec<compact_str::CompactString>>,
345    #[garde(skip)]
346    pub ignored_files: Option<Vec<compact_str::CompactString>>,
347}
348
349#[async_trait::async_trait]
350impl UpdatableModel for ServerSubuser {
351    type UpdateOptions = UpdateServerSubuserOptions;
352
353    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
354        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerSubuser>> =
355            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
356
357        &UPDATE_LISTENERS
358    }
359
360    async fn update_with_transaction(
361        &mut self,
362        state: &crate::State,
363        mut options: Self::UpdateOptions,
364        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
365    ) -> Result<(), crate::database::DatabaseError> {
366        options.validate()?;
367
368        let mut query_builder = UpdateQueryBuilder::new("server_subusers");
369
370        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
371            .await?;
372
373        query_builder
374            .set("permissions", options.permissions.as_ref())
375            .set("ignored_files", options.ignored_files.as_ref())
376            .where_eq("server_uuid", self.server.uuid)
377            .where_eq("user_uuid", self.user.uuid);
378
379        query_builder.execute(&mut **transaction).await?;
380
381        if let Some(permissions) = options.permissions {
382            self.permissions = permissions;
383        }
384        if let Some(ignored_files) = options.ignored_files {
385            self.ignored_files = ignored_files;
386        }
387
388        self.run_after_update_handlers(state, transaction).await?;
389
390        Ok(())
391    }
392}
393
394#[async_trait::async_trait]
395impl DeletableModel for ServerSubuser {
396    type DeleteOptions = ();
397
398    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
399        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerSubuser>> =
400            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
401
402        &DELETE_LISTENERS
403    }
404
405    async fn delete_with_transaction(
406        &self,
407        state: &crate::State,
408        options: Self::DeleteOptions,
409        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
410    ) -> Result<(), anyhow::Error> {
411        self.run_delete_handlers(&options, state, transaction)
412            .await?;
413
414        sqlx::query(
415            r#"
416            DELETE FROM server_subusers
417            WHERE server_subusers.server_uuid = $1 AND server_subusers.user_uuid = $2
418            "#,
419        )
420        .bind(self.server.uuid)
421        .bind(self.user.uuid)
422        .execute(&mut **transaction)
423        .await?;
424
425        self.run_after_delete_handlers(&options, state, transaction)
426            .await?;
427
428        state
429            .mail
430            .send_template(
431                state,
432                "removed_from_server",
433                self.user.email.clone(),
434                minijinja::context! {
435                    user => self.user,
436                },
437            )
438            .await;
439
440        Ok(())
441    }
442}
443
444#[schema_extension_derive::extendible]
445#[init_args(ServerSubuser, crate::State)]
446#[hook_args(crate::State)]
447#[derive(ToSchema, Serialize)]
448#[schema(title = "ServerSubuser")]
449pub struct ApiServerSubuser {
450    pub user: super::user::ApiUser,
451
452    pub permissions: Vec<compact_str::CompactString>,
453    pub ignored_files: Vec<compact_str::CompactString>,
454
455    pub created: chrono::DateTime<chrono::Utc>,
456}