Skip to main content

headless_lms_server/controllers/main_frontend/
roles.rs

1use crate::prelude::*;
2use models::{
3    error::TryToOptional,
4    pending_roles::{self, PendingRole},
5    roles::{self, RoleDomain, RoleInfo, RoleUser},
6    users,
7};
8use utoipa::{OpenApi, ToSchema};
9
10use crate::domain::authorization::skip_authorize;
11
12#[derive(OpenApi)]
13#[openapi(paths(set, unset, fetch, fetch_pending))]
14pub(crate) struct MainFrontendRolesApiDoc;
15
16async fn authorize_role_management(
17    conn: &mut PgConnection,
18    domain: RoleDomain,
19    action: Act,
20    user_id: Uuid,
21) -> ControllerResult<()> {
22    let token = match domain {
23        RoleDomain::Global => {
24            authorize(conn, action, Some(user_id), Res::GlobalPermissions).await?
25        }
26        RoleDomain::Organization(id) => {
27            authorize(conn, action, Some(user_id), Res::Organization(id)).await?
28        }
29        RoleDomain::Course(id) => authorize(conn, action, Some(user_id), Res::Course(id)).await?,
30        RoleDomain::CourseInstance(id) => {
31            authorize(conn, action, Some(user_id), Res::CourseInstance(id)).await?
32        }
33        RoleDomain::Exam(id) => authorize(conn, action, Some(user_id), Res::Exam(id)).await?,
34    };
35
36    token.authorized_ok(())
37}
38
39/**
40 * POST /api/v0/main-frontend/roles/add - Give a role to a user.
41 */
42#[utoipa::path(
43    post,
44    path = "/add",
45    operation_id = "addRole",
46    tag = "roles",
47    request_body = RoleInfo,
48    responses(
49        (status = 200, description = "Role added")
50    )
51)]
52#[instrument(skip(pool))]
53pub async fn set(
54    pool: web::Data<PgPool>,
55    role_info: web::Json<RoleInfo>,
56    user: AuthUser,
57) -> ControllerResult<HttpResponse> {
58    let mut conn = pool.acquire().await?;
59    authorize_role_management(
60        &mut conn,
61        role_info.domain,
62        Act::EditRole(role_info.role),
63        user.id,
64    )
65    .await?;
66
67    let target_user = users::get_by_email(&mut conn, &role_info.email)
68        .await
69        .optional()?;
70    if let Some(target_user) = target_user {
71        roles::insert(&mut conn, target_user.id, role_info.role, role_info.domain).await?;
72        let token = skip_authorize();
73        return token.authorized_ok(HttpResponse::Ok().finish());
74    }
75    Err(ControllerError::new(
76        ControllerErrorType::NotFound,
77        "The user either does not exist or has not logged in to this website previously."
78            .to_string(),
79        None,
80    ))
81}
82
83/**
84 * POST /api/v0/main-frontend/roles/remove - Remove a role from a user.
85 */
86#[utoipa::path(
87    post,
88    path = "/remove",
89    operation_id = "removeRole",
90    tag = "roles",
91    request_body = RoleInfo,
92    responses(
93        (status = 200, description = "Role removed")
94    )
95)]
96#[instrument(skip(pool))]
97pub async fn unset(
98    pool: web::Data<PgPool>,
99    role_info: web::Json<RoleInfo>,
100    user: AuthUser,
101) -> ControllerResult<HttpResponse> {
102    let mut conn = pool.acquire().await?;
103    authorize_role_management(
104        &mut conn,
105        role_info.domain,
106        Act::EditRole(role_info.role),
107        user.id,
108    )
109    .await?;
110    let target_user = users::get_by_email(&mut conn, &role_info.email).await?;
111    roles::remove(&mut conn, target_user.id, role_info.role, role_info.domain).await?;
112
113    let token = skip_authorize();
114    token.authorized_ok(HttpResponse::Ok().finish())
115}
116
117#[derive(Debug, Deserialize, ToSchema)]
118
119pub struct RoleQuery {
120    #[serde(skip_serializing_if = "Option::is_none")]
121    global: Option<bool>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    organization_id: Option<Uuid>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    course_id: Option<Uuid>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    course_instance_id: Option<Uuid>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    exam_id: Option<Uuid>,
130}
131
132impl TryFrom<RoleQuery> for RoleDomain {
133    type Error = ControllerError;
134
135    fn try_from(
136        RoleQuery {
137            global,
138            organization_id,
139            course_id,
140            course_instance_id,
141            exam_id,
142        }: RoleQuery,
143    ) -> Result<Self, Self::Error> {
144        let domain = if global.unwrap_or_default() {
145            RoleDomain::Global
146        } else if let Some(id) = organization_id {
147            RoleDomain::Organization(id)
148        } else if let Some(id) = course_id {
149            RoleDomain::Course(id)
150        } else if let Some(id) = course_instance_id {
151            RoleDomain::CourseInstance(id)
152        } else if let Some(id) = exam_id {
153            RoleDomain::Exam(id)
154        } else {
155            return Err(ControllerError::new(
156                ControllerErrorType::BadRequest,
157                "Invalid query".to_string(),
158                None,
159            ));
160        };
161        Ok(domain)
162    }
163}
164
165/**
166 * GET /api/v0/main-frontend/roles - Get all roles for the given domain.
167 */
168#[instrument(skip(pool))]
169#[utoipa::path(
170    get,
171    path = "",
172    operation_id = "getRoles",
173    tag = "roles",
174    params(
175        ("global" = Option<bool>, Query, description = "Global domain"),
176        ("organization_id" = Option<Uuid>, Query, description = "Organization id"),
177        ("course_id" = Option<Uuid>, Query, description = "Course id"),
178        ("course_instance_id" = Option<Uuid>, Query, description = "Course instance id"),
179        ("exam_id" = Option<Uuid>, Query, description = "Exam id")
180    ),
181    responses(
182        (status = 200, description = "Roles", body = [RoleUser])
183    )
184)]
185pub async fn fetch(
186    pool: web::Data<PgPool>,
187    query: web::Query<RoleQuery>,
188    user: AuthUser,
189) -> ControllerResult<web::Json<Vec<RoleUser>>> {
190    let mut conn = pool.acquire().await?;
191    let domain = query.into_inner().try_into()?;
192    authorize_role_management(&mut conn, domain, Act::Edit, user.id).await?;
193
194    let roles = roles::get(&mut conn, domain).await?;
195
196    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::AnyCourse).await?;
197    token.authorized_ok(web::Json(roles))
198}
199
200/**
201 * GET /api/v0/main-frontend/roles - Get all pending roles for the given domain.
202 */
203#[instrument(skip(pool))]
204#[utoipa::path(
205    get,
206    path = "/pending",
207    operation_id = "getPendingRoles",
208    tag = "roles",
209    params(
210        ("global" = Option<bool>, Query, description = "Global domain"),
211        ("organization_id" = Option<Uuid>, Query, description = "Organization id"),
212        ("course_id" = Option<Uuid>, Query, description = "Course id"),
213        ("course_instance_id" = Option<Uuid>, Query, description = "Course instance id"),
214        ("exam_id" = Option<Uuid>, Query, description = "Exam id")
215    ),
216    responses(
217        (status = 200, description = "Pending roles", body = [PendingRole])
218    )
219)]
220pub async fn fetch_pending(
221    pool: web::Data<PgPool>,
222    query: web::Query<RoleQuery>,
223    user: AuthUser,
224) -> ControllerResult<web::Json<Vec<PendingRole>>> {
225    let mut conn = pool.acquire().await?;
226    let domain = query.into_inner().try_into()?;
227    authorize_role_management(&mut conn, domain, Act::Edit, user.id).await?;
228
229    let roles = pending_roles::get_all(&mut conn, domain).await?;
230    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::AnyCourse).await?;
231    token.authorized_ok(web::Json(roles))
232}
233
234/**
235Add a route for each controller in this module.
236
237The name starts with an underline in order to appear before other functions in the module documentation.
238
239We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
240*/
241pub fn _add_routes(cfg: &mut ServiceConfig) {
242    cfg.route("/add", web::post().to(set))
243        .route("/remove", web::post().to(unset))
244        .route("", web::get().to(fetch))
245        .route("/pending", web::get().to(fetch_pending));
246}