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}