@@ -2,8 +2,8 @@ use crate::api::errors::{AppError, NoteError};
22use crate :: api:: models:: { CreateNote , ResponseNote , ResponseUser } ;
33use crate :: api:: router:: RouterState ;
44use 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} ;
88use crate :: db:: models:: User ;
99use 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