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