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};
23
24#[instrument(skip(pool, file_store, app_conf))]
29async fn get_all_organizations(
30 pool: web::Data<PgPool>,
31 file_store: web::Data<dyn FileStore>,
32 app_conf: web::Data<ApplicationConfiguration>,
33 user: Option<AuthUser>,
34) -> ControllerResult<web::Json<Vec<Organization>>> {
35 let mut conn = pool.acquire().await?;
36
37 let is_admin = if let Some(user) = user {
38 is_user_global_admin(&mut conn, user.id).await?
39 } else {
40 false
41 };
42
43 let raw_organizations = if is_admin {
45 models::organizations::all_organizations_include_hidden(&mut conn).await?
46 } else {
47 models::organizations::all_organizations(&mut conn).await?
48 };
49
50 let organizations = raw_organizations
51 .into_iter()
52 .map(|org| Organization::from_database_organization(org, file_store.as_ref(), &app_conf))
53 .collect();
54
55 let token = skip_authorize();
56 token.authorized_ok(web::Json(organizations))
57}
58
59#[instrument(skip(pool))]
63async fn get_organization_courses(
64 organization_id: web::Path<Uuid>,
65 pool: web::Data<PgPool>,
66 user: Option<AuthUser>,
67 pagination: web::Query<Pagination>,
68) -> ControllerResult<web::Json<Vec<Course>>> {
69 let mut conn = pool.acquire().await?;
70
71 let user = user.map(|u| u.id);
72 let courses = models::courses::organization_courses_visible_to_user_paginated(
73 &mut conn,
74 *organization_id,
75 user,
76 *pagination,
77 )
78 .await?;
79
80 let token = skip_authorize();
81 token.authorized_ok(web::Json(courses))
82}
83
84#[instrument(skip(pool))]
89async fn get_organization_duplicatable_courses(
90 organization_id: web::Path<Uuid>,
91 pool: web::Data<PgPool>,
92 user: AuthUser,
93) -> ControllerResult<web::Json<Vec<Course>>> {
94 let mut conn = pool.acquire().await?;
95 let courses = models::courses::get_by_organization_id(&mut conn, *organization_id).await?;
96
97 let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
100
101 let mut duplicatable_courses = Vec::new();
102 for course in courses {
103 if authorize_with_fetched_list_of_roles(
104 &mut conn,
105 Action::Duplicate,
106 Some(user.id),
107 Resource::Course(course.id),
108 &user_roles,
109 )
110 .await
111 .is_ok()
112 {
113 duplicatable_courses.push(course);
114 }
115 }
116
117 let token = skip_authorize();
118 token.authorized_ok(web::Json(duplicatable_courses))
119}
120
121#[instrument(skip(pool))]
122async fn get_organization_course_count(
123 request_organization_id: web::Path<Uuid>,
124 pool: web::Data<PgPool>,
125) -> ControllerResult<Json<CourseCount>> {
126 let mut conn = pool.acquire().await?;
127 let result =
128 models::courses::organization_course_count(&mut conn, *request_organization_id).await?;
129
130 let token = skip_authorize();
131 token.authorized_ok(Json(result))
132}
133
134#[instrument(skip(pool))]
135async fn get_organization_active_courses(
136 request_organization_id: web::Path<Uuid>,
137 pool: web::Data<PgPool>,
138 pagination: web::Query<Pagination>,
139) -> ControllerResult<Json<Vec<Course>>> {
140 let mut conn = pool.acquire().await?;
141 let courses = models::courses::get_active_courses_for_organization(
142 &mut conn,
143 *request_organization_id,
144 *pagination,
145 )
146 .await?;
147
148 let token = skip_authorize();
149 token.authorized_ok(Json(courses))
150}
151
152#[instrument(skip(pool))]
153async fn get_organization_active_courses_count(
154 request_organization_id: web::Path<Uuid>,
155 pool: web::Data<PgPool>,
156) -> ControllerResult<Json<CourseCount>> {
157 let mut conn = pool.acquire().await?;
158 let result = models::courses::get_active_courses_for_organization_count(
159 &mut conn,
160 *request_organization_id,
161 )
162 .await?;
163
164 let token = skip_authorize();
165 token.authorized_ok(Json(result))
166}
167
168#[instrument(skip(request, payload, pool, file_store, app_conf))]
183async fn set_organization_image(
184 request: HttpRequest,
185 payload: Multipart,
186 organization_id: web::Path<Uuid>,
187 pool: web::Data<PgPool>,
188 user: AuthUser,
189 file_store: web::Data<dyn FileStore>,
190 app_conf: web::Data<ApplicationConfiguration>,
191) -> ControllerResult<web::Json<Organization>> {
192 let mut conn = pool.acquire().await?;
193 let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
194 let token = authorize(
195 &mut conn,
196 Act::Edit,
197 Some(user.id),
198 Res::Organization(organization.id),
199 )
200 .await?;
201 let organization_image = upload_image_for_organization(
202 request.headers(),
203 payload,
204 &organization,
205 &file_store,
206 user,
207 &mut conn,
208 )
209 .await?
210 .to_string_lossy()
211 .to_string();
212 let updated_organization = models::organizations::update_organization_image_path(
213 &mut conn,
214 organization.id,
215 Some(organization_image),
216 )
217 .await?;
218
219 if let Some(old_image_path) = organization.organization_image_path {
221 let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
222 ControllerError::new(
223 ControllerErrorType::InternalServerError,
224 original_error.to_string(),
225 Some(original_error.into()),
226 )
227 })?;
228 file_store.delete(&file).await.map_err(|original_error| {
229 ControllerError::new(
230 ControllerErrorType::InternalServerError,
231 original_error.to_string(),
232 Some(original_error.into()),
233 )
234 })?;
235 }
236
237 let response = Organization::from_database_organization(
238 updated_organization,
239 file_store.as_ref(),
240 app_conf.as_ref(),
241 );
242 token.authorized_ok(web::Json(response))
243}
244
245#[instrument(skip(pool, file_store))]
257async fn remove_organization_image(
258 organization_id: web::Path<Uuid>,
259 pool: web::Data<PgPool>,
260 user: AuthUser,
261 file_store: web::Data<dyn FileStore>,
262) -> ControllerResult<web::Json<()>> {
263 let mut conn = pool.acquire().await?;
264 let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
265 let token = authorize(
266 &mut conn,
267 Act::Edit,
268 Some(user.id),
269 Res::Organization(organization.id),
270 )
271 .await?;
272 if let Some(organization_image_path) = organization.organization_image_path {
273 let file = PathBuf::from_str(&organization_image_path).map_err(|original_error| {
274 ControllerError::new(
275 ControllerErrorType::InternalServerError,
276 original_error.to_string(),
277 Some(original_error.into()),
278 )
279 })?;
280 let _res =
281 models::organizations::update_organization_image_path(&mut conn, organization.id, None)
282 .await?;
283 file_store.delete(&file).await.map_err(|original_error| {
284 ControllerError::new(
285 ControllerErrorType::InternalServerError,
286 original_error.to_string(),
287 Some(original_error.into()),
288 )
289 })?;
290 }
291 token.authorized_ok(web::Json(()))
292}
293
294#[instrument(skip(pool, file_store, app_conf))]
299async fn get_organization(
300 organization_id: web::Path<Uuid>,
301 pool: web::Data<PgPool>,
302 file_store: web::Data<dyn FileStore>,
303 app_conf: web::Data<ApplicationConfiguration>,
304) -> ControllerResult<web::Json<Organization>> {
305 let mut conn = pool.acquire().await?;
306 let db_organization =
307 models::organizations::get_organization(&mut conn, *organization_id).await?;
308 let organization =
309 Organization::from_database_organization(db_organization, file_store.as_ref(), &app_conf);
310
311 let token = skip_authorize();
312 token.authorized_ok(web::Json(organization))
313}
314
315#[derive(Debug, Deserialize)]
316struct OrganizationUpdatePayload {
317 name: String,
318 hidden: bool,
319 slug: String,
320}
321
322#[instrument(skip(pool))]
328async fn update_organization(
329 organization_id: web::Path<Uuid>,
330 payload: web::Json<OrganizationUpdatePayload>,
331 pool: web::Data<PgPool>,
332 user: AuthUser,
333) -> ControllerResult<web::Json<()>> {
334 let mut conn = pool.acquire().await?;
335 let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
336
337 let token = authorize(
338 &mut conn,
339 Act::Edit,
340 Some(user.id),
341 Res::Organization(organization.id),
342 )
343 .await?;
344
345 models::organizations::update_name_and_hidden(
346 &mut conn,
347 *organization_id,
348 &payload.name,
349 payload.hidden,
350 &payload.slug,
351 )
352 .await?;
353
354 token.authorized_ok(web::Json(()))
355}
356
357#[derive(Debug, Deserialize)]
358struct OrganizationCreatePayload {
359 name: String,
360 slug: String,
361 hidden: bool,
362}
363
364#[instrument(skip(pool, file_store, app_conf))]
380async fn create_organization(
381 payload: web::Json<OrganizationCreatePayload>,
382 pool: web::Data<PgPool>,
383 file_store: web::Data<dyn FileStore>,
384 app_conf: web::Data<ApplicationConfiguration>,
385 user: AuthUser,
386) -> ControllerResult<web::Json<Organization>> {
387 let mut conn = pool.acquire().await?;
388
389 let token = authorize(
390 &mut conn,
391 Action::Administrate,
392 Some(user.id),
393 Resource::GlobalPermissions,
394 )
395 .await?;
396
397 let mut tx = conn.begin().await?;
398
399 let org_id = match models::organizations::insert(
400 &mut tx,
401 PKeyPolicy::Generate,
402 &payload.name,
403 &payload.slug,
404 None,
405 payload.hidden,
406 )
407 .await
408 {
409 Ok(id) => id,
410 Err(err) => {
411 let err_str = err.to_string();
412 if err_str.contains("organizations_slug_key") {
413 return Err(ControllerError::new(
414 ControllerErrorType::BadRequest,
415 "An organization with this slug already exists.".to_string(),
416 None,
417 ));
418 }
419 return Err(err.into());
420 }
421 };
422
423 tx.commit().await?;
424
425 let db_org = models::organizations::get_organization(&mut conn, org_id).await?;
426 let org =
427 Organization::from_database_organization(db_org, file_store.as_ref(), app_conf.as_ref());
428
429 token.authorized_ok(web::Json(org))
430}
431
432#[instrument(skip(pool))]
433async fn soft_delete_organization(
434 org_id: web::Path<Uuid>,
435 pool: web::Data<PgPool>,
436 user: AuthUser,
437) -> ControllerResult<web::Json<()>> {
438 let mut conn = pool.acquire().await?;
439
440 let token = authorize(
441 &mut conn,
442 Action::Administrate,
443 Some(user.id),
444 Resource::GlobalPermissions,
445 )
446 .await?;
447
448 models::organizations::soft_delete(&mut conn, *org_id).await?;
449 token.authorized_ok(web::Json(()))
450}
451
452#[instrument(skip(pool))]
456async fn get_course_exams(
457 pool: web::Data<PgPool>,
458 organization: web::Path<Uuid>,
459) -> ControllerResult<web::Json<Vec<CourseExam>>> {
460 let mut conn = pool.acquire().await?;
461 let exams = models::exams::get_course_exams_for_organization(&mut conn, *organization).await?;
462
463 let token = skip_authorize();
464 token.authorized_ok(web::Json(exams))
465}
466
467#[instrument(skip(pool))]
471async fn get_org_exams(
472 pool: web::Data<PgPool>,
473 organization: web::Path<Uuid>,
474) -> ControllerResult<web::Json<Vec<OrgExam>>> {
475 let mut conn = pool.acquire().await?;
476 let exams = models::exams::get_exams_for_organization(&mut conn, *organization).await?;
477
478 let token = skip_authorize();
479 token.authorized_ok(web::Json(exams))
480}
481
482#[instrument(skip(pool))]
486pub async fn get_org_exam_with_exam_id(
487 pool: web::Data<PgPool>,
488 exam_id: web::Path<Uuid>,
489 user: AuthUser,
490) -> ControllerResult<web::Json<OrgExam>> {
491 let mut conn = pool.acquire().await?;
492 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
493
494 let exam = models::exams::get_organization_exam_with_exam_id(&mut conn, *exam_id).await?;
495
496 token.authorized_ok(web::Json(exam))
497}
498
499#[instrument(skip(pool))]
503async fn create_exam(
504 pool: web::Data<PgPool>,
505 payload: web::Json<NewExam>,
506 user: AuthUser,
507) -> ControllerResult<web::Json<()>> {
508 let mut conn = pool.acquire().await?;
509 let mut tx = conn.begin().await?;
510
511 let new_exam = payload.0;
512 let token = authorize(
513 &mut tx,
514 Act::CreateCoursesOrExams,
515 Some(user.id),
516 Res::Organization(new_exam.organization_id),
517 )
518 .await?;
519
520 let new_exam_id = models::exams::insert(&mut tx, PKeyPolicy::Generate, &new_exam).await?;
521 pages::insert_exam_page(
522 &mut tx,
523 new_exam_id,
524 NewPage {
525 chapter_id: None,
526 course_id: None,
527 exam_id: Some(new_exam_id),
528 front_page_of_chapter_id: None,
529 content: serde_json::Value::Array(vec![]),
530 content_search_language: Some("simple".to_string()),
531 exercise_slides: vec![],
532 exercise_tasks: vec![],
533 exercises: vec![],
534 title: "exam page".to_string(),
535 url_path: "/".to_string(),
536 },
537 user.id,
538 )
539 .await?;
540
541 models::roles::insert(
542 &mut tx,
543 user.id,
544 models::roles::UserRole::Teacher,
545 models::roles::RoleDomain::Exam(new_exam_id),
546 )
547 .await?;
548
549 tx.commit().await?;
550
551 token.authorized_ok(web::Json(()))
552}
553
554pub fn _add_routes(cfg: &mut ServiceConfig) {
562 cfg.route("", web::get().to(get_all_organizations))
563 .route("", web::post().to(create_organization))
564 .route("/{organization_id}", web::get().to(get_organization))
565 .route("/{organization_id}", web::put().to(update_organization))
566 .route(
567 "/{organization_id}",
568 web::patch().to(soft_delete_organization),
569 )
570 .route(
571 "/{organization_id}/courses",
572 web::get().to(get_organization_courses),
573 )
574 .route(
575 "/{organization_id}/courses/duplicatable",
576 web::get().to(get_organization_duplicatable_courses),
577 )
578 .route(
579 "/{organization_id}/courses/count",
580 web::get().to(get_organization_course_count),
581 )
582 .route(
583 "/{organization_id}/courses/active",
584 web::get().to(get_organization_active_courses),
585 )
586 .route(
587 "/{organization_id}/courses/active/count",
588 web::get().to(get_organization_active_courses_count),
589 )
590 .route(
591 "/{organization_id}/image",
592 web::put().to(set_organization_image),
593 )
594 .route(
595 "/{organization_id}/image",
596 web::delete().to(remove_organization_image),
597 )
598 .route(
599 "/{organization_id}/course_exams",
600 web::get().to(get_course_exams),
601 )
602 .route("/{organization_id}/org_exams", web::get().to(get_org_exams))
603 .route(
604 "/{organization_id}/fetch_org_exam",
605 web::get().to(get_org_exam_with_exam_id),
606 )
607 .route("/{organization_id}/exams", web::post().to(create_exam));
608}