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};
7
8use crate::domain::authorization::skip_authorize;
9
10async fn authorize_role_management(
11    conn: &mut PgConnection,
12    domain: RoleDomain,
13    action: Act,
14    user_id: Uuid,
15) -> ControllerResult<()> {
16    let token = match domain {
17        RoleDomain::Global => {
18            authorize(conn, action, Some(user_id), Res::GlobalPermissions).await?
19        }
20        RoleDomain::Organization(id) => {
21            authorize(conn, action, Some(user_id), Res::Organization(id)).await?
22        }
23        RoleDomain::Course(id) => authorize(conn, action, Some(user_id), Res::Course(id)).await?,
24        RoleDomain::CourseInstance(id) => {
25            authorize(conn, action, Some(user_id), Res::CourseInstance(id)).await?
26        }
27        RoleDomain::Exam(id) => authorize(conn, Act::Edit, Some(user_id), Res::Exam(id)).await?,
28    };
29
30    token.authorized_ok(())
31}
32
33/**
34 * POST /api/v0/main-frontend/roles/add - Give a role to a user.
35 */
36#[instrument(skip(pool))]
37pub async fn set(
38    pool: web::Data<PgPool>,
39    role_info: web::Json<RoleInfo>,
40    user: AuthUser,
41) -> ControllerResult<HttpResponse> {
42    let mut conn = pool.acquire().await?;
43    authorize_role_management(
44        &mut conn,
45        role_info.domain,
46        Act::EditRole(role_info.role),
47        user.id,
48    )
49    .await?;
50
51    let target_user = users::try_get_by_email(&mut conn, &role_info.email).await?;
52    if let Some(target_user) = target_user {
53        roles::insert(&mut conn, target_user.id, role_info.role, role_info.domain).await?;
54        let token = skip_authorize();
55        return token.authorized_ok(HttpResponse::Ok().finish());
56    }
57    Err(ControllerError::new(
58        ControllerErrorType::NotFound,
59        "The user either does not exist or has not logged in to this website previously."
60            .to_string(),
61        None,
62    ))
63}
64
65/**
66 * POST /api/v0/main-frontend/roles/remove - Remove a role from a user.
67 */
68#[instrument(skip(pool))]
69pub async fn unset(
70    pool: web::Data<PgPool>,
71    role_info: web::Json<RoleInfo>,
72    user: AuthUser,
73) -> ControllerResult<HttpResponse> {
74    let mut conn = pool.acquire().await?;
75    authorize_role_management(
76        &mut conn,
77        role_info.domain,
78        Act::EditRole(role_info.role),
79        user.id,
80    )
81    .await?;
82    let target_user = users::get_by_email(&mut conn, &role_info.email).await?;
83    roles::remove(&mut conn, target_user.id, role_info.role, role_info.domain).await?;
84
85    let token = skip_authorize();
86    token.authorized_ok(HttpResponse::Ok().finish())
87}
88
89#[derive(Debug, Deserialize)]
90#[cfg_attr(feature = "ts_rs", derive(TS))]
91pub struct RoleQuery {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    global: Option<bool>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    organization_id: Option<Uuid>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    course_id: Option<Uuid>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    course_instance_id: Option<Uuid>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    exam_id: Option<Uuid>,
102}
103
104impl TryFrom<RoleQuery> for RoleDomain {
105    type Error = ControllerError;
106
107    fn try_from(
108        RoleQuery {
109            global,
110            organization_id,
111            course_id,
112            course_instance_id,
113            exam_id,
114        }: RoleQuery,
115    ) -> Result<Self, Self::Error> {
116        let domain = if global.unwrap_or_default() {
117            RoleDomain::Global
118        } else if let Some(id) = organization_id {
119            RoleDomain::Organization(id)
120        } else if let Some(id) = course_id {
121            RoleDomain::Course(id)
122        } else if let Some(id) = course_instance_id {
123            RoleDomain::CourseInstance(id)
124        } else if let Some(id) = exam_id {
125            RoleDomain::Exam(id)
126        } else {
127            return Err(ControllerError::new(
128                ControllerErrorType::BadRequest,
129                "Invalid query".to_string(),
130                None,
131            ));
132        };
133        Ok(domain)
134    }
135}
136
137/**
138 * GET /api/v0/main-frontend/roles - Get all roles for the given domain.
139 */
140#[instrument(skip(pool))]
141
142pub async fn fetch(
143    pool: web::Data<PgPool>,
144    query: web::Query<RoleQuery>,
145    user: AuthUser,
146) -> ControllerResult<web::Json<Vec<RoleUser>>> {
147    let mut conn = pool.acquire().await?;
148    let domain = query.into_inner().try_into()?;
149    authorize_role_management(&mut conn, domain, Act::Edit, user.id).await?;
150
151    let roles = roles::get(&mut conn, domain).await?;
152
153    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::AnyCourse).await?;
154    token.authorized_ok(web::Json(roles))
155}
156
157/**
158 * GET /api/v0/main-frontend/roles - Get all pending roles for the given domain.
159 */
160#[instrument(skip(pool))]
161
162pub async fn fetch_pending(
163    pool: web::Data<PgPool>,
164    query: web::Query<RoleQuery>,
165    user: AuthUser,
166) -> ControllerResult<web::Json<Vec<PendingRole>>> {
167    let mut conn = pool.acquire().await?;
168    let domain = query.into_inner().try_into()?;
169    authorize_role_management(&mut conn, domain, Act::Edit, user.id).await?;
170
171    let roles = pending_roles::get_all(&mut conn, domain).await?;
172    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::AnyCourse).await?;
173    token.authorized_ok(web::Json(roles))
174}
175
176/**
177Add a route for each controller in this module.
178
179The name starts with an underline in order to appear before other functions in the module documentation.
180
181We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
182*/
183pub fn _add_routes(cfg: &mut ServiceConfig) {
184    cfg.route("/add", web::post().to(set))
185        .route("/remove", web::post().to(unset))
186        .route("", web::get().to(fetch))
187        .route("/pending", web::get().to(fetch_pending));
188}