Skip to content

Commit 4dd5039

Browse files
committed
fix: nginx upload limit
1 parent a0c3716 commit 4dd5039

File tree

13 files changed

+1148
-26
lines changed

13 files changed

+1148
-26
lines changed

backend/src/api/errors.rs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1+
use axum::body::Body;
12
use axum::http::StatusCode;
2-
use axum::Json;
33
use axum::response::{IntoResponse, Response};
4-
use serde_json::json;
54

65
pub enum AppError {
76
Note(NoteError),
@@ -32,16 +31,25 @@ impl From<UserError> for AppError {
3231

3332
impl IntoResponse for UserError {
3433
fn into_response(self) -> Response {
35-
match self {
34+
let (status, error_message) = match self {
3635
UserError::Conflict(msg, err) => {
3736
tracing::error!("User conflict error: {}", err);
38-
(StatusCode::CONFLICT, msg).into_response()
37+
(StatusCode::CONFLICT, msg)
3938
}
4039
UserError::Unknown(msg, err) => {
4140
tracing::error!("Unknown user error: {}", err);
42-
(StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
41+
(StatusCode::INTERNAL_SERVER_ERROR, msg)
4342
}
44-
}
43+
};
44+
45+
Response::builder()
46+
.status(status)
47+
.header("Content-Type", "application/json")
48+
.header("Access-Control-Allow-Origin", "*")
49+
.header("Access-Control-Allow-Methods", "*")
50+
.header("Access-Control-Allow-Headers", "content-type, authorization, accept, origin, x-requested-with")
51+
.body(Body::from(format!(r#"{{"error": "{}"}}"#, error_message)))
52+
.unwrap()
4553
}
4654
}
4755

@@ -61,28 +69,37 @@ impl From<AuthError> for AppError {
6169

6270
impl IntoResponse for AuthError {
6371
fn into_response(self) -> Response {
64-
match self {
72+
let (status, error_message) = match self {
6573
AuthError::RequestError(msg, err) => {
6674
tracing::error!("Authentication request error: {}: {}", msg, err);
67-
(StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
75+
(StatusCode::INTERNAL_SERVER_ERROR, msg)
6876
}
6977
AuthError::BadResponse(msg) => {
7078
tracing::error!("Authentication bad response: {}", msg);
71-
(StatusCode::BAD_REQUEST, msg).into_response()
79+
(StatusCode::BAD_REQUEST, msg)
7280
}
7381
AuthError::ConfigError(msg) => {
7482
tracing::error!("Authentication config error: {}", msg);
75-
(StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
83+
(StatusCode::INTERNAL_SERVER_ERROR, msg)
7684
}
7785
AuthError::InvalidToken(msg) => {
7886
tracing::error!("Invalid authentication token: {}", msg);
79-
(StatusCode::UNAUTHORIZED, msg).into_response()
87+
(StatusCode::UNAUTHORIZED, msg)
8088
}
8189
AuthError::DatabaseError(msg, err) => {
8290
tracing::error!("Authentication database error: {}: {:?}", msg, err);
83-
(StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
91+
(StatusCode::INTERNAL_SERVER_ERROR, msg)
8492
}
85-
}
93+
};
94+
95+
Response::builder()
96+
.status(status)
97+
.header("Content-Type", "application/json")
98+
.header("Access-Control-Allow-Origin", "*")
99+
.header("Access-Control-Allow-Methods", "*")
100+
.header("Access-Control-Allow-Headers", "content-type, authorization, accept, origin, x-requested-with")
101+
.body(Body::from(format!(r#"{{"error": "{}"}}"#, error_message)))
102+
.unwrap()
86103
}
87104
}
88105

@@ -115,6 +132,13 @@ impl IntoResponse for NoteError {
115132
NoteError::BadVote(msg) => (StatusCode::BAD_REQUEST, msg),
116133
};
117134

118-
(status, Json(json!({ "error": error_message }))).into_response()
135+
Response::builder()
136+
.status(status)
137+
.header("Content-Type", "application/json")
138+
.header("Access-Control-Allow-Origin", "*")
139+
.header("Access-Control-Allow-Methods", "*")
140+
.header("Access-Control-Allow-Headers", "content-type, authorization, accept, origin, x-requested-with")
141+
.body(Body::from(format!(r#"{{"error": "{}"}}"#, error_message)))
142+
.unwrap()
119143
}
120144
}

backend/src/api/handlers/notes.rs

Lines changed: 272 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::api::errors::{AppError, NoteError};
22
use crate::api::models::{CreateNote, ResponseNote, ResponseUser};
33
use crate::api::router::RouterState;
44
use crate::db::handlers::notes::{
5-
create_note, get_note_by_id, get_notes, increment_note_downloads, search_notes_by_query,
6-
update_note_preview_status,
5+
create_note, delete_note, get_note_by_id, get_notes, get_notes_by_user_id,
6+
increment_note_downloads, search_notes_by_query, update_note, update_note_preview_status,
77
};
88
use crate::db::models::User;
99
use axum::body::Bytes;
@@ -407,3 +407,273 @@ pub async fn download_note(
407407

408408
Ok((StatusCode::OK, Json("OK").into_response()))
409409
}
410+
411+
/// Get all notes uploaded by a specific user
412+
pub async fn get_user_notes(
413+
State(state): State<RouterState>,
414+
Extension(user): Extension<Option<User>>,
415+
Path(user_id): Path<Uuid>,
416+
) -> Result<(StatusCode, Response), AppError> {
417+
match get_notes_by_user_id(&state.db_wrapper, user_id, user.as_ref().map(|u| u.id)).await {
418+
Ok(notes) => {
419+
let response_notes: Vec<ResponseNote> = notes
420+
.into_iter()
421+
.map(|note| {
422+
let file_url = state
423+
.env_vars
424+
.paths
425+
.get_note_url(&format!("{}.pdf", note.note_id))
426+
.unwrap();
427+
let preview_image_url = state
428+
.env_vars
429+
.paths
430+
.get_preview_url(&format!("{}.jpg", note.note_id))
431+
.unwrap();
432+
ResponseNote::from_note_with_user(note, file_url, preview_image_url)
433+
})
434+
.collect();
435+
Ok((StatusCode::OK, Json(response_notes).into_response()))
436+
}
437+
Err(err) => Err(NoteError::DatabaseError(
438+
"Failed to fetch user notes".to_string(),
439+
err.into(),
440+
)
441+
.into()),
442+
}
443+
}
444+
445+
/// Update an existing note (owner only)
446+
pub async fn update_note_handler(
447+
State(state): State<RouterState>,
448+
Extension(user): Extension<User>,
449+
Path(note_id): Path<Uuid>,
450+
mut multipart: Multipart,
451+
) -> Result<(StatusCode, Response), AppError> {
452+
// First, verify the user owns this note
453+
let existing_note = get_note_by_id(&state.db_wrapper, note_id, Some(user.id))
454+
.await
455+
.map_err(|err| {
456+
NoteError::DatabaseError("Failed to fetch note".to_string(), err.into())
457+
})?;
458+
459+
if existing_note.note_uploader_user_id != user.id {
460+
return Err(NoteError::InvalidData("You can only edit your own notes".to_string()).into());
461+
}
462+
463+
let mut course_name = String::new();
464+
let mut course_code = String::new();
465+
let mut description: Option<String> = None;
466+
let mut professor_names: Option<Vec<String>> = None;
467+
let mut tags: Vec<String> = Vec::new();
468+
let mut file_data: Option<Bytes> = None;
469+
let mut year: usize = 2025;
470+
let mut semester: String = "Autumn".to_string();
471+
472+
let file_size_limit = state.env_vars.file_size_limit << 20;
473+
474+
// Parse multipart form data
475+
while let Ok(Some(field)) = multipart.next_field().await {
476+
let name = match field.name() {
477+
Some(name) => name.to_string(),
478+
None => continue,
479+
};
480+
481+
if name == "file" {
482+
if let Some(content_type) = field.content_type() {
483+
if content_type != "application/pdf" {
484+
return Err(NoteError::InvalidData(
485+
"Only PDF files are supported".to_string(),
486+
)
487+
.into());
488+
}
489+
}
490+
491+
let data = field
492+
.bytes()
493+
.await
494+
.map_err(|_| NoteError::UploadFailed("Failed to read file bytes".to_string()))?;
495+
496+
if data.len() > file_size_limit {
497+
return Err(NoteError::InvalidData(format!(
498+
"File size too big. Only files up to {} MiB are allowed.",
499+
file_size_limit >> 20
500+
))
501+
.into());
502+
}
503+
504+
file_data = Some(data);
505+
continue;
506+
}
507+
508+
// Handle text fields
509+
let data = field
510+
.text()
511+
.await
512+
.map_err(|_| NoteError::UploadFailed(format!("Invalid format for field: {}", name)))?;
513+
514+
match name.as_str() {
515+
"course_name" => course_name = data,
516+
"course_code" => course_code = data,
517+
"description" => {
518+
if !data.trim().is_empty() {
519+
description = Some(data);
520+
}
521+
}
522+
"professor_names" => {
523+
let names: Vec<String> = data
524+
.split(',')
525+
.map(|s| s.trim().to_string())
526+
.filter(|s| !s.is_empty())
527+
.collect();
528+
if !names.is_empty() {
529+
professor_names = Some(names);
530+
}
531+
}
532+
"tags" => {
533+
tags = data
534+
.split(',')
535+
.map(|s| s.trim().to_string())
536+
.filter(|s| !s.is_empty())
537+
.collect();
538+
}
539+
"year" => {
540+
year = data
541+
.trim()
542+
.parse::<usize>()
543+
.map_err(|_| NoteError::InvalidData("Invalid year".to_string()))?;
544+
}
545+
"semester" => {
546+
let s = data.trim();
547+
if !s.is_empty() {
548+
semester = s.to_string();
549+
}
550+
}
551+
_ => (),
552+
}
553+
}
554+
555+
// Validate required fields
556+
if course_name.trim().is_empty() {
557+
return Err(NoteError::InvalidData("Course name is required".to_string()).into());
558+
}
559+
if course_code.trim().is_empty() {
560+
return Err(NoteError::InvalidData("Course code is required".to_string()).into());
561+
}
562+
if semester.trim().is_empty() || (semester.trim() != "Autumn" && semester.trim() != "Spring") {
563+
return Err(NoteError::InvalidData(
564+
"Semester is required and must be one of: Autumn, Spring".to_string(),
565+
)
566+
.into());
567+
}
568+
569+
// Update note in database
570+
let updated_note = update_note(
571+
&state.db_wrapper,
572+
note_id,
573+
course_name,
574+
course_code,
575+
description,
576+
professor_names,
577+
tags,
578+
year,
579+
semester,
580+
)
581+
.await
582+
.map_err(|err| {
583+
NoteError::DatabaseError("Failed to update note".to_string(), err.into())
584+
})?;
585+
586+
// If a new file was provided, replace the old one
587+
if let Some(file_bytes) = file_data {
588+
let file_path = state
589+
.env_vars
590+
.paths
591+
.get_note_path(&format!("{}.pdf", note_id));
592+
let preview_path = state
593+
.env_vars
594+
.paths
595+
.get_preview_path(&format!("{}.jpg", note_id));
596+
597+
// Write new PDF file
598+
tokio::fs::write(&file_path, &file_bytes)
599+
.await
600+
.map_err(|_| NoteError::UploadFailed("Failed to save file".to_string()))?;
601+
602+
// Regenerate preview image
603+
let result = generate_preview_image(
604+
file_path.to_str().unwrap(),
605+
preview_path.to_str().unwrap(),
606+
)
607+
.await;
608+
609+
if result.is_ok() {
610+
let mut tx = state.db_wrapper.pool().begin().await.map_err(|err| {
611+
NoteError::DatabaseError("Failed to start transaction".to_string(), err.into())
612+
})?;
613+
let _ = update_note_preview_status(&mut tx, note_id, true).await;
614+
let _ = tx.commit().await;
615+
}
616+
}
617+
618+
// Fetch the updated note with user info
619+
let note_with_user = get_note_by_id(&state.db_wrapper, note_id, Some(user.id))
620+
.await
621+
.map_err(|err| {
622+
NoteError::DatabaseError("Failed to fetch updated note".to_string(), err.into())
623+
})?;
624+
625+
let file_url = state
626+
.env_vars
627+
.paths
628+
.get_note_url(&format!("{}.pdf", note_id))
629+
.unwrap();
630+
let preview_image_url = state
631+
.env_vars
632+
.paths
633+
.get_preview_url(&format!("{}.jpg", note_id))
634+
.unwrap();
635+
636+
let response_note = ResponseNote::from_note_with_user(note_with_user, file_url, preview_image_url);
637+
638+
Ok((StatusCode::OK, Json(response_note).into_response()))
639+
}
640+
641+
/// Delete a note (owner only)
642+
pub async fn delete_note_handler(
643+
State(state): State<RouterState>,
644+
Extension(user): Extension<User>,
645+
Path(note_id): Path<Uuid>,
646+
) -> Result<(StatusCode, Response), AppError> {
647+
// First, verify the user owns this note
648+
let existing_note = get_note_by_id(&state.db_wrapper, note_id, Some(user.id))
649+
.await
650+
.map_err(|err| {
651+
NoteError::DatabaseError("Failed to fetch note".to_string(), err.into())
652+
})?;
653+
654+
if existing_note.note_uploader_user_id != user.id {
655+
return Err(NoteError::InvalidData("You can only delete your own notes".to_string()).into());
656+
}
657+
658+
// Delete the note from database
659+
delete_note(&state.db_wrapper, note_id)
660+
.await
661+
.map_err(|err| {
662+
NoteError::DatabaseError("Failed to delete note".to_string(), err.into())
663+
})?;
664+
665+
// Delete the files
666+
let file_path = state
667+
.env_vars
668+
.paths
669+
.get_note_path(&format!("{}.pdf", note_id));
670+
let preview_path = state
671+
.env_vars
672+
.paths
673+
.get_preview_path(&format!("{}.jpg", note_id));
674+
675+
let _ = tokio::fs::remove_file(file_path).await;
676+
let _ = tokio::fs::remove_file(preview_path).await;
677+
678+
Ok((StatusCode::OK, Json("Note deleted successfully").into_response()))
679+
}

0 commit comments

Comments
 (0)