headless_lms_server/controllers/main_frontend/
organizations.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/organizations`.
2
3use std::{path::PathBuf, str::FromStr};
4
5use models::{
6    courses::{Course, CourseCount},
7    exams::{CourseExam, NewExam, OrgExam},
8    organizations::Organization,
9    pages::{self, NewPage},
10};
11
12use crate::controllers::auth::is_user_global_admin;
13use crate::domain::authorization::{Action as Act, Resource as Res};
14use crate::{
15    controllers::helpers::file_uploading::upload_image_for_organization,
16    domain::authorization::{
17        Action, Resource, authorize, authorize_with_fetched_list_of_roles, skip_authorize,
18    },
19    prelude::*,
20};
21
22use actix_web::web::{self, Json};
23
24/**
25GET `/api/v0/main-frontend/organizations` - Returns a list of all organizations.
26*/
27
28#[instrument(skip(pool, file_store, app_conf))]
29async fn get_all_organizations(
30    pool: web::Data<PgPool>,
31    file_store: web::Data<dyn FileStore>,
32    app_conf: web::Data<ApplicationConfiguration>,
33    user: Option<AuthUser>,
34) -> ControllerResult<web::Json<Vec<Organization>>> {
35    let mut conn = pool.acquire().await?;
36
37    let is_admin = if let Some(user) = user {
38        is_user_global_admin(&mut conn, user.id).await?
39    } else {
40        false
41    };
42
43    // Choose query based on admin status
44    let raw_organizations = if is_admin {
45        models::organizations::all_organizations_include_hidden(&mut conn).await?
46    } else {
47        models::organizations::all_organizations(&mut conn).await?
48    };
49
50    let organizations = raw_organizations
51        .into_iter()
52        .map(|org| Organization::from_database_organization(org, file_store.as_ref(), &app_conf))
53        .collect();
54
55    let token = skip_authorize();
56    token.authorized_ok(web::Json(organizations))
57}
58
59/**
60GET `/api/v0/main-frontend/organizations/{organization_id}/courses"` - Returns a list of all courses in a organization.
61*/
62#[instrument(skip(pool))]
63async fn get_organization_courses(
64    organization_id: web::Path<Uuid>,
65    pool: web::Data<PgPool>,
66    user: Option<AuthUser>,
67    pagination: web::Query<Pagination>,
68) -> ControllerResult<web::Json<Vec<Course>>> {
69    let mut conn = pool.acquire().await?;
70
71    let user = user.map(|u| u.id);
72    let courses = models::courses::organization_courses_visible_to_user_paginated(
73        &mut conn,
74        *organization_id,
75        user,
76        *pagination,
77    )
78    .await?;
79
80    let token = skip_authorize();
81    token.authorized_ok(web::Json(courses))
82}
83
84/**
85GET `/api/v0/main-frontend/organizations/{organization_id}/courses/duplicatable"` - Returns a list of all courses in a organization that the current user has permission to duplicate.
86*/
87
88#[instrument(skip(pool))]
89async fn get_organization_duplicatable_courses(
90    organization_id: web::Path<Uuid>,
91    pool: web::Data<PgPool>,
92    user: AuthUser,
93) -> ControllerResult<web::Json<Vec<Course>>> {
94    let mut conn = pool.acquire().await?;
95    let courses = models::courses::get_by_organization_id(&mut conn, *organization_id).await?;
96
97    // We filter out the courses the user does not have permission to duplicate.
98    // Prefetch roles so that we can do multiple authorization checks without repeteadly querying the database.
99    let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
100
101    let mut duplicatable_courses = Vec::new();
102    for course in courses {
103        if authorize_with_fetched_list_of_roles(
104            &mut conn,
105            Action::Duplicate,
106            Some(user.id),
107            Resource::Course(course.id),
108            &user_roles,
109        )
110        .await
111        .is_ok()
112        {
113            duplicatable_courses.push(course);
114        }
115    }
116
117    let token = skip_authorize();
118    token.authorized_ok(web::Json(duplicatable_courses))
119}
120
121#[instrument(skip(pool))]
122async fn get_organization_course_count(
123    request_organization_id: web::Path<Uuid>,
124    pool: web::Data<PgPool>,
125) -> ControllerResult<Json<CourseCount>> {
126    let mut conn = pool.acquire().await?;
127    let result =
128        models::courses::organization_course_count(&mut conn, *request_organization_id).await?;
129
130    let token = skip_authorize();
131    token.authorized_ok(Json(result))
132}
133
134#[instrument(skip(pool))]
135async fn get_organization_active_courses(
136    request_organization_id: web::Path<Uuid>,
137    pool: web::Data<PgPool>,
138    pagination: web::Query<Pagination>,
139) -> ControllerResult<Json<Vec<Course>>> {
140    let mut conn = pool.acquire().await?;
141    let courses = models::courses::get_active_courses_for_organization(
142        &mut conn,
143        *request_organization_id,
144        *pagination,
145    )
146    .await?;
147
148    let token = skip_authorize();
149    token.authorized_ok(Json(courses))
150}
151
152#[instrument(skip(pool))]
153async fn get_organization_active_courses_count(
154    request_organization_id: web::Path<Uuid>,
155    pool: web::Data<PgPool>,
156) -> ControllerResult<Json<CourseCount>> {
157    let mut conn = pool.acquire().await?;
158    let result = models::courses::get_active_courses_for_organization_count(
159        &mut conn,
160        *request_organization_id,
161    )
162    .await?;
163
164    let token = skip_authorize();
165    token.authorized_ok(Json(result))
166}
167
168/**
169PUT `/api/v0/main-frontend/organizations/:organizations_id/image` - Sets or updates the chapter image.
170
171# Example
172
173Request:
174```http
175PUT /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
176Content-Type: multipart/form-data
177
178BINARY_DATA
179```
180*/
181
182#[instrument(skip(request, payload, pool, file_store, app_conf))]
183async fn set_organization_image(
184    request: HttpRequest,
185    payload: Multipart,
186    organization_id: web::Path<Uuid>,
187    pool: web::Data<PgPool>,
188    user: AuthUser,
189    file_store: web::Data<dyn FileStore>,
190    app_conf: web::Data<ApplicationConfiguration>,
191) -> ControllerResult<web::Json<Organization>> {
192    let mut conn = pool.acquire().await?;
193    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
194    let token = authorize(
195        &mut conn,
196        Act::Edit,
197        Some(user.id),
198        Res::Organization(organization.id),
199    )
200    .await?;
201    let organization_image = upload_image_for_organization(
202        request.headers(),
203        payload,
204        &organization,
205        &file_store,
206        user,
207        &mut conn,
208    )
209    .await?
210    .to_string_lossy()
211    .to_string();
212    let updated_organization = models::organizations::update_organization_image_path(
213        &mut conn,
214        organization.id,
215        Some(organization_image),
216    )
217    .await?;
218
219    // Remove old image if one exists.
220    if let Some(old_image_path) = organization.organization_image_path {
221        let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
222            ControllerError::new(
223                ControllerErrorType::InternalServerError,
224                original_error.to_string(),
225                Some(original_error.into()),
226            )
227        })?;
228        file_store.delete(&file).await.map_err(|original_error| {
229            ControllerError::new(
230                ControllerErrorType::InternalServerError,
231                original_error.to_string(),
232                Some(original_error.into()),
233            )
234        })?;
235    }
236
237    let response = Organization::from_database_organization(
238        updated_organization,
239        file_store.as_ref(),
240        app_conf.as_ref(),
241    );
242    token.authorized_ok(web::Json(response))
243}
244
245/**
246DELETE `/api/v0/main-frontend/organizations/:organizations_id/image` - Removes the organizations image.
247
248# Example
249
250Request:
251```http
252DELETE /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
253```
254*/
255
256#[instrument(skip(pool, file_store))]
257async fn remove_organization_image(
258    organization_id: web::Path<Uuid>,
259    pool: web::Data<PgPool>,
260    user: AuthUser,
261    file_store: web::Data<dyn FileStore>,
262) -> ControllerResult<web::Json<()>> {
263    let mut conn = pool.acquire().await?;
264    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
265    let token = authorize(
266        &mut conn,
267        Act::Edit,
268        Some(user.id),
269        Res::Organization(organization.id),
270    )
271    .await?;
272    if let Some(organization_image_path) = organization.organization_image_path {
273        let file = PathBuf::from_str(&organization_image_path).map_err(|original_error| {
274            ControllerError::new(
275                ControllerErrorType::InternalServerError,
276                original_error.to_string(),
277                Some(original_error.into()),
278            )
279        })?;
280        let _res =
281            models::organizations::update_organization_image_path(&mut conn, organization.id, None)
282                .await?;
283        file_store.delete(&file).await.map_err(|original_error| {
284            ControllerError::new(
285                ControllerErrorType::InternalServerError,
286                original_error.to_string(),
287                Some(original_error.into()),
288            )
289        })?;
290    }
291    token.authorized_ok(web::Json(()))
292}
293
294/**
295GET `/api/v0/main-frontend/organizations/{organization_id}` - Returns an organizations with id.
296*/
297
298#[instrument(skip(pool, file_store, app_conf))]
299async fn get_organization(
300    organization_id: web::Path<Uuid>,
301    pool: web::Data<PgPool>,
302    file_store: web::Data<dyn FileStore>,
303    app_conf: web::Data<ApplicationConfiguration>,
304) -> ControllerResult<web::Json<Organization>> {
305    let mut conn = pool.acquire().await?;
306    let db_organization =
307        models::organizations::get_organization(&mut conn, *organization_id).await?;
308    let organization =
309        Organization::from_database_organization(db_organization, file_store.as_ref(), &app_conf);
310
311    let token = skip_authorize();
312    token.authorized_ok(web::Json(organization))
313}
314
315#[derive(Debug, Deserialize)]
316struct OrganizationUpdatePayload {
317    name: String,
318    hidden: bool,
319    slug: String,
320}
321
322/**
323PUT `/api/v0/main-frontend/organizations/{organization_id}`
324
325Updates an organization's name, hidden status, and slug.
326*/
327#[instrument(skip(pool))]
328async fn update_organization(
329    organization_id: web::Path<Uuid>,
330    payload: web::Json<OrganizationUpdatePayload>,
331    pool: web::Data<PgPool>,
332    user: AuthUser,
333) -> ControllerResult<web::Json<()>> {
334    let mut conn = pool.acquire().await?;
335    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
336
337    let token = authorize(
338        &mut conn,
339        Act::Edit,
340        Some(user.id),
341        Res::Organization(organization.id),
342    )
343    .await?;
344
345    models::organizations::update_name_and_hidden(
346        &mut conn,
347        *organization_id,
348        &payload.name,
349        payload.hidden,
350        &payload.slug,
351    )
352    .await?;
353
354    token.authorized_ok(web::Json(()))
355}
356
357#[derive(Debug, Deserialize)]
358struct OrganizationCreatePayload {
359    name: String,
360    slug: String,
361    hidden: bool,
362}
363
364/// POST `/api/v0/main-frontend/organizations`
365/// Creates a new organization with the given name, slug, and visibility status.
366///
367/// # Request body (JSON)
368/// {
369///     "name": "Example Organization",
370///     "slug": "example-org",
371///     "hidden": false
372/// }
373///
374/// # Response
375/// Returns the created organization.
376///
377/// # Permissions
378/// Only users with the `Admin` role can access this endpoint.
379#[instrument(skip(pool, file_store, app_conf))]
380async fn create_organization(
381    payload: web::Json<OrganizationCreatePayload>,
382    pool: web::Data<PgPool>,
383    file_store: web::Data<dyn FileStore>,
384    app_conf: web::Data<ApplicationConfiguration>,
385    user: AuthUser,
386) -> ControllerResult<web::Json<Organization>> {
387    let mut conn = pool.acquire().await?;
388
389    let token = authorize(
390        &mut conn,
391        Action::Administrate,
392        Some(user.id),
393        Resource::GlobalPermissions,
394    )
395    .await?;
396
397    let mut tx = conn.begin().await?;
398
399    let org_id = match models::organizations::insert(
400        &mut tx,
401        PKeyPolicy::Generate,
402        &payload.name,
403        &payload.slug,
404        None,
405        payload.hidden,
406    )
407    .await
408    {
409        Ok(id) => id,
410        Err(err) => {
411            let err_str = err.to_string();
412            if err_str.contains("organizations_slug_key") {
413                return Err(ControllerError::new(
414                    ControllerErrorType::BadRequest,
415                    "An organization with this slug already exists.".to_string(),
416                    None,
417                ));
418            }
419            return Err(err.into());
420        }
421    };
422
423    tx.commit().await?;
424
425    let db_org = models::organizations::get_organization(&mut conn, org_id).await?;
426    let org =
427        Organization::from_database_organization(db_org, file_store.as_ref(), app_conf.as_ref());
428
429    token.authorized_ok(web::Json(org))
430}
431
432#[instrument(skip(pool))]
433async fn soft_delete_organization(
434    org_id: web::Path<Uuid>,
435    pool: web::Data<PgPool>,
436    user: AuthUser,
437) -> ControllerResult<web::Json<()>> {
438    let mut conn = pool.acquire().await?;
439
440    let token = authorize(
441        &mut conn,
442        Action::Administrate,
443        Some(user.id),
444        Resource::GlobalPermissions,
445    )
446    .await?;
447
448    models::organizations::soft_delete(&mut conn, *org_id).await?;
449    token.authorized_ok(web::Json(()))
450}
451
452/**
453GET `/api/v0/main-frontend/organizations/{organization_id}/course_exams` - Returns an organizations exams in CourseExam form.
454*/
455#[instrument(skip(pool))]
456async fn get_course_exams(
457    pool: web::Data<PgPool>,
458    organization: web::Path<Uuid>,
459) -> ControllerResult<web::Json<Vec<CourseExam>>> {
460    let mut conn = pool.acquire().await?;
461    let exams = models::exams::get_course_exams_for_organization(&mut conn, *organization).await?;
462
463    let token = skip_authorize();
464    token.authorized_ok(web::Json(exams))
465}
466
467/**
468GET `/api/v0/main-frontend/organizations/{organization_id}/exams` - Returns an organizations exams in Exam form.
469*/
470#[instrument(skip(pool))]
471async fn get_org_exams(
472    pool: web::Data<PgPool>,
473    organization: web::Path<Uuid>,
474) -> ControllerResult<web::Json<Vec<OrgExam>>> {
475    let mut conn = pool.acquire().await?;
476    let exams = models::exams::get_exams_for_organization(&mut conn, *organization).await?;
477
478    let token = skip_authorize();
479    token.authorized_ok(web::Json(exams))
480}
481
482/**
483GET `/api/v0/main-frontend/organizations/{exam_id}/fetch_org_exam
484*/
485#[instrument(skip(pool))]
486pub async fn get_org_exam_with_exam_id(
487    pool: web::Data<PgPool>,
488    exam_id: web::Path<Uuid>,
489    user: AuthUser,
490) -> ControllerResult<web::Json<OrgExam>> {
491    let mut conn = pool.acquire().await?;
492    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
493
494    let exam = models::exams::get_organization_exam_with_exam_id(&mut conn, *exam_id).await?;
495
496    token.authorized_ok(web::Json(exam))
497}
498
499/**
500POST `/api/v0/main-frontend/organizations/{organization_id}/exams` - Creates new exam for the organization.
501*/
502#[instrument(skip(pool))]
503async fn create_exam(
504    pool: web::Data<PgPool>,
505    payload: web::Json<NewExam>,
506    user: AuthUser,
507) -> ControllerResult<web::Json<()>> {
508    let mut conn = pool.acquire().await?;
509    let mut tx = conn.begin().await?;
510
511    let new_exam = payload.0;
512    let token = authorize(
513        &mut tx,
514        Act::CreateCoursesOrExams,
515        Some(user.id),
516        Res::Organization(new_exam.organization_id),
517    )
518    .await?;
519
520    let new_exam_id = models::exams::insert(&mut tx, PKeyPolicy::Generate, &new_exam).await?;
521    pages::insert_exam_page(
522        &mut tx,
523        new_exam_id,
524        NewPage {
525            chapter_id: None,
526            course_id: None,
527            exam_id: Some(new_exam_id),
528            front_page_of_chapter_id: None,
529            content: serde_json::Value::Array(vec![]),
530            content_search_language: Some("simple".to_string()),
531            exercise_slides: vec![],
532            exercise_tasks: vec![],
533            exercises: vec![],
534            title: "exam page".to_string(),
535            url_path: "/".to_string(),
536        },
537        user.id,
538    )
539    .await?;
540
541    models::roles::insert(
542        &mut tx,
543        user.id,
544        models::roles::UserRole::Teacher,
545        models::roles::RoleDomain::Exam(new_exam_id),
546    )
547    .await?;
548
549    tx.commit().await?;
550
551    token.authorized_ok(web::Json(()))
552}
553
554/**
555Add a route for each controller in this module.
556
557The name starts with an underline in order to appear before other functions in the module documentation.
558
559We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
560*/
561pub fn _add_routes(cfg: &mut ServiceConfig) {
562    cfg.route("", web::get().to(get_all_organizations))
563        .route("", web::post().to(create_organization))
564        .route("/{organization_id}", web::get().to(get_organization))
565        .route("/{organization_id}", web::put().to(update_organization))
566        .route(
567            "/{organization_id}",
568            web::patch().to(soft_delete_organization),
569        )
570        .route(
571            "/{organization_id}/courses",
572            web::get().to(get_organization_courses),
573        )
574        .route(
575            "/{organization_id}/courses/duplicatable",
576            web::get().to(get_organization_duplicatable_courses),
577        )
578        .route(
579            "/{organization_id}/courses/count",
580            web::get().to(get_organization_course_count),
581        )
582        .route(
583            "/{organization_id}/courses/active",
584            web::get().to(get_organization_active_courses),
585        )
586        .route(
587            "/{organization_id}/courses/active/count",
588            web::get().to(get_organization_active_courses_count),
589        )
590        .route(
591            "/{organization_id}/image",
592            web::put().to(set_organization_image),
593        )
594        .route(
595            "/{organization_id}/image",
596            web::delete().to(remove_organization_image),
597        )
598        .route(
599            "/{organization_id}/course_exams",
600            web::get().to(get_course_exams),
601        )
602        .route("/{organization_id}/org_exams", web::get().to(get_org_exams))
603        .route(
604            "/{organization_id}/fetch_org_exam",
605            web::get().to(get_org_exam_with_exam_id),
606        )
607        .route("/{organization_id}/exams", web::post().to(create_exam));
608}