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::{
10 course_custom_privacy_policy_checkbox_texts::CourseCustomPrivacyPolicyCheckboxText,
11 marketing_consents::UserMarketingConsent,
12};
13use headless_lms_models::{partner_block::PartnersBlock, privacy_link::PrivacyLink};
14use headless_lms_utils::ip_to_country::IpToCountryMapper;
15use isbot::Bots;
16use models::{
17 chapters::ChapterWithStatus,
18 course_instances::CourseInstance,
19 course_modules::CourseModule,
20 courses::Course,
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<Course>> {
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))
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<Course>>> {
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
714 .authorized_ok(web::Json(unfiltered_language_versions));
715 } else {
716 return Err(ControllerError::new(
717 ControllerErrorType::Unauthorized,
718 "Please log in".to_string(),
719 None,
720 ));
721 }
722 }
723 token.authorized_ok(web::Json(language_versions))
724}
725
726#[instrument(skip(pool))]
730async fn get_page_by_course_id_and_language_group(
731 info: web::Path<(Uuid, Uuid)>,
732 pool: web::Data<PgPool>,
733) -> ControllerResult<web::Json<Page>> {
734 let mut conn = pool.acquire().await?;
735 let (course_id, page_language_group_id) = info.into_inner();
736
737 let page: Page = models::pages::get_page_by_course_id_and_language_group(
738 &mut conn,
739 course_id,
740 page_language_group_id,
741 )
742 .await?;
743 let token = skip_authorize();
744 token.authorized_ok(web::Json(page))
745}
746
747#[instrument(skip(pool))]
751async fn student_country(
752 query: web::Path<(Uuid, Uuid, String)>,
753 pool: web::Data<PgPool>,
754 user: AuthUser,
755) -> ControllerResult<Json<bool>> {
756 let mut conn = pool.acquire().await?;
757 let (course_id, course_instance_id, country_code) = query.into_inner();
758
759 models::student_countries::insert(
760 &mut conn,
761 user.id,
762 course_id,
763 course_instance_id,
764 &country_code,
765 )
766 .await?;
767 let token = skip_authorize();
768
769 token.authorized_ok(Json(true))
770}
771
772#[instrument(skip(pool))]
776async fn get_student_countries(
777 query: web::Path<(Uuid, Uuid)>,
778 pool: web::Data<PgPool>,
779 user: AuthUser,
780) -> ControllerResult<web::Json<HashMap<String, u32>>> {
781 let mut conn = pool.acquire().await?;
782 let token = skip_authorize();
783 let (course_id, course_instance_id) = query.into_inner();
784
785 let country_codes: Vec<String> =
786 models::student_countries::get_countries(&mut conn, course_id, course_instance_id)
787 .await?
788 .into_iter()
789 .map(|c| (c.country_code))
790 .collect();
791
792 let mut frequency: HashMap<String, u32> = HashMap::new();
793 for code in country_codes {
794 *frequency.entry(code).or_insert(0) += 1
795 }
796
797 token.authorized_ok(web::Json(frequency))
798}
799
800#[instrument(skip(pool))]
804async fn get_student_country(
805 course_instance_id: web::Path<Uuid>,
806 pool: web::Data<PgPool>,
807 user: AuthUser,
808) -> ControllerResult<web::Json<StudentCountry>> {
809 let mut conn = pool.acquire().await?;
810 let token = skip_authorize();
811 let res = models::student_countries::get_selected_country_by_user_id(
812 &mut conn,
813 user.id,
814 *course_instance_id,
815 )
816 .await?;
817
818 token.authorized_ok(web::Json(res))
819}
820
821#[instrument(skip(pool))]
825async fn get_research_form_with_course_id(
826 course_id: web::Path<Uuid>,
827 user: AuthUser,
828 pool: web::Data<PgPool>,
829) -> ControllerResult<web::Json<Option<ResearchForm>>> {
830 let mut conn = pool.acquire().await?;
831 let user_id = Some(user.id);
832
833 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
834
835 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
836 .await
837 .optional()?;
838
839 token.authorized_ok(web::Json(res))
840}
841
842#[instrument(skip(pool))]
846async fn get_research_form_questions_with_course_id(
847 course_id: web::Path<Uuid>,
848 user: AuthUser,
849 pool: web::Data<PgPool>,
850) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
851 let mut conn = pool.acquire().await?;
852 let user_id = Some(user.id);
853
854 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
855 let res =
856 models::research_forms::get_research_form_questions_with_course_id(&mut conn, *course_id)
857 .await?;
858
859 token.authorized_ok(web::Json(res))
860}
861
862#[instrument(skip(pool, payload))]
867async fn upsert_course_research_form_answer(
868 payload: web::Json<NewResearchFormQuestionAnswer>,
869 pool: web::Data<PgPool>,
870 course_id: web::Path<Uuid>,
871 user: AuthUser,
872) -> ControllerResult<web::Json<Uuid>> {
873 let mut conn = pool.acquire().await?;
874 let user_id = Some(user.id);
875
876 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
877 let answer = payload;
878 let res =
879 models::research_forms::upsert_research_form_anwser(&mut conn, *course_id, &answer).await?;
880
881 token.authorized_ok(web::Json(res))
882}
883
884#[instrument(skip(pool))]
888async fn get_research_form_answers_with_user_id(
889 course_id: web::Path<Uuid>,
890 user: AuthUser,
891 pool: web::Data<PgPool>,
892) -> ControllerResult<web::Json<Vec<ResearchFormQuestionAnswer>>> {
893 let mut conn = pool.acquire().await?;
894 let user_id = Some(user.id);
895
896 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
897
898 let res = models::research_forms::get_research_form_answers_with_user_id(
899 &mut conn, *course_id, user.id,
900 )
901 .await?;
902
903 token.authorized_ok(web::Json(res))
904}
905
906#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
907#[cfg_attr(feature = "ts_rs", derive(TS))]
908pub struct UserMarketingConsentPayload {
909 pub course_language_groups_id: Uuid,
910 pub email_subscription: bool,
911 pub marketing_consent: bool,
912}
913
914#[instrument(skip(pool, payload))]
918async fn update_marketing_consent(
919 payload: web::Json<UserMarketingConsentPayload>,
920 pool: web::Data<PgPool>,
921 course_id: web::Path<Uuid>,
922 user: AuthUser,
923) -> ControllerResult<web::Json<Uuid>> {
924 let mut conn = pool.acquire().await?;
925 let user_id = Some(user.id);
926
927 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
928
929 let email_subscription = if payload.email_subscription {
930 "subscribed"
931 } else {
932 "unsubscribed"
933 };
934
935 let result = models::marketing_consents::upsert_marketing_consent(
936 &mut conn,
937 *course_id,
938 payload.course_language_groups_id,
939 &user.id,
940 email_subscription,
941 payload.marketing_consent,
942 )
943 .await?;
944
945 token.authorized_ok(web::Json(result))
946}
947
948#[instrument(skip(pool))]
952async fn fetch_user_marketing_consent(
953 pool: web::Data<PgPool>,
954 course_id: web::Path<Uuid>,
955 user: AuthUser,
956) -> ControllerResult<web::Json<Option<UserMarketingConsent>>> {
957 let mut conn = pool.acquire().await?;
958 let user_id = Some(user.id);
959
960 let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;
961
962 let result =
963 models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
964 .await
965 .ok();
966
967 token.authorized_ok(web::Json(result))
968}
969
970#[instrument(skip(pool))]
974async fn get_partners_block(
975 path: web::Path<Uuid>,
976 pool: web::Data<PgPool>,
977) -> ControllerResult<web::Json<PartnersBlock>> {
978 let course_id = path.into_inner();
979 let mut conn = pool.acquire().await?;
980 let partner_block = models::partner_block::get_partner_block(&mut conn, course_id).await?;
981 let token = skip_authorize();
982 token.authorized_ok(web::Json(partner_block))
983}
984
985#[instrument(skip(pool))]
989async fn get_privacy_link(
990 course_id: web::Path<Uuid>,
991 pool: web::Data<PgPool>,
992) -> ControllerResult<web::Json<Vec<PrivacyLink>>> {
993 let mut conn = pool.acquire().await?;
994 let privacy_link = models::privacy_link::get_privacy_link(&mut conn, *course_id).await?;
995 let token = skip_authorize();
996 token.authorized_ok(web::Json(privacy_link))
997}
998
999#[instrument(skip(pool))]
1003async fn get_custom_privacy_policy_checkbox_texts(
1004 course_id: web::Path<Uuid>,
1005 pool: web::Data<PgPool>,
1006 user: AuthUser, ) -> ControllerResult<web::Json<Vec<CourseCustomPrivacyPolicyCheckboxText>>> {
1008 let mut conn = pool.acquire().await?;
1009
1010 let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?;
1011
1012 let texts = models::course_custom_privacy_policy_checkbox_texts::get_all_by_course_id(
1013 &mut conn, *course_id,
1014 )
1015 .await?;
1016
1017 token.authorized_ok(web::Json(texts))
1018}
1019
1020pub fn _add_routes(cfg: &mut ServiceConfig) {
1028 cfg.route("/{course_id}", web::get().to(get_course))
1029 .route("/{course_id}/chapters", web::get().to(get_chapters))
1030 .route(
1031 "/{course_id}/course-instances",
1032 web::get().to(get_course_instances),
1033 )
1034 .route(
1035 "/{course_id}/current-instance",
1036 web::get().to(get_current_course_instance),
1037 )
1038 .route("/{course_id}/feedback", web::post().to(feedback))
1039 .route(
1040 "/{course_id}/page-by-path/{url_path:.*}",
1041 web::get().to(get_course_page_by_path),
1042 )
1043 .route(
1044 "/{course_id}/search-pages-with-phrase",
1045 web::post().to(search_pages_with_phrase),
1046 )
1047 .route(
1048 "/{course_id}/language-versions",
1049 web::get().to(get_all_course_language_versions),
1050 )
1051 .route(
1052 "/{course_id}/search-pages-with-words",
1053 web::post().to(search_pages_with_words),
1054 )
1055 .route(
1056 "/{course_id}/user-settings",
1057 web::get().to(get_user_course_settings),
1058 )
1059 .route(
1060 "/{course_id}/top-level-pages",
1061 web::get().to(get_public_top_level_pages),
1062 )
1063 .route("/{course_id}/propose-edit", web::post().to(propose_edit))
1064 .route("/{course_id}/glossary", web::get().to(glossary))
1065 .route(
1066 "/{course_id}/references",
1067 web::get().to(get_material_references_by_course_id),
1068 )
1069 .route(
1070 "/{course_id}/pages/by-language-group-id/{page_language_group_id}",
1071 web::get().to(get_page_by_course_id_and_language_group),
1072 )
1073 .route("/{course_id}/pages", web::get().to(get_public_course_pages))
1074 .route(
1075 "/{course_id}/course-instances/{course_instance_id}/student-countries/{country_code}",
1076 web::post().to(student_country),
1077 )
1078 .route(
1079 "/{course_instance_id}/student-country",
1080 web::get().to(get_student_country),
1081 )
1082 .route(
1083 "/{course_id}/course-instances/{course_instance_id}/student-countries",
1084 web::get().to(get_student_countries),
1085 )
1086 .route(
1087 "/{course_id}/research-consent-form-questions-answer",
1088 web::post().to(upsert_course_research_form_answer),
1089 )
1090 .route(
1091 "/{courseId}/research-consent-form-user-answers",
1092 web::get().to(get_research_form_answers_with_user_id),
1093 )
1094 .route(
1095 "/{course_id}/research-consent-form",
1096 web::get().to(get_research_form_with_course_id),
1097 )
1098 .route(
1099 "/{course_id}/partners-block",
1100 web::get().to(get_partners_block),
1101 )
1102 .route("/{course_id}/privacy-link", web::get().to(get_privacy_link))
1103 .route(
1104 "/{course_id}/research-consent-form-questions",
1105 web::get().to(get_research_form_questions_with_course_id),
1106 )
1107 .route(
1108 "/{course_id}/user-marketing-consent",
1109 web::post().to(update_marketing_consent),
1110 )
1111 .route(
1112 "/{course_id}/fetch-user-marketing-consent",
1113 web::get().to(fetch_user_marketing_consent),
1114 )
1115 .route(
1116 "/{course_id}/custom-privacy-policy-checkbox-texts",
1117 web::get().to(get_custom_privacy_policy_checkbox_texts),
1118 );
1119}