1use std::{collections::HashMap, net::IpAddr, path::Path};
4
5use actix_http::header::{self, X_FORWARDED_FOR};
6use actix_web::web::Json;
7use chrono::Utc;
8use futures::{FutureExt, future::OptionFuture};
9use headless_lms_models::courses::{CourseLanguageVersionNavigationInfo, CourseMaterialCourse};
10use headless_lms_models::{
11 course_custom_privacy_policy_checkbox_texts::CourseCustomPrivacyPolicyCheckboxText,
12 marketing_consents::UserMarketingConsent,
13};
14use headless_lms_models::{partner_block::PartnersBlock, privacy_link::PrivacyLink};
15use headless_lms_utils::ip_to_country::IpToCountryMapper;
16use isbot::Bots;
17use models::{
18 chapters::ChapterWithStatus,
19 course_instances::CourseInstance,
20 course_modules::CourseModule,
21 courses::{self, get_nondeleted_course_id_by_slug},
22 feedback,
23 feedback::NewFeedback,
24 glossary::Term,
25 material_references::MaterialReference,
26 page_visit_datum::NewPageVisitDatum,
27 page_visit_datum_daily_visit_hashing_keys::{
28 GenerateAnonymousIdentifierInput, generate_anonymous_identifier,
29 },
30 pages::{CoursePageWithUserData, Page, PageSearchResult, PageVisibility, SearchRequest},
31 proposed_page_edits::{self, NewProposedPageEdits},
32 research_forms::{
33 NewResearchFormQuestionAnswer, ResearchForm, ResearchFormQuestion,
34 ResearchFormQuestionAnswer,
35 },
36 student_countries::StudentCountry,
37 user_course_settings::UserCourseSettings,
38};
39
40use crate::{
41 domain::authorization::{
42 Action, Resource, authorize_access_to_course_material,
43 authorize_with_fetched_list_of_roles, can_user_view_chapter, skip_authorize,
44 },
45 prelude::*,
46};
47
48#[instrument(skip(pool))]
52async fn get_course(
53 course_id: web::Path<Uuid>,
54 pool: web::Data<PgPool>,
55) -> ControllerResult<web::Json<CourseMaterialCourse>> {
56 let mut conn = pool.acquire().await?;
57 let course = models::courses::get_course(&mut conn, *course_id).await?;
58 let token = skip_authorize();
59 token.authorized_ok(web::Json(course.into()))
60}
61
62#[instrument(skip(pool, ip_to_country_mapper, req, file_store, app_conf))]
72async fn get_course_page_by_path(
73 params: web::Path<(String, String)>,
74 pool: web::Data<PgPool>,
75 user: Option<AuthUser>,
76 ip_to_country_mapper: web::Data<IpToCountryMapper>,
77 req: HttpRequest,
78 file_store: web::Data<dyn FileStore>,
79 app_conf: web::Data<ApplicationConfiguration>,
80) -> ControllerResult<web::Json<CoursePageWithUserData>> {
81 let mut conn = pool.acquire().await?;
82
83 let (course_slug, raw_page_path) = params.into_inner();
84 let path = if raw_page_path.starts_with('/') {
85 raw_page_path
86 } else {
87 format!("/{}", raw_page_path)
88 };
89 let user_id = user.map(|u| u.id);
90 let course_data = get_nondeleted_course_id_by_slug(&mut conn, &course_slug).await?;
91 let page_with_user_data = models::pages::get_page_with_user_data_by_path(
92 &mut conn,
93 user_id,
94 &course_data,
95 &path,
96 file_store.as_ref(),
97 &app_conf,
98 )
99 .await?;
100
101 if !can_user_view_chapter(
103 &mut conn,
104 user_id,
105 page_with_user_data.page.course_id,
106 page_with_user_data.page.chapter_id,
107 )
108 .await?
109 {
110 return Err(ControllerError::new(
111 ControllerErrorType::Unauthorized,
112 "Chapter is not open yet.".to_string(),
113 None,
114 ));
115 }
116
117 let token = authorize_access_to_course_material(
118 &mut conn,
119 user_id,
120 page_with_user_data.page.course_id.ok_or_else(|| {
121 ControllerError::new(
122 ControllerErrorType::NotFound,
123 "Course not found".to_string(),
124 None,
125 )
126 })?,
127 )
128 .await?;
129
130 let temp_request_information =
131 derive_information_from_requester(req, ip_to_country_mapper).await?;
132
133 let RequestInformation {
134 ip,
135 referrer,
136 utm_source,
137 utm_medium,
138 utm_campaign,
139 utm_term,
140 utm_content,
141 country,
142 user_agent,
143 has_bot_user_agent,
144 browser_admits_its_a_bot,
145 browser,
146 browser_version,
147 operating_system,
148 operating_system_version,
149 device_type,
150 } = temp_request_information.data;
151
152 let course_or_exam_id = page_with_user_data
153 .page
154 .course_id
155 .unwrap_or_else(|| page_with_user_data.page.exam_id.unwrap_or_else(Uuid::nil));
156 let anonymous_identifier = generate_anonymous_identifier(
157 &mut conn,
158 GenerateAnonymousIdentifierInput {
159 user_agent,
160 ip_address: ip.map(|ip| ip.to_string()).unwrap_or_default(),
161 course_id: course_or_exam_id,
162 },
163 )
164 .await?;
165
166 models::page_visit_datum::insert(
167 &mut conn,
168 NewPageVisitDatum {
169 course_id: page_with_user_data.page.course_id,
170 page_id: page_with_user_data.page.id,
171 country,
172 browser,
173 browser_version,
174 operating_system,
175 operating_system_version,
176 device_type,
177 referrer,
178 is_bot: has_bot_user_agent || browser_admits_its_a_bot,
179 utm_source,
180 utm_medium,
181 utm_campaign,
182 utm_term,
183 utm_content,
184 anonymous_identifier,
185 exam_id: page_with_user_data.page.exam_id,
186 },
187 )
188 .await?;
189
190 token.authorized_ok(web::Json(page_with_user_data))
191}
192
193struct RequestInformation {
194 ip: Option<IpAddr>,
195 user_agent: String,
196 referrer: Option<String>,
197 utm_source: Option<String>,
198 utm_medium: Option<String>,
199 utm_campaign: Option<String>,
200 utm_term: Option<String>,
201 utm_content: Option<String>,
202 country: Option<String>,
203 has_bot_user_agent: bool,
204 browser_admits_its_a_bot: bool,
205 browser: Option<String>,
206 browser_version: Option<String>,
207 operating_system: Option<String>,
208 operating_system_version: Option<String>,
209 device_type: Option<String>,
210}
211
212async fn derive_information_from_requester(
214 req: HttpRequest,
215 ip_to_country_mapper: web::Data<IpToCountryMapper>,
216) -> ControllerResult<RequestInformation> {
217 let mut headers = req.headers().clone();
218 let x_real_ip = headers.get("X-Real-IP");
219 let x_forwarded_for = headers.get(X_FORWARDED_FOR);
220 let connection_info = req.connection_info();
221 let peer_address = connection_info.peer_addr();
222 let headers_clone = headers.clone();
223 let user_agent = headers_clone.get(header::USER_AGENT);
224 let bots = Bots::default();
225 let has_bot_user_agent = user_agent
226 .and_then(|ua| ua.to_str().ok())
227 .map(|ua| bots.is_bot(ua))
228 .unwrap_or(true);
229 let header_totally_not_a_bot = headers.get("totally-not-a-bot");
231 let browser_admits_its_a_bot = header_totally_not_a_bot.is_none();
232 if has_bot_user_agent || browser_admits_its_a_bot {
233 warn!(
234 ?has_bot_user_agent,
235 ?browser_admits_its_a_bot,
236 ?user_agent,
237 ?header_totally_not_a_bot,
238 "The requester is a bot"
239 )
240 }
241
242 let user_agent_parser = woothee::parser::Parser::new();
243 let parsed_user_agent = user_agent
244 .and_then(|ua| ua.to_str().ok())
245 .and_then(|ua| user_agent_parser.parse(ua));
246
247 let ip: Option<IpAddr> = connection_info
248 .realip_remote_addr()
249 .and_then(|ip| ip.parse::<IpAddr>().ok());
250
251 info!(
252 "Ip {:?}, x_real_ip {:?}, x_forwarded_for {:?}, peer_address {:?}",
253 ip, x_real_ip, x_forwarded_for, peer_address
254 );
255
256 let country = ip
257 .and_then(|ip| ip_to_country_mapper.map_ip_to_country(&ip))
258 .map(|c| c.to_string());
259
260 let utm_tags = headers
261 .remove("utm-tags")
262 .next()
263 .and_then(|utms| String::from_utf8(utms.as_bytes().to_vec()).ok())
264 .and_then(|utms| serde_json::from_str::<serde_json::Value>(&utms).ok())
265 .and_then(|o| o.as_object().cloned());
266
267 let utm_source = utm_tags
268 .clone()
269 .and_then(|mut tags| tags.remove("utm_source"))
270 .and_then(|v| v.as_str().map(|s| s.to_string()));
271
272 let utm_medium = utm_tags
273 .clone()
274 .and_then(|mut tags| tags.remove("utm_medium"))
275 .and_then(|v| v.as_str().map(|s| s.to_string()));
276
277 let utm_campaign = utm_tags
278 .clone()
279 .and_then(|mut tags| tags.remove("utm_campaign"))
280 .and_then(|v| v.as_str().map(|s| s.to_string()));
281
282 let utm_term = utm_tags
283 .clone()
284 .and_then(|mut tags| tags.remove("utm_term"))
285 .and_then(|v| v.as_str().map(|s| s.to_string()));
286
287 let utm_content = utm_tags
288 .and_then(|mut tags| tags.remove("utm_content"))
289 .and_then(|v| v.as_str().map(|s| s.to_string()));
290
291 let referrer = headers
292 .get("Orignal-Referrer")
293 .and_then(|r| r.to_str().ok())
294 .map(|r| r.to_string());
295
296 let browser = parsed_user_agent.as_ref().map(|ua| ua.name.to_string());
297 let browser_version = parsed_user_agent.as_ref().map(|ua| ua.version.to_string());
298 let operating_system = parsed_user_agent.as_ref().map(|ua| ua.os.to_string());
299 let operating_system_version = parsed_user_agent
300 .as_ref()
301 .map(|ua| ua.os_version.to_string());
302 let device_type = parsed_user_agent.as_ref().map(|ua| ua.category.to_string());
303 let token = skip_authorize();
304 token.authorized_ok(RequestInformation {
305 ip,
306 user_agent: user_agent
307 .and_then(|ua| ua.to_str().ok())
308 .unwrap_or_default()
309 .to_string(),
310 referrer,
311 utm_source,
312 utm_medium,
313 utm_campaign,
314 utm_term,
315 utm_content,
316 country,
317 has_bot_user_agent,
318 browser_admits_its_a_bot,
319 browser,
320 browser_version,
321 operating_system,
322 operating_system_version,
323 device_type,
324 })
325}
326
327#[instrument(skip(pool))]
331async fn get_current_course_instance(
332 pool: web::Data<PgPool>,
333 course_id: web::Path<Uuid>,
334 user: Option<AuthUser>,
335) -> ControllerResult<web::Json<Option<CourseInstance>>> {
336 let mut conn = pool.acquire().await?;
337 if let Some(user) = user {
338 let instance = models::course_instances::current_course_instance_of_user(
339 &mut conn, user.id, *course_id,
340 )
341 .await?;
342 let token = skip_authorize();
343 token.authorized_ok(web::Json(instance))
344 } else {
345 Err(ControllerError::new(
346 ControllerErrorType::NotFound,
347 "User not found".to_string(),
348 None,
349 ))
350 }
351}
352
353async fn get_course_instances(
357 pool: web::Data<PgPool>,
358 course_id: web::Path<Uuid>,
359) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
360 let mut conn = pool.acquire().await?;
361 let instances =
362 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
363 let token = skip_authorize();
364 token.authorized_ok(web::Json(instances))
365}
366
367#[instrument(skip(pool))]
373async fn get_public_course_pages(
374 course_id: web::Path<Uuid>,
375 pool: web::Data<PgPool>,
376) -> ControllerResult<web::Json<Vec<Page>>> {
377 let mut conn = pool.acquire().await?;
378 let pages: Vec<Page> = models::pages::get_all_by_course_id_and_visibility(
379 &mut conn,
380 *course_id,
381 PageVisibility::Public,
382 )
383 .await?;
384 let token = skip_authorize();
385 token.authorized_ok(web::Json(pages))
386}
387
388#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
389#[cfg_attr(feature = "ts_rs", derive(TS))]
390pub struct ChaptersWithStatus {
391 pub is_previewable: bool,
392 pub modules: Vec<CourseMaterialCourseModule>,
393}
394
395#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
396#[cfg_attr(feature = "ts_rs", derive(TS))]
397pub struct CourseMaterialCourseModule {
398 pub chapters: Vec<ChapterWithStatus>,
399 pub id: Uuid,
400 pub is_default: bool,
401 pub name: Option<String>,
402 pub order_number: i32,
403}
404
405#[instrument(skip(pool, file_store, app_conf))]
410async fn get_chapters(
411 course_id: web::Path<Uuid>,
412 user: Option<AuthUser>,
413 pool: web::Data<PgPool>,
414 file_store: web::Data<dyn FileStore>,
415 app_conf: web::Data<ApplicationConfiguration>,
416) -> ControllerResult<web::Json<ChaptersWithStatus>> {
417 let mut conn = pool.acquire().await?;
418 let is_previewable = OptionFuture::from(user.map(|u| {
419 authorize(&mut conn, Act::Teach, Some(u.id), Res::Course(*course_id)).map(|r| r.ok())
420 }))
421 .await
422 .is_some();
423 let token = skip_authorize();
424 let course_modules = models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
425 let chapters = models::chapters::course_chapters(&mut conn, *course_id)
426 .await?
427 .into_iter()
428 .map(|chapter| {
429 let chapter_image_url = chapter
430 .chapter_image_path
431 .as_ref()
432 .map(|path| file_store.get_download_url(Path::new(&path), &app_conf));
433 ChapterWithStatus::from_database_chapter_timestamp_and_image_url(
434 chapter,
435 Utc::now(),
436 chapter_image_url,
437 )
438 })
439 .collect();
440 let modules = collect_course_modules(course_modules, chapters)?.data;
441 token.authorized_ok(web::Json(ChaptersWithStatus {
442 is_previewable,
443 modules,
444 }))
445}
446
447fn collect_course_modules(
449 course_modules: Vec<CourseModule>,
450 chapters: Vec<ChapterWithStatus>,
451) -> ControllerResult<Vec<CourseMaterialCourseModule>> {
452 let mut course_modules: HashMap<Uuid, CourseMaterialCourseModule> = course_modules
453 .into_iter()
454 .map(|course_module| {
455 (
456 course_module.id,
457 CourseMaterialCourseModule {
458 chapters: vec![],
459 id: course_module.id,
460 is_default: course_module.name.is_none(),
461 name: course_module.name,
462 order_number: course_module.order_number,
463 },
464 )
465 })
466 .collect();
467 for chapter in chapters {
468 course_modules
469 .get_mut(&chapter.course_module_id)
470 .ok_or_else(|| {
471 ControllerError::new(
472 ControllerErrorType::InternalServerError,
473 "Module data mismatch.".to_string(),
474 None,
475 )
476 })?
477 .chapters
478 .push(chapter);
479 }
480 let token = skip_authorize();
481 token.authorized_ok(course_modules.into_values().collect())
482}
483
484#[instrument(skip(pool))]
488async fn get_user_course_settings(
489 pool: web::Data<PgPool>,
490 course_id: web::Path<Uuid>,
491 user: Option<AuthUser>,
492) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
493 let mut conn = pool.acquire().await?;
494 if let Some(user) = user {
495 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
496 &mut conn, user.id, *course_id,
497 )
498 .await?;
499 let token = skip_authorize();
500 token.authorized_ok(web::Json(settings))
501 } else {
502 Err(ControllerError::new(
503 ControllerErrorType::NotFound,
504 "User not found".to_string(),
505 None,
506 ))
507 }
508}
509
510#[instrument(skip(pool))]
529async fn search_pages_with_phrase(
530 course_id: web::Path<Uuid>,
531 payload: web::Json<SearchRequest>,
532 pool: web::Data<PgPool>,
533) -> ControllerResult<web::Json<Vec<PageSearchResult>>> {
534 let mut conn = pool.acquire().await?;
535 let res =
536 models::pages::get_page_search_results_for_phrase(&mut conn, *course_id, &payload).await?;
537 let token = skip_authorize();
538 token.authorized_ok(web::Json(res))
539}
540
541#[instrument(skip(pool))]
560async fn search_pages_with_words(
561 course_id: web::Path<Uuid>,
562 payload: web::Json<SearchRequest>,
563 pool: web::Data<PgPool>,
564) -> ControllerResult<web::Json<Vec<PageSearchResult>>> {
565 let mut conn = pool.acquire().await?;
566 let res =
567 models::pages::get_page_search_results_for_words(&mut conn, *course_id, &payload).await?;
568 let token = skip_authorize();
569 token.authorized_ok(web::Json(res))
570}
571
572pub async fn feedback(
576 course_id: web::Path<Uuid>,
577 new_feedback: web::Json<Vec<NewFeedback>>,
578 pool: web::Data<PgPool>,
579 user: Option<AuthUser>,
580) -> ControllerResult<web::Json<Vec<Uuid>>> {
581 let mut conn = pool.acquire().await?;
582 let fs = new_feedback.into_inner();
583 let user_id = user.as_ref().map(|u| u.id);
584
585 for f in &fs {
587 if f.feedback_given.len() > 1000 {
588 return Err(ControllerError::new(
589 ControllerErrorType::BadRequest,
590 "Feedback given too long: max 1000".to_string(),
591 None,
592 ));
593 }
594 if f.related_blocks.len() > 100 {
595 return Err(ControllerError::new(
596 ControllerErrorType::BadRequest,
597 "Too many related blocks: max 100".to_string(),
598 None,
599 ));
600 }
601 for block in &f.related_blocks {
602 if block.text.as_ref().map(|t| t.len()).unwrap_or_default() > 10000 {
603 return Err(ControllerError::new(
604 ControllerErrorType::BadRequest,
605 "Block text too long: max 10000".to_string(),
606 None,
607 ));
608 }
609 }
610 }
611
612 let mut tx = conn.begin().await?;
613 let mut ids = vec![];
614 for f in fs {
615 let id = feedback::insert(&mut tx, PKeyPolicy::Generate, user_id, *course_id, f).await?;
616 ids.push(id);
617 }
618 tx.commit().await?;
619 let token = skip_authorize();
620 token.authorized_ok(web::Json(ids))
621}
622
623async fn propose_edit(
627 course_slug: web::Path<String>,
628 edits: web::Json<NewProposedPageEdits>,
629 pool: web::Data<PgPool>,
630 user: Option<AuthUser>,
631) -> ControllerResult<web::Json<Uuid>> {
632 let mut conn = pool.acquire().await?;
633 let course = courses::get_course_by_slug(&mut conn, course_slug.as_str()).await?;
634 let (id, _) = proposed_page_edits::insert(
635 &mut conn,
636 PKeyPolicy::Generate,
637 course.id,
638 user.map(|u| u.id),
639 &edits.into_inner(),
640 )
641 .await?;
642 let token = skip_authorize();
643 token.authorized_ok(web::Json(id))
644}
645
646#[instrument(skip(pool))]
647async fn glossary(
648 pool: web::Data<PgPool>,
649 course_id: web::Path<Uuid>,
650) -> ControllerResult<web::Json<Vec<Term>>> {
651 let mut conn = pool.acquire().await?;
652 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
653 let token = skip_authorize();
654 token.authorized_ok(web::Json(glossary))
655}
656
657#[instrument(skip(pool))]
658async fn get_material_references_by_course_id(
659 course_id: web::Path<Uuid>,
660 pool: web::Data<PgPool>,
661 user: Option<AuthUser>,
662) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
663 let mut conn = pool.acquire().await?;
664 let token =
665 authorize_access_to_course_material(&mut conn, user.map(|u| u.id), *course_id).await?;
666 let res =
667 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
668
669 token.authorized_ok(web::Json(res))
670}
671
672#[instrument(skip(pool))]
676async fn get_public_top_level_pages(
677 course_id: web::Path<Uuid>,
678 pool: web::Data<PgPool>,
679) -> ControllerResult<web::Json<Vec<Page>>> {
680 let mut conn = pool.acquire().await?;
681 let page = models::pages::get_course_top_level_pages_by_course_id_and_visibility(
682 &mut conn,
683 *course_id,
684 PageVisibility::Public,
685 )
686 .await?;
687 let token = skip_authorize();
688 token.authorized_ok(web::Json(page))
689}
690
691#[instrument(skip(pool))]
695async fn get_all_course_language_versions_navigation_info_from_page(
696 pool: web::Data<PgPool>,
697 path: web::Path<(Uuid, Uuid)>,
698 user: Option<AuthUser>,
699) -> ControllerResult<web::Json<Vec<CourseLanguageVersionNavigationInfo>>> {
700 let mut conn = pool.acquire().await?;
701 let (course_id, page_id) = path.into_inner();
702 let token = skip_authorize();
703 let course = models::courses::get_course(&mut conn, course_id).await?;
704
705 let unfiltered_language_versions =
706 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
707
708 let all_pages_in_same_page_language_group =
709 models::page_language_groups::get_all_pages_in_page_language_group_mapping(
710 &mut conn, page_id,
711 )
712 .await?;
713
714 let mut accessible_courses = unfiltered_language_versions
715 .clone()
716 .into_iter()
717 .filter(|c| !c.is_draft)
718 .collect::<Vec<_>>();
719
720 if let Some(user_id) = user.map(|u| u.id) {
722 let user_roles = models::roles::get_roles(&mut conn, user_id).await?;
723
724 for course_version in unfiltered_language_versions.iter().filter(|c| c.is_draft) {
725 if authorize_with_fetched_list_of_roles(
726 &mut conn,
727 Action::ViewMaterial,
728 Some(user_id),
729 Resource::Course(course_version.id),
730 &user_roles,
731 )
732 .await
733 .is_ok()
734 {
735 accessible_courses.push(course_version.clone());
736 }
737 }
738 }
739
740 token.authorized_ok(web::Json(
741 accessible_courses
742 .into_iter()
743 .map(|c| {
744 let page_language_group_navigation_info =
745 all_pages_in_same_page_language_group.get(&CourseOrExamId::Course(c.id));
746 CourseLanguageVersionNavigationInfo::from_course_and_page_info(
747 &c,
748 page_language_group_navigation_info,
749 )
750 })
751 .collect(),
752 ))
753}
754
755#[instrument(skip(pool))]
759async fn get_page_by_course_id_and_language_group(
760 info: web::Path<(Uuid, Uuid)>,
761 pool: web::Data<PgPool>,
762) -> ControllerResult<web::Json<Page>> {
763 let mut conn = pool.acquire().await?;
764 let (course_id, page_language_group_id) = info.into_inner();
765
766 let page: Page = models::pages::get_page_by_course_id_and_language_group(
767 &mut conn,
768 course_id,
769 page_language_group_id,
770 )
771 .await?;
772 let token = skip_authorize();
773 token.authorized_ok(web::Json(page))
774}
775
776#[instrument(skip(pool))]
780async fn student_country(
781 query: web::Path<(Uuid, Uuid, String)>,
782 pool: web::Data<PgPool>,
783 user: AuthUser,
784) -> ControllerResult<Json<bool>> {
785 let mut conn = pool.acquire().await?;
786 let (course_id, course_instance_id, country_code) = query.into_inner();
787
788 models::student_countries::insert(
789 &mut conn,
790 user.id,
791 course_id,
792 course_instance_id,
793 &country_code,
794 )
795 .await?;
796 let token = skip_authorize();
797
798 token.authorized_ok(Json(true))
799}
800
801#[instrument(skip(pool))]
805async fn get_student_countries(
806 query: web::Path<(Uuid, Uuid)>,
807 pool: web::Data<PgPool>,
808 user: AuthUser,
809) -> ControllerResult<web::Json<HashMap<String, u32>>> {
810 let mut conn = pool.acquire().await?;
811 let token = skip_authorize();
812 let (course_id, course_instance_id) = query.into_inner();
813
814 let country_codes: Vec<String> =
815 models::student_countries::get_countries(&mut conn, course_id, course_instance_id)
816 .await?
817 .into_iter()
818 .map(|c| (c.country_code))
819 .collect();
820
821 let mut frequency: HashMap<String, u32> = HashMap::new();
822 for code in country_codes {
823 *frequency.entry(code).or_insert(0) += 1
824 }
825
826 token.authorized_ok(web::Json(frequency))
827}
828
829#[instrument(skip(pool))]
833async fn get_student_country(
834 course_instance_id: web::Path<Uuid>,
835 pool: web::Data<PgPool>,
836 user: AuthUser,
837) -> ControllerResult<web::Json<StudentCountry>> {
838 let mut conn = pool.acquire().await?;
839 let token = skip_authorize();
840 let res = models::student_countries::get_selected_country_by_user_id(
841 &mut conn,
842 user.id,
843 *course_instance_id,
844 )
845 .await?;
846
847 token.authorized_ok(web::Json(res))
848}
849
850#[instrument(skip(pool))]
854async fn get_research_form_with_course_id(
855 course_id: web::Path<Uuid>,
856 user: AuthUser,
857 pool: web::Data<PgPool>,
858) -> ControllerResult<web::Json<Option<ResearchForm>>> {
859 let mut conn = pool.acquire().await?;
860 let user_id = Some(user.id);
861
862 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
863
864 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
865 .await
866 .optional()?;
867
868 token.authorized_ok(web::Json(res))
869}
870
871#[instrument(skip(pool))]
875async fn get_research_form_questions_with_course_id(
876 course_id: web::Path<Uuid>,
877 user: AuthUser,
878 pool: web::Data<PgPool>,
879) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
880 let mut conn = pool.acquire().await?;
881 let user_id = Some(user.id);
882
883 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
884 let res =
885 models::research_forms::get_research_form_questions_with_course_id(&mut conn, *course_id)
886 .await?;
887
888 token.authorized_ok(web::Json(res))
889}
890
891#[instrument(skip(pool, payload))]
896async fn upsert_course_research_form_answer(
897 payload: web::Json<NewResearchFormQuestionAnswer>,
898 pool: web::Data<PgPool>,
899 course_id: web::Path<Uuid>,
900 user: AuthUser,
901) -> ControllerResult<web::Json<Uuid>> {
902 let mut conn = pool.acquire().await?;
903 let user_id = Some(user.id);
904
905 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
906 let answer = payload;
907 let res =
908 models::research_forms::upsert_research_form_anwser(&mut conn, *course_id, &answer).await?;
909
910 token.authorized_ok(web::Json(res))
911}
912
913#[instrument(skip(pool))]
917async fn get_research_form_answers_with_user_id(
918 course_id: web::Path<Uuid>,
919 user: AuthUser,
920 pool: web::Data<PgPool>,
921) -> ControllerResult<web::Json<Vec<ResearchFormQuestionAnswer>>> {
922 let mut conn = pool.acquire().await?;
923 let user_id = Some(user.id);
924
925 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
926
927 let res = models::research_forms::get_research_form_answers_with_user_id(
928 &mut conn, *course_id, user.id,
929 )
930 .await?;
931
932 token.authorized_ok(web::Json(res))
933}
934
935#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
936#[cfg_attr(feature = "ts_rs", derive(TS))]
937pub struct UserMarketingConsentPayload {
938 pub course_language_groups_id: Uuid,
939 pub email_subscription: bool,
940 pub marketing_consent: bool,
941}
942
943#[instrument(skip(pool, payload))]
947async fn update_marketing_consent(
948 payload: web::Json<UserMarketingConsentPayload>,
949 pool: web::Data<PgPool>,
950 course_id: web::Path<Uuid>,
951 user: AuthUser,
952) -> ControllerResult<web::Json<Uuid>> {
953 let mut conn = pool.acquire().await?;
954 let user_id = Some(user.id);
955
956 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
957
958 let email_subscription = if payload.email_subscription {
959 "subscribed"
960 } else {
961 "unsubscribed"
962 };
963
964 let result = models::marketing_consents::upsert_marketing_consent(
965 &mut conn,
966 *course_id,
967 payload.course_language_groups_id,
968 &user.id,
969 email_subscription,
970 payload.marketing_consent,
971 )
972 .await?;
973
974 token.authorized_ok(web::Json(result))
975}
976
977#[instrument(skip(pool))]
981async fn fetch_user_marketing_consent(
982 pool: web::Data<PgPool>,
983 course_id: web::Path<Uuid>,
984 user: AuthUser,
985) -> ControllerResult<web::Json<Option<UserMarketingConsent>>> {
986 let mut conn = pool.acquire().await?;
987 let user_id = Some(user.id);
988
989 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
990
991 let result =
992 models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
993 .await
994 .ok();
995
996 token.authorized_ok(web::Json(result))
997}
998
999#[instrument(skip(pool))]
1003async fn get_partners_block(
1004 path: web::Path<Uuid>,
1005 pool: web::Data<PgPool>,
1006) -> ControllerResult<web::Json<Option<PartnersBlock>>> {
1007 let course_id = path.into_inner();
1008 let mut conn = pool.acquire().await?;
1009 let partner_block = models::partner_block::get_partner_block(&mut conn, course_id)
1010 .await
1011 .optional()?;
1012 let token = skip_authorize();
1013 token.authorized_ok(web::Json(partner_block))
1014}
1015
1016#[instrument(skip(pool))]
1020async fn get_privacy_link(
1021 course_id: web::Path<Uuid>,
1022 pool: web::Data<PgPool>,
1023) -> ControllerResult<web::Json<Vec<PrivacyLink>>> {
1024 let mut conn = pool.acquire().await?;
1025 let privacy_link = models::privacy_link::get_privacy_link(&mut conn, *course_id).await?;
1026 let token = skip_authorize();
1027 token.authorized_ok(web::Json(privacy_link))
1028}
1029
1030#[instrument(skip(pool))]
1034async fn get_custom_privacy_policy_checkbox_texts(
1035 course_id: web::Path<Uuid>,
1036 pool: web::Data<PgPool>,
1037 user: AuthUser, ) -> ControllerResult<web::Json<Vec<CourseCustomPrivacyPolicyCheckboxText>>> {
1039 let mut conn = pool.acquire().await?;
1040
1041 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1042
1043 let texts = models::course_custom_privacy_policy_checkbox_texts::get_all_by_course_id(
1044 &mut conn, *course_id,
1045 )
1046 .await?;
1047
1048 token.authorized_ok(web::Json(texts))
1049}
1050
1051pub fn _add_routes(cfg: &mut ServiceConfig) {
1059 cfg.route("/{course_id}", web::get().to(get_course))
1060 .route("/{course_id}/chapters", web::get().to(get_chapters))
1061 .route(
1062 "/{course_id}/course-instances",
1063 web::get().to(get_course_instances),
1064 )
1065 .route(
1066 "/{course_id}/current-instance",
1067 web::get().to(get_current_course_instance),
1068 )
1069 .route("/{course_id}/feedback", web::post().to(feedback))
1070 .route(
1071 "/{course_id}/page-by-path/{url_path:.*}",
1072 web::get().to(get_course_page_by_path),
1073 )
1074 .route(
1075 "/{course_id}/search-pages-with-phrase",
1076 web::post().to(search_pages_with_phrase),
1077 )
1078 .route(
1079 "/{course_id}/language-versions-navigation-info/from-page/{page_id}",
1080 web::get().to(get_all_course_language_versions_navigation_info_from_page),
1081 )
1082 .route(
1083 "/{course_id}/search-pages-with-words",
1084 web::post().to(search_pages_with_words),
1085 )
1086 .route(
1087 "/{course_id}/user-settings",
1088 web::get().to(get_user_course_settings),
1089 )
1090 .route(
1091 "/{course_id}/top-level-pages",
1092 web::get().to(get_public_top_level_pages),
1093 )
1094 .route("/{course_id}/propose-edit", web::post().to(propose_edit))
1095 .route("/{course_id}/glossary", web::get().to(glossary))
1096 .route(
1097 "/{course_id}/references",
1098 web::get().to(get_material_references_by_course_id),
1099 )
1100 .route(
1101 "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1102 web::get().to(get_page_by_course_id_and_language_group),
1103 )
1104 .route("/{course_id}/pages", web::get().to(get_public_course_pages))
1105 .route(
1106 "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1107 web::post().to(student_country),
1108 )
1109 .route(
1110 "/{course_instance_id}/student-country",
1111 web::get().to(get_student_country),
1112 )
1113 .route(
1114 "/{course_id}/course-instances/{course_instance_id}/student-countries",
1115 web::get().to(get_student_countries),
1116 )
1117 .route(
1118 "/{course_id}/research-consent-form-questions-answer",
1119 web::post().to(upsert_course_research_form_answer),
1120 )
1121 .route(
1122 "/{courseId}/research-consent-form-user-answers",
1123 web::get().to(get_research_form_answers_with_user_id),
1124 )
1125 .route(
1126 "/{course_id}/research-consent-form",
1127 web::get().to(get_research_form_with_course_id),
1128 )
1129 .route(
1130 "/{course_id}/partners-block",
1131 web::get().to(get_partners_block),
1132 )
1133 .route("/{course_id}/privacy-link", web::get().to(get_privacy_link))
1134 .route(
1135 "/{course_id}/research-consent-form-questions",
1136 web::get().to(get_research_form_questions_with_course_id),
1137 )
1138 .route(
1139 "/{course_id}/user-marketing-consent",
1140 web::post().to(update_marketing_consent),
1141 )
1142 .route(
1143 "/{course_id}/fetch-user-marketing-consent",
1144 web::get().to(fetch_user_marketing_consent),
1145 )
1146 .route(
1147 "/{course_id}/custom-privacy-policy-checkbox-texts",
1148 web::get().to(get_custom_privacy_policy_checkbox_texts),
1149 );
1150}