diff --git a/api/coverage.out b/api/coverage.out index 3be7789..f0ebb9e 100644 --- a/api/coverage.out +++ b/api/coverage.out @@ -28,18 +28,18 @@ mymodule/auth.go:182.2,183.16 2 0 mymodule/auth.go:183.16,188.3 4 0 mymodule/auth.go:190.2,204.4 4 0 mymodule/auth.go:207.68,208.71 1 0 -mymodule/auth.go:208.71,211.17 2 0 -mymodule/auth.go:211.17,212.31 1 0 -mymodule/auth.go:212.31,217.5 3 0 -mymodule/auth.go:219.4,221.10 3 0 -mymodule/auth.go:225.3,229.14 3 0 -mymodule/auth.go:229.14,234.4 3 0 -mymodule/auth.go:237.3,237.43 1 0 -mymodule/auth.go:237.43,242.4 3 0 -mymodule/auth.go:244.3,244.23 1 0 -mymodule/auth.go:248.45,251.16 3 0 -mymodule/auth.go:251.16,253.3 1 0 -mymodule/auth.go:254.2,254.50 1 0 +mymodule/auth.go:208.71,213.17 4 0 +mymodule/auth.go:213.17,214.31 1 0 +mymodule/auth.go:214.31,219.5 3 0 +mymodule/auth.go:221.4,223.10 3 0 +mymodule/auth.go:227.3,231.14 3 0 +mymodule/auth.go:231.14,236.4 3 0 +mymodule/auth.go:239.3,239.43 1 0 +mymodule/auth.go:239.43,244.4 3 0 +mymodule/auth.go:246.3,246.23 1 0 +mymodule/auth.go:250.45,253.16 3 0 +mymodule/auth.go:253.16,255.3 1 0 +mymodule/auth.go:256.2,256.50 1 0 mymodule/main.go:75.43,116.2 28 1 mymodule/main.go:118.13,124.16 4 0 mymodule/main.go:124.16,126.3 1 0 diff --git a/api/main.go b/api/main.go index 386f421..cca7abb 100644 --- a/api/main.go +++ b/api/main.go @@ -5,13 +5,14 @@ import ( "encoding/json" "flag" "fmt" - "io" "log" "net/http" "os" "path/filepath" "strconv" - + "io" + + _ "github.com/go-sql-driver/mysql" "github.com/gorilla/mux" "github.com/joho/godotenv" @@ -74,22 +75,16 @@ type Book struct { func (app *App) setupRouter() *mux.Router { r := mux.NewRouter() - // Public routes (no token verification) - r.HandleFunc("/login", app.LoginUser).Methods("POST") - r.HandleFunc("/signup", app.SignupUser).Methods("POST") - - protected := r.NewRoute().Subrouter() - protected.Use(app.VerifySessionToken) // Apply the middleware globally to protected routes // Attach the App's methods to each route - protected.HandleFunc("/", app.Home).Methods("GET") + r.HandleFunc("/", app.Home).Methods("GET") r.HandleFunc("/info", app.Info).Methods("GET") r.HandleFunc("/books", app.GetAllBooks).Methods("GET") - protected.HandleFunc("/authors", app.GetAuthors).Methods("GET") + r.HandleFunc("/authors", app.GetAuthors).Methods("GET") r.HandleFunc("/authorsbooks", app.GetAuthorsAndBooks).Methods("GET") r.HandleFunc("/authors/{id}", app.GetAuthorBooksByID).Methods("GET") r.HandleFunc("/books/{id}", app.GetBookByID).Methods("GET") r.HandleFunc("/subscribers/{id}", app.GetSubscribersByBookID).Methods("GET") - protected.HandleFunc("/subscribers", app.GetAllSubscribers).Methods("GET") + r.HandleFunc("/subscribers", app.GetAllSubscribers).Methods("GET") // Routes for creating resources r.HandleFunc("/authors/new", app.AddAuthor).Methods("POST") @@ -114,6 +109,9 @@ func (app *App) setupRouter() *mux.Router { r.HandleFunc("/search_books", app.SearchBooks).Methods("GET") r.HandleFunc("/search_authors", app.SearchAuthors).Methods("GET") + // Routes for login + r.HandleFunc("/signup", app.SignupUser).Methods("POST") + r.HandleFunc("/login", app.LoginUser).Methods("POST") return r } @@ -176,7 +174,7 @@ func initDB(username, password, hostname, port, dbname string) (*sql.DB, error) dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, hostname, port, dbname) // Open a connection to the database - db, err := sqlOpen("mysql", dsn) // Use the sqlOpen variable here + db, err := sqlOpen("mysql", dsn) // Use the sqlOpen variable here if err != nil { return nil, fmt.Errorf("failed to connect to the database: %w", err) } @@ -203,162 +201,162 @@ func (app *App) Info(w http.ResponseWriter, r *http.Request) { // HandleError is a method of App that handles errors by logging them and sending an appropriate HTTP response func (app *App) HandleError(w http.ResponseWriter, message string, err error, status int) { - app.Logger.Printf("%s: %v", message, err) - http.Error(w, message, status) + app.Logger.Printf("%s: %v", message, err) + http.Error(w, message, status) } func RespondWithJSON(w http.ResponseWriter, status int, payload interface{}) { - // Set Content-Type header to application/json - w.Header().Set("Content-Type", "application/json") - // Attempt to encode the payload - if err := json.NewEncoder(w).Encode(payload); err != nil { - log.Printf("Error encoding response: %v", err) - // Set Content-Type header again in case of error - w.Header().Set("Content-Type", "application/json") - // Set status to 500 - w.WriteHeader(http.StatusInternalServerError) - // Write the error message, including a newline - w.Write([]byte("Error encoding response\n")) - return - } - // Set the status code - w.WriteHeader(status) + // Set Content-Type header to application/json + w.Header().Set("Content-Type", "application/json") + // Attempt to encode the payload + if err := json.NewEncoder(w).Encode(payload); err != nil { + log.Printf("Error encoding response: %v", err) + // Set Content-Type header again in case of error + w.Header().Set("Content-Type", "application/json") + // Set status to 500 + w.WriteHeader(http.StatusInternalServerError) + // Write the error message, including a newline + w.Write([]byte("Error encoding response\n")) + return + } + // Set the status code + w.WriteHeader(status) } // HandleError handles errors by logging them and sending an appropriate HTTP response func HandleError(w http.ResponseWriter, logger *log.Logger, message string, err error, status int) { - // Log the error message with additional context - logger.Printf("%s: %v", message, err) - // Send an HTTP error response with the given status code - http.Error(w, message, status) + // Log the error message with additional context + logger.Printf("%s: %v", message, err) + // Send an HTTP error response with the given status code + http.Error(w, message, status) } // GetIDFromRequest extracts and validates an ID parameter from the URL func GetIDFromRequest(r *http.Request, paramName string) (int, error) { - // Retrieve the parameter from the URL - vars := mux.Vars(r) - idStr := vars[paramName] - // Convert the string parameter to an integer - id, err := strconv.Atoi(idStr) - if err != nil { - // Return an error if conversion fails, using a lowercase message - return 0, fmt.Errorf("invalid %s: %v", paramName, err) - } - return id, nil + // Retrieve the parameter from the URL + vars := mux.Vars(r) + idStr := vars[paramName] + // Convert the string parameter to an integer + id, err := strconv.Atoi(idStr) + if err != nil { + // Return an error if conversion fails, using a lowercase message + return 0, fmt.Errorf("invalid %s: %v", paramName, err) + } + return id, nil } // ScanAuthors processes rows from the SQL query and returns a list of authors func ScanAuthors(rows *sql.Rows) ([]Author, error) { - defer rows.Close() + defer rows.Close() - var authors []Author + var authors []Author - for rows.Next() { - var author Author - if err := rows.Scan(&author.ID, &author.Lastname, &author.Firstname, &author.Photo); err != nil { - return nil, err - } - authors = append(authors, author) - } + for rows.Next() { + var author Author + if err := rows.Scan(&author.ID, &author.Lastname, &author.Firstname, &author.Photo); err != nil { + return nil, err + } + authors = append(authors, author) + } - if err := rows.Err(); err != nil { - return nil, err - } + if err := rows.Err(); err != nil { + return nil, err + } - return authors, nil + return authors, nil } // ValidateAuthorData checks if the required fields for an author are present func ValidateAuthorData(author Author) error { - if author.Firstname == "" || author.Lastname == "" { - return fmt.Errorf("firstname and lastname are required fields") // Lowercase error message - } - return nil + if author.Firstname == "" || author.Lastname == "" { + return fmt.Errorf("firstname and lastname are required fields") // Lowercase error message + } + return nil } // ValidateBookData checks if the required fields for a book are present func ValidateBookData(book Book) error { - // Ensure Title and AuthorID are not empty or zero - if book.Title == "" || book.AuthorID == 0 { - return fmt.Errorf("title and authorID are required fields") // Lowercase error message - } - return nil + // Ensure Title and AuthorID are not empty or zero + if book.Title == "" || book.AuthorID == 0 { + return fmt.Errorf("title and authorID are required fields") // Lowercase error message + } + return nil } // SearchAuthors searches for authors based on a query parameter func (app *App) SearchAuthors(w http.ResponseWriter, r *http.Request) { - // Log the entry to the handler for debugging purposes - app.Logger.Println("SearchAuthors handler called") - - // Get the "query" parameter from the URL - query := r.URL.Query().Get("query") - if query == "" { - // If the query parameter is missing, return a 400 Bad Request error - HandleError(w, app.Logger, "Query parameter is required", nil, http.StatusBadRequest) - return - } + // Log the entry to the handler for debugging purposes + app.Logger.Println("SearchAuthors handler called") + + // Get the "query" parameter from the URL + query := r.URL.Query().Get("query") + if query == "" { + // If the query parameter is missing, return a 400 Bad Request error + HandleError(w, app.Logger, "Query parameter is required", nil, http.StatusBadRequest) + return + } - // Prepare the SQL query with an ORDER BY clause to ensure consistent result order - sqlQuery := ` + // Prepare the SQL query with an ORDER BY clause to ensure consistent result order + sqlQuery := ` SELECT id, Firstname, Lastname, photo FROM authors WHERE Firstname LIKE ? OR Lastname LIKE ? ORDER BY Lastname, Firstname ` - // Execute the SQL query to fetch authors based on the query parameter - rows, err := app.DB.Query(sqlQuery, "%"+query+"%", "%"+query+"%") - if err != nil { - // Log error executing the query and return a 500 Internal Server Error - HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) - return - } - defer rows.Close() // Always close rows after use to release resources + // Execute the SQL query to fetch authors based on the query parameter + rows, err := app.DB.Query(sqlQuery, "%"+query+"%", "%"+query+"%") + if err != nil { + // Log error executing the query and return a 500 Internal Server Error + HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) + return + } + defer rows.Close() // Always close rows after use to release resources - // Use the utility function to scan and process the SQL rows - authors, err := ScanAuthors(rows) - if err != nil { - // Log error scanning the rows and return a 500 Internal Server Error - HandleError(w, app.Logger, "Error scanning authors", err, http.StatusInternalServerError) - return - } + // Use the utility function to scan and process the SQL rows + authors, err := ScanAuthors(rows) + if err != nil { + // Log error scanning the rows and return a 500 Internal Server Error + HandleError(w, app.Logger, "Error scanning authors", err, http.StatusInternalServerError) + return + } - // Send the JSON response using the utility function - RespondWithJSON(w, http.StatusOK, authors) + // Send the JSON response using the utility function + RespondWithJSON(w, http.StatusOK, authors) } // ScanBooks processes rows from the SQL query and returns a list of books with author information func ScanBooks(rows *sql.Rows) ([]BookAuthorInfo, error) { - defer rows.Close() + defer rows.Close() - var books []BookAuthorInfo + var books []BookAuthorInfo - for rows.Next() { - var book BookAuthorInfo - if err := rows.Scan(&book.BookID, &book.BookTitle, &book.AuthorID, &book.BookPhoto, &book.IsBorrowed, &book.BookDetails, &book.AuthorLastname, &book.AuthorFirstname); err != nil { - return nil, err - } - books = append(books, book) - } + for rows.Next() { + var book BookAuthorInfo + if err := rows.Scan(&book.BookID, &book.BookTitle, &book.AuthorID, &book.BookPhoto, &book.IsBorrowed, &book.BookDetails, &book.AuthorLastname, &book.AuthorFirstname); err != nil { + return nil, err + } + books = append(books, book) + } - if err := rows.Err(); err != nil { - return nil, err - } + if err := rows.Err(); err != nil { + return nil, err + } - return books, nil + return books, nil } // SearchBooks searches for books based on a query parameter func (app *App) SearchBooks(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("SearchBooks handler called") + app.Logger.Println("SearchBooks handler called") - query := r.URL.Query().Get("query") - if query == "" { - HandleError(w, app.Logger, "Query parameter is required", nil, http.StatusBadRequest) - return - } + query := r.URL.Query().Get("query") + if query == "" { + HandleError(w, app.Logger, "Query parameter is required", nil, http.StatusBadRequest) + return + } - sqlQuery := ` + sqlQuery := ` SELECT books.id AS book_id, books.title AS book_title, @@ -374,52 +372,53 @@ func (app *App) SearchBooks(w http.ResponseWriter, r *http.Request) { ORDER BY books.title, authors.Lastname, authors.Firstname ` - rows, err := app.DB.Query(sqlQuery, "%"+query+"%", "%"+query+"%", "%"+query+"%") - if err != nil { - HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) - return - } - defer rows.Close() + rows, err := app.DB.Query(sqlQuery, "%"+query+"%", "%"+query+"%", "%"+query+"%") + if err != nil { + HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) + return + } + defer rows.Close() - books, err := ScanBooks(rows) - if err != nil { - HandleError(w, app.Logger, "Error scanning books", err, http.StatusInternalServerError) - return - } + books, err := ScanBooks(rows) + if err != nil { + HandleError(w, app.Logger, "Error scanning books", err, http.StatusInternalServerError) + return + } - RespondWithJSON(w, http.StatusOK, books) + RespondWithJSON(w, http.StatusOK, books) } + // GetAuthors retrieves all authors from the database func (app *App) GetAuthors(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("GetAuthors handler called") + app.Logger.Println("GetAuthors handler called") - sqlQuery := ` + sqlQuery := ` SELECT id, Lastname, Firstname, photo FROM authors ORDER BY Lastname, Firstname ` - rows, err := app.DB.Query(sqlQuery) - if err != nil { - HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) - return - } - defer rows.Close() + rows, err := app.DB.Query(sqlQuery) + if err != nil { + HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) + return + } + defer rows.Close() - authors, err := ScanAuthors(rows) - if err != nil { - HandleError(w, app.Logger, "Error scanning authors", err, http.StatusInternalServerError) - return - } + authors, err := ScanAuthors(rows) + if err != nil { + HandleError(w, app.Logger, "Error scanning authors", err, http.StatusInternalServerError) + return + } - RespondWithJSON(w, http.StatusOK, authors) + RespondWithJSON(w, http.StatusOK, authors) } // GetAllBooks retrieves all books from the database along with the author's first and last name func (app *App) GetAllBooks(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("GetAllBooks handler called") + app.Logger.Println("GetAllBooks handler called") - query := ` + query := ` SELECT books.id AS book_id, books.title AS book_title, @@ -434,61 +433,61 @@ func (app *App) GetAllBooks(w http.ResponseWriter, r *http.Request) { ORDER BY books.title, authors.Lastname, authors.Firstname ` - rows, err := app.DB.Query(query) - if err != nil { - HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) - return - } + rows, err := app.DB.Query(query) + if err != nil { + HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) + return + } - books, err := ScanBooks(rows) - if err != nil { - HandleError(w, app.Logger, "Error scanning books", err, http.StatusInternalServerError) - return - } + books, err := ScanBooks(rows) + if err != nil { + HandleError(w, app.Logger, "Error scanning books", err, http.StatusInternalServerError) + return + } - RespondWithJSON(w, http.StatusOK, books) + RespondWithJSON(w, http.StatusOK, books) } // GetAuthorsAndBooks retrieves information about authors and their books func (app *App) GetAuthorsAndBooks(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("GetAuthorsAndBooks handler called") + app.Logger.Println("GetAuthorsAndBooks handler called") - query := ` + query := ` SELECT a.Firstname AS author_firstname, a.Lastname AS author_lastname, b.title AS book_title, b.photo AS book_photo FROM authors_books ab JOIN authors a ON ab.author_id = a.id JOIN books b ON ab.book_id = b.id ` - rows, err := app.DB.Query(query) - if err != nil { - app.Logger.Printf("Query error: %v", err) - HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) - return - } - defer rows.Close() + rows, err := app.DB.Query(query) + if err != nil { + app.Logger.Printf("Query error: %v", err) + HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) + return + } + defer rows.Close() - authorsAndBooks, err := ScanAuthorAndBooks(rows) - if err != nil { - app.Logger.Printf("Scan error: %v", err) - HandleError(w, app.Logger, "Error scanning authors and books", err, http.StatusInternalServerError) - return - } + authorsAndBooks, err := ScanAuthorAndBooks(rows) + if err != nil { + app.Logger.Printf("Scan error: %v", err) + HandleError(w, app.Logger, "Error scanning authors and books", err, http.StatusInternalServerError) + return + } - RespondWithJSON(w, http.StatusOK, authorsAndBooks) + RespondWithJSON(w, http.StatusOK, authorsAndBooks) } // GetAuthorBooksByID retrieves information about an author and their books by the author's ID func (app *App) GetAuthorBooksByID(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("GetAuthorBooksByID handler called") + app.Logger.Println("GetAuthorBooksByID handler called") - authorID, err := GetIDFromRequest(r, "id") - if err != nil { - app.HandleError(w, "Invalid author ID", err, http.StatusBadRequest) - return - } + authorID, err := GetIDFromRequest(r, "id") + if err != nil { + app.HandleError(w, "Invalid author ID", err, http.StatusBadRequest) + return + } - query := ` + query := ` SELECT a.Firstname AS author_firstname, a.Lastname AS author_lastname, b.title AS book_title, b.photo AS book_photo FROM authors_books ab JOIN authors a ON ab.author_id = a.id @@ -496,52 +495,52 @@ func (app *App) GetAuthorBooksByID(w http.ResponseWriter, r *http.Request) { WHERE a.id = ? ` - rows, err := app.DB.Query(query, authorID) - if err != nil { - app.HandleError(w, "Error executing query", err, http.StatusInternalServerError) - return - } - defer rows.Close() + rows, err := app.DB.Query(query, authorID) + if err != nil { + app.HandleError(w, "Error executing query", err, http.StatusInternalServerError) + return + } + defer rows.Close() - authorAndBooks, err := ScanAuthorAndBooks(rows) - if err != nil { - app.HandleError(w, "Error scanning results", err, http.StatusInternalServerError) - return - } + authorAndBooks, err := ScanAuthorAndBooks(rows) + if err != nil { + app.HandleError(w, "Error scanning results", err, http.StatusInternalServerError) + return + } - RespondWithJSON(w, http.StatusOK, authorAndBooks) + RespondWithJSON(w, http.StatusOK, authorAndBooks) } // ScanAuthorAndBooks processes rows from the SQL query and returns a list of author and book information func ScanAuthorAndBooks(rows *sql.Rows) ([]AuthorBook, error) { - var authorsAndBooks []AuthorBook - - for rows.Next() { - var authorBook AuthorBook - if err := rows.Scan(&authorBook.AuthorFirstname, &authorBook.AuthorLastname, &authorBook.BookTitle, &authorBook.BookPhoto); err != nil { - return nil, err - } - authorsAndBooks = append(authorsAndBooks, authorBook) - } + var authorsAndBooks []AuthorBook + + for rows.Next() { + var authorBook AuthorBook + if err := rows.Scan(&authorBook.AuthorFirstname, &authorBook.AuthorLastname, &authorBook.BookTitle, &authorBook.BookPhoto); err != nil { + return nil, err + } + authorsAndBooks = append(authorsAndBooks, authorBook) + } - if err := rows.Err(); err != nil { - return nil, err - } + if err := rows.Err(); err != nil { + return nil, err + } - return authorsAndBooks, nil + return authorsAndBooks, nil } // GetBookByID retrieves information about a specific book based on its ID func (app *App) GetBookByID(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("GetBookByID handler called") + app.Logger.Println("GetBookByID handler called") - bookID, err := GetIDFromRequest(r, "id") - if err != nil { - HandleError(w, app.Logger, "Invalid book ID", err, http.StatusBadRequest) - return - } + bookID, err := GetIDFromRequest(r, "id") + if err != nil { + HandleError(w, app.Logger, "Invalid book ID", err, http.StatusBadRequest) + return + } - query := ` + query := ` SELECT books.title AS book_title, books.author_id AS author_id, @@ -556,27 +555,28 @@ func (app *App) GetBookByID(w http.ResponseWriter, r *http.Request) { WHERE books.id = ? ` - rows, err := app.DB.Query(query, bookID) - if err != nil { - HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) - return - } - defer rows.Close() - - var book BookAuthorInfo - if rows.Next() { - if err := rows.Scan(&book.BookTitle, &book.AuthorID, &book.BookPhoto, &book.IsBorrowed, &book.BookID, &book.BookDetails, &book.AuthorLastname, &book.AuthorFirstname); err != nil { - HandleError(w, app.Logger, "Error scanning book", err, http.StatusInternalServerError) - return - } - } else { - HandleError(w, app.Logger, "Book not found", nil, http.StatusNotFound) - return - } + rows, err := app.DB.Query(query, bookID) + if err != nil { + HandleError(w, app.Logger, "Error executing query", err, http.StatusInternalServerError) + return + } + defer rows.Close() + + var book BookAuthorInfo + if rows.Next() { + if err := rows.Scan(&book.BookTitle, &book.AuthorID, &book.BookPhoto, &book.IsBorrowed, &book.BookID, &book.BookDetails, &book.AuthorLastname, &book.AuthorFirstname); err != nil { + HandleError(w, app.Logger, "Error scanning book", err, http.StatusInternalServerError) + return + } + } else { + HandleError(w, app.Logger, "Book not found", nil, http.StatusNotFound) + return + } - RespondWithJSON(w, http.StatusOK, book) + RespondWithJSON(w, http.StatusOK, book) } + // GetSubscribersByBookID retrieves the list of subscribers who have borrowed a specific book based on the book's ID func (app *App) GetSubscribersByBookID(w http.ResponseWriter, r *http.Request) { bookID, err := GetIDFromRequest(r, "id") @@ -614,20 +614,20 @@ func (app *App) GetSubscribersByBookID(w http.ResponseWriter, r *http.Request) { } func ScanRows(rows *sql.Rows, subscribers *[]Subscriber) error { - for rows.Next() { - var subscriber Subscriber - if err := rows.Scan(&subscriber.Lastname, &subscriber.Firstname, &subscriber.Email); err != nil { - return err - } - *subscribers = append(*subscribers, subscriber) - } - return rows.Err() + for rows.Next() { + var subscriber Subscriber + if err := rows.Scan(&subscriber.Lastname, &subscriber.Firstname, &subscriber.Email); err != nil { + return err + } + *subscribers = append(*subscribers, subscriber) + } + return rows.Err() } // GetAllSubscribers retrieves all subscribers from the database func (app *App) GetAllSubscribers(w http.ResponseWriter, r *http.Request) { query := "SELECT lastname, firstname, email FROM subscribers" - + rows, err := app.DB.Query(query) if err != nil { HandleError(w, app.Logger, "Error querying the database", err, http.StatusInternalServerError) @@ -646,107 +646,107 @@ func (app *App) GetAllSubscribers(w http.ResponseWriter, r *http.Request) { var mkdirAll = os.MkdirAll var osCreate = os.Create -var ioCopy = io.Copy +var ioCopy = io.Copy // AddAuthorPhoto handles the upload of an author's photo and updates the database func (app *App) AddAuthorPhoto(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("AddAuthorPhoto handler called") + app.Logger.Println("AddAuthorPhoto handler called") - if r.Method != http.MethodPost { - HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) - return - } + if r.Method != http.MethodPost { + HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) + return + } - authorID, err := GetIDFromRequest(r, "id") - if err != nil { - HandleError(w, app.Logger, "Invalid author ID", err, http.StatusBadRequest) - return - } + authorID, err := GetIDFromRequest(r, "id") + if err != nil { + HandleError(w, app.Logger, "Invalid author ID", err, http.StatusBadRequest) + return + } - file, _, err := r.FormFile("file") - if err != nil { - HandleError(w, app.Logger, "Error getting file from request", err, http.StatusInternalServerError) - return - } - defer file.Close() + file, _, err := r.FormFile("file") + if err != nil { + HandleError(w, app.Logger, "Error getting file from request", err, http.StatusInternalServerError) + return + } + defer file.Close() - filename := "fullsize.jpg" - ext := filepath.Ext(filename) + filename := "fullsize.jpg" + ext := filepath.Ext(filename) - photoDir := "./upload/" + strconv.Itoa(authorID) - photoPath := photoDir + "/fullsize" + ext + photoDir := "./upload/" + strconv.Itoa(authorID) + photoPath := photoDir + "/fullsize" + ext - if err := mkdirAll(photoDir, 0777); err != nil { - HandleError(w, app.Logger, "Error creating directories", err, http.StatusInternalServerError) - return - } + if err := mkdirAll(photoDir, 0777); err != nil { + HandleError(w, app.Logger, "Error creating directories", err, http.StatusInternalServerError) + return + } - out, err := osCreate(photoPath) - if err != nil { - HandleError(w, app.Logger, "Error creating file on disk", err, http.StatusInternalServerError) - return - } - defer out.Close() + out, err := osCreate(photoPath) + if err != nil { + HandleError(w, app.Logger, "Error creating file on disk", err, http.StatusInternalServerError) + return + } + defer out.Close() - if _, err := ioCopy(out, file); err != nil { - HandleError(w, app.Logger, "Error saving file", err, http.StatusInternalServerError) - return - } + if _, err := ioCopy(out, file); err != nil { + HandleError(w, app.Logger, "Error saving file", err, http.StatusInternalServerError) + return + } - query := `UPDATE authors SET photo = ? WHERE id = ?` - if _, err := app.DB.Exec(query, photoPath, authorID); err != nil { - HandleError(w, app.Logger, "Failed to update author photo", err, http.StatusInternalServerError) - return - } + query := `UPDATE authors SET photo = ? WHERE id = ?` + if _, err := app.DB.Exec(query, photoPath, authorID); err != nil { + HandleError(w, app.Logger, "Failed to update author photo", err, http.StatusInternalServerError) + return + } - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "File uploaded successfully: %s\n", photoPath) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "File uploaded successfully: %s\n", photoPath) } var jsonEncoder = json.NewEncoder // AddAuthor adds a new author to the database func (app *App) AddAuthor(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("AddAuthor handler called") + app.Logger.Println("AddAuthor handler called") - if r.Method != http.MethodPost { - HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) - return - } + if r.Method != http.MethodPost { + HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) + return + } - var author Author - if err := json.NewDecoder(r.Body).Decode(&author); err != nil { - HandleError(w, app.Logger, "Invalid JSON data", err, http.StatusBadRequest) - return - } - defer r.Body.Close() + var author Author + if err := json.NewDecoder(r.Body).Decode(&author); err != nil { + HandleError(w, app.Logger, "Invalid JSON data", err, http.StatusBadRequest) + return + } + defer r.Body.Close() - if err := ValidateAuthorData(author); err != nil { - HandleError(w, app.Logger, err.Error(), nil, http.StatusBadRequest) - return - } + if err := ValidateAuthorData(author); err != nil { + HandleError(w, app.Logger, err.Error(), nil, http.StatusBadRequest) + return + } - query := `INSERT INTO authors (lastname, firstname, photo) VALUES (?, ?, ?)` - result, err := app.DB.Exec(query, author.Lastname, author.Firstname, "") - if err != nil { - HandleError(w, app.Logger, "Failed to insert author", err, http.StatusInternalServerError) - return - } + query := `INSERT INTO authors (lastname, firstname, photo) VALUES (?, ?, ?)` + result, err := app.DB.Exec(query, author.Lastname, author.Firstname, "") + if err != nil { + HandleError(w, app.Logger, "Failed to insert author", err, http.StatusInternalServerError) + return + } - id, err := result.LastInsertId() - if err != nil || id == 0 { - HandleError(w, app.Logger, "Failed to get last insert ID", err, http.StatusInternalServerError) - return - } + id, err := result.LastInsertId() + if err != nil || id == 0 { + HandleError(w, app.Logger, "Failed to get last insert ID", err, http.StatusInternalServerError) + return + } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) - response := map[string]int{"id": int(id)} - if err := jsonEncoder(w).Encode(response); err != nil { - app.Logger.Printf("JSON encoding error: %v", err) - http.Error(w, "Error encoding response", http.StatusInternalServerError) - } + response := map[string]int{"id": int(id)} + if err := jsonEncoder(w).Encode(response); err != nil { + app.Logger.Printf("JSON encoding error: %v", err) + http.Error(w, "Error encoding response", http.StatusInternalServerError) + } } // Define a global variable for os.MkdirAll to allow overriding in tests @@ -754,99 +754,98 @@ var osMkdirAll = os.MkdirAll // AddBookPhoto handles the upload of a book's photo and updates the database func (app *App) AddBookPhoto(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("AddBookPhoto handler called") + app.Logger.Println("AddBookPhoto handler called") - if r.Method != http.MethodPost { - HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) - return - } + if r.Method != http.MethodPost { + HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) + return + } - bookID, err := GetIDFromRequest(r, "id") - if err != nil { - HandleError(w, app.Logger, "Invalid book ID", err, http.StatusBadRequest) - return - } + bookID, err := GetIDFromRequest(r, "id") + if err != nil { + HandleError(w, app.Logger, "Invalid book ID", err, http.StatusBadRequest) + return + } - file, _, err := r.FormFile("file") - if err != nil { - HandleError(w, app.Logger, "Error getting file from request", err, http.StatusInternalServerError) - return - } - defer file.Close() + file, _, err := r.FormFile("file") + if err != nil { + HandleError(w, app.Logger, "Error getting file from request", err, http.StatusInternalServerError) + return + } + defer file.Close() - photoDir := "./upload/books/" + strconv.Itoa(bookID) - photoPath := photoDir + "/fullsize.jpg" + photoDir := "./upload/books/" + strconv.Itoa(bookID) + photoPath := photoDir + "/fullsize.jpg" - if err := osMkdirAll(photoDir, 0777); err != nil { - HandleError(w, app.Logger, "Error creating directories", err, http.StatusInternalServerError) - return - } + if err := osMkdirAll(photoDir, 0777); err != nil { + HandleError(w, app.Logger, "Error creating directories", err, http.StatusInternalServerError) + return + } - out, err := osCreate(photoPath) - if err != nil { - HandleError(w, app.Logger, "Error creating file on disk", err, http.StatusInternalServerError) - return - } - defer out.Close() + out, err := osCreate(photoPath) + if err != nil { + HandleError(w, app.Logger, "Error creating file on disk", err, http.StatusInternalServerError) + return + } + defer out.Close() - if _, err := ioCopy(out, file); err != nil { - HandleError(w, app.Logger, "Error saving file", err, http.StatusInternalServerError) - return - } + if _, err := ioCopy(out, file); err != nil { + HandleError(w, app.Logger, "Error saving file", err, http.StatusInternalServerError) + return + } - query := `UPDATE books SET photo = ? WHERE id = ?` - if _, err := app.DB.Exec(query, photoPath, bookID); err != nil { - HandleError(w, app.Logger, "Failed to update book photo", err, http.StatusInternalServerError) - return - } + query := `UPDATE books SET photo = ? WHERE id = ?` + if _, err := app.DB.Exec(query, photoPath, bookID); err != nil { + HandleError(w, app.Logger, "Failed to update book photo", err, http.StatusInternalServerError) + return + } - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "File uploaded successfully: %s\n", photoPath) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "File uploaded successfully: %s\n", photoPath) } // AddBook adds a new book to the database func (app *App) AddBook(w http.ResponseWriter, r *http.Request) { - app.Logger.Println("AddBook handler called") + app.Logger.Println("AddBook handler called") - if r.Method != http.MethodPost { - HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) - return - } + if r.Method != http.MethodPost { + HandleError(w, app.Logger, "Only POST method is supported", nil, http.StatusMethodNotAllowed) + return + } - var book Book - if err := json.NewDecoder(r.Body).Decode(&book); err != nil { - HandleError(w, app.Logger, "Invalid JSON data", err, http.StatusBadRequest) - return - } - defer r.Body.Close() + var book Book + if err := json.NewDecoder(r.Body).Decode(&book); err != nil { + HandleError(w, app.Logger, "Invalid JSON data", err, http.StatusBadRequest) + return + } + defer r.Body.Close() - if err := ValidateBookData(book); err != nil { - HandleError(w, app.Logger, err.Error(), nil, http.StatusBadRequest) - return - } + if err := ValidateBookData(book); err != nil { + HandleError(w, app.Logger, err.Error(), nil, http.StatusBadRequest) + return + } - query := `INSERT INTO books (title, photo, details, author_id, is_borrowed) VALUES (?, ?, ?, ?, ?)` - result, err := app.DB.Exec(query, book.Title, "", book.Details, book.AuthorID, book.IsBorrowed) - if err != nil { - HandleError(w, app.Logger, "Failed to insert book", err, http.StatusInternalServerError) - return - } + query := `INSERT INTO books (title, photo, details, author_id, is_borrowed) VALUES (?, ?, ?, ?, ?)` + result, err := app.DB.Exec(query, book.Title, "", book.Details, book.AuthorID, book.IsBorrowed) + if err != nil { + HandleError(w, app.Logger, "Failed to insert book", err, http.StatusInternalServerError) + return + } - id, err := result.LastInsertId() - if err != nil || id == 0 { - HandleError(w, app.Logger, "Failed to get last insert ID", err, http.StatusInternalServerError) - return - } + id, err := result.LastInsertId() + if err != nil || id == 0 { + HandleError(w, app.Logger, "Failed to get last insert ID", err, http.StatusInternalServerError) + return + } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - response := map[string]int{"id": int(id)} - if err := json.NewEncoder(w).Encode(response); err != nil { - app.Logger.Printf("JSON encoding error: %v", err) - http.Error(w, "Error encoding response", http.StatusInternalServerError) - } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + response := map[string]int{"id": int(id)} + if err := json.NewEncoder(w).Encode(response); err != nil { + app.Logger.Printf("JSON encoding error: %v", err) + http.Error(w, "Error encoding response", http.StatusInternalServerError) + } } - // AddSubscriber adds a new subscriber to the database func (app *App) AddSubscriber(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -886,6 +885,7 @@ func (app *App) AddSubscriber(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]int{"id": int(id)}) } + // BorrowBook handles borrowing a book by a subscriber func (app *App) BorrowBook(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -985,6 +985,7 @@ func (app *App) ReturnBorrowedBook(w http.ResponseWriter, r *http.Request) { return } + _, err = app.DB.Exec("UPDATE books SET is_borrowed = FALSE WHERE id = ?", requestBody.BookID) if err != nil { app.Logger.Printf("Database error: %v", err) @@ -999,51 +1000,52 @@ func (app *App) ReturnBorrowedBook(w http.ResponseWriter, r *http.Request) { // UpdateAuthor updates an existing author in the database func (app *App) UpdateAuthor(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPut && r.Method != http.MethodPost { - http.Error(w, "Only PUT or POST methods are supported", http.StatusMethodNotAllowed) - return - } + if r.Method != http.MethodPut && r.Method != http.MethodPost { + HandleError(w, app.Logger, "Only PUT or POST methods are supported", nil, http.StatusMethodNotAllowed) + return + } - vars := mux.Vars(r) - authorID, err := strconv.Atoi(vars["id"]) - if err != nil { - http.Error(w, "Invalid author ID", http.StatusBadRequest) - return - } + authorID, err := GetIDFromRequest(r, "id") + if err != nil { + HandleError(w, app.Logger, "Invalid author ID", err, http.StatusBadRequest) + return + } - var author Author - err = json.NewDecoder(r.Body).Decode(&author) - if err != nil { - http.Error(w, "Invalid JSON data", http.StatusBadRequest) - return - } - defer r.Body.Close() + var author Author + if err := json.NewDecoder(r.Body).Decode(&author); err != nil { + HandleError(w, app.Logger, "Invalid JSON data", err, http.StatusBadRequest) + return + } + defer r.Body.Close() - if author.Firstname == "" || author.Lastname == "" { - http.Error(w, "Firstname and Lastname are required fields", http.StatusBadRequest) - return - } + if author.Firstname == "" || author.Lastname == "" { + HandleError(w, app.Logger, "Firstname and Lastname are required fields", nil, http.StatusBadRequest) + return + } - query := ` - UPDATE authors - SET lastname = ?, firstname = ?, photo = ? - WHERE id = ? - ` + query := ` + UPDATE authors + SET lastname = ?, firstname = ?, photo = ? + WHERE id = ? + ` - result, err := app.DB.Exec(query, author.Lastname, author.Firstname, author.Photo, authorID) - if err != nil { - app.Logger.Printf("Failed to update author: %v", err) - http.Error(w, fmt.Sprintf("Failed to update author: %v", err), http.StatusInternalServerError) - return - } + result, err := app.DB.Exec(query, author.Lastname, author.Firstname, author.Photo, authorID) + if err != nil { + HandleError(w, app.Logger, "Failed to update author", err, http.StatusInternalServerError) + return + } - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { - http.Error(w, "Author not found", http.StatusNotFound) - return - } + rowsAffected, err := result.RowsAffected() + if err != nil { + HandleError(w, app.Logger, "Failed to retrieve affected rows", err, http.StatusInternalServerError) + return + } + if rowsAffected == 0 { + HandleError(w, app.Logger, "Author not found", nil, http.StatusNotFound) + return + } - fmt.Fprintf(w, "Author updated successfully") + RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Author updated successfully"}) } // UpdateBook handles the updating of an existing book in the database