1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9 collections::BTreeMap,
10 sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct Role {
16 pub uuid: uuid::Uuid,
17
18 pub name: compact_str::CompactString,
19 pub description: Option<compact_str::CompactString>,
20
21 pub require_two_factor: bool,
22
23 pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
24 pub server_permissions: Arc<Vec<compact_str::CompactString>>,
25
26 pub created: chrono::NaiveDateTime,
27
28 extension_data: super::ModelExtensionData,
29}
30
31impl BaseModel for Role {
32 const NAME: &'static str = "role";
33
34 fn get_extension_list() -> &'static super::ModelExtensionList {
35 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
36 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
37
38 &EXTENSIONS
39 }
40
41 fn get_extension_data(&self) -> &super::ModelExtensionData {
42 &self.extension_data
43 }
44
45 #[inline]
46 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
47 let prefix = prefix.unwrap_or_default();
48
49 BTreeMap::from([
50 ("roles.uuid", compact_str::format_compact!("{prefix}uuid")),
51 ("roles.name", compact_str::format_compact!("{prefix}name")),
52 (
53 "roles.description",
54 compact_str::format_compact!("{prefix}description"),
55 ),
56 (
57 "roles.require_two_factor",
58 compact_str::format_compact!("{prefix}require_two_factor"),
59 ),
60 (
61 "roles.admin_permissions",
62 compact_str::format_compact!("{prefix}admin_permissions"),
63 ),
64 (
65 "roles.server_permissions",
66 compact_str::format_compact!("{prefix}server_permissions"),
67 ),
68 (
69 "roles.created",
70 compact_str::format_compact!("{prefix}created"),
71 ),
72 ])
73 }
74
75 #[inline]
76 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
77 let prefix = prefix.unwrap_or_default();
78
79 Ok(Self {
80 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
81 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
82 description: row
83 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
84 require_two_factor: row
85 .try_get(compact_str::format_compact!("{prefix}require_two_factor").as_str())?,
86 admin_permissions: Arc::new(
87 row.try_get(compact_str::format_compact!("{prefix}admin_permissions").as_str())?,
88 ),
89 server_permissions: Arc::new(
90 row.try_get(compact_str::format_compact!("{prefix}server_permissions").as_str())?,
91 ),
92 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
93 extension_data: Self::map_extensions(prefix, row)?,
94 })
95 }
96
97 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
98 vec![compact_str::format_compact!(
99 "{}::{}",
100 Self::NAME,
101 self.uuid
102 )]
103 }
104}
105
106impl Role {
107 async fn user_uuids(
108 &self,
109 database: &crate::database::Database,
110 ) -> Result<Vec<uuid::Uuid>, sqlx::Error> {
111 sqlx::query_scalar(
112 r#"
113 SELECT users.uuid
114 FROM users
115 WHERE users.role_uuid = $1
116 "#,
117 )
118 .bind(self.uuid)
119 .fetch_all(database.read())
120 .await
121 }
122
123 async fn invalidate_cached_with_users(
124 &self,
125 database: &crate::database::Database,
126 ) -> Result<(), sqlx::Error> {
127 Self::invalidate_cached(database, self.uuid).await;
128 for user_uuid in self.user_uuids(database).await? {
129 super::user::User::invalidate_cached(database, user_uuid).await;
130 }
131
132 Ok(())
133 }
134
135 pub async fn all_with_pagination(
136 database: &crate::database::Database,
137 page: i64,
138 per_page: i64,
139 search: Option<&str>,
140 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
141 super::PaginatedQueryBuilder::new(
142 Self::columns_sql(None),
143 format!(
144 r#"
145 FROM roles
146 WHERE {search}
147 "#,
148 search = super::search_sql(1, &["roles.name"], &["roles.uuid"]),
149 ),
150 "roles.created, roles.uuid",
151 )
152 .bind(search)
153 .fetch(database, page, per_page)
154 .await
155 }
156}
157
158#[async_trait::async_trait]
159impl IntoAdminApiObject for Role {
160 type AdminApiObject = AdminApiRole;
161 type ExtraArgs<'a> = ();
162
163 async fn into_admin_api_object<'a>(
164 self,
165 state: &crate::State,
166 _args: Self::ExtraArgs<'a>,
167 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
168 let api_object = AdminApiRole::init_hooks(&self, state).await?;
169
170 let api_object = finish_extendible!(
171 AdminApiRole {
172 uuid: self.uuid,
173 name: self.name,
174 description: self.description,
175 require_two_factor: self.require_two_factor,
176 admin_permissions: self.admin_permissions,
177 server_permissions: self.server_permissions,
178 created: self.created.and_utc(),
179 },
180 api_object,
181 state
182 )?;
183
184 Ok(api_object)
185 }
186}
187
188#[async_trait::async_trait]
189impl ByUuid for Role {
190 async fn by_uuid(
191 database: &crate::database::Database,
192 uuid: uuid::Uuid,
193 ) -> Result<Self, crate::database::DatabaseError> {
194 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
195 r#"
196 SELECT {}
197 FROM roles
198 WHERE roles.uuid = $1
199 "#,
200 Self::columns_sql(None)
201 )))
202 .bind(uuid)
203 .fetch_one(database.read())
204 .await?;
205
206 Self::map(None, &row)
207 }
208
209 async fn by_uuid_with_transaction(
210 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
211 uuid: uuid::Uuid,
212 ) -> Result<Self, crate::database::DatabaseError> {
213 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
214 r#"
215 SELECT {}
216 FROM roles
217 WHERE roles.uuid = $1
218 "#,
219 Self::columns_sql(None)
220 )))
221 .bind(uuid)
222 .fetch_one(&mut **transaction)
223 .await?;
224
225 Self::map(None, &row)
226 }
227}
228
229#[derive(ToSchema, Deserialize, Validate)]
230pub struct CreateRoleOptions {
231 #[garde(length(chars, min = 1, max = 255))]
232 #[schema(min_length = 1, max_length = 255)]
233 pub name: compact_str::CompactString,
234 #[garde(length(chars, min = 1, max = 1024))]
235 #[schema(min_length = 1, max_length = 1024)]
236 pub description: Option<compact_str::CompactString>,
237 #[garde(skip)]
238 pub require_two_factor: bool,
239 #[garde(custom(crate::permissions::validate_admin_permissions))]
240 pub admin_permissions: Vec<compact_str::CompactString>,
241 #[garde(custom(crate::permissions::validate_server_permissions))]
242 pub server_permissions: Vec<compact_str::CompactString>,
243}
244
245#[async_trait::async_trait]
246impl CreatableModel for Role {
247 type CreateOptions<'a> = CreateRoleOptions;
248 type CreateResult = Self;
249
250 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
251 static CREATE_LISTENERS: LazyLock<CreateListenerList<Role>> =
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 mut query_builder = InsertQueryBuilder::new("roles");
265
266 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
267
268 query_builder
269 .set("name", &options.name)
270 .set("description", &options.description)
271 .set("require_two_factor", options.require_two_factor)
272 .set("admin_permissions", &options.admin_permissions)
273 .set("server_permissions", &options.server_permissions);
274
275 let row = query_builder
276 .returning(&Self::columns_sql(None))
277 .fetch_one(&mut **transaction)
278 .await?;
279 let mut role = Self::map(None, &row)?;
280
281 Self::run_after_create_handlers(&mut role, &options, state, transaction).await?;
282
283 Ok(role)
284 }
285}
286
287#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
288pub struct UpdateRoleOptions {
289 #[garde(length(chars, min = 1, max = 255))]
290 #[schema(min_length = 1, max_length = 255)]
291 pub name: Option<compact_str::CompactString>,
292 #[garde(length(chars, min = 1, max = 1024))]
293 #[schema(min_length = 1, max_length = 1024)]
294 #[serde(
295 default,
296 skip_serializing_if = "Option::is_none",
297 with = "::serde_with::rust::double_option"
298 )]
299 pub description: Option<Option<compact_str::CompactString>>,
300 #[garde(skip)]
301 pub require_two_factor: Option<bool>,
302 #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
303 pub admin_permissions: Option<Vec<compact_str::CompactString>>,
304 #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
305 pub server_permissions: Option<Vec<compact_str::CompactString>>,
306}
307
308#[async_trait::async_trait]
309impl UpdatableModel for Role {
310 type UpdateOptions = UpdateRoleOptions;
311
312 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
313 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Role>> =
314 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
315
316 &UPDATE_LISTENERS
317 }
318
319 async fn update(
320 &mut self,
321 state: &crate::State,
322 options: Self::UpdateOptions,
323 ) -> Result<(), crate::database::DatabaseError> {
324 let access_changed = options
325 .admin_permissions
326 .as_ref()
327 .is_some_and(|permissions| permissions != self.admin_permissions.as_ref())
328 || options
329 .server_permissions
330 .as_ref()
331 .is_some_and(|permissions| permissions != self.server_permissions.as_ref())
332 || options
333 .require_two_factor
334 .is_some_and(|require_two_factor| require_two_factor != self.require_two_factor);
335
336 let mut transaction = state.database.write().begin().await?;
337
338 if let Err(err) = self
339 .update_with_transaction(state, options, &mut transaction)
340 .await
341 {
342 transaction.rollback().await?;
343 return Err(err);
344 }
345
346 transaction.commit().await?;
347
348 self.invalidate_cached_with_users(&state.database).await?;
349
350 if access_changed {
351 match self.user_uuids(&state.database).await {
352 Ok(user_uuids) => super::node::Node::invalidate_permissions_everywhere(
353 &state.database,
354 super::node::PermissionsInvalidation {
355 users: user_uuids,
356 ..Default::default()
357 },
358 ),
359 Err(err) => {
360 tracing::error!(role = %self.uuid, "failed to invalidate permissions in wings: {:?}", err);
361 }
362 }
363 }
364
365 Ok(())
366 }
367
368 async fn update_with_transaction(
369 &mut self,
370 state: &crate::State,
371 mut options: Self::UpdateOptions,
372 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
373 ) -> Result<(), crate::database::DatabaseError> {
374 options.validate()?;
375
376 let mut query_builder = UpdateQueryBuilder::new("roles");
377
378 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
379 .await?;
380
381 query_builder
382 .set("name", options.name.as_ref())
383 .set(
384 "description",
385 options.description.as_ref().map(|d| d.as_ref()),
386 )
387 .set("require_two_factor", options.require_two_factor)
388 .set("admin_permissions", options.admin_permissions.as_ref())
389 .set("server_permissions", options.server_permissions.as_ref())
390 .where_eq("uuid", self.uuid);
391
392 query_builder.execute(&mut **transaction).await?;
393
394 if let Some(name) = options.name {
395 self.name = name;
396 }
397 if let Some(description) = options.description {
398 self.description = description;
399 }
400 if let Some(require_two_factor) = options.require_two_factor {
401 self.require_two_factor = require_two_factor;
402 }
403 if let Some(admin_permissions) = options.admin_permissions {
404 self.admin_permissions = Arc::new(admin_permissions);
405 }
406 if let Some(server_permissions) = options.server_permissions {
407 self.server_permissions = Arc::new(server_permissions);
408 }
409
410 self.run_after_update_handlers(state, transaction).await?;
411
412 Ok(())
413 }
414}
415
416#[async_trait::async_trait]
417impl DeletableModel for Role {
418 type DeleteOptions = ();
419
420 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
421 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Role>> =
422 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
423
424 &DELETE_LISTENERS
425 }
426
427 async fn delete(
428 &self,
429 state: &crate::State,
430 options: Self::DeleteOptions,
431 ) -> Result<(), anyhow::Error> {
432 let user_uuids = self.user_uuids(&state.database).await?;
433
434 let mut transaction = state.database.write().begin().await?;
435
436 if let Err(err) = self
437 .delete_with_transaction(state, options, &mut transaction)
438 .await
439 {
440 transaction.rollback().await?;
441 return Err(err);
442 }
443
444 transaction.commit().await?;
445
446 Self::invalidate_cached(&state.database, self.uuid).await;
447 for &user_uuid in &user_uuids {
448 super::user::User::invalidate_cached(&state.database, user_uuid).await;
449 }
450 super::node::Node::invalidate_permissions_everywhere(
451 &state.database,
452 super::node::PermissionsInvalidation {
453 users: user_uuids,
454 ..Default::default()
455 },
456 );
457
458 Ok(())
459 }
460
461 async fn delete_with_transaction(
462 &self,
463 state: &crate::State,
464 options: Self::DeleteOptions,
465 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
466 ) -> Result<(), anyhow::Error> {
467 self.run_delete_handlers(&options, state, transaction)
468 .await?;
469
470 sqlx::query(
471 r#"
472 DELETE FROM roles
473 WHERE roles.uuid = $1
474 "#,
475 )
476 .bind(self.uuid)
477 .execute(&mut **transaction)
478 .await?;
479
480 self.run_after_delete_handlers(&options, state, transaction)
481 .await?;
482
483 Ok(())
484 }
485}
486
487#[derive(Validate)]
488pub struct DuplicateRoleOptions {
489 #[garde(length(chars, min = 1, max = 255))]
490 pub name: compact_str::CompactString,
491}
492
493#[async_trait::async_trait]
494impl DuplicableModel for Role {
495 type DuplicateOptions<'a> = DuplicateRoleOptions;
496 type DuplicateResult = Self;
497
498 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
499 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Role>> =
500 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
501
502 &DUPLICATE_LISTENERS
503 }
504
505 async fn duplicate_with_transaction(
506 &self,
507 state: &crate::State,
508 options: Self::DuplicateOptions<'_>,
509 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
510 ) -> Result<Self, crate::database::DatabaseError> {
511 options.validate()?;
512
513 self.run_duplicate_handlers(&options, state, transaction)
514 .await?;
515
516 let mut query_builder = InsertQueryBuilder::new("roles");
517
518 query_builder
519 .set("name", &options.name)
520 .set("description", &self.description)
521 .set("require_two_factor", self.require_two_factor)
522 .set("admin_permissions", self.admin_permissions.as_ref())
523 .set("server_permissions", self.server_permissions.as_ref());
524
525 let row = query_builder
526 .returning(&Self::columns_sql(None))
527 .fetch_one(&mut **transaction)
528 .await?;
529 let mut role = Self::map(None, &row)?;
530
531 self.run_after_duplicate_handlers(&mut role, &options, state, transaction)
532 .await?;
533
534 Ok(role)
535 }
536}
537
538#[schema_extension_derive::extendible]
539#[init_args(Role, crate::State)]
540#[hook_args(crate::State)]
541#[derive(ToSchema, Serialize)]
542#[schema(title = "Role")]
543pub struct AdminApiRole {
544 pub uuid: uuid::Uuid,
545
546 pub name: compact_str::CompactString,
547 pub description: Option<compact_str::CompactString>,
548
549 pub require_two_factor: bool,
550
551 pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
552 pub server_permissions: Arc<Vec<compact_str::CompactString>>,
553
554 pub created: chrono::DateTime<chrono::Utc>,
555}