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