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 let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
262 ControllerError::new(
263 ControllerErrorType::InternalServerError,
264 original_error.to_string(),
265 Some(original_error.into()),
266 )
267 })?;
268 file_store.delete(&file).await.map_err(|original_error| {
269 ControllerError::new(
270 ControllerErrorType::InternalServerError,
271 original_error.to_string(),
272 Some(original_error.into()),
273 )
274 })?;
275 }
276
277 let response =
278 Chapter::from_database_chapter(&updated_chapter, file_store.as_ref(), app_conf.as_ref());
279
280 token.authorized_ok(web::Json(response))
281}
282
283#[instrument(skip(pool, file_store))]
295#[utoipa::path(
296 delete,
297 path = "/{chapter_id}/image",
298 operation_id = "deleteChapterImage",
299 tag = "chapters",
300 params(
301 ("chapter_id" = Uuid, Path, description = "Chapter id")
302 ),
303 responses(
304 (status = 200, description = "Deleted chapter image")
305 )
306)]
307async fn remove_chapter_image(
308 chapter_id: web::Path<Uuid>,
309 pool: web::Data<PgPool>,
310 user: AuthUser,
311 file_store: web::Data<dyn FileStore>,
312) -> ControllerResult<web::Json<()>> {
313 let mut conn = pool.acquire().await?;
314 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
315 let token = authorize(
316 &mut conn,
317 Act::Edit,
318 Some(user.id),
319 Res::Course(chapter.course_id),
320 )
321 .await?;
322 if let Some(chapter_image_path) = chapter.chapter_image_path {
323 let file = PathBuf::from_str(&chapter_image_path).map_err(|original_error| {
324 ControllerError::new(
325 ControllerErrorType::InternalServerError,
326 original_error.to_string(),
327 Some(original_error.into()),
328 )
329 })?;
330 let _res = models::chapters::update_chapter_image_path(&mut conn, chapter.id, None).await?;
331 file_store.delete(&file).await.map_err(|original_error| {
332 ControllerError::new(
333 ControllerErrorType::InternalServerError,
334 original_error.to_string(),
335 Some(original_error.into()),
336 )
337 })?;
338 }
339 token.authorized_ok(web::Json(()))
340}
341
342#[utoipa::path(
346 get,
347 path = "/{course_id}/all-chapters-for-course",
348 operation_id = "getCourseChapters",
349 tag = "chapters",
350 params(
351 ("course_id" = Uuid, Path, description = "Course id")
352 ),
353 responses(
354 (status = 200, description = "Course chapters", body = Vec<DatabaseChapter>)
355 )
356)]
357async fn get_all_chapters_by_course_id(
358 course_id: web::Path<Uuid>,
359 pool: web::Data<PgPool>,
360 user: AuthUser,
361) -> ControllerResult<web::Json<Vec<DatabaseChapter>>> {
362 let mut conn = pool.acquire().await?;
363 let token = authorize(&mut conn, Act::View, Some(user.id), Res::Course(*course_id)).await?;
364
365 let mut chapters = models::chapters::course_chapters(&mut conn, *course_id).await?;
366
367 chapters.sort_by(|a, b| a.chapter_number.cmp(&b.chapter_number));
368
369 token.authorized_ok(web::Json(chapters))
370}
371
372pub fn _add_routes(cfg: &mut ServiceConfig) {
380 cfg.route("", web::post().to(post_new_chapter))
381 .route("/{chapter_id}", web::delete().to(delete_chapter))
382 .route("/{chapter_id}", web::put().to(update_chapter))
383 .route("/{chapter_id}/image", web::put().to(set_chapter_image))
384 .route(
385 "/{chapter_id}/image",
386 web::delete().to(remove_chapter_image),
387 )
388 .route(
389 "/{course_id}/all-chapters-for-course",
390 web::get().to(get_all_chapters_by_course_id),
391 );
392}