package handlers import ( "bookhoard/internal/database" "bookhoard/internal/services" "context" "net/http" "os" "path/filepath" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v5" ) type LibraryHandler struct { db *database.Queries libraryService *services.LibraryService } func NewLibraryHandler(db *database.Queries) *LibraryHandler { return &LibraryHandler{ db: db, libraryService: services.NewLibraryService(db), } } // Request/Response types type CreateLibraryRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` Type string `json:"type" validate:"required"` } type UpdateLibraryRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` } type AddLibraryFolderRequest struct { FolderPath string `json:"folder_path" validate:"required"` } type SetLibraryVisibilityRequest struct { LibraryID string `json:"library_id" validate:"required"` IsVisible bool `json:"is_visible"` } // GetLibraryTypes retrieves all available library types func (h *LibraryHandler) GetLibraryTypes(c *echo.Context) error { types, err := h.libraryService.GetLibraryTypes(c.Request().Context()) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, types) } // CreateLibrary creates a new library func (h *LibraryHandler) CreateLibrary(c *echo.Context) error { user := c.Get("user").(database.Users) userUUID := user.ID.Bytes var req CreateLibraryRequest 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()}) } library, err := h.libraryService.CreateLibrary( c.Request().Context(), req.Name, req.Description, req.Type, pgtype.UUID{Bytes: userUUID, Valid: true}, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusCreated, library) } // GetLibrary retrieves a specific library func (h *LibraryHandler) GetLibrary(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } library, err := h.libraryService.GetLibrary(c.Request().Context(), libraryID) if err != nil { return c.JSON(http.StatusNotFound, map[string]string{"error": "library not found"}) } return c.JSON(http.StatusOK, library) } // ListLibraries retrieves all libraries (admin only) func (h *LibraryHandler) ListLibraries(c *echo.Context) error { libraries, err := h.libraryService.ListLibraries(c.Request().Context()) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{"data": libraries}) } // GetUserVisibleLibraries retrieves libraries visible to the current user func (h *LibraryHandler) GetUserVisibleLibraries(c *echo.Context) error { user := MustGetAuthenticatedUser(c) libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), user.ID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, libraries) } // UpdateLibrary updates an existing library func (h *LibraryHandler) UpdateLibrary(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } var req UpdateLibraryRequest 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()}) } library, err := h.libraryService.UpdateLibrary( c.Request().Context(), libraryID, req.Name, req.Description, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, library) } // DeleteLibrary deletes a library func (h *LibraryHandler) DeleteLibrary(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } err = h.libraryService.DeleteLibrary(c.Request().Context(), libraryID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.NoContent(http.StatusNoContent) } // AddLibraryFolder adds a folder to a library func (h *LibraryHandler) AddLibraryFolder(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } var req AddLibraryFolderRequest 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()}) } // Path traversal protection - detect and block .. in path if strings.Contains(req.FolderPath, "..") { return c.JSON(http.StatusBadRequest, map[string]string{"error": "path traversal not allowed"}) } // Clean the path to remove any redundant separators or . references cleanPath := filepath.Clean(req.FolderPath) // Validate that folder path exists and is accessible fileInfo, err := os.Stat(cleanPath) if err != nil { if os.IsNotExist(err) { return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder path does not exist"}) } return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder is not accessible"}) } // Ensure it's actually a directory, not a file if !fileInfo.IsDir() { return c.JSON(http.StatusBadRequest, map[string]string{"error": "path must be a directory"}) } folder, err := h.libraryService.AddLibraryFolder( c.Request().Context(), libraryID, cleanPath, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusCreated, folder) } // GetLibraryFolders retrieves all folders for a library func (h *LibraryHandler) GetLibraryFolders(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } folders, err := h.libraryService.GetLibraryFolders(c.Request().Context(), libraryID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, folders) } // DeleteLibraryFolder removes a folder from a library func (h *LibraryHandler) DeleteLibraryFolder(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } var req AddLibraryFolderRequest // reuse same structure for folder_path 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()}) } err = h.libraryService.DeleteLibraryFolder(c.Request().Context(), libraryID, req.FolderPath) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.NoContent(http.StatusNoContent) } // BrowseDirectories returns directory listings for folder browser UI func (h *LibraryHandler) BrowseDirectories(c *echo.Context) error { path := c.QueryParam("path") if path == "" { path = "/" // Start from root } dirs, currentPath, parentPath, err := h.libraryService.BrowseDirectories(c.Request().Context(), path) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "current_path": currentPath, "parent_path": parentPath, "directories": dirs, }) } // SetLibraryVisibility sets library visibility for a user func (h *LibraryHandler) SetLibraryVisibility(c *echo.Context) error { user := c.Get("user").(database.Users) var req SetLibraryVisibilityRequest 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()}) } libraryID, err := parseUUID(req.LibraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } visibility, err := h.libraryService.SetLibraryVisibility( c.Request().Context(), user.ID, libraryID, req.IsVisible, ) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, visibility) } // GetLibraryStats retrieves statistics for a library func (h *LibraryHandler) GetLibraryStats(c *echo.Context) error { libraryID, err := parseUUID(c.Param("id")) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } stats, err := h.libraryService.GetLibraryStats(c.Request().Context(), libraryID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, stats) } // Helper function func parseUUID(uuidStr string) (pgtype.UUID, error) { parsedUUID, err := uuid.Parse(uuidStr) if err != nil { return pgtype.UUID{}, err } return pgtype.UUID{Bytes: [16]byte(parsedUUID), Valid: true}, nil } // GetUserVisibleLibrariesData returns libraries for SSR (not JSON response) func (h *LibraryHandler) GetUserVisibleLibrariesData(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) { return h.libraryService.GetUserVisibleLibraries(ctx, userID) } // GetLibraryTypeData returns all library types for SSR (not JSON response) func (h *LibraryHandler) GetLibraryTypeData(ctx context.Context) ([]database.LibraryTypes, error) { return h.libraryService.GetLibraryTypes(ctx) } // ListLibrariesData returns all libraries for SSR (admin only, not JSON response) func (h *LibraryHandler) ListLibrariesData(ctx context.Context) ([]database.ListLibrariesRow, error) { return h.libraryService.ListLibraries(ctx) }