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
29impl BaseModel for Role {
30    const NAME: &'static str = "role";
31
32    #[inline]
33    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
34        let prefix = prefix.unwrap_or_default();
35
36        BTreeMap::from([
37            ("roles.uuid", compact_str::format_compact!("{prefix}uuid")),
38            ("roles.name", compact_str::format_compact!("{prefix}name")),
39            (
40                "roles.description",
41                compact_str::format_compact!("{prefix}description"),
42            ),
43            (
44                "roles.require_two_factor",
45                compact_str::format_compact!("{prefix}require_two_factor"),
46            ),
47            (
48                "roles.admin_permissions",
49                compact_str::format_compact!("{prefix}admin_permissions"),
50            ),
51            (
52                "roles.server_permissions",
53                compact_str::format_compact!("{prefix}server_permissions"),
54            ),
55            (
56                "roles.created",
57                compact_str::format_compact!("{prefix}created"),
58            ),
59        ])
60    }
61
62    #[inline]
63    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
64        let prefix = prefix.unwrap_or_default();
65
66        Ok(Self {
67            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
68            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
69            description: row
70                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
71            require_two_factor: row
72                .try_get(compact_str::format_compact!("{prefix}require_two_factor").as_str())?,
73            admin_permissions: Arc::new(
74                row.try_get(compact_str::format_compact!("{prefix}admin_permissions").as_str())?,
75            ),
76            server_permissions: Arc::new(
77                row.try_get(compact_str::format_compact!("{prefix}server_permissions").as_str())?,
78            ),
79            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
80        })
81    }
82}
83
84impl Role {
85    pub async fn all_with_pagination(
86        database: &crate::database::Database,
87        page: i64,
88        per_page: i64,
89        search: Option<&str>,
90    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
91        let offset = (page - 1) * per_page;
92
93        let rows = sqlx::query(&format!(
94            r#"
95            SELECT {}, COUNT(*) OVER() AS total_count
96            FROM roles
97            WHERE ($1 IS NULL OR roles.name ILIKE '%' || $1 || '%')
98            ORDER BY roles.created
99            LIMIT $2 OFFSET $3
100            "#,
101            Self::columns_sql(None)
102        ))
103        .bind(search)
104        .bind(per_page)
105        .bind(offset)
106        .fetch_all(database.read())
107        .await?;
108
109        Ok(super::Pagination {
110            total: rows
111                .first()
112                .map_or(Ok(0), |row| row.try_get("total_count"))?,
113            per_page,
114            page,
115            data: rows
116                .into_iter()
117                .map(|row| Self::map(None, &row))
118                .try_collect_vec()?,
119        })
120    }
121
122    #[inline]
123    pub fn into_admin_api_object(self) -> AdminApiRole {
124        AdminApiRole {
125            uuid: self.uuid,
126            name: self.name,
127            description: self.description,
128            admin_permissions: self.admin_permissions,
129            server_permissions: self.server_permissions,
130            created: self.created.and_utc(),
131        }
132    }
133}
134
135#[async_trait::async_trait]
136impl ByUuid for Role {
137    async fn by_uuid(
138        database: &crate::database::Database,
139        uuid: uuid::Uuid,
140    ) -> Result<Self, crate::database::DatabaseError> {
141        let row = sqlx::query(&format!(
142            r#"
143            SELECT {}
144            FROM roles
145            WHERE roles.uuid = $1
146            "#,
147            Self::columns_sql(None)
148        ))
149        .bind(uuid)
150        .fetch_one(database.read())
151        .await?;
152
153        Self::map(None, &row)
154    }
155}
156
157#[derive(ToSchema, Deserialize, Validate)]
158pub struct CreateRoleOptions {
159    #[garde(length(chars, min = 3, max = 255))]
160    #[schema(min_length = 3, max_length = 255)]
161    pub name: compact_str::CompactString,
162    #[garde(length(chars, min = 1, max = 1024))]
163    #[schema(min_length = 1, max_length = 1024)]
164    pub description: Option<compact_str::CompactString>,
165    #[garde(skip)]
166    pub require_two_factor: bool,
167    #[garde(custom(crate::permissions::validate_admin_permissions))]
168    pub admin_permissions: Vec<compact_str::CompactString>,
169    #[garde(custom(crate::permissions::validate_server_permissions))]
170    pub server_permissions: Vec<compact_str::CompactString>,
171}
172
173#[async_trait::async_trait]
174impl CreatableModel for Role {
175    type CreateOptions<'a> = CreateRoleOptions;
176    type CreateResult = Self;
177
178    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
179        static CREATE_LISTENERS: LazyLock<CreateListenerList<Role>> =
180            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
181
182        &CREATE_LISTENERS
183    }
184
185    async fn create(
186        state: &crate::State,
187        mut options: Self::CreateOptions<'_>,
188    ) -> Result<Self, crate::database::DatabaseError> {
189        options.validate()?;
190
191        let mut transaction = state.database.write().begin().await?;
192
193        let mut query_builder = InsertQueryBuilder::new("roles");
194
195        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
196            .await?;
197
198        query_builder
199            .set("name", &options.name)
200            .set("description", &options.description)
201            .set("require_two_factor", options.require_two_factor)
202            .set("admin_permissions", &options.admin_permissions)
203            .set("server_permissions", &options.server_permissions);
204
205        let row = query_builder
206            .returning(&Self::columns_sql(None))
207            .fetch_one(&mut *transaction)
208            .await?;
209        let role = Self::map(None, &row)?;
210
211        transaction.commit().await?;
212
213        Ok(role)
214    }
215}
216
217#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
218pub struct UpdateRoleOptions {
219    #[garde(length(chars, min = 3, max = 255))]
220    #[schema(min_length = 3, max_length = 255)]
221    pub name: Option<compact_str::CompactString>,
222    #[garde(length(chars, min = 1, max = 1024))]
223    #[schema(min_length = 1, max_length = 1024)]
224    #[serde(
225        default,
226        skip_serializing_if = "Option::is_none",
227        with = "::serde_with::rust::double_option"
228    )]
229    pub description: Option<Option<compact_str::CompactString>>,
230    #[garde(skip)]
231    pub require_two_factor: Option<bool>,
232    #[garde(inner(custom(crate::permissions::validate_admin_permissions)))]
233    pub admin_permissions: Option<Vec<compact_str::CompactString>>,
234    #[garde(inner(custom(crate::permissions::validate_server_permissions)))]
235    pub server_permissions: Option<Vec<compact_str::CompactString>>,
236}
237
238#[async_trait::async_trait]
239impl UpdatableModel for Role {
240    type UpdateOptions = UpdateRoleOptions;
241
242    fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
243        static UPDATE_LISTENERS: LazyLock<UpdateListenerList<Role>> =
244            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
245
246        &UPDATE_LISTENERS
247    }
248
249    async fn update(
250        &mut self,
251        state: &crate::State,
252        mut options: Self::UpdateOptions,
253    ) -> Result<(), crate::database::DatabaseError> {
254        options.validate()?;
255
256        let mut transaction = state.database.write().begin().await?;
257
258        let mut query_builder = UpdateQueryBuilder::new("roles");
259
260        Self::run_update_handlers(
261            self,
262            &mut options,
263            &mut query_builder,
264            state,
265            &mut transaction,
266        )
267        .await?;
268
269        query_builder
270            .set("name", options.name.as_ref())
271            .set(
272                "description",
273                options.description.as_ref().map(|d| d.as_ref()),
274            )
275            .set("require_two_factor", options.require_two_factor)
276            .set("admin_permissions", options.admin_permissions.as_ref())
277            .set("server_permissions", options.server_permissions.as_ref())
278            .where_eq("uuid", self.uuid);
279
280        query_builder.execute(&mut *transaction).await?;
281
282        if let Some(name) = options.name {
283            self.name = name;
284        }
285        if let Some(description) = options.description {
286            self.description = description;
287        }
288        if let Some(require_two_factor) = options.require_two_factor {
289            self.require_two_factor = require_two_factor;
290        }
291        if let Some(admin_permissions) = options.admin_permissions {
292            self.admin_permissions = Arc::new(admin_permissions);
293        }
294        if let Some(server_permissions) = options.server_permissions {
295            self.server_permissions = Arc::new(server_permissions);
296        }
297
298        transaction.commit().await?;
299
300        Ok(())
301    }
302}
303
304#[async_trait::async_trait]
305impl DeletableModel for Role {
306    type DeleteOptions = ();
307
308    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
309        static DELETE_LISTENERS: LazyLock<DeleteListenerList<Role>> =
310            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
311
312        &DELETE_LISTENERS
313    }
314
315    async fn delete(
316        &self,
317        state: &crate::State,
318        options: Self::DeleteOptions,
319    ) -> Result<(), anyhow::Error> {
320        let mut transaction = state.database.write().begin().await?;
321
322        self.run_delete_handlers(&options, state, &mut transaction)
323            .await?;
324
325        sqlx::query(
326            r#"
327            DELETE FROM roles
328            WHERE roles.uuid = $1
329            "#,
330        )
331        .bind(self.uuid)
332        .execute(&mut *transaction)
333        .await?;
334
335        transaction.commit().await?;
336
337        Ok(())
338    }
339}
340
341#[derive(ToSchema, Serialize)]
342#[schema(title = "Role")]
343pub struct AdminApiRole {
344    pub uuid: uuid::Uuid,
345
346    pub name: compact_str::CompactString,
347    pub description: Option<compact_str::CompactString>,
348
349    pub admin_permissions: Arc<Vec<compact_str::CompactString>>,
350    pub server_permissions: Arc<Vec<compact_str::CompactString>>,
351
352    pub created: chrono::DateTime<chrono::Utc>,
353}