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
98impl Role {
99    pub async fn all_with_pagination(
100        database: &crate::database::Database,
101        page: i64,
102        per_page: i64,
103        search: Option<&str>,
104    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
105        let offset = (page - 1) * per_page;
106
107        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
108            r#"
109            SELECT {}, COUNT(*) OVER() AS total_count
110            FROM roles
111            WHERE ($1 IS NULL OR roles.name ILIKE '%' || $1 || '%')
112            ORDER BY roles.created
113            LIMIT $2 OFFSET $3
114            "#,
115            Self::columns_sql(None)
116        )))
117        .bind(search)
118        .bind(per_page)
119        .bind(offset)
120        .fetch_all(database.read())
121        .await?;
122
123        Ok(super::Pagination {
124            total: rows
125                .first()
126                .map_or(Ok(0), |row| row.try_get("total_count"))?,
127            per_page,
128            page,
129            data: rows
130                .into_iter()
131                .map(|row| Self::map(None, &row))
132                .try_collect_vec()?,
133        })
134    }
135}
136
137#[async_trait::async_trait]
138impl IntoAdminApiObject for Role {
139    type AdminApiObject = AdminApiRole;
140    type ExtraArgs<'a> = ();
141
142    async fn into_admin_api_object<'a>(
143        self,
144        state: &crate::State,
145        _args: Self::ExtraArgs<'a>,
146    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
147        let api_object = AdminApiRole::init_hooks(&self, state).await?;
148
149        let api_object = finish_extendible!(
150            AdminApiRole {
151                uuid: self.uuid,
152                name: self.name,
153                description: self.description,
154                require_two_factor: self.require_two_factor,
155                admin_permissions: self.admin_permissions,
156                server_permissions: self.server_permissions,
157                created: self.created.and_utc(),
158            },
159            api_object,
160            state
161        )?;
162
163        Ok(api_object)
164    }
165}
166
167#[async_trait::async_trait]
168impl ByUuid for Role {
169    async fn by_uuid(
170        database: &crate::database::Database,
171        uuid: uuid::Uuid,
172    ) -> Result<Self, crate::database::DatabaseError> {
173        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
174            r#"
175            SELECT {}
176            FROM roles
177            WHERE roles.uuid = $1
178            "#,
179            Self::columns_sql(None)
180        )))
181        .bind(uuid)
182        .fetch_one(database.read())
183        .await?;
184
185        Self::map(None, &row)
186    }
187
188    async fn by_uuid_with_transaction(
189        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
190        uuid: uuid::Uuid,
191    ) -> Result<Self, crate::database::DatabaseError> {
192        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
193            r#"
194            SELECT {}
195            FROM roles
196            WHERE roles.uuid = $1
197            "#,
198            Self::columns_sql(None)
199        )))
200        .bind(uuid)
201        .fetch_one(&mut **transaction)
202        .await?;
203
204        Self::map(None, &row)
205    }
206}
207
208#[derive(ToSchema, Deserialize, Validate)]
209pub struct CreateRoleOptions {
210    #[garde(length(chars, min = 1, max = 255))]
211    #[schema(min_length = 1, max_length = 255)]
212    pub name: compact_str::CompactString,
213    #[garde(length(chars, min = 1, max = 1024))]
214    #[schema(min_length = 1, max_length = 1024)]
215    pub description: Option<compact_str::CompactString>,
216    #[garde(skip)]
217    pub require_two_factor: bool,
218    #[garde(custom(crate::permissions::validate_admin_permissions))]
219    pub admin_permissions: Vec<compact_str::CompactString>,
220    #[garde(custom(crate::permissions::validate_server_permissions))]
221    pub server_permissions: Vec<compact_str::CompactString>,
222}
223
224#[async_trait::async_trait]
225impl CreatableModel for Role {
226    type CreateOptions<'a> = CreateRoleOptions;
227    type CreateResult = Self;
228
229    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
230        static CREATE_LISTENERS: LazyLock<CreateListenerList<Role>> =
231            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
232
233        &CREATE_LISTENERS
234    }
235
236    async fn create_with_transaction(
237        state: &crate::State,
238        mut options: Self::CreateOptions<'_>,
239        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
240    ) -> Result<Self, crate::database::DatabaseError> {
241        options.validate()?;
242
243        let mut query_builder = InsertQueryBuilder::new("roles");
244
245        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
246
247        query_builder
248            .set("name", &options.name)
249            .set("description", &options.description)
250            .set("require_two_factor", options.require_two_factor)
251            .set("admin_permissions", &options.admin_permissions)
252            .set("server_permissions", &options.server_permissions);
253
254        let row = query_builder
255            .returning(&Self::columns_sql(None))
256            .fetch_one(&mut **transaction)
257            .await?;
258        let mut role = Self::map(None, &row)?;
259
260        Self::run_after_create_handlers(&mut role, &options, state, transaction).await?;
261
262        Ok(role)
263    }
264}
265
266#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
267pub struct UpdateRoleOptions {
268    #[garde(length(chars, min = 1, max = 255))]
269    #[schema(min_length = 1, max_length = 255)]
270    pub name: Option<compact_str::CompactString>,
271    #[garde(length(chars, min = 1, max = 1024))]
272    #[schema(min_length = 1, max_length = 1024)]
273    #[serde(
274        default,
275        skip_serializing_if = "Option::is_none",
276        with = "::serde_with::rust::double_option"
277    )]
278    pub description: Option<Option<compact_str::CompactString>>,
279    #[garde(skip)]
280    pub require_two_factor: Option<bool>,
281    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
282    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
283    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
284    pub server_permissions: Option<Vec<compact_str::CompactString>>,
285}
286
287#[async_trait::async_trait]
288impl UpdatableModel for Role {
289    type UpdateOptions = UpdateRoleOptions;
290
291    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
292        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Role>> =
293            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
294
295        &UPDATE_LISTENERS
296    }
297
298    async fn update_with_transaction(
299        &mut self,
300        state: &crate::State,
301        mut options: Self::UpdateOptions,
302        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
303    ) -> Result<(), crate::database::DatabaseError> {
304        options.validate()?;
305
306        let mut query_builder = UpdateQueryBuilder::new("roles");
307
308        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
309            .await?;
310
311        query_builder
312            .set("name", options.name.as_ref())
313            .set(
314                "description",
315                options.description.as_ref().map(|d| d.as_ref()),
316            )
317            .set("require_two_factor", options.require_two_factor)
318            .set("admin_permissions", options.admin_permissions.as_ref())
319            .set("server_permissions", options.server_permissions.as_ref())
320            .where_eq("uuid", self.uuid);
321
322        query_builder.execute(&mut **transaction).await?;
323
324        if let Some(name) = options.name {
325            self.name = name;
326        }
327        if let Some(description) = options.description {
328            self.description = description;
329        }
330        if let Some(require_two_factor) = options.require_two_factor {
331            self.require_two_factor = require_two_factor;
332        }
333        if let Some(admin_permissions) = options.admin_permissions {
334            self.admin_permissions = Arc::new(admin_permissions);
335        }
336        if let Some(server_permissions) = options.server_permissions {
337            self.server_permissions = Arc::new(server_permissions);
338        }
339
340        self.run_after_update_handlers(state, transaction).await?;
341
342        Ok(())
343    }
344}
345
346#[async_trait::async_trait]
347impl DeletableModel for Role {
348    type DeleteOptions = ();
349
350    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
351        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Role>> =
352            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
353
354        &DELETE_LISTENERS
355    }
356
357    async fn delete_with_transaction(
358        &self,
359        state: &crate::State,
360        options: Self::DeleteOptions,
361        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
362    ) -> Result<(), anyhow::Error> {
363        self.run_delete_handlers(&options, state, transaction)
364            .await?;
365
366        sqlx::query(
367            r#"
368            DELETE FROM roles
369            WHERE roles.uuid = $1
370            "#,
371        )
372        .bind(self.uuid)
373        .execute(&mut **transaction)
374        .await?;
375
376        self.run_after_delete_handlers(&options, state, transaction)
377            .await?;
378
379        Ok(())
380    }
381}
382
383#[derive(Validate)]
384pub struct DuplicateRoleOptions {
385    #[garde(length(chars, min = 1, max = 255))]
386    pub name: compact_str::CompactString,
387}
388
389#[async_trait::async_trait]
390impl DuplicableModel for Role {
391    type DuplicateOptions<'a> = DuplicateRoleOptions;
392
393    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
394        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Role>> =
395            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
396
397        &DUPLICATE_LISTENERS
398    }
399
400    async fn duplicate_with_transaction(
401        &self,
402        state: &crate::State,
403        options: Self::DuplicateOptions<'_>,
404        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
405    ) -> Result<Self, crate::database::DatabaseError> {
406        options.validate()?;
407
408        self.run_duplicate_handlers(&options, state, transaction)
409            .await?;
410
411        let mut query_builder = InsertQueryBuilder::new("roles");
412
413        query_builder
414            .set("name", &options.name)
415            .set("description", &self.description)
416            .set("require_two_factor", self.require_two_factor)
417            .set("admin_permissions", self.admin_permissions.as_ref())
418            .set("server_permissions", self.server_permissions.as_ref());
419
420        let row = query_builder
421            .returning(&Self::columns_sql(None))
422            .fetch_one(&mut **transaction)
423            .await?;
424        let mut role = Self::map(None, &row)?;
425
426        self.run_after_duplicate_handlers(&mut role, &options, state, transaction)
427            .await?;
428
429        Ok(role)
430    }
431}
432
433#[schema_extension_derive::extendible]
434#[init_args(Role, crate::State)]
435#[hook_args(crate::State)]
436#[derive(ToSchema, Serialize)]
437#[schema(title = "Role")]
438pub struct AdminApiRole {
439    pub uuid: uuid::Uuid,
440
441    pub name: compact_str::CompactString,
442    pub description: Option<compact_str::CompactString>,
443
444    pub require_two_factor: bool,
445
446    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
447    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
448
449    pub created: chrono::DateTime<chrono::Utc>,
450}