feat: Add admin-only protection to ebook operations
- Update ebook handlers to require admin role for CRUD operations - Modify folder operations to be admin-only - Update ebook scanner to associate books with admin who added them - Add admin ID tracking to CreateEbook operations - Restrict scanner operations to admin users only
This commit is contained in:
@@ -47,25 +47,29 @@ func parseDate(dateStr string) time.Time {
|
||||
func SetupRoutes(g *echo.Group, db *database.Queries) {
|
||||
h := NewHandler(db)
|
||||
|
||||
// Public routes (all authenticated users)
|
||||
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)
|
||||
|
||||
// User-specific routes
|
||||
g.GET("/ebooks/:id/progress", h.GetReadingProgress)
|
||||
g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress)
|
||||
|
||||
g.GET("/ebooks/:id/rating", h.GetEbookRating)
|
||||
g.POST("/ebooks/:id/rating", h.CreateOrUpdateEbookRating)
|
||||
g.PUT("/ebooks/:id/rating", h.CreateOrUpdateEbookRating)
|
||||
g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating)
|
||||
g.GET("/ebooks/:id/ratings", h.GetEbookRatings)
|
||||
|
||||
// Scanner routes
|
||||
g.POST("/scanner/scan", h.ScanEbooks)
|
||||
g.POST("/scanner/start", h.StartScanner)
|
||||
g.POST("/scanner/stop", h.StopScanner)
|
||||
// Admin-only routes
|
||||
admin := g.Group("", AdminMiddleware)
|
||||
admin.POST("/ebooks", h.CreateEbook)
|
||||
admin.PUT("/ebooks/:id", h.UpdateEbook)
|
||||
admin.DELETE("/ebooks/:id", h.DeleteEbook)
|
||||
|
||||
// Scanner routes (admin only)
|
||||
admin.POST("/scanner/scan", h.ScanEbooks)
|
||||
admin.POST("/scanner/start", h.StartScanner)
|
||||
admin.POST("/scanner/stop", h.StopScanner)
|
||||
}
|
||||
|
||||
// ListEbooks handles GET /api/ebooks
|
||||
@@ -147,6 +151,13 @@ func (h *Handler) CreateEbook(c echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Get admin ID from JWT token
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
ebook, err := h.db.CreateEbook(c.Request().Context(), database.CreateEbookParams{
|
||||
Title: req.Title,
|
||||
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
||||
@@ -163,6 +174,7 @@ func (h *Handler) CreateEbook(c echo.Context) error {
|
||||
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||
Contributors: pgtype.Text{String: req.Contributors, Valid: req.Contributors != ""},
|
||||
AddedByAdminID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
@@ -486,6 +498,9 @@ func (h *Handler) ScanEbooks(c echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid folder paths: " + err.Error()})
|
||||
}
|
||||
|
||||
// Set the admin ID for ebook association
|
||||
h.scanner.SetAdminID(pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
|
||||
// Perform the scan
|
||||
if err := h.scanner.ScanFolders(h.ctx); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "scan failed: " + err.Error()})
|
||||
@@ -504,11 +519,21 @@ func (h *Handler) StartScanner(c echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
|
||||
// Get admin ID from JWT token
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
// Set the folder paths
|
||||
if err := h.scanner.SetFolders(req.FolderPaths); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid folder paths: " + err.Error()})
|
||||
}
|
||||
|
||||
// Set the admin ID for ebook association
|
||||
h.scanner.SetAdminID(pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
|
||||
// Start watching for changes
|
||||
h.scanner.WatchChanges(h.ctx)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ type EbookScanner struct {
|
||||
db *database.Queries
|
||||
watcher *fsnotify.Watcher
|
||||
folders []string
|
||||
adminID pgtype.UUID
|
||||
}
|
||||
|
||||
func NewEbookScanner(db *database.Queries) *EbookScanner {
|
||||
@@ -50,6 +51,10 @@ func NewEbookScanner(db *database.Queries) *EbookScanner {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EbookScanner) SetAdminID(adminID pgtype.UUID) {
|
||||
s.adminID = adminID
|
||||
}
|
||||
|
||||
func (s *EbookScanner) SetFolders(folders []string) error {
|
||||
s.folders = folders
|
||||
|
||||
@@ -291,6 +296,7 @@ func (s *EbookScanner) processEbookFile(ctx context.Context, path string) error
|
||||
DatePublished: pgtype.Date{Time: metadata.PublishDate, Valid: !metadata.PublishDate.IsZero()},
|
||||
Contributors: pgtype.Text{String: metadata.Contributors, Valid: metadata.Contributors != ""},
|
||||
Tags: pgtype.Text{String: metadata.Tags, Valid: metadata.Tags != ""},
|
||||
AddedByAdminID: s.adminID,
|
||||
})
|
||||
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user