Skip to main content

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