config: simplify to single base_url with computed paths
Refactor configuration to use one source of truth for base URL - Add GetBaseURL() to config package: queries system_config table first, falls back to BASE_URL env var - Update SidecarHandler to accept config and use single base_url - Compute opds/api paths from base_url instead of storing separately: - OPDS: base_url + /opds - API: base_url + /api - Device Sync: base_url + /api/sync - Simplify OPDSHandler.getBaseURLs() to compute opds path - Remove need for separate opds_base_url and api_base_url columns Previously the system stored three separate URL config values that were usually the same domain with different paths. Now store only base_url and compute the paths, eliminating configuration redundancy.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -44,6 +45,30 @@ func (c *Config) DatabaseURL() string {
|
||||
c.DatabaseUser, c.DatabasePassword, c.DatabaseHost, c.DatabasePort, c.DatabaseName)
|
||||
}
|
||||
|
||||
// GetBaseURL returns the base URL from system configuration database with fallback to config/env var
|
||||
func GetBaseURL(ctx context.Context, db interface{}) string {
|
||||
// Try to get from database first
|
||||
type SystemConfigQuerier interface {
|
||||
GetSystemConfig(ctx context.Context, key string) (SystemConfigRow, error)
|
||||
}
|
||||
|
||||
if querier, ok := db.(SystemConfigQuerier); ok {
|
||||
config, err := querier.GetSystemConfig(ctx, "base_url")
|
||||
if err == nil && config.Value != "" {
|
||||
return config.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return empty string - caller should use their own fallback
|
||||
return ""
|
||||
}
|
||||
|
||||
// SystemConfigRow represents a system configuration row
|
||||
type SystemConfigRow struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
|
||||
@@ -38,19 +38,15 @@ func NewOPDSHandler(db *database.Queries, libraryService *services.LibraryServic
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get base URLs from system config
|
||||
// Helper function to get base URL from system config
|
||||
func (h *OPDSHandler) getBaseURLs(c *echo.Context) (string, string, error) {
|
||||
baseURL, err := h.db.GetSystemConfig(c.Request().Context(), "base_url")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get base_url from config: %w", err)
|
||||
}
|
||||
|
||||
opdsBaseURL, err := h.db.GetSystemConfig(c.Request().Context(), "opds_base_url")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get opds_base_url from config: %w", err)
|
||||
}
|
||||
|
||||
return baseURL.Value, opdsBaseURL.Value, nil
|
||||
opdsBaseURL := baseURL.Value + "/opds"
|
||||
return baseURL.Value, opdsBaseURL, nil
|
||||
}
|
||||
|
||||
// GetDeviceCatalog returns the OPDS catalog feed for a device
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/config"
|
||||
"bookhoard/internal/database"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -13,11 +14,12 @@ import (
|
||||
)
|
||||
|
||||
type SidecarHandler struct {
|
||||
db *database.Queries
|
||||
db *database.Queries
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewSidecarHandler(db *database.Queries) *SidecarHandler {
|
||||
return &SidecarHandler{db: db}
|
||||
func NewSidecarHandler(db *database.Queries, cfg *config.Config) *SidecarHandler {
|
||||
return &SidecarHandler{db: db, cfg: cfg}
|
||||
}
|
||||
|
||||
type SidecarConfig struct {
|
||||
@@ -79,13 +81,15 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
|
||||
userID := device.UserID.Bytes
|
||||
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
||||
|
||||
// Get system config
|
||||
opdsBaseURL, _ := h.db.GetSystemConfig(ctx, "opds_base_url")
|
||||
apiBaseURL, _ := h.db.GetSystemConfig(ctx, "api_base_url")
|
||||
// Get base URL and compute paths
|
||||
baseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||
if baseURL.Value == "" {
|
||||
baseURL.Value = h.cfg.BaseURL
|
||||
}
|
||||
|
||||
// Generate URLs
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL.Value, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/sync/kobo", apiBaseURL.Value)
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL.Value, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL.Value)
|
||||
|
||||
// Get user's visible libraries with media items
|
||||
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
||||
@@ -172,8 +176,8 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
|
||||
Bookhoard: SidecarBookhoardConfig{
|
||||
OPDSCatalog: opdsCatalogURL,
|
||||
SyncAPI: syncAPIURL,
|
||||
OPDSBaseURL: opdsBaseURL.Value,
|
||||
APIBaseURL: apiBaseURL.Value,
|
||||
OPDSBaseURL: baseURL.Value + "/opds",
|
||||
APIBaseURL: baseURL.Value + "/api",
|
||||
DeviceID: deviceID.String(),
|
||||
DeviceToken: device.AuthToken,
|
||||
},
|
||||
@@ -215,13 +219,15 @@ func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
|
||||
userID := device.UserID.Bytes
|
||||
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
||||
|
||||
// Get system config
|
||||
opdsBaseURL, _ := h.db.GetSystemConfig(ctx, "opds_base_url")
|
||||
apiBaseURL, _ := h.db.GetSystemConfig(ctx, "api_base_url")
|
||||
// Get base URL and compute paths
|
||||
baseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||
if baseURL.Value == "" {
|
||||
baseURL.Value = h.cfg.BaseURL
|
||||
}
|
||||
|
||||
// Generate URLs
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL.Value, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/sync/kobo", apiBaseURL.Value)
|
||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL.Value, deviceID.String())
|
||||
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL.Value)
|
||||
|
||||
// Get user's visible libraries with media items
|
||||
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
||||
@@ -303,8 +309,8 @@ func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
|
||||
Bookhoard: SidecarBookhoardConfig{
|
||||
OPDSCatalog: opdsCatalogURL,
|
||||
SyncAPI: syncAPIURL,
|
||||
OPDSBaseURL: opdsBaseURL.Value,
|
||||
APIBaseURL: apiBaseURL.Value,
|
||||
OPDSBaseURL: baseURL.Value + "/opds",
|
||||
APIBaseURL: baseURL.Value + "/api",
|
||||
DeviceID: deviceID.String(),
|
||||
DeviceToken: device.AuthToken,
|
||||
},
|
||||
@@ -394,6 +400,56 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for HTMX request
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
// Fetch updated base_url for template
|
||||
baseURL, err := h.db.GetSystemConfig(ctx, "base_url")
|
||||
if err != nil || baseURL.Value == "" {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to fetch updated configuration</div>`)
|
||||
}
|
||||
|
||||
// Render success message with updated form
|
||||
return c.HTML(http.StatusOK, fmt.Sprintf(`
|
||||
<div class="mb-4 p-4 rounded-lg" style="background-color: var(--bg-secondary); border: 1px solid var(--accent);">
|
||||
<p style="color: var(--text-success);">✅ Settings saved successfully!</p>
|
||||
</div>
|
||||
<form id="settings-form" hx-put="/api/system/config" hx-target="#settings-form" hx-swap="outerHTML">
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-xl font-semibold mb-6" style="color: var(--text-primary)">Base URL</h3>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-primary)">Base URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="base_url"
|
||||
value="%s"
|
||||
placeholder="https://books.example.com"
|
||||
class="w-full px-4 py-2 rounded-lg border"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm mt-1" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com)</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit" class="btn-primary px-6 py-2 rounded-lg font-medium">
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--text-primary)">URL Paths</h3>
|
||||
<div class="space-y-2 text-sm" style="color: var(--text-secondary);">
|
||||
<p><strong>OPDS:</strong> %s/opds</p>
|
||||
<p><strong>API:</strong> %s/api</p>
|
||||
<p><strong>Device Sync:</strong> %s/api/sync</p>
|
||||
</div>
|
||||
</div>
|
||||
`, baseURL.Value, baseURL.Value, baseURL.Value))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"status": "success",
|
||||
"message": "System configuration updated",
|
||||
|
||||
Reference in New Issue
Block a user