Skip to main content

shared/models/
role.rs

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        let offset = (page - 1) * per_page;
142
143        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
144            r#"
145            SELECT {}, COUNT(*) OVER() AS total_count
146            FROM roles
147            WHERE ($1 IS NULL OR roles.name ILIKE '%' || $1 || '%')
148            ORDER BY roles.created
149            LIMIT $2 OFFSET $3
150            "#,
151            Self::columns_sql(None)
152        )))
153        .bind(search)
154        .bind(per_page)
155        .bind(offset)
156        .fetch_all(database.read())
157        .await?;
158
159        Ok(super::Pagination {
160            total: rows
161                .first()
162                .map_or(Ok(0), |row| row.try_get("total_count"))?,
163            per_page,
164            page,
165            data: rows
166                .into_iter()
167                .map(|row| Self::map(None, &row))
168                .try_collect_vec()?,
169        })
170    }
171}
172
173#[async_trait::async_trait]
174impl IntoAdminApiObject for Role {
175    type AdminApiObject = AdminApiRole;
176    type ExtraArgs<'a> = ();
177
178    async fn into_admin_api_object<'a>(
179        self,
180        state: &crate::State,
181        _args: Self::ExtraArgs<'a>,
182    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
183        let api_object = AdminApiRole::init_hooks(&self, state).await?;
184
185        let api_object = finish_extendible!(
186            AdminApiRole {
187                uuid: self.uuid,
188                name: self.name,
189                description: self.description,
190                require_two_factor: self.require_two_factor,
191                admin_permissions: self.admin_permissions,
192                server_permissions: self.server_permissions,
193                created: self.created.and_utc(),
194            },
195            api_object,
196            state
197        )?;
198
199        Ok(api_object)
200    }
201}
202
203#[async_trait::async_trait]
204impl ByUuid for Role {
205    async fn by_uuid(
206        database: &crate::database::Database,
207        uuid: uuid::Uuid,
208    ) -> Result<Self, crate::database::DatabaseError> {
209        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
210            r#"
211            SELECT {}
212            FROM roles
213            WHERE roles.uuid = $1
214            "#,
215            Self::columns_sql(None)
216        )))
217        .bind(uuid)
218        .fetch_one(database.read())
219        .await?;
220
221        Self::map(None, &row)
222    }
223
224    async fn by_uuid_with_transaction(
225        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
226        uuid: uuid::Uuid,
227    ) -> Result<Self, crate::database::DatabaseError> {
228        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
229            r#"
230            SELECT {}
231            FROM roles
232            WHERE roles.uuid = $1
233            "#,
234            Self::columns_sql(None)
235        )))
236        .bind(uuid)
237        .fetch_one(&mut **transaction)
238        .await?;
239
240        Self::map(None, &row)
241    }
242}
243
244#[derive(ToSchema, Deserialize, Validate)]
245pub struct CreateRoleOptions {
246    #[garde(length(chars, min = 1, max = 255))]
247    #[schema(min_length = 1, max_length = 255)]
248    pub name: compact_str::CompactString,
249    #[garde(length(chars, min = 1, max = 1024))]
250    #[schema(min_length = 1, max_length = 1024)]
251    pub description: Option<compact_str::CompactString>,
252    #[garde(skip)]
253    pub require_two_factor: bool,
254    #[garde(custom(crate::permissions::validate_admin_permissions))]
255    pub admin_permissions: Vec<compact_str::CompactString>,
256    #[garde(custom(crate::permissions::validate_server_permissions))]
257    pub server_permissions: Vec<compact_str::CompactString>,
258}
259
260#[async_trait::async_trait]
261impl CreatableModel for Role {
262    type CreateOptions<'a> = CreateRoleOptions;
263    type CreateResult = Self;
264
265    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
266        static CREATE_LISTENERS: LazyLock<CreateListenerList<Role>> =
267            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
268
269        &CREATE_LISTENERS
270    }
271
272    async fn create_with_transaction(
273        state: &crate::State,
274        mut options: Self::CreateOptions<'_>,
275        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
276    ) -> Result<Self, crate::database::DatabaseError> {
277        options.validate()?;
278
279        let mut query_builder = InsertQueryBuilder::new("roles");
280
281        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
282
283        query_builder
284            .set("name", &options.name)
285            .set("description", &options.description)
286            .set("require_two_factor", options.require_two_factor)
287            .set("admin_permissions", &options.admin_permissions)
288            .set("server_permissions", &options.server_permissions);
289
290        let row = query_builder
291            .returning(&Self::columns_sql(None))
292            .fetch_one(&mut **transaction)
293            .await?;
294        let mut role = Self::map(None, &row)?;
295
296        Self::run_after_create_handlers(&mut role, &options, state, transaction).await?;
297
298        Ok(role)
299    }
300}
301
302#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
303pub struct UpdateRoleOptions {
304    #[garde(length(chars, min = 1, max = 255))]
305    #[schema(min_length = 1, max_length = 255)]
306    pub name: Option<compact_str::CompactString>,
307    #[garde(length(chars, min = 1, max = 1024))]
308    #[schema(min_length = 1, max_length = 1024)]
309    #[serde(
310        default,
311        skip_serializing_if = "Option::is_none",
312        with = "::serde_with::rust::double_option"
313    )]
314    pub description: Option<Option<compact_str::CompactString>>,
315    #[garde(skip)]
316    pub require_two_factor: Option<bool>,
317    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
318    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
319    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
320    pub server_permissions: Option<Vec<compact_str::CompactString>>,
321}
322
323#[async_trait::async_trait]
324impl UpdatableModel for Role {
325    type UpdateOptions = UpdateRoleOptions;
326
327    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
328        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Role>> =
329            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
330
331        &UPDATE_LISTENERS
332    }
333
334    async fn update(
335        &mut self,
336        state: &crate::State,
337        options: Self::UpdateOptions,
338    ) -> Result<(), crate::database::DatabaseError> {
339        let mut transaction = state.database.write().begin().await?;
340
341        if let Err(err) = self
342            .update_with_transaction(state, options, &mut transaction)
343            .await
344        {
345            transaction.rollback().await?;
346            return Err(err);
347        }
348
349        transaction.commit().await?;
350
351        self.invalidate_cached_with_users(&state.database).await?;
352
353        Ok(())
354    }
355
356    async fn update_with_transaction(
357        &mut self,
358        state: &crate::State,
359        mut options: Self::UpdateOptions,
360        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
361    ) -> Result<(), crate::database::DatabaseError> {
362        options.validate()?;
363
364        let mut query_builder = UpdateQueryBuilder::new("roles");
365
366        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
367            .await?;
368
369        query_builder
370            .set("name", options.name.as_ref())
371            .set(
372                "description",
373                options.description.as_ref().map(|d| d.as_ref()),
374            )
375            .set("require_two_factor", options.require_two_factor)
376            .set("admin_permissions", options.admin_permissions.as_ref())
377            .set("server_permissions", options.server_permissions.as_ref())
378            .where_eq("uuid", self.uuid);
379
380        query_builder.execute(&mut **transaction).await?;
381
382        if let Some(name) = options.name {
383            self.name = name;
384        }
385        if let Some(description) = options.description {
386            self.description = description;
387        }
388        if let Some(require_two_factor) = options.require_two_factor {
389            self.require_two_factor = require_two_factor;
390        }
391        if let Some(admin_permissions) = options.admin_permissions {
392            self.admin_permissions = Arc::new(admin_permissions);
393        }
394        if let Some(server_permissions) = options.server_permissions {
395            self.server_permissions = Arc::new(server_permissions);
396        }
397
398        self.run_after_update_handlers(state, transaction).await?;
399
400        Ok(())
401    }
402}
403
404#[async_trait::async_trait]
405impl DeletableModel for Role {
406    type DeleteOptions = ();
407
408    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
409        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Role>> =
410            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
411
412        &DELETE_LISTENERS
413    }
414
415    async fn delete(
416        &self,
417        state: &crate::State,
418        options: Self::DeleteOptions,
419    ) -> Result<(), anyhow::Error> {
420        let user_uuids = self.user_uuids(&state.database).await?;
421
422        let mut transaction = state.database.write().begin().await?;
423
424        if let Err(err) = self
425            .delete_with_transaction(state, options, &mut transaction)
426            .await
427        {
428            transaction.rollback().await?;
429            return Err(err);
430        }
431
432        transaction.commit().await?;
433
434        Self::invalidate_cached(&state.database, self.uuid).await;
435        for user_uuid in user_uuids {
436            super::user::User::invalidate_cached(&state.database, user_uuid).await;
437        }
438
439        Ok(())
440    }
441
442    async fn delete_with_transaction(
443        &self,
444        state: &crate::State,
445        options: Self::DeleteOptions,
446        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
447    ) -> Result<(), anyhow::Error> {
448        self.run_delete_handlers(&options, state, transaction)
449            .await?;
450
451        sqlx::query(
452            r#"
453            DELETE FROM roles
454            WHERE roles.uuid = $1
455            "#,
456        )
457        .bind(self.uuid)
458        .execute(&mut **transaction)
459        .await?;
460
461        self.run_after_delete_handlers(&options, state, transaction)
462            .await?;
463
464        Ok(())
465    }
466}
467
468#[derive(Validate)]
469pub struct DuplicateRoleOptions {
470    #[garde(length(chars, min = 1, max = 255))]
471    pub name: compact_str::CompactString,
472}
473
474#[async_trait::async_trait]
475impl DuplicableModel for Role {
476    type DuplicateOptions<'a> = DuplicateRoleOptions;
477
478    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
479        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Role>> =
480            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
481
482        &DUPLICATE_LISTENERS
483    }
484
485    async fn duplicate_with_transaction(
486        &self,
487        state: &crate::State,
488        options: Self::DuplicateOptions<'_>,
489        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
490    ) -> Result<Self, crate::database::DatabaseError> {
491        options.validate()?;
492
493        self.run_duplicate_handlers(&options, state, transaction)
494            .await?;
495
496        let mut query_builder = InsertQueryBuilder::new("roles");
497
498        query_builder
499            .set("name", &options.name)
500            .set("description", &self.description)
501            .set("require_two_factor", self.require_two_factor)
502            .set("admin_permissions", self.admin_permissions.as_ref())
503            .set("server_permissions", self.server_permissions.as_ref());
504
505        let row = query_builder
506            .returning(&Self::columns_sql(None))
507            .fetch_one(&mut **transaction)
508            .await?;
509        let mut role = Self::map(None, &row)?;
510
511        self.run_after_duplicate_handlers(&mut role, &options, state, transaction)
512            .await?;
513
514        Ok(role)
515    }
516}
517
518#[schema_extension_derive::extendible]
519#[init_args(Role, crate::State)]
520#[hook_args(crate::State)]
521#[derive(ToSchema, Serialize)]
522#[schema(title = "Role")]
523pub struct AdminApiRole {
524    pub uuid: uuid::Uuid,
525
526    pub name: compact_str::CompactString,
527    pub description: Option<compact_str::CompactString>,
528
529    pub require_two_factor: bool,
530
531    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
532    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
533
534    pub created: chrono::DateTime<chrono::Utc>,
535}