Implement backend reader infrastructure with SSR route and library access control
- Fix function signatures in reader.go (c echo.Context -> c *echo.Context) - Replace non-existent UserHasLibraryAccess with GetUserVisibleLibraries pattern - Implement SSR reader route in router/reader.go with proper access control - Add inline library access checking following existing codebase patterns - Fix ReadingProgress struct to use LastReadAt instead of CreatedAt/UpdatedAt - Ensure all reader endpoints use consistent library access validation This provides the backend foundation for the reader feature with proper access control and SSR rendering capabilities.
This commit is contained in:
+112
-8
@@ -1,11 +1,16 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"bookhoard/internal/services"
|
||||
"bookhoard/templates"
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
@@ -16,7 +21,7 @@ func registerReaderRoutes(cfg *Config) {
|
||||
readerService := services.NewReaderService(cfg.Queries, cfg.Worker)
|
||||
cfg.ReaderHandler = handlers.NewReaderHandler(
|
||||
cfg.Queries,
|
||||
cfg.LibraryService,
|
||||
services.NewLibraryService(cfg.Queries),
|
||||
readerService,
|
||||
cfg.Worker,
|
||||
)
|
||||
@@ -28,18 +33,117 @@ func registerReaderRoutes(cfg *Config) {
|
||||
|
||||
// Reader page (SSR)
|
||||
reader.GET("/:mediaItemId", func(c *echo.Context) error {
|
||||
// Get user
|
||||
user, err := getTemplateUserWithTheme(c, cfg)
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||
}
|
||||
|
||||
// Call the handler to get data
|
||||
// Then render template
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Reader(user, metadata, progress, bookmarks).Render(c.Request().Context(), &buf)
|
||||
// Parse mediaItemID
|
||||
mediaItemID := c.Param("mediaItemId")
|
||||
parsedUUID, err := uuid.Parse(mediaItemID)
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error loading reader", "render_error")
|
||||
return renderErrorPage(c, "Invalid media item ID", "invalid_id")
|
||||
}
|
||||
// Fetch media item (directly, no handler)
|
||||
mediaItem, err := cfg.Queries.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Media item not found", "not_found")
|
||||
}
|
||||
// Check library access (using pattern from opds.go)
|
||||
userUUID, _ := uuid.Parse(user.ID)
|
||||
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Failed to check access", "access_error")
|
||||
}
|
||||
|
||||
visible := false
|
||||
for _, lib := range libraries {
|
||||
if lib.ID == mediaItem.LibraryID {
|
||||
visible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !visible {
|
||||
return renderErrorPage(c, "Access denied", "access_denied")
|
||||
}
|
||||
// Get reading progress
|
||||
var progress database.ReadingProgress
|
||||
progress, err = cfg.Queries.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
|
||||
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
||||
UserID: uuidToPGType(userUUID),
|
||||
})
|
||||
if err != nil && err != pgx.ErrNoRows {
|
||||
progress = database.ReadingProgress{}
|
||||
}
|
||||
// Get bookmarks
|
||||
bookmarks, _ := cfg.Queries.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{
|
||||
MediaItemID: pgtype.UUID{Bytes: parsedUUID, Valid: true},
|
||||
UserID: uuidToPGType(userUUID),
|
||||
})
|
||||
// Convert to template types
|
||||
mediaUUID, _ := uuid.FromBytes(mediaItem.ID.Bytes[0:16])
|
||||
metadata := templates.ReaderMetadata{
|
||||
MediaItemID: mediaUUID.String(),
|
||||
Title: mediaItem.Title,
|
||||
Author: textToString(mediaItem.Author),
|
||||
CoverImagePath: textToString(mediaItem.CoverImagePath),
|
||||
LibraryType: mediaItem.FormatGroup,
|
||||
MimeType: textToString(mediaItem.MimeType),
|
||||
FilePath: mediaItem.FilePath,
|
||||
TotalPages: int(mediaItem.PageCount.Int32),
|
||||
ChapterCount: int(mediaItem.ChapterCount.Int32),
|
||||
}
|
||||
// Progress conversion (inline)
|
||||
progressUUID, _ := uuid.FromBytes(progress.ID.Bytes[0:16])
|
||||
progressMediaUUID, _ := uuid.FromBytes(progress.MediaItemID.Bytes[0:16])
|
||||
progressUserUUID, _ := uuid.FromBytes(progress.UserID.Bytes[0:16])
|
||||
templateProgress := templates.ReadingProgress{
|
||||
ID: progressUUID.String(),
|
||||
MediaItemID: progressMediaUUID.String(),
|
||||
UserID: progressUserUUID.String(),
|
||||
CurrentPage: int(progress.CurrentPage.Int32),
|
||||
TotalPages: int(progress.TotalPages.Int32),
|
||||
Percentage: progress.Percentage.Float64,
|
||||
EpubCfi: textToString(progress.Epubcfi),
|
||||
LastReadAt: progress.LastReadAt.Time,
|
||||
}
|
||||
// Bookmarks conversion (inline, with loop)
|
||||
templateBookmarks := make([]templates.Bookmark, len(bookmarks))
|
||||
for i, b := range bookmarks {
|
||||
bookmarkUUID, _ := uuid.FromBytes(b.ID.Bytes[0:16])
|
||||
bookmarkMediaUUID, _ := uuid.FromBytes(b.MediaItemID.Bytes[0:16])
|
||||
bookmarkUserUUID, _ := uuid.FromBytes(b.UserID.Bytes[0:16])
|
||||
|
||||
var pageNumber *int
|
||||
if b.PageNumber.Valid {
|
||||
pn := int(b.PageNumber.Int32)
|
||||
pageNumber = &pn
|
||||
}
|
||||
|
||||
var chapterNumber *int
|
||||
if b.ChapterNumber.Valid {
|
||||
cn := int(b.ChapterNumber.Int32)
|
||||
chapterNumber = &cn
|
||||
}
|
||||
|
||||
templateBookmarks[i] = templates.Bookmark{
|
||||
ID: bookmarkUUID.String(),
|
||||
MediaItemID: bookmarkMediaUUID.String(),
|
||||
UserID: bookmarkUserUUID.String(),
|
||||
PageNumber: pageNumber,
|
||||
ChapterNumber: chapterNumber,
|
||||
CfiPosition: textToString(b.CfiPosition),
|
||||
Title: b.Title,
|
||||
Position: textToString(b.Position),
|
||||
Notes: textToString(b.Notes),
|
||||
CreatedAt: b.CreatedAt.Time,
|
||||
}
|
||||
}
|
||||
// 8. Render template
|
||||
var buf bytes.Buffer
|
||||
err = templates.Reader(user, metadata, templateProgress, templateBookmarks).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error rendering reader", "render_error")
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user