- Backend: Add server-side validation with go-playground/validator/v10 - Frontend: Add toast notifications for API errors with @zerodevx/svelte-toast - UI: Complete Tokyo Night theme redesign with modern animations - Docs: Update COMPLETE_DOCUMENTATION.md and README.md with all enhancements - Validation: Email format, password strength, and input sanitization - UX: Real-time error feedback, loading states, and responsive design
253 lines
7.9 KiB
Go
253 lines
7.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookmann/internal/database"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *database.Queries
|
|
}
|
|
|
|
func NewHandler(db *database.Queries) *Handler {
|
|
return &Handler{
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
func SetupRoutes(g *echo.Group, db *database.Queries) {
|
|
h := NewHandler(db)
|
|
|
|
g.GET("/ebooks", h.ListEbooks)
|
|
g.GET("/ebooks/:id", h.GetEbook)
|
|
g.POST("/ebooks", h.CreateEbook)
|
|
g.PUT("/ebooks/:id", h.UpdateEbook)
|
|
g.DELETE("/ebooks/:id", h.DeleteEbook)
|
|
|
|
g.GET("/ebooks/:id/progress", h.GetReadingProgress)
|
|
g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress)
|
|
}
|
|
|
|
// ListEbooks handles GET /api/ebooks
|
|
func (h *Handler) ListEbooks(c echo.Context) error {
|
|
limitStr := c.QueryParam("limit")
|
|
offsetStr := c.QueryParam("offset")
|
|
|
|
limit := int32(20) // default
|
|
if limitStr != "" {
|
|
if l, err := strconv.Atoi(limitStr); err == nil {
|
|
limit = int32(l)
|
|
}
|
|
}
|
|
|
|
offset := int32(0)
|
|
if offsetStr != "" {
|
|
if o, err := strconv.Atoi(offsetStr); err == nil {
|
|
offset = int32(o)
|
|
}
|
|
}
|
|
|
|
ebooks, err := h.db.ListEbooks(c.Request().Context(), database.ListEbooksParams{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, ebooks)
|
|
}
|
|
|
|
// GetEbook handles GET /api/ebooks/:id
|
|
func (h *Handler) GetEbook(c echo.Context) error {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
}
|
|
|
|
ebook, err := h.db.GetEbook(c.Request().Context(), pgtype.UUID{Bytes: id, Valid: true})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, ebook)
|
|
}
|
|
|
|
// CreateEbookRequest represents the request for creating an ebook
|
|
type CreateEbookRequest struct {
|
|
Title string `json:"title" validate:"required,min=1,max=500"`
|
|
Author string `json:"author"`
|
|
ISBN string `json:"isbn"`
|
|
Description string `json:"description"`
|
|
FilePath string `json:"file_path" validate:"required"`
|
|
FileSize int64 `json:"file_size" validate:"required,min=1"`
|
|
MimeType string `json:"mime_type" validate:"required"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
}
|
|
|
|
// CreateEbook handles POST /api/ebooks
|
|
func (h *Handler) CreateEbook(c echo.Context) error {
|
|
var req CreateEbookRequest
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
ebook, err := h.db.CreateEbook(c.Request().Context(), database.CreateEbookParams{
|
|
Title: req.Title,
|
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
|
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
|
FilePath: req.FilePath,
|
|
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
|
MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
|
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
|
})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, ebook)
|
|
}
|
|
|
|
// UpdateEbookRequest represents the request for updating an ebook
|
|
type UpdateEbookRequest struct {
|
|
Title string `json:"title" validate:"required,min=1,max=500"`
|
|
Author string `json:"author"`
|
|
ISBN string `json:"isbn"`
|
|
Description string `json:"description"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
}
|
|
|
|
// UpdateEbook handles PUT /api/ebooks/:id
|
|
func (h *Handler) UpdateEbook(c echo.Context) error {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
}
|
|
|
|
var req UpdateEbookRequest
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
ebook, err := h.db.UpdateEbook(c.Request().Context(), database.UpdateEbookParams{
|
|
ID: pgtype.UUID{Bytes: id, Valid: true},
|
|
Title: req.Title,
|
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
|
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
|
})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, ebook)
|
|
}
|
|
|
|
// DeleteEbook handles DELETE /api/ebooks/:id
|
|
func (h *Handler) DeleteEbook(c echo.Context) error {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
}
|
|
|
|
err = h.db.DeleteEbook(c.Request().Context(), pgtype.UUID{Bytes: id, Valid: true})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
// GetReadingProgress handles GET /api/ebooks/:id/progress
|
|
func (h *Handler) GetReadingProgress(c echo.Context) error {
|
|
ebookIdStr := c.Param("id")
|
|
userID := c.Get("user_id").(string)
|
|
|
|
ebookId, err := uuid.Parse(ebookIdStr)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
|
}
|
|
|
|
progress, err := h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
|
|
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
})
|
|
if err != nil {
|
|
// If no progress found, return default
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"ebook_id": ebookIdStr,
|
|
"user_id": userID,
|
|
"current_page": 0,
|
|
"total_pages": nil,
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, progress)
|
|
}
|
|
|
|
// UpdateReadingProgressRequest represents the request for updating reading progress
|
|
type UpdateReadingProgressRequest struct {
|
|
CurrentPage int32 `json:"current_page" validate:"required,min=0"`
|
|
TotalPages int32 `json:"total_pages" validate:"omitempty,min=1"`
|
|
}
|
|
|
|
// UpdateReadingProgress handles PUT /api/ebooks/:id/progress
|
|
func (h *Handler) UpdateReadingProgress(c echo.Context) error {
|
|
ebookIdStr := c.Param("id")
|
|
userID := c.Get("user_id").(string)
|
|
|
|
ebookId, err := uuid.Parse(ebookIdStr)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
|
}
|
|
|
|
var req UpdateReadingProgressRequest
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
progress, err := h.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{
|
|
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true},
|
|
TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0},
|
|
})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, progress)
|
|
}
|