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::{
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#[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 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#[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#[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 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#[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 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#[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#[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#[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#[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#[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#[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#[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#[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
796pub 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}