- helpers.go: Promote getText() from a local closure in frontend.go
to a package-level function so it can be used by resolveLibrary.
Add resolveLibrary(c, cfg, user.ID) helper that:
1. Reads library_id query param (explicit navigation wins)
2. Falls back to selectedLibrary cookie — validates __all__
sentinel or real UUID, rejects garbage values silently
3. Falls back to user's first visible library
Returns LibraryResolution struct with LibraryID, IsAll, LibUUID,
Libraries, and FirstID — eliminating repeated boilerplate across
all SSR routes.
- frontend.go: Replace manual library resolution boilerplate in 5
SSR route handlers (series, tags/detail, bookshelf, dashboard,
collections/:id) with resolveLibrary(). Each route now gets cookie-
aware library selection for free. Collection detail correctly
handles All Libraries mode for both system and user collections.
Dashboard no longer makes a redundant second GetUserVisibleLibraries
call.
165 lines
4.0 KiB
Go
165 lines
4.0 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/url"
|
|
|
|
"bookhoard/internal/database"
|
|
"bookhoard/templates"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, error) {
|
|
userID := c.Get("user_id").(string)
|
|
userEmail := c.Get("user_email").(string)
|
|
userUsername := c.Get("user_username").(string)
|
|
userRole := c.Get("user_role").(string)
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
log.Printf("getTemplateUserWithTheme failed: invalid UUID '%s': %v", userID, err)
|
|
return templates.User{}, err
|
|
}
|
|
|
|
userDB, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID))
|
|
if err != nil {
|
|
log.Printf("getTemplateUserWithTheme failed: database query error for user ID %s: %v", userID, err)
|
|
return templates.User{}, err
|
|
}
|
|
|
|
userTheme := "tokyo-night"
|
|
if userDB.Theme.Valid {
|
|
userTheme = userDB.Theme.String
|
|
}
|
|
|
|
userTimezone := "UTC"
|
|
if userDB.Timezone.Valid {
|
|
userTimezone = userDB.Timezone.String
|
|
}
|
|
|
|
// Extract JWT token for WebSocket authentication
|
|
token := ""
|
|
if cookie, err := c.Cookie("token"); err == nil {
|
|
token = cookie.Value
|
|
}
|
|
|
|
return templates.User{
|
|
ID: userID,
|
|
Email: userEmail,
|
|
Username: userUsername,
|
|
Role: userRole,
|
|
Theme: userTheme,
|
|
Token: token,
|
|
Timezone: userTimezone,
|
|
}, nil
|
|
}
|
|
|
|
func convertPending(pending []map[string]interface{}) []templates.PendingRegistrationData {
|
|
result := make([]templates.PendingRegistrationData, len(pending))
|
|
for i, p := range pending {
|
|
result[i] = templates.PendingRegistrationData{
|
|
RegistrationID: p["registration_id"].(string),
|
|
DeviceName: p["device_name"].(string),
|
|
DeviceType: p["device_type"].(string),
|
|
ExpiresAt: p["expires_at"].(string),
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func pingDB(cfg *Config, ctx context.Context) error {
|
|
if cfg.DBPool != nil {
|
|
if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok {
|
|
return pool.Ping(ctx)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseUUID(s string) (uuid.UUID, error) {
|
|
return uuid.Parse(s)
|
|
}
|
|
|
|
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
|
return pgtype.UUID{Bytes: u, Valid: true}
|
|
}
|
|
|
|
const selectedLibraryCookie = "selectedLibrary"
|
|
const allLibrariesSentinel = "__all__"
|
|
|
|
type LibraryResolution struct {
|
|
LibraryID string
|
|
IsAll bool
|
|
LibUUID pgtype.UUID
|
|
Libraries []templates.LibraryData
|
|
FirstID string
|
|
}
|
|
|
|
func getText(t pgtype.Text) string {
|
|
if t.Valid {
|
|
return t.String
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolution {
|
|
res := LibraryResolution{}
|
|
|
|
userU, _ := uuid.Parse(userUUID)
|
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userU))
|
|
if err != nil {
|
|
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
|
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
}
|
|
|
|
res.Libraries = make([]templates.LibraryData, len(libraries))
|
|
for i, lib := range libraries {
|
|
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
res.Libraries[i] = templates.LibraryData{
|
|
ID: libUUID.String(),
|
|
Name: lib.Name,
|
|
Description: getText(lib.Description),
|
|
TypeName: lib.TypeName,
|
|
}
|
|
}
|
|
|
|
if len(libraries) > 0 {
|
|
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
|
res.FirstID = libUUID.String()
|
|
}
|
|
|
|
libraryID := c.QueryParam("library_id")
|
|
if libraryID == "" {
|
|
if cookie, err := c.Cookie(selectedLibraryCookie); err == nil {
|
|
val, _ := url.QueryUnescape(cookie.Value)
|
|
if val == allLibrariesSentinel {
|
|
res.IsAll = true
|
|
res.LibraryID = ""
|
|
return res
|
|
}
|
|
if _, parseErr := uuid.Parse(val); parseErr == nil {
|
|
libraryID = val
|
|
}
|
|
}
|
|
}
|
|
|
|
if libraryID == "" {
|
|
res.LibraryID = res.FirstID
|
|
if res.LibraryID != "" {
|
|
parsed, _ := uuid.Parse(res.LibraryID)
|
|
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
|
|
}
|
|
return res
|
|
}
|
|
|
|
res.LibraryID = libraryID
|
|
parsed, _ := uuid.Parse(libraryID)
|
|
res.LibUUID = pgtype.UUID{Bytes: parsed, Valid: true}
|
|
return res
|
|
}
|