1use axum::{
8 body::Bytes,
9 extract::{Json, Multipart},
10 http::StatusCode,
11 Extension,
12};
13use color_eyre::eyre::{ContextCompat, Result};
14use http::HeaderMap;
15use serde::Serialize;
16use tokio::fs;
17
18use std::collections::HashMap;
19
20use axum::extract::{Query, State};
21use serde::Deserialize;
22
23use crate::{
24 auth::{self, Auth},
25 pathutils::PaperCategory,
26 qp::{self, AdminDashboardQP, Exam, WithUrl},
27};
28
29use super::{AppError, BackendResponse, RouterState, Status};
30
31type HandlerReturn<T> = Result<(StatusCode, BackendResponse<T>), AppError>;
33
34pub async fn healthcheck() -> HandlerReturn<()> {
36 Ok(BackendResponse::ok("Hello, World.".into(), ()))
37}
38
39pub async fn get_unapproved(
41 State(state): State<RouterState>,
42) -> HandlerReturn<Vec<AdminDashboardQP>> {
43 let papers: Vec<AdminDashboardQP> = state.db.get_unapproved_papers().await?;
44
45 let papers = papers
46 .iter()
47 .map(|paper| paper.clone().with_url(&state.env_vars))
48 .collect::<Result<Vec<qp::AdminDashboardQP>, color_eyre::eyre::Error>>()?;
49
50 Ok(BackendResponse::ok(
51 format!("Successfully fetched {} papers.", papers.len()),
52 papers,
53 ))
54}
55
56pub async fn search(
62 State(state): State<RouterState>,
63 Query(params): Query<HashMap<String, String>>,
64) -> HandlerReturn<Vec<qp::BaseQP>> {
65 let response = if let Some(query) = params.get("query") {
66 let exam_query_str = params
67 .get("exam")
68 .map(|value| value.to_owned())
69 .unwrap_or("".into());
70
71 if let Ok(exam_filter) = exam_query_str
72 .split(',')
73 .filter(|val| !val.trim().is_empty())
74 .map(|val| Exam::try_from(&val.to_owned()))
75 .collect::<Result<Vec<Exam>, _>>()
76 {
77 let papers = state.db.search_papers(query, exam_filter).await?;
78
79 let papers = papers
80 .iter()
81 .map(|paper| paper.clone().with_url(&state.env_vars))
82 .collect::<Result<Vec<qp::BaseQP>, color_eyre::eyre::Error>>()?;
83
84 Ok(BackendResponse::ok(
85 format!("Successfully fetched {} papers.", papers.len()),
86 papers,
87 ))
88 } else {
89 Ok(BackendResponse::error(
90 "Invalid `exam` URL parameter.".into(),
91 StatusCode::BAD_REQUEST,
92 ))
93 }
94 } else {
95 Ok(BackendResponse::error(
96 "`query` URL parameter is required.".into(),
97 StatusCode::BAD_REQUEST,
98 ))
99 };
100
101 response
102}
103
104#[derive(Deserialize)]
105pub struct OAuthReq {
107 code: String,
108}
109
110#[derive(Serialize)]
111pub struct OAuthRes {
113 token: String,
114}
115
116pub async fn oauth(
120 State(state): State<RouterState>,
121 Json(body): Json<OAuthReq>,
122) -> HandlerReturn<OAuthRes> {
123 if let Some(token) = auth::authenticate_user(&body.code, &state.env_vars).await? {
124 Ok(BackendResponse::ok(
125 "Successfully authorized the user.".into(),
126 OAuthRes { token },
127 ))
128 } else {
129 Ok(BackendResponse::error(
130 "Error: User unauthorized.".into(),
131 StatusCode::UNAUTHORIZED,
132 ))
133 }
134}
135
136#[derive(Serialize)]
137pub struct ProfileRes {
139 token: String,
140 username: String,
141}
142
143pub async fn profile(Extension(auth): Extension<Auth>) -> HandlerReturn<ProfileRes> {
145 Ok(BackendResponse::ok(
146 "Successfully authorized the user.".into(),
147 ProfileRes {
148 token: auth.jwt,
149 username: auth.username,
150 },
151 ))
152}
153
154#[derive(Deserialize)]
155pub struct EditReq {
157 pub id: i32,
158 pub course_code: Option<String>,
159 pub course_name: Option<String>,
160 pub year: Option<i32>,
161 pub semester: Option<String>,
162 pub exam: Option<String>,
163 pub note: Option<String>,
164 pub approve_status: Option<bool>,
165}
166
167pub async fn edit(
173 Extension(auth): Extension<Auth>,
174 State(state): State<RouterState>,
175 Json(body): Json<EditReq>,
176) -> HandlerReturn<AdminDashboardQP> {
177 let (tx, old_filelink, new_qp) = state
179 .db
180 .edit_paper(body, &auth.username, &state.env_vars)
181 .await?;
182
183 let old_filepath = state.env_vars.paths.get_path_from_slug(&old_filelink);
185 let new_filepath = state.env_vars.paths.get_path_from_slug(&new_qp.qp.filelink);
186
187 if old_filepath != new_filepath {
188 if let Err(e) = fs::copy(old_filepath, new_filepath).await {
189 tracing::error!("Error copying file: {}", e);
190
191 tx.rollback().await?;
192 Ok(BackendResponse::error(
193 "Error copying question paper file.".into(),
194 StatusCode::INTERNAL_SERVER_ERROR,
195 ))
196 } else {
197 tx.commit().await?;
199
200 Ok(BackendResponse::ok(
201 "Successfully updated paper details.".into(),
202 new_qp.with_url(&state.env_vars)?,
203 ))
204 }
205 } else {
206 tx.commit().await?;
208 Ok(BackendResponse::ok(
209 "Successfully updated paper details.".into(),
210 new_qp.with_url(&state.env_vars)?,
211 ))
212 }
213}
214
215#[derive(Deserialize)]
216pub struct FileDetails {
218 pub course_code: String,
219 pub course_name: String,
220 pub year: i32,
221 pub exam: String,
222 pub semester: String,
223 pub filename: String,
224 pub note: String,
225}
226
227const FILE_SIZE_LIMIT: usize = 10 << 20;
229#[derive(Serialize)]
230pub struct UploadStatus {
232 filename: String,
234 status: Status,
236 message: String,
238}
239
240pub async fn upload(
244 State(state): State<RouterState>,
245 mut multipart: Multipart,
246) -> HandlerReturn<Vec<UploadStatus>> {
247 let mut files = Vec::<(HeaderMap, Bytes)>::new();
248 let mut file_details: String = "".into();
249
250 while let Some(field) = multipart.next_field().await.unwrap() {
251 let name = field.name().unwrap().to_string();
252
253 if name == "files" {
254 files.push((field.headers().clone(), field.bytes().await?));
255 } else if name == "file_details" {
256 if file_details.is_empty() {
257 file_details = field.text().await?;
258 } else {
259 return Ok(BackendResponse::error(
260 "Error: Multiple `file_details` fields found.".into(),
261 StatusCode::BAD_REQUEST,
262 ));
263 }
264 }
265 }
266
267 let files = files;
268 let file_details: Vec<FileDetails> = serde_json::from_str(&file_details)?;
269
270 if files.len() > state.env_vars.max_upload_limit {
271 return Ok(BackendResponse::error(
272 format!(
273 "Only upto {} files can be uploaded. Found {}.",
274 state.env_vars.max_upload_limit,
275 files.len()
276 ),
277 StatusCode::BAD_REQUEST,
278 ));
279 }
280
281 if files.len() != file_details.len() {
282 return Ok(BackendResponse::error(
283 "Error: Number of files and file details array length do not match.".into(),
284 StatusCode::BAD_REQUEST,
285 ));
286 }
287
288 let files_iter = files.iter().zip(file_details.iter());
289 let mut upload_statuses = Vec::<UploadStatus>::new();
290
291 for ((file_headers, file_data), details) in files_iter {
292 let filename = details.filename.to_owned();
293
294 if file_data.len() > FILE_SIZE_LIMIT {
295 upload_statuses.push(UploadStatus {
296 filename,
297 status: Status::Error,
298 message: format!(
299 "File size too big. Only files upto {} MiB are allowed.",
300 FILE_SIZE_LIMIT >> 20
301 ),
302 });
303 continue;
304 }
305
306 if let Some(content_type) = file_headers.get("content-type") {
307 if content_type != "application/pdf" {
308 upload_statuses.push(UploadStatus {
309 filename: filename.to_owned(),
310 status: Status::Error,
311 message: "Only PDFs are supported.".into(),
312 });
313 continue;
314 }
315 } else {
316 upload_statuses.push(UploadStatus {
317 filename,
318 status: Status::Error,
319 message: "`content-type` header not found. File type could not be determined."
320 .into(),
321 });
322 continue;
323 }
324
325 let (mut tx, id) = state.db.insert_new_uploaded_qp(details).await?;
327
328 let filelink_slug = state
330 .env_vars
331 .paths
332 .get_slug(&format!("{}.pdf", id), PaperCategory::Unapproved);
333
334 if state
336 .db
337 .update_filelink(&mut tx, id, &filelink_slug)
338 .await
339 .is_ok()
340 {
341 let filepath = state.env_vars.paths.get_path_from_slug(&filelink_slug);
342
343 if fs::write(&filepath, file_data).await.is_ok() {
345 if tx.commit().await.is_ok() {
346 upload_statuses.push(UploadStatus {
347 filename,
348 status: Status::Success,
349 message: "Succesfully uploaded file.".into(),
350 });
351 continue;
352 } else {
353 fs::remove_file(filepath).await?;
355 upload_statuses.push(UploadStatus {
356 filename,
357 status: Status::Success,
358 message: "Succesfully uploaded file.".into(),
359 });
360 continue;
361 }
362 } else {
363 tx.rollback().await?;
364 }
365
366 } else {
368 tx.rollback().await?;
369
370 upload_statuses.push(UploadStatus {
371 filename,
372 status: Status::Error,
373 message: "Error updating the filelink".into(),
374 });
375 continue;
376 }
377
378 upload_statuses.push(UploadStatus {
379 filename,
380 status: Status::Error,
381 message: "THIS SHOULD NEVER HAPPEN. REPORT IMMEDIATELY. ALSO THIS WOULDN'T HAPPEN IF RUST HAD STABLE ASYNC CLOSURES.".into(),
382 });
383 }
384
385 Ok(BackendResponse::ok(
386 format!("Successfully processed {} files", upload_statuses.len()),
387 upload_statuses,
388 ))
389}
390
391#[derive(Deserialize)]
392pub struct DeleteReq {
394 id: i32,
395}
396
397pub async fn delete(
401 State(state): State<RouterState>,
402 Json(body): Json<DeleteReq>,
403) -> HandlerReturn<()> {
404 let paper_deleted = state.db.soft_delete(body.id).await?;
405
406 if paper_deleted {
407 Ok(BackendResponse::ok(
408 "Succesfully deleted the paper.".into(),
409 (),
410 ))
411 } else {
412 Ok(BackendResponse::error(
413 "No paper was changed. Either the paper does not exist, is a library paper (cannot be deleted), or is already deleted.".into(),
414 StatusCode::BAD_REQUEST,
415 ))
416 }
417}
418
419pub async fn similar(
428 State(state): State<RouterState>,
429 Query(body): Query<HashMap<String, String>>,
430) -> HandlerReturn<Vec<AdminDashboardQP>> {
431 if !body.contains_key("course_code") {
432 return Ok(BackendResponse::error(
433 "Error: `course_code` is required.".into(),
434 StatusCode::BAD_REQUEST,
435 ));
436 }
437
438 let papers = state
439 .db
440 .get_similar_papers(
441 body.get("course_code")
442 .context("Expected course code to be here.")?,
443 body.get("year")
444 .map(|year| year.parse::<i32>())
445 .transpose()?,
446 body.get("semester"),
447 body.get("exam"),
448 )
449 .await?;
450
451 Ok(BackendResponse::ok(
452 format!("Found {} similar papers.", papers.len()),
453 papers
454 .iter()
455 .map(|paper| paper.to_owned().with_url(&state.env_vars))
456 .collect::<Result<Vec<AdminDashboardQP>>>()?,
457 ))
458}