headless_lms_server/controllers/course_material/
pages.rs1use std::collections::HashSet;
4
5use crate::{
6 domain::authorization::{
7 AuthorizationToken, authorize_access_to_course_material, skip_authorize,
8 },
9 prelude::*,
10};
11use models::pages::{
12 IsChapterFrontPage, Page, PageChapterAndCourseInformation, PageNavigationInformation,
13 PageVisibility,
14};
15use utoipa::OpenApi;
16
17#[derive(OpenApi)]
18#[openapi(paths(
19 get_by_exam_id,
20 get_chapter_front_page,
21 get_page_navigation,
22 get_chapter_and_course_information,
23 get_url_path,
24 is_chapter_front_page
25))]
26pub(crate) struct CourseMaterialPagesApiDoc;
27
28fn page_not_found() -> ControllerError {
29 controller_err!(NotFound, "Page not found".to_string())
30}
31
32async fn authorize_page_parent_access(
33 conn: &mut PgConnection,
34 user_id: Option<Uuid>,
35 page: &Page,
36) -> Result<(AuthorizationToken, PageVisibility), ControllerError> {
37 if page.deleted_at.is_some() {
38 return Err(page_not_found());
39 }
40 if let Some(course_id) = page.course_id {
41 let token = authorize_access_to_course_material(conn, user_id, course_id).await?;
42 let can_view_hidden_pages = if let Some(user_id) = user_id {
43 authorize(
44 conn,
45 Act::ViewMaterial,
46 Some(user_id),
47 Res::Course(course_id),
48 )
49 .await
50 .is_ok()
51 } else {
52 false
53 };
54 if page.hidden && !can_view_hidden_pages {
55 return Err(page_not_found());
56 }
57 let visibility = if can_view_hidden_pages {
58 PageVisibility::Any
59 } else {
60 PageVisibility::Public
61 };
62 Ok((token, visibility))
63 } else if let Some(exam_id) = page.exam_id {
64 let user_id = user_id.ok_or_else(|| {
65 controller_err!(
66 Unauthorized,
67 "Authentication required for exam page".to_string()
68 )
69 })?;
70 let token = authorize(conn, Act::View, Some(user_id), Res::Exam(exam_id)).await?;
71 Ok((token, PageVisibility::Any))
72 } else {
73 Err(page_not_found())
74 }
75}
76
77async fn filter_navigation_by_visibility(
78 conn: &mut PgConnection,
79 mut navigation: PageNavigationInformation,
80 visibility: PageVisibility,
81) -> Result<PageNavigationInformation, ControllerError> {
82 let Some(chapter_front) = navigation.chapter_front_page.as_ref() else {
83 return Ok(navigation);
84 };
85 let visible_page_ids =
86 models::pages::get_by_ids_and_visibility(conn, &[chapter_front.page_id], visibility)
87 .await?
88 .into_iter()
89 .map(|page| page.id)
90 .collect::<HashSet<_>>();
91
92 if !visible_page_ids.contains(&chapter_front.page_id) {
93 navigation.chapter_front_page = None;
94 }
95
96 Ok(navigation)
97}
98
99#[utoipa::path(
103 get,
104 path = "/exam/{page_id}",
105 operation_id = "getCourseMaterialPageByExamId",
106 tag = "course-material-pages",
107 params(
108 ("page_id" = Uuid, Path, description = "Exam id")
109 ),
110 responses(
111 (status = 200, description = "Exam page", body = Page)
112 )
113)]
114#[instrument(skip(pool))]
115async fn get_by_exam_id(
116 exam_id: web::Path<Uuid>,
117 pool: web::Data<PgPool>,
118 auth: AuthUser,
119) -> ControllerResult<web::Json<Page>> {
120 let mut conn = pool.acquire().await?;
121 let token = authorize(&mut conn, Act::View, Some(auth.id), Res::Exam(*exam_id)).await?;
122 let page = models::pages::get_by_exam_id(&mut conn, *exam_id).await?;
123 let page = models::pages::filter_course_material_page(&mut conn, Some(auth.id), page).await?;
124 token.authorized_ok(web::Json(page))
125}
126
127#[utoipa::path(
131 get,
132 path = "/{current_page_id}/chapter-front-page",
133 operation_id = "getCourseMaterialChapterFrontPage",
134 tag = "course-material-pages",
135 params(
136 ("current_page_id" = Uuid, Path, description = "Current page id")
137 ),
138 responses(
139 (status = 200, description = "Chapter front page", body = Option<Page>)
140 )
141)]
142#[instrument(skip(pool))]
143async fn get_chapter_front_page(
144 page_id: web::Path<Uuid>,
145 pool: web::Data<PgPool>,
146 auth: Option<AuthUser>,
147) -> ControllerResult<web::Json<Option<Page>>> {
148 let mut conn = pool.acquire().await?;
149 let user_id = auth.map(|u| u.id);
150 let current_page = models::pages::get_page(&mut conn, *page_id).await?;
151 let (token, visibility) =
152 authorize_page_parent_access(&mut conn, user_id, ¤t_page).await?;
153 let chapter_front_page = match current_page.chapter_id {
154 Some(chapter_id) => {
155 models::pages::get_front_page_by_chapter_id(&mut conn, chapter_id).await?
156 }
157 None => None,
158 };
159 let chapter_front_page = match chapter_front_page {
160 Some(page) => {
161 let visible_pages =
162 models::pages::get_by_ids_and_visibility(&mut conn, &[page.id], visibility).await?;
163 if visible_pages.is_empty() {
164 None
165 } else {
166 Some(models::pages::filter_course_material_page(&mut conn, user_id, page).await?)
167 }
168 }
169 None => None,
170 };
171 token.authorized_ok(web::Json(chapter_front_page))
172}
173
174#[utoipa::path(
178 get,
179 path = "/{current_page_id}/page-navigation",
180 operation_id = "getCourseMaterialPageNavigation",
181 tag = "course-material-pages",
182 params(
183 ("current_page_id" = Uuid, Path, description = "Current page id")
184 ),
185 responses(
186 (status = 200, description = "Page navigation information", body = PageNavigationInformation)
187 )
188)]
189#[instrument(skip(pool))]
190async fn get_page_navigation(
191 page_id: web::Path<Uuid>,
192 pool: web::Data<PgPool>,
193 auth: Option<AuthUser>,
194) -> ControllerResult<web::Json<PageNavigationInformation>> {
195 let mut conn = pool.acquire().await?;
196 let user_id = auth.map(|u| u.id);
197 let current_page = models::pages::get_page(&mut conn, *page_id).await?;
198 let (token, visibility) =
199 authorize_page_parent_access(&mut conn, user_id, ¤t_page).await?;
200 let res = models::pages::get_page_navigation_data(&mut conn, *page_id, visibility).await?;
201 let res = filter_navigation_by_visibility(&mut conn, res, visibility).await?;
202
203 token.authorized_ok(web::Json(res))
204}
205
206#[utoipa::path(
210 get,
211 path = "/{current_page_id}/chapter-and-course-information",
212 operation_id = "getCourseMaterialPageChapterAndCourseInformation",
213 tag = "course-material-pages",
214 params(
215 ("current_page_id" = Uuid, Path, description = "Current page id")
216 ),
217 responses(
218 (status = 200, description = "Page chapter and course information", body = PageChapterAndCourseInformation)
219 )
220)]
221#[instrument(skip(pool))]
222async fn get_chapter_and_course_information(
223 page_id: web::Path<Uuid>,
224 pool: web::Data<PgPool>,
225) -> ControllerResult<web::Json<PageChapterAndCourseInformation>> {
226 let mut conn = pool.acquire().await?;
227 let res = models::pages::get_page_chapter_and_course_information(&mut conn, *page_id).await?;
228
229 let token = skip_authorize();
230 token.authorized_ok(web::Json(res))
231}
232
233#[utoipa::path(
241 get,
242 path = "/{current_page_id}/url-path",
243 operation_id = "getCourseMaterialPageUrlPath",
244 tag = "course-material-pages",
245 params(
246 ("current_page_id" = Uuid, Path, description = "Current page id")
247 ),
248 responses(
249 (status = 200, description = "Page URL path", body = String)
250 )
251)]
252#[instrument(skip(pool))]
253async fn get_url_path(
254 page_id: web::Path<Uuid>,
255 pool: web::Data<PgPool>,
256 auth: Option<AuthUser>,
257) -> ControllerResult<String> {
258 let mut conn = pool.acquire().await?;
259 let page = models::pages::get_page(&mut conn, *page_id).await?;
260 let user_id = auth.map(|u| u.id);
261
262 let (token, _) = authorize_page_parent_access(&mut conn, user_id, &page).await?;
263 token.authorized_ok(page.url_path)
264}
265
266#[utoipa::path(
267 get,
268 path = "/{current_page_id}/is-chapter-front-page",
269 operation_id = "getCourseMaterialIsPageChapterFrontPage",
270 tag = "course-material-pages",
271 params(
272 ("current_page_id" = Uuid, Path, description = "Current page id")
273 ),
274 responses(
275 (status = 200, description = "Whether page is chapter front page", body = IsChapterFrontPage)
276 )
277)]
278#[instrument(skip(pool))]
279async fn is_chapter_front_page(
280 page_id: web::Path<Uuid>,
281 pool: web::Data<PgPool>,
282) -> ControllerResult<web::Json<IsChapterFrontPage>> {
283 let mut conn = pool.acquire().await?;
284 let is_chapter_front_page = models::pages::is_chapter_front_page(&mut conn, *page_id).await?;
285 let token = skip_authorize();
286 token.authorized_ok(web::Json(is_chapter_front_page))
287}
288
289pub fn _add_routes(cfg: &mut ServiceConfig) {
290 cfg.route("/exam/{page_id}", web::get().to(get_by_exam_id))
291 .route(
292 "/{current_page_id}/chapter-front-page",
293 web::get().to(get_chapter_front_page),
294 )
295 .route("/{current_page_id}/url-path", web::get().to(get_url_path))
296 .route(
297 "/{current_page_id}/chapter-and-course-information",
298 web::get().to(get_chapter_and_course_information),
299 )
300 .route(
301 "/{current_page_id}/is-chapter-front-page",
302 web::get().to(is_chapter_front_page),
303 )
304 .route(
305 "/{current_page_id}/page-navigation",
306 web::get().to(get_page_navigation),
307 );
308}