1use 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#[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 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#[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#[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 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#[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 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#[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#[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#[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#[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#[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#[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#[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#[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
772pub 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}