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(&mut conn, chapter_id).await?;
136 return token.authorized_ok(web::Json(Chapter::from_database_chapter(
137 &deleted_chapter,
138 file_store.as_ref(),
139 app_conf.as_ref(),
140 )));
141}
142
143#[instrument(skip(payload, pool, file_store, app_conf))]
163#[utoipa::path(
164 put,
165 path = "/{chapter_id}",
166 operation_id = "updateChapter",
167 tag = "chapters",
168 params(
169 ("chapter_id" = Uuid, Path, description = "Chapter id")
170 ),
171 request_body = ChapterUpdate,
172 responses(
173 (status = 200, description = "Updated chapter", body = Chapter)
174 )
175)]
176async fn update_chapter(
177 payload: web::Json<ChapterUpdate>,
178 chapter_id: web::Path<String>,
179 pool: web::Data<PgPool>,
180 user: AuthUser,
181 file_store: web::Data<dyn FileStore>,
182 app_conf: web::Data<ApplicationConfiguration>,
183) -> ControllerResult<web::Json<Chapter>> {
184 let mut conn = pool.acquire().await?;
185 let chapter_id = Uuid::from_str(&chapter_id)?;
186 let course_id = models::chapters::get_course_id(&mut conn, chapter_id).await?;
187 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
188 let course_update = payload.0;
189 let chapter = models::chapters::update_chapter(&mut conn, chapter_id, course_update).await?;
190
191 let response = Chapter::from_database_chapter(&chapter, file_store.as_ref(), app_conf.as_ref());
192
193 token.authorized_ok(web::Json(response))
194}
195
196#[instrument(skip(request, payload, pool, file_store, app_conf))]
211#[utoipa::path(
212 put,
213 path = "/{chapter_id}/image",
214 operation_id = "updateChapterImage",
215 tag = "chapters",
216 params(
217 ("chapter_id" = Uuid, Path, description = "Chapter id")
218 ),
219 request_body(content = inline(ChapterImageUploadPayload), content_type = "multipart/form-data"),
220 responses(
221 (status = 200, description = "Updated chapter image", body = Chapter)
222 )
223)]
224async fn set_chapter_image(
225 request: HttpRequest,
226 payload: Multipart,
227 chapter_id: web::Path<Uuid>,
228 pool: web::Data<PgPool>,
229 user: AuthUser,
230 file_store: web::Data<dyn FileStore>,
231 app_conf: web::Data<ApplicationConfiguration>,
232) -> ControllerResult<web::Json<Chapter>> {
233 let mut conn = pool.acquire().await?;
234 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
235 let token = authorize(
236 &mut conn,
237 Act::Edit,
238 Some(user.id),
239 Res::Course(chapter.course_id),
240 )
241 .await?;
242
243 let course = models::courses::get_course(&mut conn, chapter.course_id).await?;
244 let chapter_image = upload_file_from_cms(
245 request.headers(),
246 payload,
247 StoreKind::Course(course.id),
248 file_store.as_ref(),
249 &mut conn,
250 user,
251 )
252 .await?
253 .to_string_lossy()
254 .to_string();
255 let updated_chapter =
256 models::chapters::update_chapter_image_path(&mut conn, chapter.id, Some(chapter_image))
257 .await?;
258
259 if let Some(old_image_path) = chapter.chapter_image_path
261 && !models::chapters::chapter_image_path_is_referenced(&mut conn, &old_image_path).await?
262 {
263 let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
264 ControllerError::new(
265 ControllerErrorType::InternalServerError,
266 original_error.to_string(),
267 Some(original_error.into()),
268 )
269 })?;
270 file_store.delete(&file).await.map_err(|original_error| {
271 ControllerError::new(
272 ControllerErrorType::InternalServerError,
273 original_error.to_string(),
274 Some(original_error.into()),
275 )
276 })?;
277 }
278
279 let response =
280 Chapter::from_database_chapter(&updated_chapter, file_store.as_ref(), app_conf.as_ref());
281
282 token.authorized_ok(web::Json(response))
283}
284
285#[instrument(skip(pool, file_store))]
297#[utoipa::path(
298 delete,
299 path = "/{chapter_id}/image",
300 operation_id = "deleteChapterImage",
301 tag = "chapters",
302 params(
303 ("chapter_id" = Uuid, Path, description = "Chapter id")
304 ),
305 responses(
306 (status = 200, description = "Deleted chapter image")
307 )
308)]
309async fn remove_chapter_image(
310 chapter_id: web::Path<Uuid>,
311 pool: web::Data<PgPool>,
312 user: AuthUser,
313 file_store: web::Data<dyn FileStore>,
314) -> ControllerResult<web::Json<()>> {
315 let mut conn = pool.acquire().await?;
316 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
317 let token = authorize(
318 &mut conn,
319 Act::Edit,
320 Some(user.id),
321 Res::Course(chapter.course_id),
322 )
323 .await?;
324 if let Some(chapter_image_path) = chapter.chapter_image_path {
325 let _res = models::chapters::update_chapter_image_path(&mut conn, chapter.id, None).await?;
326 if !models::chapters::chapter_image_path_is_referenced(&mut conn, &chapter_image_path)
328 .await?
329 {
330 let file = PathBuf::from_str(&chapter_image_path).map_err(|original_error| {
331 controller_err!(
332 InternalServerError,
333 original_error.to_string(),
334 original_error
335 )
336 })?;
337 file_store.delete(&file).await.map_err(|original_error| {
338 controller_err!(
339 InternalServerError,
340 original_error.to_string(),
341 original_error
342 )
343 })?;
344 }
345 }
346 token.authorized_ok(web::Json(()))
347}
348
349#[utoipa::path(
353 get,
354 path = "/{course_id}/all-chapters-for-course",
355 operation_id = "getCourseChapters",
356 tag = "chapters",
357 params(
358 ("course_id" = Uuid, Path, description = "Course id")
359 ),
360 responses(
361 (status = 200, description = "Course chapters", body = Vec<DatabaseChapter>)
362 )
363)]
364async fn get_all_chapters_by_course_id(
365 course_id: web::Path<Uuid>,
366 pool: web::Data<PgPool>,
367 user: AuthUser,
368) -> ControllerResult<web::Json<Vec<DatabaseChapter>>> {
369 let mut conn = pool.acquire().await?;
370 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Course(*course_id)).await?;
371
372 let mut chapters = models::chapters::get_course_chapters(&mut conn, *course_id).await?;
373
374 chapters.sort_by_key(|a| a.chapter_number);
375
376 token.authorized_ok(web::Json(chapters))
377}
378
379pub fn _add_routes(cfg: &mut ServiceConfig) {
387 cfg.route("", web::post().to(post_new_chapter))
388 .route("/{chapter_id}", web::delete().to(delete_chapter))
389 .route("/{chapter_id}", web::put().to(update_chapter))
390 .route("/{chapter_id}/image", web::put().to(set_chapter_image))
391 .route(
392 "/{chapter_id}/image",
393 web::delete().to(remove_chapter_image),
394 )
395 .route(
396 "/{course_id}/all-chapters-for-course",
397 web::get().to(get_all_chapters_by_course_id),
398 );
399}