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(|| std::sync::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(&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                admin_permissions: self.admin_permissions,
155                server_permissions: self.server_permissions,
156                created: self.created.and_utc(),
157            },
158            api_object,
159            state
160        )?;
161
162        Ok(api_object)
163    }
164}
165
166#[async_trait::async_trait]
167impl ByUuid for Role {
168    async fn by_uuid(
169        database: &crate::database::Database,
170        uuid: uuid::Uuid,
171    ) -> Result<Self, crate::database::DatabaseError> {
172        let row = sqlx::query(&format!(
173            r#"
174            SELECT {}
175            FROM roles
176            WHERE roles.uuid = $1
177            "#,
178            Self::columns_sql(None)
179        ))
180        .bind(uuid)
181        .fetch_one(database.read())
182        .await?;
183
184        Self::map(None, &row)
185    }
186
187    async fn by_uuid_with_transaction(
188        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
189        uuid: uuid::Uuid,
190    ) -> Result<Self, crate::database::DatabaseError> {
191        let row = sqlx::query(&format!(
192            r#"
193            SELECT {}
194            FROM roles
195            WHERE roles.uuid = $1
196            "#,
197            Self::columns_sql(None)
198        ))
199        .bind(uuid)
200        .fetch_one(&mut **transaction)
201        .await?;
202
203        Self::map(None, &row)
204    }
205}
206
207#[derive(ToSchema, Deserialize, Validate)]
208pub struct CreateRoleOptions {
209    #[garde(length(chars, min = 1, max = 255))]
210    #[schema(min_length = 1, max_length = 255)]
211    pub name: compact_str::CompactString,
212    #[garde(length(chars, min = 1, max = 1024))]
213    #[schema(min_length = 1, max_length = 1024)]
214    pub description: Option<compact_str::CompactString>,
215    #[garde(skip)]
216    pub require_two_factor: bool,
217    #[garde(custom(crate::permissions::validate_admin_permissions))]
218    pub admin_permissions: Vec<compact_str::CompactString>,
219    #[garde(custom(crate::permissions::validate_server_permissions))]
220    pub server_permissions: Vec<compact_str::CompactString>,
221}
222
223#[async_trait::async_trait]
224impl CreatableModel for Role {
225    type CreateOptions<'a> = CreateRoleOptions;
226    type CreateResult = Self;
227
228    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
229        static CREATE_LISTENERS: LazyLock<CreateListenerList<Role>> =
230            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
231
232        &CREATE_LISTENERS
233    }
234
235    async fn create_with_transaction(
236        state: &crate::State,
237        mut options: Self::CreateOptions<'_>,
238        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
239    ) -> Result<Self, crate::database::DatabaseError> {
240        options.validate()?;
241
242        let mut query_builder = InsertQueryBuilder::new("roles");
243
244        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
245
246        query_builder
247            .set("name", &options.name)
248            .set("description", &options.description)
249            .set("require_two_factor", options.require_two_factor)
250            .set("admin_permissions", &options.admin_permissions)
251            .set("server_permissions", &options.server_permissions);
252
253        let row = query_builder
254            .returning(&Self::columns_sql(None))
255            .fetch_one(&mut **transaction)
256            .await?;
257        let mut role = Self::map(None, &row)?;
258
259        Self::run_after_create_handlers(&mut role, &options, state, transaction).await?;
260
261        Ok(role)
262    }
263}
264
265#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
266pub struct UpdateRoleOptions {
267    #[garde(length(chars, min = 1, max = 255))]
268    #[schema(min_length = 1, max_length = 255)]
269    pub name: Option<compact_str::CompactString>,
270    #[garde(length(chars, min = 1, max = 1024))]
271    #[schema(min_length = 1, max_length = 1024)]
272    #[serde(
273        default,
274        skip_serializing_if = "Option::is_none",
275        with = "::serde_with::rust::double_option"
276    )]
277    pub description: Option<Option<compact_str::CompactString>>,
278    #[garde(skip)]
279    pub require_two_factor: Option<bool>,
280    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
281    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
282    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
283    pub server_permissions: Option<Vec<compact_str::CompactString>>,
284}
285
286#[async_trait::async_trait]
287impl UpdatableModel for Role {
288    type UpdateOptions = UpdateRoleOptions;
289
290    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
291        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Role>> =
292            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
293
294        &UPDATE_LISTENERS
295    }
296
297    async fn update_with_transaction(
298        &mut self,
299        state: &crate::State,
300        mut options: Self::UpdateOptions,
301        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
302    ) -> Result<(), crate::database::DatabaseError> {
303        options.validate()?;
304
305        let mut query_builder = UpdateQueryBuilder::new("roles");
306
307        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
308            .await?;
309
310        query_builder
311            .set("name", options.name.as_ref())
312            .set(
313                "description",
314                options.description.as_ref().map(|d| d.as_ref()),
315            )
316            .set("require_two_factor", options.require_two_factor)
317            .set("admin_permissions", options.admin_permissions.as_ref())
318            .set("server_permissions", options.server_permissions.as_ref())
319            .where_eq("uuid", self.uuid);
320
321        query_builder.execute(&mut **transaction).await?;
322
323        if let Some(name) = options.name {
324            self.name = name;
325        }
326        if let Some(description) = options.description {
327            self.description = description;
328        }
329        if let Some(require_two_factor) = options.require_two_factor {
330            self.require_two_factor = require_two_factor;
331        }
332        if let Some(admin_permissions) = options.admin_permissions {
333            self.admin_permissions = Arc::new(admin_permissions);
334        }
335        if let Some(server_permissions) = options.server_permissions {
336            self.server_permissions = Arc::new(server_permissions);
337        }
338
339        self.run_after_update_handlers(state, transaction).await?;
340
341        Ok(())
342    }
343}
344
345#[async_trait::async_trait]
346impl DeletableModel for Role {
347    type DeleteOptions = ();
348
349    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
350        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Role>> =
351            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
352
353        &DELETE_LISTENERS
354    }
355
356    async fn delete_with_transaction(
357        &self,
358        state: &crate::State,
359        options: Self::DeleteOptions,
360        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
361    ) -> Result<(), anyhow::Error> {
362        self.run_delete_handlers(&options, state, transaction)
363            .await?;
364
365        sqlx::query(
366            r#"
367            DELETE FROM roles
368            WHERE roles.uuid = $1
369            "#,
370        )
371        .bind(self.uuid)
372        .execute(&mut **transaction)
373        .await?;
374
375        self.run_after_delete_handlers(&options, state, transaction)
376            .await?;
377
378        Ok(())
379    }
380}
381
382#[schema_extension_derive::extendible]
383#[init_args(Role, crate::State)]
384#[hook_args(crate::State)]
385#[derive(ToSchema, Serialize)]
386#[schema(title = "Role")]
387pub struct AdminApiRole {
388    pub uuid: uuid::Uuid,
389
390    pub name: compact_str::CompactString,
391    pub description: Option<compact_str::CompactString>,
392
393    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
394    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
395
396    pub created: chrono::DateTime<chrono::Utc>,
397}