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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -44,6 +45,30 @@ func (c *Config) DatabaseURL() string {
|
|||||||
c.DatabaseUser, c.DatabasePassword, c.DatabaseHost, c.DatabasePort, c.DatabaseName)
|
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 {
|
func getEnv(key, defaultValue string) string {
|
||||||
if value := os.Getenv(key); value != "" {
|
if value := os.Getenv(key); value != "" {
|
||||||
return 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) {
|
func (h *OPDSHandler) getBaseURLs(c *echo.Context) (string, string, error) {
|
||||||
baseURL, err := h.db.GetSystemConfig(c.Request().Context(), "base_url")
|
baseURL, err := h.db.GetSystemConfig(c.Request().Context(), "base_url")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("failed to get base_url from config: %w", err)
|
return "", "", fmt.Errorf("failed to get base_url from config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opdsBaseURL, err := h.db.GetSystemConfig(c.Request().Context(), "opds_base_url")
|
opdsBaseURL := baseURL.Value + "/opds"
|
||||||
if err != nil {
|
return baseURL.Value, opdsBaseURL, nil
|
||||||
return "", "", fmt.Errorf("failed to get opds_base_url from config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseURL.Value, opdsBaseURL.Value, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeviceCatalog returns the OPDS catalog feed for a device
|
// GetDeviceCatalog returns the OPDS catalog feed for a device
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bookhoard/internal/config"
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -13,11 +14,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SidecarHandler struct {
|
type SidecarHandler struct {
|
||||||
db *database.Queries
|
db *database.Queries
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSidecarHandler(db *database.Queries) *SidecarHandler {
|
func NewSidecarHandler(db *database.Queries, cfg *config.Config) *SidecarHandler {
|
||||||
return &SidecarHandler{db: db}
|
return &SidecarHandler{db: db, cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidecarConfig struct {
|
type SidecarConfig struct {
|
||||||
@@ -79,13 +81,15 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
|
|||||||
userID := device.UserID.Bytes
|
userID := device.UserID.Bytes
|
||||||
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
||||||
|
|
||||||
// Get system config
|
// Get base URL and compute paths
|
||||||
opdsBaseURL, _ := h.db.GetSystemConfig(ctx, "opds_base_url")
|
baseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||||
apiBaseURL, _ := h.db.GetSystemConfig(ctx, "api_base_url")
|
if baseURL.Value == "" {
|
||||||
|
baseURL.Value = h.cfg.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
// Generate URLs
|
// Generate URLs
|
||||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL.Value, deviceID.String())
|
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL.Value, deviceID.String())
|
||||||
syncAPIURL := fmt.Sprintf("%s/sync/kobo", apiBaseURL.Value)
|
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL.Value)
|
||||||
|
|
||||||
// Get user's visible libraries with media items
|
// Get user's visible libraries with media items
|
||||||
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
||||||
@@ -172,8 +176,8 @@ func (h *SidecarHandler) GetSidecarConfig(c *echo.Context) error {
|
|||||||
Bookhoard: SidecarBookhoardConfig{
|
Bookhoard: SidecarBookhoardConfig{
|
||||||
OPDSCatalog: opdsCatalogURL,
|
OPDSCatalog: opdsCatalogURL,
|
||||||
SyncAPI: syncAPIURL,
|
SyncAPI: syncAPIURL,
|
||||||
OPDSBaseURL: opdsBaseURL.Value,
|
OPDSBaseURL: baseURL.Value + "/opds",
|
||||||
APIBaseURL: apiBaseURL.Value,
|
APIBaseURL: baseURL.Value + "/api",
|
||||||
DeviceID: deviceID.String(),
|
DeviceID: deviceID.String(),
|
||||||
DeviceToken: device.AuthToken,
|
DeviceToken: device.AuthToken,
|
||||||
},
|
},
|
||||||
@@ -215,13 +219,15 @@ func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
|
|||||||
userID := device.UserID.Bytes
|
userID := device.UserID.Bytes
|
||||||
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
|
||||||
|
|
||||||
// Get system config
|
// Get base URL and compute paths
|
||||||
opdsBaseURL, _ := h.db.GetSystemConfig(ctx, "opds_base_url")
|
baseURL, _ := h.db.GetSystemConfig(ctx, "base_url")
|
||||||
apiBaseURL, _ := h.db.GetSystemConfig(ctx, "api_base_url")
|
if baseURL.Value == "" {
|
||||||
|
baseURL.Value = h.cfg.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
// Generate URLs
|
// Generate URLs
|
||||||
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL.Value, deviceID.String())
|
opdsCatalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", baseURL.Value, deviceID.String())
|
||||||
syncAPIURL := fmt.Sprintf("%s/sync/kobo", apiBaseURL.Value)
|
syncAPIURL := fmt.Sprintf("%s/api/sync/kobo", baseURL.Value)
|
||||||
|
|
||||||
// Get user's visible libraries with media items
|
// Get user's visible libraries with media items
|
||||||
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
mediaItems, err := h.db.GetUserMediaItemsForSync(ctx, pgUserID)
|
||||||
@@ -303,8 +309,8 @@ func (h *SidecarHandler) DownloadSidecarConfig(c *echo.Context) error {
|
|||||||
Bookhoard: SidecarBookhoardConfig{
|
Bookhoard: SidecarBookhoardConfig{
|
||||||
OPDSCatalog: opdsCatalogURL,
|
OPDSCatalog: opdsCatalogURL,
|
||||||
SyncAPI: syncAPIURL,
|
SyncAPI: syncAPIURL,
|
||||||
OPDSBaseURL: opdsBaseURL.Value,
|
OPDSBaseURL: baseURL.Value + "/opds",
|
||||||
APIBaseURL: apiBaseURL.Value,
|
APIBaseURL: baseURL.Value + "/api",
|
||||||
DeviceID: deviceID.String(),
|
DeviceID: deviceID.String(),
|
||||||
DeviceToken: device.AuthToken,
|
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{
|
return c.JSON(http.StatusOK, map[string]string{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "System configuration updated",
|
"message": "System configuration updated",
|
||||||
|
|||||||
Reference in New Issue
Block a user