headless_lms_server/controllers/main_frontend/
roles.rs

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