1use std::{path::PathBuf, str::FromStr, sync::Arc};
4
5use headless_lms_models::chapters::DatabaseChapter;
6use models::chapters::{Chapter, ChapterUpdate, NewChapter};
7use utoipa::{OpenApi, ToSchema};
8
9use crate::{
10 domain::{
11 models_requests::{self, JwtKey},
12 request_id::RequestId,
13 },
14 prelude::*,
15};
16
17#[derive(OpenApi)]
18#[openapi(paths(
19 post_new_chapter,
20 delete_chapter,
21 update_chapter,
22 set_chapter_image,
23 remove_chapter_image,
24 get_all_chapters_by_course_id
25))]
26pub(crate) struct MainFrontendChaptersApiDoc;
27
28#[allow(dead_code)]
29#[derive(Debug, ToSchema)]
30struct ChapterImageUploadPayload {
31 #[schema(content_media_type = "application/octet-stream")]
32 file: Vec<u8>,
33}
34
35#[instrument(skip(pool, file_store, app_conf))]
54#[utoipa::path(
55 post,
56 path = "",
57 operation_id = "createChapter",
58 tag = "chapters",
59 request_body = NewChapter,
60 responses(
61 (status = 200, description = "Created chapter", body = Chapter)
62 )
63)]
64async fn post_new_chapter(
65 request_id: RequestId,
66 pool: web::Data<PgPool>,
67 payload: web::Json<NewChapter>,
68 user: AuthUser,
69 file_store: web::Data<dyn FileStore>,
70 app_conf: web::Data<ApplicationConfiguration>,
71 jwt_key: web::Data<JwtKey>,
72) -> ControllerResult<web::Json<Chapter>> {
73 let mut conn = pool.acquire().await?;
74 let token = authorize(
75 &mut conn,
76 Act::Edit,
77 Some(user.id),
78 Res::Course(payload.course_id),
79 )
80 .await?;
81 let new_chapter = payload.0;
82 let (database_chapter, ..) = models::library::content_management::create_new_chapter(
83 &mut conn,
84 PKeyPolicy::Generate,
85 &new_chapter,
86 user.id,
87 models_requests::make_spec_fetcher(
88 app_conf.base_url.clone(),
89 request_id.0,
90 Arc::clone(&jwt_key),
91 ),
92 models_requests::fetch_service_info,
93 )
94 .await?;
95 return token.authorized_ok(web::Json(Chapter::from_database_chapter(
96 &database_chapter,
97 file_store.as_ref(),
98 app_conf.as_ref(),
99 )));
100}
101
102#[instrument(skip(pool, file_store, app_conf))]
107#[utoipa::path(
108 delete,
109 path = "/{chapter_id}",
110 operation_id = "deleteChapter",
111 tag = "chapters",
112 params(
113 ("chapter_id" = Uuid, Path, description = "Chapter id")
114 ),
115 responses(
116 (status = 200, description = "Deleted chapter", body = Chapter)
117 )
118)]
119async fn delete_chapter(
120 chapter_id: web::Path<String>,
121 pool: web::Data<PgPool>,
122 user: AuthUser,
123 file_store: web::Data<dyn FileStore>,
124 app_conf: web::Data<ApplicationConfiguration>,
125) -> ControllerResult<web::Json<Chapter>> {
126 let mut conn = pool.acquire().await?;
127 let chapter_id = Uuid::from_str(&chapter_id)?;
128 let token = authorize(
129 &mut conn,
130 Act::Edit,
131 Some(user.id),
132 Res::Chapter(chapter_id),
133 )
134 .await?;
135 let deleted_chapter = models::chapters::delete_chapter(
136 &mut conn,
137 chapter_id,
138 models::entity_history::HistoryAuthor::user(user.id),
139 )
140 .await?;
141 return token.authorized_ok(web::Json(Chapter::from_database_chapter(
142 &deleted_chapter,
143 file_store.as_ref(),
144 app_conf.as_ref(),
145 )));
146}
147
148#[instrument(skip(payload, pool, file_store, app_conf))]
168#[utoipa::path(
169 put,
170 path = "/{chapter_id}",
171 operation_id = "updateChapter",
172 tag = "chapters",
173 params(
174 ("chapter_id" = Uuid, Path, description = "Chapter id")
175 ),
176 request_body = ChapterUpdate,
177 responses(
178 (status = 200, description = "Updated chapter", body = Chapter)
179 )
180)]
181async fn update_chapter(
182 payload: web::Json<ChapterUpdate>,
183 chapter_id: web::Path<String>,
184 pool: web::Data<PgPool>,
185 user: AuthUser,
186 file_store: web::Data<dyn FileStore>,
187 app_conf: web::Data<ApplicationConfiguration>,
188) -> ControllerResult<web::Json<Chapter>> {
189 let mut conn = pool.acquire().await?;
190 let chapter_id = Uuid::from_str(&chapter_id)?;
191 let course_id = models::chapters::get_course_id(&mut conn, chapter_id).await?;
192 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
193 let course_update = payload.0;
194 let chapter =
195 models::chapters::update_chapter(&mut conn, chapter_id, course_update, user.id).await?;
196
197 let response = Chapter::from_database_chapter(&chapter, file_store.as_ref(), app_conf.as_ref());
198
199 token.authorized_ok(web::Json(response))
200}
201
202#[instrument(skip(request, payload, pool, file_store, app_conf))]
217#[utoipa::path(
218 put,
219 path = "/{chapter_id}/image",
220 operation_id = "updateChapterImage",
221 tag = "chapters",
222 params(
223 ("chapter_id" = Uuid, Path, description = "Chapter id")
224 ),
225 request_body(content = inline(ChapterImageUploadPayload), content_type = "multipart/form-data"),
226 responses(
227 (status = 200, description = "Updated chapter image", body = Chapter)
228 )
229)]
230async fn set_chapter_image(
231 request: HttpRequest,
232 payload: Multipart,
233 chapter_id: web::Path<Uuid>,
234 pool: web::Data<PgPool>,
235 user: AuthUser,
236 file_store: web::Data<dyn FileStore>,
237 app_conf: web::Data<ApplicationConfiguration>,
238) -> ControllerResult<web::Json<Chapter>> {
239 let mut conn = pool.acquire().await?;
240 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
241 let token = authorize(
242 &mut conn,
243 Act::Edit,
244 Some(user.id),
245 Res::Course(chapter.course_id),
246 )
247 .await?;
248
249 let course = models::courses::get_course(&mut conn, chapter.course_id).await?;
250 let chapter_image = upload_file_from_cms(
251 request.headers(),
252 payload,
253 MediaOwner::Course(course.id),
254 file_store.as_ref(),
255 &mut conn,
256 user,
257 )
258 .await?
259 .to_string_lossy()
260 .to_string();
261 let updated_chapter = models::chapters::update_chapter_image_path(
262 &mut conn,
263 chapter.id,
264 Some(chapter_image),
265 user.id,
266 )
267 .await?;
268
269 if let Some(old_image_path) = chapter.chapter_image_path
271 && !models::chapters::chapter_image_path_is_referenced(&mut conn, &old_image_path).await?
272 {
273 let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
274 ControllerError::new(
275 ControllerErrorType::InternalServerError,
276 original_error.to_string(),
277 Some(original_error.into()),
278 )
279 })?;
280 file_store.delete(&file).await.map_err(|original_error| {
281 ControllerError::new(
282 ControllerErrorType::InternalServerError,
283 original_error.to_string(),
284 Some(original_error.into()),
285 )
286 })?;
287 }
288
289 let response =
290 Chapter::from_database_chapter(&updated_chapter, file_store.as_ref(), app_conf.as_ref());
291
292 token.authorized_ok(web::Json(response))
293}
294
295#[instrument(skip(pool, file_store))]
307#[utoipa::path(
308 delete,
309 path = "/{chapter_id}/image",
310 operation_id = "deleteChapterImage",
311 tag = "chapters",
312 params(
313 ("chapter_id" = Uuid, Path, description = "Chapter id")
314 ),
315 responses(
316 (status = 200, description = "Deleted chapter image")
317 )
318)]
319async fn remove_chapter_image(
320 chapter_id: web::Path<Uuid>,
321 pool: web::Data<PgPool>,
322 user: AuthUser,
323 file_store: web::Data<dyn FileStore>,
324) -> ControllerResult<web::Json<()>> {
325 let mut conn = pool.acquire().await?;
326 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
327 let token = authorize(
328 &mut conn,
329 Act::Edit,
330 Some(user.id),
331 Res::Course(chapter.course_id),
332 )
333 .await?;
334 if let Some(chapter_image_path) = chapter.chapter_image_path {
335 let _res =
336 models::chapters::update_chapter_image_path(&mut conn, chapter.id, None, user.id)
337 .await?;
338 if !models::chapters::chapter_image_path_is_referenced(&mut conn, &chapter_image_path)
340 .await?
341 {
342 let file = PathBuf::from_str(&chapter_image_path).map_err(|original_error| {
343 controller_err!(
344 InternalServerError,
345 original_error.to_string(),
346 original_error
347 )
348 })?;
349 file_store.delete(&file).await.map_err(|original_error| {
350 controller_err!(
351 InternalServerError,
352 original_error.to_string(),
353 original_error
354 )
355 })?;
356 }
357 }
358 token.authorized_ok(web::Json(()))
359}
360
361#[utoipa::path(
365 get,
366 path = "/{course_id}/all-chapters-for-course",
367 operation_id = "getCourseChapters",
368 tag = "chapters",
369 params(
370 ("course_id" = Uuid, Path, description = "Course id")
371 ),
372 responses(
373 (status = 200, description = "Course chapters", body = Vec<DatabaseChapter>)
374 )
375)]
376async fn get_all_chapters_by_course_id(
377 course_id: web::Path<Uuid>,
378 pool: web::Data<PgPool>,
379 user: AuthUser,
380) -> ControllerResult<web::Json<Vec<DatabaseChapter>>> {
381 let mut conn = pool.acquire().await?;
382 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Course(*course_id)).await?;
383
384 let mut chapters = models::chapters::get_course_chapters(&mut conn, *course_id).await?;
385
386 chapters.sort_by_key(|a| a.chapter_number);
387
388 token.authorized_ok(web::Json(chapters))
389}
390
391pub fn _add_routes(cfg: &mut ServiceConfig) {
399 cfg.route("", web::post().to(post_new_chapter))
400 .route("/{chapter_id}", web::delete().to(delete_chapter))
401 .route("/{chapter_id}", web::put().to(update_chapter))
402 .route("/{chapter_id}/image", web::put().to(set_chapter_image))
403 .route(
404 "/{chapter_id}/image",
405 web::delete().to(remove_chapter_image),
406 )
407 .route(
408 "/{course_id}/all-chapters-for-course",
409 web::get().to(get_all_chapters_by_course_id),
410 );
411}