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 exercise_deadline_overrides =
426 models::chapters::exercise_deadline_overrides_by_chapter_for_course(&mut conn, *course_id)
427 .await?;
428 let chapters = models::chapters::course_chapters(&mut conn, *course_id)
429 .await?
430 .into_iter()
431 .map(|chapter| {
432 let chapter_image_url = chapter
433 .chapter_image_path
434 .as_ref()
435 .map(|path| file_store.get_download_url(Path::new(&path), &app_conf));
436 let exercise_deadline_overrides = exercise_deadline_overrides.get(&chapter.id).copied();
437 ChapterWithStatus::from_database_chapter_timestamp_and_image_url(
438 chapter,
439 Utc::now(),
440 chapter_image_url,
441 exercise_deadline_overrides,
442 )
443 })
444 .collect();
445 let modules = collect_course_modules(course_modules, chapters)?.data;
446 token.authorized_ok(web::Json(ChaptersWithStatus {
447 is_previewable,
448 modules,
449 }))
450}
451
452fn collect_course_modules(
454 course_modules: Vec<CourseModule>,
455 chapters: Vec<ChapterWithStatus>,
456) -> ControllerResult<Vec<CourseMaterialCourseModule>> {
457 let mut course_modules: HashMap<Uuid, CourseMaterialCourseModule> = course_modules
458 .into_iter()
459 .map(|course_module| {
460 (
461 course_module.id,
462 CourseMaterialCourseModule {
463 chapters: vec![],
464 id: course_module.id,
465 is_default: course_module.name.is_none(),
466 name: course_module.name,
467 order_number: course_module.order_number,
468 },
469 )
470 })
471 .collect();
472 for chapter in chapters {
473 course_modules
474 .get_mut(&chapter.course_module_id)
475 .ok_or_else(|| {
476 ControllerError::new(
477 ControllerErrorType::InternalServerError,
478 "Module data mismatch.".to_string(),
479 None,
480 )
481 })?
482 .chapters
483 .push(chapter);
484 }
485 let token = skip_authorize();
486 token.authorized_ok(course_modules.into_values().collect())
487}
488
489#[instrument(skip(pool))]
493async fn get_user_course_settings(
494 pool: web::Data<PgPool>,
495 course_id: web::Path<Uuid>,
496 user: Option<AuthUser>,
497) -> ControllerResult<web::Json<Option<UserCourseSettings>>> {
498 let mut conn = pool.acquire().await?;
499 if let Some(user) = user {
500 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
501 &mut conn, user.id, *course_id,
502 )
503 .await?;
504 let token = skip_authorize();
505 token.authorized_ok(web::Json(settings))
506 } else {
507 Err(ControllerError::new(
508 ControllerErrorType::NotFound,
509 "User not found".to_string(),
510 None,
511 ))
512 }
513}
514
515#[instrument(skip(pool))]
534async fn search_pages_with_phrase(
535 course_id: web::Path<Uuid>,
536 payload: web::Json<SearchRequest>,
537 pool: web::Data<PgPool>,
538) -> ControllerResult<web::Json<Vec<PageSearchResult>>> {
539 let mut conn = pool.acquire().await?;
540 let res =
541 models::pages::get_page_search_results_for_phrase(&mut conn, *course_id, &payload).await?;
542 let token = skip_authorize();
543 token.authorized_ok(web::Json(res))
544}
545
546#[instrument(skip(pool))]
565async fn search_pages_with_words(
566 course_id: web::Path<Uuid>,
567 payload: web::Json<SearchRequest>,
568 pool: web::Data<PgPool>,
569) -> ControllerResult<web::Json<Vec<PageSearchResult>>> {
570 let mut conn = pool.acquire().await?;
571 let res =
572 models::pages::get_page_search_results_for_words(&mut conn, *course_id, &payload).await?;
573 let token = skip_authorize();
574 token.authorized_ok(web::Json(res))
575}
576
577pub async fn feedback(
581 course_id: web::Path<Uuid>,
582 new_feedback: web::Json<Vec<NewFeedback>>,
583 pool: web::Data<PgPool>,
584 user: Option<AuthUser>,
585) -> ControllerResult<web::Json<Vec<Uuid>>> {
586 let mut conn = pool.acquire().await?;
587 let fs = new_feedback.into_inner();
588 let user_id = user.as_ref().map(|u| u.id);
589
590 for f in &fs {
592 if f.feedback_given.len() > 1000 {
593 return Err(ControllerError::new(
594 ControllerErrorType::BadRequest,
595 "Feedback given too long: max 1000".to_string(),
596 None,
597 ));
598 }
599 if f.related_blocks.len() > 100 {
600 return Err(ControllerError::new(
601 ControllerErrorType::BadRequest,
602 "Too many related blocks: max 100".to_string(),
603 None,
604 ));
605 }
606 for block in &f.related_blocks {
607 if block.text.as_ref().map(|t| t.len()).unwrap_or_default() > 10000 {
608 return Err(ControllerError::new(
609 ControllerErrorType::BadRequest,
610 "Block text too long: max 10000".to_string(),
611 None,
612 ));
613 }
614 }
615 }
616
617 let mut tx = conn.begin().await?;
618 let mut ids = vec![];
619 for f in fs {
620 let id = feedback::insert(&mut tx, PKeyPolicy::Generate, user_id, *course_id, f).await?;
621 ids.push(id);
622 }
623 tx.commit().await?;
624 let token = skip_authorize();
625 token.authorized_ok(web::Json(ids))
626}
627
628async fn propose_edit(
632 course_slug: web::Path<String>,
633 edits: web::Json<NewProposedPageEdits>,
634 pool: web::Data<PgPool>,
635 user: Option<AuthUser>,
636) -> ControllerResult<web::Json<Uuid>> {
637 let mut conn = pool.acquire().await?;
638 let course = courses::get_course_by_slug(&mut conn, course_slug.as_str()).await?;
639 let (id, _) = proposed_page_edits::insert(
640 &mut conn,
641 PKeyPolicy::Generate,
642 course.id,
643 user.map(|u| u.id),
644 &edits.into_inner(),
645 )
646 .await?;
647 let token = skip_authorize();
648 token.authorized_ok(web::Json(id))
649}
650
651#[instrument(skip(pool))]
652async fn glossary(
653 pool: web::Data<PgPool>,
654 course_id: web::Path<Uuid>,
655) -> ControllerResult<web::Json<Vec<Term>>> {
656 let mut conn = pool.acquire().await?;
657 let glossary = models::glossary::fetch_for_course(&mut conn, *course_id).await?;
658 let token = skip_authorize();
659 token.authorized_ok(web::Json(glossary))
660}
661
662#[instrument(skip(pool))]
663async fn get_material_references_by_course_id(
664 course_id: web::Path<Uuid>,
665 pool: web::Data<PgPool>,
666 user: Option<AuthUser>,
667) -> ControllerResult<web::Json<Vec<MaterialReference>>> {
668 let mut conn = pool.acquire().await?;
669 let token =
670 authorize_access_to_course_material(&mut conn, user.map(|u| u.id), *course_id).await?;
671 let res =
672 models::material_references::get_references_by_course_id(&mut conn, *course_id).await?;
673
674 token.authorized_ok(web::Json(res))
675}
676
677#[instrument(skip(pool))]
681async fn get_public_top_level_pages(
682 course_id: web::Path<Uuid>,
683 pool: web::Data<PgPool>,
684) -> ControllerResult<web::Json<Vec<Page>>> {
685 let mut conn = pool.acquire().await?;
686 let page = models::pages::get_course_top_level_pages_by_course_id_and_visibility(
687 &mut conn,
688 *course_id,
689 PageVisibility::Public,
690 )
691 .await?;
692 let token = skip_authorize();
693 token.authorized_ok(web::Json(page))
694}
695
696#[instrument(skip(pool))]
700async fn get_all_course_language_versions_navigation_info_from_page(
701 pool: web::Data<PgPool>,
702 path: web::Path<(Uuid, Uuid)>,
703 user: Option<AuthUser>,
704) -> ControllerResult<web::Json<Vec<CourseLanguageVersionNavigationInfo>>> {
705 let mut conn = pool.acquire().await?;
706 let (course_id, page_id) = path.into_inner();
707 let token = skip_authorize();
708 let course = models::courses::get_course(&mut conn, course_id).await?;
709
710 let unfiltered_language_versions =
711 models::courses::get_all_language_versions_of_course(&mut conn, &course).await?;
712
713 let all_pages_in_same_page_language_group =
714 models::page_language_groups::get_all_pages_in_page_language_group_mapping(
715 &mut conn, page_id,
716 )
717 .await?;
718
719 let mut accessible_courses = unfiltered_language_versions
720 .clone()
721 .into_iter()
722 .filter(|c| !c.is_draft)
723 .collect::<Vec<_>>();
724
725 if let Some(user_id) = user.map(|u| u.id) {
727 let user_roles = models::roles::get_roles(&mut conn, user_id).await?;
728
729 for course_version in unfiltered_language_versions.iter().filter(|c| c.is_draft) {
730 if authorize_with_fetched_list_of_roles(
731 &mut conn,
732 Action::ViewMaterial,
733 Some(user_id),
734 Resource::Course(course_version.id),
735 &user_roles,
736 )
737 .await
738 .is_ok()
739 {
740 accessible_courses.push(course_version.clone());
741 }
742 }
743 }
744
745 token.authorized_ok(web::Json(
746 accessible_courses
747 .into_iter()
748 .map(|c| {
749 let page_language_group_navigation_info =
750 all_pages_in_same_page_language_group.get(&CourseOrExamId::Course(c.id));
751 CourseLanguageVersionNavigationInfo::from_course_and_page_info(
752 &c,
753 page_language_group_navigation_info,
754 )
755 })
756 .collect(),
757 ))
758}
759
760#[instrument(skip(pool))]
764async fn get_page_by_course_id_and_language_group(
765 info: web::Path<(Uuid, Uuid)>,
766 pool: web::Data<PgPool>,
767) -> ControllerResult<web::Json<Page>> {
768 let mut conn = pool.acquire().await?;
769 let (course_id, page_language_group_id) = info.into_inner();
770
771 let page: Page = models::pages::get_page_by_course_id_and_language_group(
772 &mut conn,
773 course_id,
774 page_language_group_id,
775 )
776 .await?;
777 let token = skip_authorize();
778 token.authorized_ok(web::Json(page))
779}
780
781#[instrument(skip(pool))]
785async fn student_country(
786 query: web::Path<(Uuid, Uuid, String)>,
787 pool: web::Data<PgPool>,
788 user: AuthUser,
789) -> ControllerResult<Json<bool>> {
790 let mut conn = pool.acquire().await?;
791 let (course_id, course_instance_id, country_code) = query.into_inner();
792
793 models::student_countries::insert(
794 &mut conn,
795 user.id,
796 course_id,
797 course_instance_id,
798 &country_code,
799 )
800 .await?;
801 let token = skip_authorize();
802
803 token.authorized_ok(Json(true))
804}
805
806#[instrument(skip(pool))]
810async fn get_student_countries(
811 query: web::Path<(Uuid, Uuid)>,
812 pool: web::Data<PgPool>,
813 user: AuthUser,
814) -> ControllerResult<web::Json<HashMap<String, u32>>> {
815 let mut conn = pool.acquire().await?;
816 let token = skip_authorize();
817 let (course_id, course_instance_id) = query.into_inner();
818
819 let country_codes: Vec<String> =
820 models::student_countries::get_countries(&mut conn, course_id, course_instance_id)
821 .await?
822 .into_iter()
823 .map(|c| c.country_code)
824 .collect();
825
826 let mut frequency: HashMap<String, u32> = HashMap::new();
827 for code in country_codes {
828 *frequency.entry(code).or_insert(0) += 1
829 }
830
831 token.authorized_ok(web::Json(frequency))
832}
833
834#[instrument(skip(pool))]
838async fn get_student_country(
839 course_instance_id: web::Path<Uuid>,
840 pool: web::Data<PgPool>,
841 user: AuthUser,
842) -> ControllerResult<web::Json<StudentCountry>> {
843 let mut conn = pool.acquire().await?;
844 let token = skip_authorize();
845 let res = models::student_countries::get_selected_country_by_user_id(
846 &mut conn,
847 user.id,
848 *course_instance_id,
849 )
850 .await?;
851
852 token.authorized_ok(web::Json(res))
853}
854
855#[instrument(skip(pool))]
859async fn get_research_form_with_course_id(
860 course_id: web::Path<Uuid>,
861 user: AuthUser,
862 pool: web::Data<PgPool>,
863) -> ControllerResult<web::Json<Option<ResearchForm>>> {
864 let mut conn = pool.acquire().await?;
865 let user_id = Some(user.id);
866
867 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
868
869 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
870 .await
871 .optional()?;
872
873 token.authorized_ok(web::Json(res))
874}
875
876#[instrument(skip(pool))]
880async fn get_research_form_questions_with_course_id(
881 course_id: web::Path<Uuid>,
882 user: AuthUser,
883 pool: web::Data<PgPool>,
884) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
885 let mut conn = pool.acquire().await?;
886 let user_id = Some(user.id);
887
888 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
889 let res =
890 models::research_forms::get_research_form_questions_with_course_id(&mut conn, *course_id)
891 .await?;
892
893 token.authorized_ok(web::Json(res))
894}
895
896#[instrument(skip(pool, payload))]
901async fn upsert_course_research_form_answer(
902 payload: web::Json<NewResearchFormQuestionAnswer>,
903 pool: web::Data<PgPool>,
904 course_id: web::Path<Uuid>,
905 user: AuthUser,
906) -> ControllerResult<web::Json<Uuid>> {
907 let mut conn = pool.acquire().await?;
908 let user_id = Some(user.id);
909
910 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
911 let answer = payload;
912 let res =
913 models::research_forms::upsert_research_form_anwser(&mut conn, *course_id, &answer).await?;
914
915 token.authorized_ok(web::Json(res))
916}
917
918#[instrument(skip(pool))]
922async fn get_research_form_answers_with_user_id(
923 course_id: web::Path<Uuid>,
924 user: AuthUser,
925 pool: web::Data<PgPool>,
926) -> ControllerResult<web::Json<Vec<ResearchFormQuestionAnswer>>> {
927 let mut conn = pool.acquire().await?;
928 let user_id = Some(user.id);
929
930 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
931
932 let res = models::research_forms::get_research_form_answers_with_user_id(
933 &mut conn, *course_id, user.id,
934 )
935 .await?;
936
937 token.authorized_ok(web::Json(res))
938}
939
940#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
941#[cfg_attr(feature = "ts_rs", derive(TS))]
942pub struct UserMarketingConsentPayload {
943 pub course_language_groups_id: Uuid,
944 pub email_subscription: bool,
945 pub marketing_consent: bool,
946}
947
948#[instrument(skip(pool, payload))]
952async fn update_marketing_consent(
953 payload: web::Json<UserMarketingConsentPayload>,
954 pool: web::Data<PgPool>,
955 course_id: web::Path<Uuid>,
956 user: AuthUser,
957) -> ControllerResult<web::Json<Uuid>> {
958 let mut conn = pool.acquire().await?;
959 let user_id = Some(user.id);
960
961 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
962
963 let email_subscription = if payload.email_subscription {
964 "subscribed"
965 } else {
966 "unsubscribed"
967 };
968
969 let result = models::marketing_consents::upsert_marketing_consent(
970 &mut conn,
971 *course_id,
972 payload.course_language_groups_id,
973 &user.id,
974 email_subscription,
975 payload.marketing_consent,
976 )
977 .await?;
978
979 token.authorized_ok(web::Json(result))
980}
981
982#[instrument(skip(pool))]
986async fn fetch_user_marketing_consent(
987 pool: web::Data<PgPool>,
988 course_id: web::Path<Uuid>,
989 user: AuthUser,
990) -> ControllerResult<web::Json<Option<UserMarketingConsent>>> {
991 let mut conn = pool.acquire().await?;
992 let user_id = Some(user.id);
993
994 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
995
996 let result =
997 models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
998 .await
999 .ok();
1000
1001 token.authorized_ok(web::Json(result))
1002}
1003
1004#[instrument(skip(pool))]
1008async fn get_partners_block(
1009 path: web::Path<Uuid>,
1010 pool: web::Data<PgPool>,
1011) -> ControllerResult<web::Json<Option<PartnersBlock>>> {
1012 let course_id = path.into_inner();
1013 let mut conn = pool.acquire().await?;
1014 let partner_block = models::partner_block::get_partner_block(&mut conn, course_id)
1015 .await
1016 .optional()?;
1017 let token = skip_authorize();
1018 token.authorized_ok(web::Json(partner_block))
1019}
1020
1021#[instrument(skip(pool))]
1025async fn get_privacy_link(
1026 course_id: web::Path<Uuid>,
1027 pool: web::Data<PgPool>,
1028) -> ControllerResult<web::Json<Vec<PrivacyLink>>> {
1029 let mut conn = pool.acquire().await?;
1030 let privacy_link = models::privacy_link::get_privacy_link(&mut conn, *course_id).await?;
1031 let token = skip_authorize();
1032 token.authorized_ok(web::Json(privacy_link))
1033}
1034
1035#[instrument(skip(pool))]
1039async fn get_custom_privacy_policy_checkbox_texts(
1040 course_id: web::Path<Uuid>,
1041 pool: web::Data<PgPool>,
1042 user: AuthUser, ) -> ControllerResult<web::Json<Vec<CourseCustomPrivacyPolicyCheckboxText>>> {
1044 let mut conn = pool.acquire().await?;
1045
1046 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1047
1048 let texts = models::course_custom_privacy_policy_checkbox_texts::get_all_by_course_id(
1049 &mut conn, *course_id,
1050 )
1051 .await?;
1052
1053 token.authorized_ok(web::Json(texts))
1054}
1055
1056#[instrument(skip(pool))]
1062async fn get_user_chapter_locks(
1063 course_id: web::Path<Uuid>,
1064 pool: web::Data<PgPool>,
1065 user: AuthUser,
1066) -> ControllerResult<web::Json<Vec<models::user_chapter_locking_statuses::UserChapterLockingStatus>>>
1067{
1068 use models::user_chapter_locking_statuses;
1069 let mut conn = pool.acquire().await?;
1070 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1071
1072 let statuses =
1073 user_chapter_locking_statuses::get_by_user_and_course(&mut conn, user.id, *course_id)
1074 .await?;
1075
1076 token.authorized_ok(web::Json(statuses))
1077}
1078
1079pub fn _add_routes(cfg: &mut ServiceConfig) {
1087 cfg.route("/{course_id}", web::get().to(get_course))
1088 .route("/{course_id}/chapters", web::get().to(get_chapters))
1089 .route(
1090 "/{course_id}/course-instances",
1091 web::get().to(get_course_instances),
1092 )
1093 .route(
1094 "/{course_id}/current-instance",
1095 web::get().to(get_current_course_instance),
1096 )
1097 .route("/{course_id}/feedback", web::post().to(feedback))
1098 .route(
1099 "/{course_id}/page-by-path/{url_path:.*}",
1100 web::get().to(get_course_page_by_path),
1101 )
1102 .route(
1103 "/{course_id}/search-pages-with-phrase",
1104 web::post().to(search_pages_with_phrase),
1105 )
1106 .route(
1107 "/{course_id}/language-versions-navigation-info/from-page/{page_id}",
1108 web::get().to(get_all_course_language_versions_navigation_info_from_page),
1109 )
1110 .route(
1111 "/{course_id}/search-pages-with-words",
1112 web::post().to(search_pages_with_words),
1113 )
1114 .route(
1115 "/{course_id}/user-settings",
1116 web::get().to(get_user_course_settings),
1117 )
1118 .route(
1119 "/{course_id}/top-level-pages",
1120 web::get().to(get_public_top_level_pages),
1121 )
1122 .route("/{course_id}/propose-edit", web::post().to(propose_edit))
1123 .route("/{course_id}/glossary", web::get().to(glossary))
1124 .route(
1125 "/{course_id}/references",
1126 web::get().to(get_material_references_by_course_id),
1127 )
1128 .route(
1129 "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1130 web::get().to(get_page_by_course_id_and_language_group),
1131 )
1132 .route("/{course_id}/pages", web::get().to(get_public_course_pages))
1133 .route(
1134 "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1135 web::post().to(student_country),
1136 )
1137 .route(
1138 "/{course_instance_id}/student-country",
1139 web::get().to(get_student_country),
1140 )
1141 .route(
1142 "/{course_id}/course-instances/{course_instance_id}/student-countries",
1143 web::get().to(get_student_countries),
1144 )
1145 .route(
1146 "/{course_id}/research-consent-form-questions-answer",
1147 web::post().to(upsert_course_research_form_answer),
1148 )
1149 .route(
1150 "/{courseId}/research-consent-form-user-answers",
1151 web::get().to(get_research_form_answers_with_user_id),
1152 )
1153 .route(
1154 "/{course_id}/research-consent-form",
1155 web::get().to(get_research_form_with_course_id),
1156 )
1157 .route(
1158 "/{course_id}/partners-block",
1159 web::get().to(get_partners_block),
1160 )
1161 .route("/{course_id}/privacy-link", web::get().to(get_privacy_link))
1162 .route(
1163 "/{course_id}/research-consent-form-questions",
1164 web::get().to(get_research_form_questions_with_course_id),
1165 )
1166 .route(
1167 "/{course_id}/user-marketing-consent",
1168 web::post().to(update_marketing_consent),
1169 )
1170 .route(
1171 "/{course_id}/fetch-user-marketing-consent",
1172 web::get().to(fetch_user_marketing_consent),
1173 )
1174 .route(
1175 "/{course_id}/custom-privacy-policy-checkbox-texts",
1176 web::get().to(get_custom_privacy_policy_checkbox_texts),
1177 )
1178 .route(
1179 "/{course_id}/user-chapter-locks",
1180 web::get().to(get_user_chapter_locks),
1181 );
1182}