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", value_type = String, format = Binary)]
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 user: Option<AuthUser>,
439) -> ControllerResult<web::Json<Organization>> {
440 let mut conn = pool.acquire().await?;
441 let db_organization =
442 models::organizations::get_organization(&mut conn, *organization_id).await?;
443 if db_organization.deleted_at.is_some() {
444 return Err(organization_not_found());
445 }
446 let token = if db_organization.hidden {
447 let Some(user) = user else {
448 return Err(organization_not_found());
449 };
450 match authorize(
451 &mut conn,
452 Act::Edit,
453 Some(user.id),
454 Res::Organization(db_organization.id),
455 )
456 .await
457 {
458 Ok(token) => token,
459 Err(err) if matches!(err.error_type(), ControllerErrorType::Forbidden) => {
460 return Err(organization_not_found());
461 }
462 Err(err) => return Err(err),
463 }
464 } else {
465 skip_authorize()
466 };
467 let organization =
468 Organization::from_database_organization(db_organization, file_store.as_ref(), &app_conf);
469
470 token.authorized_ok(web::Json(organization))
471}
472
473fn organization_not_found() -> ControllerError {
474 controller_err!(NotFound, "Organization not found".to_string())
475}
476
477#[derive(Debug, Deserialize, ToSchema)]
478struct OrganizationUpdatePayload {
479 name: String,
480 hidden: bool,
481 slug: String,
482}
483
484#[utoipa::path(
490 put,
491 path = "/{organization_id}",
492 operation_id = "updateOrganization",
493 tag = "organizations",
494 params(
495 ("organization_id" = Uuid, Path, description = "Organization id")
496 ),
497 request_body = OrganizationUpdatePayload,
498 responses(
499 (status = 200, description = "Organization updated")
500 )
501)]
502#[instrument(skip(pool))]
503async fn update_organization(
504 organization_id: web::Path<Uuid>,
505 payload: web::Json<OrganizationUpdatePayload>,
506 pool: web::Data<PgPool>,
507 user: AuthUser,
508) -> ControllerResult<web::Json<()>> {
509 let mut conn = pool.acquire().await?;
510 let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
511
512 let token = authorize(
513 &mut conn,
514 Act::Edit,
515 Some(user.id),
516 Res::Organization(organization.id),
517 )
518 .await?;
519
520 models::organizations::update_name_and_hidden(
521 &mut conn,
522 *organization_id,
523 &payload.name,
524 payload.hidden,
525 &payload.slug,
526 )
527 .await?;
528
529 token.authorized_ok(web::Json(()))
530}
531
532#[derive(Debug, Deserialize, ToSchema)]
533struct OrganizationCreatePayload {
534 name: String,
535 slug: String,
536 hidden: bool,
537}
538
539#[utoipa::path(
555 post,
556 path = "",
557 operation_id = "createOrganization",
558 tag = "organizations",
559 request_body = OrganizationCreatePayload,
560 responses(
561 (status = 200, description = "Created organization", body = serde_json::Value)
562 )
563)]
564#[instrument(skip(pool, file_store, app_conf))]
565async fn create_organization(
566 payload: web::Json<OrganizationCreatePayload>,
567 pool: web::Data<PgPool>,
568 file_store: web::Data<dyn FileStore>,
569 app_conf: web::Data<ApplicationConfiguration>,
570 user: AuthUser,
571) -> ControllerResult<web::Json<Organization>> {
572 let mut conn = pool.acquire().await?;
573
574 let token = authorize(
575 &mut conn,
576 Action::Administrate,
577 Some(user.id),
578 Resource::GlobalPermissions,
579 )
580 .await?;
581
582 let mut tx = conn.begin().await?;
583
584 let org_id = match models::organizations::insert(
585 &mut tx,
586 PKeyPolicy::Generate,
587 &payload.name,
588 &payload.slug,
589 None,
590 payload.hidden,
591 )
592 .await
593 {
594 Ok(id) => id,
595 Err(err) => {
596 let err_str = err.to_string();
597 if err_str.contains("organizations_slug_key") {
598 return Err(ControllerError::new(
599 ControllerErrorType::BadRequest,
600 "An organization with this slug already exists.".to_string(),
601 None,
602 ));
603 }
604 return Err(err.into());
605 }
606 };
607
608 tx.commit().await?;
609
610 let db_org = models::organizations::get_organization(&mut conn, org_id).await?;
611 let org =
612 Organization::from_database_organization(db_org, file_store.as_ref(), app_conf.as_ref());
613
614 token.authorized_ok(web::Json(org))
615}
616
617#[utoipa::path(
618 patch,
619 path = "/{organization_id}",
620 operation_id = "softDeleteOrganization",
621 tag = "organizations",
622 params(
623 ("organization_id" = Uuid, Path, description = "Organization id")
624 ),
625 responses(
626 (status = 200, description = "Organization soft deleted")
627 )
628)]
629#[instrument(skip(pool))]
630async fn soft_delete_organization(
631 org_id: web::Path<Uuid>,
632 pool: web::Data<PgPool>,
633 user: AuthUser,
634) -> ControllerResult<web::Json<()>> {
635 let mut conn = pool.acquire().await?;
636
637 let token = authorize(
638 &mut conn,
639 Action::Administrate,
640 Some(user.id),
641 Resource::GlobalPermissions,
642 )
643 .await?;
644
645 models::organizations::soft_delete(&mut conn, *org_id).await?;
646 token.authorized_ok(web::Json(()))
647}
648
649#[utoipa::path(
653 get,
654 path = "/{organization_id}/course_exams",
655 operation_id = "getOrganizationCourseExams",
656 tag = "organizations",
657 params(
658 ("organization_id" = Uuid, Path, description = "Organization id")
659 ),
660 responses(
661 (status = 200, description = "Organization course exams", body = [CourseExam])
662 )
663)]
664#[instrument(skip(pool))]
665async fn get_course_exams(
666 pool: web::Data<PgPool>,
667 organization: web::Path<Uuid>,
668) -> ControllerResult<web::Json<Vec<CourseExam>>> {
669 let mut conn = pool.acquire().await?;
670 let exams = models::exams::get_course_exams_for_organization(&mut conn, *organization).await?;
671
672 let token = skip_authorize();
673 token.authorized_ok(web::Json(exams))
674}
675
676#[utoipa::path(
680 get,
681 path = "/{organization_id}/org_exams",
682 operation_id = "getOrganizationExams",
683 tag = "organizations",
684 params(
685 ("organization_id" = Uuid, Path, description = "Organization id")
686 ),
687 responses(
688 (status = 200, description = "Organization exams", body = [OrgExam])
689 )
690)]
691#[instrument(skip(pool))]
692async fn get_org_exams(
693 pool: web::Data<PgPool>,
694 organization: web::Path<Uuid>,
695) -> ControllerResult<web::Json<Vec<OrgExam>>> {
696 let mut conn = pool.acquire().await?;
697 let exams = models::exams::get_exams_for_organization(&mut conn, *organization).await?;
698
699 let token = skip_authorize();
700 token.authorized_ok(web::Json(exams))
701}
702
703#[utoipa::path(
707 get,
708 path = "/{exam_id}/fetch_org_exam",
709 operation_id = "getOrganizationExamByExamId",
710 tag = "organizations",
711 params(
712 ("exam_id" = Uuid, Path, description = "Exam id")
713 ),
714 responses(
715 (status = 200, description = "Organization exam", body = OrgExam)
716 )
717)]
718#[instrument(skip(pool))]
719pub async fn get_org_exam_with_exam_id(
720 pool: web::Data<PgPool>,
721 exam_id: web::Path<Uuid>,
722 user: AuthUser,
723) -> ControllerResult<web::Json<OrgExam>> {
724 let mut conn = pool.acquire().await?;
725 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
726
727 let exam = models::exams::get_organization_exam_with_exam_id(&mut conn, *exam_id).await?;
728
729 token.authorized_ok(web::Json(exam))
730}
731
732#[utoipa::path(
736 post,
737 path = "/{organization_id}/exams",
738 operation_id = "createOrganizationExam",
739 tag = "organizations",
740 params(
741 ("organization_id" = Uuid, Path, description = "Organization id")
742 ),
743 request_body = NewExam,
744 responses(
745 (status = 200, description = "Organization exam created")
746 )
747)]
748#[instrument(skip(pool))]
749async fn create_exam(
750 pool: web::Data<PgPool>,
751 payload: web::Json<NewExam>,
752 user: AuthUser,
753) -> ControllerResult<web::Json<()>> {
754 let mut conn = pool.acquire().await?;
755 let mut tx = conn.begin().await?;
756
757 let new_exam = payload.0;
758 let token = authorize(
759 &mut tx,
760 Act::CreateCoursesOrExams,
761 Some(user.id),
762 Res::Organization(new_exam.organization_id),
763 )
764 .await?;
765
766 let new_exam_id = models::exams::insert(&mut tx, PKeyPolicy::Generate, &new_exam).await?;
767 pages::insert_exam_page(
768 &mut tx,
769 new_exam_id,
770 NewPage {
771 chapter_id: None,
772 course_id: None,
773 exam_id: Some(new_exam_id),
774 front_page_of_chapter_id: None,
775 content: vec![],
776 content_search_language: Some("simple".to_string()),
777 exercise_slides: vec![],
778 exercise_tasks: vec![],
779 exercises: vec![],
780 title: "exam page".to_string(),
781 url_path: "/".to_string(),
782 },
783 user.id,
784 )
785 .await?;
786
787 models::roles::insert(
788 &mut tx,
789 user.id,
790 models::roles::UserRole::Teacher,
791 models::roles::RoleDomain::Exam(new_exam_id),
792 )
793 .await?;
794
795 tx.commit().await?;
796
797 token.authorized_ok(web::Json(()))
798}
799
800pub fn _add_routes(cfg: &mut ServiceConfig) {
808 cfg.route("", web::get().to(get_all_organizations))
809 .route("", web::post().to(create_organization))
810 .route("/{organization_id}", web::get().to(get_organization))
811 .route("/{organization_id}", web::put().to(update_organization))
812 .route(
813 "/{organization_id}",
814 web::patch().to(soft_delete_organization),
815 )
816 .route(
817 "/{organization_id}/courses",
818 web::get().to(get_organization_courses),
819 )
820 .route(
821 "/{organization_id}/courses/duplicatable",
822 web::get().to(get_organization_duplicatable_courses),
823 )
824 .route(
825 "/{organization_id}/courses/count",
826 web::get().to(get_organization_course_count),
827 )
828 .route(
829 "/{organization_id}/courses/active",
830 web::get().to(get_organization_active_courses),
831 )
832 .route(
833 "/{organization_id}/courses/active/count",
834 web::get().to(get_organization_active_courses_count),
835 )
836 .route(
837 "/{organization_id}/image",
838 web::put().to(set_organization_image),
839 )
840 .route(
841 "/{organization_id}/image",
842 web::delete().to(remove_organization_image),
843 )
844 .route(
845 "/{organization_id}/course_exams",
846 web::get().to(get_course_exams),
847 )
848 .route("/{organization_id}/org_exams", web::get().to(get_org_exams))
849 .route(
850 "/{exam_id}/fetch_org_exam",
851 web::get().to(get_org_exam_with_exam_id),
852 )
853 .route("/{organization_id}/exams", web::post().to(create_exam));
854}