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}