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};
23use utoipa::{OpenApi, ToSchema};
24
25#[derive(OpenApi)]
26#[openapi(paths(
27    get_all_organizations,
28    create_organization,
29    get_organization,
30    update_organization,
31    soft_delete_organization,
32    get_organization_courses,
33    get_organization_duplicatable_courses,
34    get_organization_course_count,
35    get_organization_active_courses,
36    get_organization_active_courses_count,
37    set_organization_image,
38    remove_organization_image,
39    get_course_exams,
40    get_org_exams,
41    get_org_exam_with_exam_id,
42    create_exam
43))]
44pub(crate) struct MainFrontendOrganizationsApiDoc;
45
46#[allow(dead_code)]
47#[derive(Debug, ToSchema)]
48struct OrganizationImageUploadPayload {
49    #[schema(content_media_type = "application/octet-stream")]
50    file: Vec<u8>,
51}
52
53/**
54GET `/api/v0/main-frontend/organizations` - Returns a list of all organizations.
55*/
56#[utoipa::path(
57    get,
58    path = "",
59    operation_id = "getOrganizations",
60    tag = "organizations",
61    responses(
62        (status = 200, description = "Organizations", body = [Organization])
63    )
64)]
65#[instrument(skip(pool, file_store, app_conf))]
66async fn get_all_organizations(
67    pool: web::Data<PgPool>,
68    file_store: web::Data<dyn FileStore>,
69    app_conf: web::Data<ApplicationConfiguration>,
70    user: Option<AuthUser>,
71) -> ControllerResult<web::Json<Vec<Organization>>> {
72    let mut conn = pool.acquire().await?;
73
74    let is_admin = if let Some(user) = user {
75        is_user_global_admin(&mut conn, user.id).await?
76    } else {
77        false
78    };
79
80    // Choose query based on admin status
81    let raw_organizations = if is_admin {
82        models::organizations::all_organizations_include_hidden(&mut conn).await?
83    } else {
84        models::organizations::all_organizations(&mut conn).await?
85    };
86
87    let organizations = raw_organizations
88        .into_iter()
89        .map(|org| Organization::from_database_organization(org, file_store.as_ref(), &app_conf))
90        .collect();
91
92    let token = skip_authorize();
93    token.authorized_ok(web::Json(organizations))
94}
95
96/**
97GET `/api/v0/main-frontend/organizations/{organization_id}/courses"` - Returns a list of all courses in a organization.
98*/
99#[utoipa::path(
100    get,
101    path = "/{organization_id}/courses",
102    operation_id = "getOrganizationCourses",
103    tag = "organizations",
104    params(
105        ("organization_id" = Uuid, Path, description = "Organization id"),
106        ("page" = Option<i64>, Query, description = "Page number"),
107        ("limit" = Option<i64>, Query, description = "Page size")
108    ),
109    responses(
110        (status = 200, description = "Organization courses", body = [Course])
111    )
112)]
113#[instrument(skip(pool))]
114async fn get_organization_courses(
115    organization_id: web::Path<Uuid>,
116    pool: web::Data<PgPool>,
117    user: Option<AuthUser>,
118    pagination: web::Query<Pagination>,
119) -> ControllerResult<web::Json<Vec<Course>>> {
120    let mut conn = pool.acquire().await?;
121
122    let user = user.map(|u| u.id);
123    let courses = models::courses::organization_courses_visible_to_user_paginated(
124        &mut conn,
125        *organization_id,
126        user,
127        *pagination,
128    )
129    .await?;
130
131    let token = skip_authorize();
132    token.authorized_ok(web::Json(courses))
133}
134
135/**
136GET `/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.
137*/
138#[utoipa::path(
139    get,
140    path = "/{organization_id}/courses/duplicatable",
141    operation_id = "getOrganizationDuplicatableCourses",
142    tag = "organizations",
143    params(
144        ("organization_id" = Uuid, Path, description = "Organization id")
145    ),
146    responses(
147        (status = 200, description = "Duplicatable organization courses", body = [Course])
148    )
149)]
150#[instrument(skip(pool))]
151async fn get_organization_duplicatable_courses(
152    organization_id: web::Path<Uuid>,
153    pool: web::Data<PgPool>,
154    user: AuthUser,
155) -> ControllerResult<web::Json<Vec<Course>>> {
156    let mut conn = pool.acquire().await?;
157    let courses = models::courses::get_by_organization_id(&mut conn, *organization_id).await?;
158
159    // We filter out the courses the user does not have permission to duplicate.
160    // Prefetch roles so that we can do multiple authorization checks without repeteadly querying the database.
161    let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
162
163    let mut duplicatable_courses = Vec::new();
164    for course in courses {
165        if authorize_with_fetched_list_of_roles(
166            &mut conn,
167            Action::Duplicate,
168            Some(user.id),
169            Resource::Course(course.id),
170            &user_roles,
171        )
172        .await
173        .is_ok()
174        {
175            duplicatable_courses.push(course);
176        }
177    }
178
179    let token = skip_authorize();
180    token.authorized_ok(web::Json(duplicatable_courses))
181}
182
183#[utoipa::path(
184    get,
185    path = "/{organization_id}/courses/count",
186    operation_id = "getOrganizationCourseCount",
187    tag = "organizations",
188    params(
189        ("organization_id" = Uuid, Path, description = "Organization id")
190    ),
191    responses(
192        (status = 200, description = "Organization course count", body = CourseCount)
193    )
194)]
195#[instrument(skip(pool))]
196async fn get_organization_course_count(
197    request_organization_id: web::Path<Uuid>,
198    pool: web::Data<PgPool>,
199) -> ControllerResult<Json<CourseCount>> {
200    let mut conn = pool.acquire().await?;
201    let result =
202        models::courses::organization_course_count(&mut conn, *request_organization_id).await?;
203
204    let token = skip_authorize();
205    token.authorized_ok(Json(result))
206}
207
208#[utoipa::path(
209    get,
210    path = "/{organization_id}/courses/active",
211    operation_id = "getOrganizationActiveCourses",
212    tag = "organizations",
213    params(
214        ("organization_id" = Uuid, Path, description = "Organization id"),
215        ("page" = Option<i64>, Query, description = "Page number"),
216        ("limit" = Option<i64>, Query, description = "Page size")
217    ),
218    responses(
219        (status = 200, description = "Active organization courses", body = [Course])
220    )
221)]
222#[instrument(skip(pool))]
223async fn get_organization_active_courses(
224    request_organization_id: web::Path<Uuid>,
225    pool: web::Data<PgPool>,
226    pagination: web::Query<Pagination>,
227) -> ControllerResult<Json<Vec<Course>>> {
228    let mut conn = pool.acquire().await?;
229    let courses = models::courses::get_active_courses_for_organization(
230        &mut conn,
231        *request_organization_id,
232        *pagination,
233    )
234    .await?;
235
236    let token = skip_authorize();
237    token.authorized_ok(Json(courses))
238}
239
240#[utoipa::path(
241    get,
242    path = "/{organization_id}/courses/active/count",
243    operation_id = "getOrganizationActiveCourseCount",
244    tag = "organizations",
245    params(
246        ("organization_id" = Uuid, Path, description = "Organization id")
247    ),
248    responses(
249        (status = 200, description = "Active organization course count", body = CourseCount)
250    )
251)]
252#[instrument(skip(pool))]
253async fn get_organization_active_courses_count(
254    request_organization_id: web::Path<Uuid>,
255    pool: web::Data<PgPool>,
256) -> ControllerResult<Json<CourseCount>> {
257    let mut conn = pool.acquire().await?;
258    let result = models::courses::get_active_courses_for_organization_count(
259        &mut conn,
260        *request_organization_id,
261    )
262    .await?;
263
264    let token = skip_authorize();
265    token.authorized_ok(Json(result))
266}
267
268/**
269PUT `/api/v0/main-frontend/organizations/:organizations_id/image` - Sets or updates the chapter image.
270
271# Example
272
273Request:
274```http
275PUT /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
276Content-Type: multipart/form-data
277
278BINARY_DATA
279```
280*/
281#[utoipa::path(
282    put,
283    path = "/{organization_id}/image",
284    operation_id = "updateOrganizationImage",
285    tag = "organizations",
286    params(
287        ("organization_id" = Uuid, Path, description = "Organization id")
288    ),
289    request_body(content = inline(OrganizationImageUploadPayload), content_type = "multipart/form-data"),
290    responses(
291        (status = 200, description = "Updated organization", body = serde_json::Value)
292    )
293)]
294#[instrument(skip(request, payload, pool, file_store, app_conf))]
295async fn set_organization_image(
296    request: HttpRequest,
297    payload: Multipart,
298    organization_id: web::Path<Uuid>,
299    pool: web::Data<PgPool>,
300    user: AuthUser,
301    file_store: web::Data<dyn FileStore>,
302    app_conf: web::Data<ApplicationConfiguration>,
303) -> ControllerResult<web::Json<Organization>> {
304    let mut conn = pool.acquire().await?;
305    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
306    let token = authorize(
307        &mut conn,
308        Act::Edit,
309        Some(user.id),
310        Res::Organization(organization.id),
311    )
312    .await?;
313    let organization_image = upload_image_for_organization(
314        request.headers(),
315        payload,
316        &organization,
317        &file_store,
318        user,
319        &mut conn,
320    )
321    .await?
322    .to_string_lossy()
323    .to_string();
324    let updated_organization = models::organizations::update_organization_image_path(
325        &mut conn,
326        organization.id,
327        Some(organization_image),
328    )
329    .await?;
330
331    // Remove old image if one exists.
332    if let Some(old_image_path) = organization.organization_image_path {
333        let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
334            ControllerError::new(
335                ControllerErrorType::InternalServerError,
336                original_error.to_string(),
337                Some(original_error.into()),
338            )
339        })?;
340        file_store.delete(&file).await.map_err(|original_error| {
341            ControllerError::new(
342                ControllerErrorType::InternalServerError,
343                original_error.to_string(),
344                Some(original_error.into()),
345            )
346        })?;
347    }
348
349    let response = Organization::from_database_organization(
350        updated_organization,
351        file_store.as_ref(),
352        app_conf.as_ref(),
353    );
354    token.authorized_ok(web::Json(response))
355}
356
357/**
358DELETE `/api/v0/main-frontend/organizations/:organizations_id/image` - Removes the organizations image.
359
360# Example
361
362Request:
363```http
364DELETE /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
365```
366*/
367#[utoipa::path(
368    delete,
369    path = "/{organization_id}/image",
370    operation_id = "deleteOrganizationImage",
371    tag = "organizations",
372    params(
373        ("organization_id" = Uuid, Path, description = "Organization id")
374    ),
375    responses(
376        (status = 200, description = "Organization image removed")
377    )
378)]
379#[instrument(skip(pool, file_store))]
380async fn remove_organization_image(
381    organization_id: web::Path<Uuid>,
382    pool: web::Data<PgPool>,
383    user: AuthUser,
384    file_store: web::Data<dyn FileStore>,
385) -> ControllerResult<web::Json<()>> {
386    let mut conn = pool.acquire().await?;
387    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
388    let token = authorize(
389        &mut conn,
390        Act::Edit,
391        Some(user.id),
392        Res::Organization(organization.id),
393    )
394    .await?;
395    if let Some(organization_image_path) = organization.organization_image_path {
396        let file = PathBuf::from_str(&organization_image_path).map_err(|original_error| {
397            ControllerError::new(
398                ControllerErrorType::InternalServerError,
399                original_error.to_string(),
400                Some(original_error.into()),
401            )
402        })?;
403        let _res =
404            models::organizations::update_organization_image_path(&mut conn, organization.id, None)
405                .await?;
406        file_store.delete(&file).await.map_err(|original_error| {
407            ControllerError::new(
408                ControllerErrorType::InternalServerError,
409                original_error.to_string(),
410                Some(original_error.into()),
411            )
412        })?;
413    }
414    token.authorized_ok(web::Json(()))
415}
416
417/**
418GET `/api/v0/main-frontend/organizations/{organization_id}` - Returns an organizations with id.
419*/
420#[utoipa::path(
421    get,
422    path = "/{organization_id}",
423    operation_id = "getOrganization",
424    tag = "organizations",
425    params(
426        ("organization_id" = Uuid, Path, description = "Organization id")
427    ),
428    responses(
429        (status = 200, description = "Organization", body = Organization)
430    )
431)]
432#[instrument(skip(pool, file_store, app_conf))]
433async fn get_organization(
434    organization_id: web::Path<Uuid>,
435    pool: web::Data<PgPool>,
436    file_store: web::Data<dyn FileStore>,
437    app_conf: web::Data<ApplicationConfiguration>,
438) -> ControllerResult<web::Json<Organization>> {
439    let mut conn = pool.acquire().await?;
440    let db_organization =
441        models::organizations::get_organization(&mut conn, *organization_id).await?;
442    let organization =
443        Organization::from_database_organization(db_organization, file_store.as_ref(), &app_conf);
444
445    let token = skip_authorize();
446    token.authorized_ok(web::Json(organization))
447}
448
449#[derive(Debug, Deserialize, ToSchema)]
450struct OrganizationUpdatePayload {
451    name: String,
452    hidden: bool,
453    slug: String,
454}
455
456/**
457PUT `/api/v0/main-frontend/organizations/{organization_id}`
458
459Updates an organization's name, hidden status, and slug.
460*/
461#[utoipa::path(
462    put,
463    path = "/{organization_id}",
464    operation_id = "updateOrganization",
465    tag = "organizations",
466    params(
467        ("organization_id" = Uuid, Path, description = "Organization id")
468    ),
469    request_body = OrganizationUpdatePayload,
470    responses(
471        (status = 200, description = "Organization updated")
472    )
473)]
474#[instrument(skip(pool))]
475async fn update_organization(
476    organization_id: web::Path<Uuid>,
477    payload: web::Json<OrganizationUpdatePayload>,
478    pool: web::Data<PgPool>,
479    user: AuthUser,
480) -> ControllerResult<web::Json<()>> {
481    let mut conn = pool.acquire().await?;
482    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
483
484    let token = authorize(
485        &mut conn,
486        Act::Edit,
487        Some(user.id),
488        Res::Organization(organization.id),
489    )
490    .await?;
491
492    models::organizations::update_name_and_hidden(
493        &mut conn,
494        *organization_id,
495        &payload.name,
496        payload.hidden,
497        &payload.slug,
498    )
499    .await?;
500
501    token.authorized_ok(web::Json(()))
502}
503
504#[derive(Debug, Deserialize, ToSchema)]
505struct OrganizationCreatePayload {
506    name: String,
507    slug: String,
508    hidden: bool,
509}
510
511/// POST `/api/v0/main-frontend/organizations`
512/// Creates a new organization with the given name, slug, and visibility status.
513///
514/// # Request body (JSON)
515/// {
516///     "name": "Example Organization",
517///     "slug": "example-org",
518///     "hidden": false
519/// }
520///
521/// # Response
522/// Returns the created organization.
523///
524/// # Permissions
525/// Only users with the `Admin` role can access this endpoint.
526#[utoipa::path(
527    post,
528    path = "",
529    operation_id = "createOrganization",
530    tag = "organizations",
531    request_body = OrganizationCreatePayload,
532    responses(
533        (status = 200, description = "Created organization", body = serde_json::Value)
534    )
535)]
536#[instrument(skip(pool, file_store, app_conf))]
537async fn create_organization(
538    payload: web::Json<OrganizationCreatePayload>,
539    pool: web::Data<PgPool>,
540    file_store: web::Data<dyn FileStore>,
541    app_conf: web::Data<ApplicationConfiguration>,
542    user: AuthUser,
543) -> ControllerResult<web::Json<Organization>> {
544    let mut conn = pool.acquire().await?;
545
546    let token = authorize(
547        &mut conn,
548        Action::Administrate,
549        Some(user.id),
550        Resource::GlobalPermissions,
551    )
552    .await?;
553
554    let mut tx = conn.begin().await?;
555
556    let org_id = match models::organizations::insert(
557        &mut tx,
558        PKeyPolicy::Generate,
559        &payload.name,
560        &payload.slug,
561        None,
562        payload.hidden,
563    )
564    .await
565    {
566        Ok(id) => id,
567        Err(err) => {
568            let err_str = err.to_string();
569            if err_str.contains("organizations_slug_key") {
570                return Err(ControllerError::new(
571                    ControllerErrorType::BadRequest,
572                    "An organization with this slug already exists.".to_string(),
573                    None,
574                ));
575            }
576            return Err(err.into());
577        }
578    };
579
580    tx.commit().await?;
581
582    let db_org = models::organizations::get_organization(&mut conn, org_id).await?;
583    let org =
584        Organization::from_database_organization(db_org, file_store.as_ref(), app_conf.as_ref());
585
586    token.authorized_ok(web::Json(org))
587}
588
589#[utoipa::path(
590    patch,
591    path = "/{organization_id}",
592    operation_id = "softDeleteOrganization",
593    tag = "organizations",
594    params(
595        ("organization_id" = Uuid, Path, description = "Organization id")
596    ),
597    responses(
598        (status = 200, description = "Organization soft deleted")
599    )
600)]
601#[instrument(skip(pool))]
602async fn soft_delete_organization(
603    org_id: web::Path<Uuid>,
604    pool: web::Data<PgPool>,
605    user: AuthUser,
606) -> ControllerResult<web::Json<()>> {
607    let mut conn = pool.acquire().await?;
608
609    let token = authorize(
610        &mut conn,
611        Action::Administrate,
612        Some(user.id),
613        Resource::GlobalPermissions,
614    )
615    .await?;
616
617    models::organizations::soft_delete(&mut conn, *org_id).await?;
618    token.authorized_ok(web::Json(()))
619}
620
621/**
622GET `/api/v0/main-frontend/organizations/{organization_id}/course_exams` - Returns an organizations exams in CourseExam form.
623*/
624#[utoipa::path(
625    get,
626    path = "/{organization_id}/course_exams",
627    operation_id = "getOrganizationCourseExams",
628    tag = "organizations",
629    params(
630        ("organization_id" = Uuid, Path, description = "Organization id")
631    ),
632    responses(
633        (status = 200, description = "Organization course exams", body = [CourseExam])
634    )
635)]
636#[instrument(skip(pool))]
637async fn get_course_exams(
638    pool: web::Data<PgPool>,
639    organization: web::Path<Uuid>,
640) -> ControllerResult<web::Json<Vec<CourseExam>>> {
641    let mut conn = pool.acquire().await?;
642    let exams = models::exams::get_course_exams_for_organization(&mut conn, *organization).await?;
643
644    let token = skip_authorize();
645    token.authorized_ok(web::Json(exams))
646}
647
648/**
649GET `/api/v0/main-frontend/organizations/{organization_id}/exams` - Returns an organizations exams in Exam form.
650*/
651#[utoipa::path(
652    get,
653    path = "/{organization_id}/org_exams",
654    operation_id = "getOrganizationExams",
655    tag = "organizations",
656    params(
657        ("organization_id" = Uuid, Path, description = "Organization id")
658    ),
659    responses(
660        (status = 200, description = "Organization exams", body = [OrgExam])
661    )
662)]
663#[instrument(skip(pool))]
664async fn get_org_exams(
665    pool: web::Data<PgPool>,
666    organization: web::Path<Uuid>,
667) -> ControllerResult<web::Json<Vec<OrgExam>>> {
668    let mut conn = pool.acquire().await?;
669    let exams = models::exams::get_exams_for_organization(&mut conn, *organization).await?;
670
671    let token = skip_authorize();
672    token.authorized_ok(web::Json(exams))
673}
674
675/**
676GET `/api/v0/main-frontend/organizations/{exam_id}/fetch_org_exam
677*/
678#[utoipa::path(
679    get,
680    path = "/{exam_id}/fetch_org_exam",
681    operation_id = "getOrganizationExamByExamId",
682    tag = "organizations",
683    params(
684        ("exam_id" = Uuid, Path, description = "Exam id")
685    ),
686    responses(
687        (status = 200, description = "Organization exam", body = OrgExam)
688    )
689)]
690#[instrument(skip(pool))]
691pub async fn get_org_exam_with_exam_id(
692    pool: web::Data<PgPool>,
693    exam_id: web::Path<Uuid>,
694    user: AuthUser,
695) -> ControllerResult<web::Json<OrgExam>> {
696    let mut conn = pool.acquire().await?;
697    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
698
699    let exam = models::exams::get_organization_exam_with_exam_id(&mut conn, *exam_id).await?;
700
701    token.authorized_ok(web::Json(exam))
702}
703
704/**
705POST `/api/v0/main-frontend/organizations/{organization_id}/exams` - Creates new exam for the organization.
706*/
707#[utoipa::path(
708    post,
709    path = "/{organization_id}/exams",
710    operation_id = "createOrganizationExam",
711    tag = "organizations",
712    params(
713        ("organization_id" = Uuid, Path, description = "Organization id")
714    ),
715    request_body = NewExam,
716    responses(
717        (status = 200, description = "Organization exam created")
718    )
719)]
720#[instrument(skip(pool))]
721async fn create_exam(
722    pool: web::Data<PgPool>,
723    payload: web::Json<NewExam>,
724    user: AuthUser,
725) -> ControllerResult<web::Json<()>> {
726    let mut conn = pool.acquire().await?;
727    let mut tx = conn.begin().await?;
728
729    let new_exam = payload.0;
730    let token = authorize(
731        &mut tx,
732        Act::CreateCoursesOrExams,
733        Some(user.id),
734        Res::Organization(new_exam.organization_id),
735    )
736    .await?;
737
738    let new_exam_id = models::exams::insert(&mut tx, PKeyPolicy::Generate, &new_exam).await?;
739    pages::insert_exam_page(
740        &mut tx,
741        new_exam_id,
742        NewPage {
743            chapter_id: None,
744            course_id: None,
745            exam_id: Some(new_exam_id),
746            front_page_of_chapter_id: None,
747            content: vec![],
748            content_search_language: Some("simple".to_string()),
749            exercise_slides: vec![],
750            exercise_tasks: vec![],
751            exercises: vec![],
752            title: "exam page".to_string(),
753            url_path: "/".to_string(),
754        },
755        user.id,
756    )
757    .await?;
758
759    models::roles::insert(
760        &mut tx,
761        user.id,
762        models::roles::UserRole::Teacher,
763        models::roles::RoleDomain::Exam(new_exam_id),
764    )
765    .await?;
766
767    tx.commit().await?;
768
769    token.authorized_ok(web::Json(()))
770}
771
772/**
773Add a route for each controller in this module.
774
775The name starts with an underline in order to appear before other functions in the module documentation.
776
777We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
778*/
779pub fn _add_routes(cfg: &mut ServiceConfig) {
780    cfg.route("", web::get().to(get_all_organizations))
781        .route("", web::post().to(create_organization))
782        .route("/{organization_id}", web::get().to(get_organization))
783        .route("/{organization_id}", web::put().to(update_organization))
784        .route(
785            "/{organization_id}",
786            web::patch().to(soft_delete_organization),
787        )
788        .route(
789            "/{organization_id}/courses",
790            web::get().to(get_organization_courses),
791        )
792        .route(
793            "/{organization_id}/courses/duplicatable",
794            web::get().to(get_organization_duplicatable_courses),
795        )
796        .route(
797            "/{organization_id}/courses/count",
798            web::get().to(get_organization_course_count),
799        )
800        .route(
801            "/{organization_id}/courses/active",
802            web::get().to(get_organization_active_courses),
803        )
804        .route(
805            "/{organization_id}/courses/active/count",
806            web::get().to(get_organization_active_courses_count),
807        )
808        .route(
809            "/{organization_id}/image",
810            web::put().to(set_organization_image),
811        )
812        .route(
813            "/{organization_id}/image",
814            web::delete().to(remove_organization_image),
815        )
816        .route(
817            "/{organization_id}/course_exams",
818            web::get().to(get_course_exams),
819        )
820        .route("/{organization_id}/org_exams", web::get().to(get_org_exams))
821        .route(
822            "/{exam_id}/fetch_org_exam",
823            web::get().to(get_org_exam_with_exam_id),
824        )
825        .route("/{organization_id}/exams", web::post().to(create_exam));
826}