Features: - Complete dashboard redesign with improved UI components and layout - Implement custom section builder for personalized book organization - Add new events tracking system for user interactions - Enhance search functionality with better static search.js - Update TypeScript type definitions for API responses Backend: - Update Go dependencies in go.mod - Add new frontend routes in router Templates: - Update admin and dashboard templates with new components Frontend: - Refactor analytics, collections, conflicts, and queue modules - Add new documentation features in docs.ts - Implement linking between books and collections - Add toast notifications for user feedback - Include placeholder book SVG asset This commit consolidates multiple feature additions and improvements across the entire stack including backend, templates, and frontend.
635 lines
19 KiB
Go
635 lines
19 KiB
Go
package router
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/handlers"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/templates"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func renderErrorPage(c echo.Context, message string, errorType string) error {
|
|
var buf bytes.Buffer
|
|
err := templates.ErrorPage(message, errorType).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
log.Printf("renderErrorPage failed to render template: %v", err)
|
|
return c.HTML(http.StatusInternalServerError, "Internal server error")
|
|
}
|
|
return c.HTML(http.StatusInternalServerError, buf.String())
|
|
}
|
|
|
|
func registerFrontendRoutes(cfg *Config) {
|
|
e := cfg.Echo
|
|
|
|
// ============================================================================
|
|
// FRONTEND ROUTES - DO NOT DELETE
|
|
// These routes serve Server-Side Rendered (SSR) HTML pages for the web UI.
|
|
// They are NOT API endpoints and should NOT be removed during refactors.
|
|
// All authenticated frontend routes use the jwtMiddleware to validate tokens.
|
|
// ============================================================================
|
|
|
|
// Use existing JWT middleware (sets database user object in context)
|
|
jwtMiddleware := createJWTMiddleware(cfg)
|
|
|
|
// Protected route group for API routes
|
|
protected := e.Group("/api", jwtMiddleware)
|
|
|
|
// ============================================================================
|
|
// PUBLIC FRONTEND ROUTES (No authentication required)
|
|
// ============================================================================
|
|
|
|
// Public routes for login and registration pages
|
|
e.GET("/login", func(c echo.Context) error {
|
|
var buf bytes.Buffer
|
|
sessionExpired := c.QueryParam("session") == "expired"
|
|
deleted := c.QueryParam("deleted") == "true"
|
|
err := templates.Login(sessionExpired, deleted).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
e.GET("/register", func(c echo.Context) error {
|
|
var buf bytes.Buffer
|
|
err := templates.Register().Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Root route - landing page with smart login detection
|
|
e.GET("/", func(c echo.Context) error {
|
|
var buf bytes.Buffer
|
|
var err error
|
|
|
|
tokenString := c.Request().Header.Get("Authorization")
|
|
if tokenString != "" && len(tokenString) > 7 && tokenString[:7] == "Bearer " {
|
|
tokenString = tokenString[7:]
|
|
} else {
|
|
cookie, err := c.Cookie("token")
|
|
if err == nil {
|
|
tokenString = cookie.Value
|
|
}
|
|
}
|
|
|
|
loggedIn := false
|
|
if tokenString != "" {
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
return []byte(cfg.Cfg.JWTSecret), nil
|
|
})
|
|
loggedIn = err == nil && token.Valid
|
|
}
|
|
|
|
err = templates.Index(loggedIn).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// ============================================================================
|
|
// PROTECTED FRONTEND ROUTES (Authentication required)
|
|
// ============================================================================
|
|
|
|
// Protected frontend routes (no /api prefix)
|
|
frontendProtected := e.Group("", jwtMiddleware, ensureUserExistsMiddleware(cfg))
|
|
|
|
// Helper to extract text from pgtype.Text
|
|
getText := func(t pgtype.Text) string {
|
|
if t.Valid {
|
|
return t.String
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Dashboard page
|
|
frontendProtected.GET("/dashboard", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
|
|
libraryID := c.QueryParam("library_id")
|
|
if libraryID == "" {
|
|
userUUID, _ := uuid.Parse(user.ID)
|
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
if err == nil && len(libraries) > 0 {
|
|
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
|
|
libraryID = libUUID.String()
|
|
}
|
|
}
|
|
|
|
libUUID, _ := uuid.Parse(libraryID)
|
|
userUUID, _ := uuid.Parse(user.ID)
|
|
|
|
prefs, _ := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
|
|
|
|
limit := 20
|
|
if prefs.ItemsPerSection.Int32 > 0 {
|
|
limit = int(prefs.ItemsPerSection.Int32)
|
|
}
|
|
var sections []services.DashboardSection
|
|
sections, err = cfg.DashboardService.GetDashboardSections(
|
|
c.Request().Context(),
|
|
userUUID,
|
|
libUUID,
|
|
limit,
|
|
prefs.CollectionOrder,
|
|
prefs.HiddenCollections,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Dashboard sections query failed: %v", err)
|
|
sections = []services.DashboardSection{}
|
|
errorMsg = "Error loading dashboard"
|
|
}
|
|
|
|
userUUID2, _ := uuid.Parse(user.ID)
|
|
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID2))
|
|
if err != nil {
|
|
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
|
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
if errorMsg == "" {
|
|
errorMsg = "Error loading libraries"
|
|
}
|
|
}
|
|
|
|
libData := make([]templates.LibraryData, len(libraries))
|
|
for i, lib := range libraries {
|
|
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
libData[i] = templates.LibraryData{
|
|
ID: libUUID.String(),
|
|
Name: lib.Name,
|
|
Description: getText(lib.Description),
|
|
TypeName: lib.TypeName,
|
|
}
|
|
}
|
|
|
|
sectionData := handlers.BuildSections(sections)
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Dashboard(user, sectionData, libData, libraryID, errorMsg).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Collections page
|
|
frontendProtected.GET("/collections", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
var collections []database.Collections
|
|
|
|
collections, err = cfg.CollectionHandler.GetCollectionsData(c)
|
|
if err != nil {
|
|
log.Printf("GetCollectionsData failed: %v", err)
|
|
collections = []database.Collections{}
|
|
errorMsg = "Error loading collections"
|
|
}
|
|
|
|
colData := make([]templates.CollectionData, len(collections))
|
|
for i, col := range collections {
|
|
colData[i] = templates.CollectionData{
|
|
ID: uuid.UUID(col.ID.Bytes).String(),
|
|
Name: col.Name,
|
|
Description: getText(col.Description),
|
|
Color: getText(col.Color),
|
|
Icon: getText(col.Icon),
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Collection(user, colData, errorMsg).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Custom Section Builder page
|
|
frontendProtected.GET("/custom-section", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
var libraries []database.GetUserVisibleLibrariesRow
|
|
|
|
userUUID, _ := uuid.Parse(user.ID)
|
|
libraries, err = cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
|
|
if err != nil {
|
|
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
|
libraries = []database.GetUserVisibleLibrariesRow{}
|
|
errorMsg = "Error loading libraries"
|
|
}
|
|
|
|
libData := make([]templates.LibraryData, len(libraries))
|
|
for i, lib := range libraries {
|
|
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
libData[i] = templates.LibraryData{
|
|
ID: libUUID.String(),
|
|
Name: lib.Name,
|
|
Description: getText(lib.Description),
|
|
TypeName: lib.TypeName,
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.CustomSectionBuilder(user, libData, errorMsg).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Progress page
|
|
frontendProtected.GET("/progress", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
var progressData []handlers.ProgressWithMedia
|
|
|
|
progressData, err = cfg.ScannerHandler.GetAllProgressData(c)
|
|
if err != nil {
|
|
log.Printf("GetAllProgressData failed: %v", err)
|
|
progressData = []handlers.ProgressWithMedia{}
|
|
errorMsg = "Error loading progress"
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Progress(user, progressData, errorMsg).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Devices page
|
|
frontendProtected.GET("/devices", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
var devices []handlers.DeviceInfo
|
|
var pendingList []templates.PendingRegistrationData
|
|
|
|
devices, err = cfg.DeviceHandler.GetDevicesData(c)
|
|
if err != nil {
|
|
log.Printf("GetDevicesData failed: %v", err)
|
|
devices = []handlers.DeviceInfo{}
|
|
errorMsg = "Error loading devices"
|
|
}
|
|
|
|
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
|
if err != nil {
|
|
log.Printf("GetPendingRegistrationsData failed: %v", err)
|
|
pendingList = []templates.PendingRegistrationData{}
|
|
if errorMsg == "" {
|
|
errorMsg = "Error loading pending registrations"
|
|
}
|
|
} else {
|
|
pendingList = convertPending(pendingMaps)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Devices(user, devices, pendingList, errorMsg, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Conflicts page
|
|
frontendProtected.GET("/conflicts", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
var conflictsData []handlers.ConflictDetailResponse
|
|
var total, unresolved int
|
|
|
|
conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c)
|
|
if err != nil {
|
|
log.Printf("GetConflictsData failed: %v", err)
|
|
conflictsData = []handlers.ConflictDetailResponse{}
|
|
total = 0
|
|
unresolved = 0
|
|
errorMsg = "Error loading conflicts"
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Analytics page
|
|
frontendProtected.GET("/analytics", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
var buf bytes.Buffer
|
|
err = templates.Analytics(user).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// Profile page (all users)
|
|
frontendProtected.GET("/profile", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Profile(user).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// ============================================================================
|
|
// ADMIN FRONTEND ROUTES
|
|
// ============================================================================
|
|
|
|
// Admin routes
|
|
frontendProtected.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
var buf bytes.Buffer
|
|
err = templates.Admin(user).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}))
|
|
|
|
frontendProtected.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
var buf bytes.Buffer
|
|
err = templates.Admin(user).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}))
|
|
|
|
frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
// Fetch all libraries (admin-only)
|
|
libraries, err := cfg.LibraryHandler.ListLibrariesData(c.Request().Context())
|
|
if err != nil {
|
|
log.Printf("ListLibrariesData failed: %v", err)
|
|
libraries = []database.ListLibrariesRow{}
|
|
}
|
|
|
|
// Convert to template LibraryData
|
|
libData := make([]templates.LibraryData, len(libraries))
|
|
for i, lib := range libraries {
|
|
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
|
libData[i] = templates.LibraryData{
|
|
ID: libUUID.String(),
|
|
Name: lib.Name,
|
|
Description: getText(lib.Description),
|
|
TypeName: lib.TypeName,
|
|
}
|
|
}
|
|
|
|
// Fetch all users for visibility management
|
|
users, err := cfg.Queries.ListUsers(c.Request().Context())
|
|
if err != nil {
|
|
log.Printf("ListUsers failed: %v", err)
|
|
users = []database.ListUsersRow{}
|
|
}
|
|
|
|
// Convert to template User types
|
|
userData := make([]templates.User, len(users))
|
|
for i, u := range users {
|
|
userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16])
|
|
userData[i] = templates.User{
|
|
ID: userUUID.String(),
|
|
Username: u.Username,
|
|
Email: u.Email,
|
|
Role: u.Role,
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.AdminLibrary(user, libData, userData).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}))
|
|
|
|
// Admin users page
|
|
frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
// Fetch all users
|
|
users, err := cfg.Queries.ListUsers(c.Request().Context())
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading users", "users_load_error")
|
|
}
|
|
|
|
// Count admins for UI protection
|
|
adminCount := 0
|
|
for _, u := range users {
|
|
if u.Role == "admin" {
|
|
adminCount++
|
|
}
|
|
}
|
|
|
|
// Convert to template users
|
|
templateUsers := make([]templates.User, len(users))
|
|
for i, u := range users {
|
|
templateUsers[i] = templates.User{
|
|
ID: uuid.UUID(u.ID.Bytes).String(),
|
|
Username: u.Username,
|
|
Email: u.Email,
|
|
Role: u.Role,
|
|
Theme: getText(u.Theme),
|
|
FirstName: getText(u.FirstName),
|
|
LastName: getText(u.LastName),
|
|
CreatedAt: u.CreatedAt.Time,
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.AdminUsers(user, templateUsers, adminCount).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}))
|
|
|
|
// Admin: Get profile modal for editing user
|
|
frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c echo.Context) error {
|
|
// Get target user ID from URL
|
|
targetUserID := c.Param("id")
|
|
parsedUUID, err := uuid.Parse(targetUserID)
|
|
if err != nil {
|
|
return c.HTML(http.StatusBadRequest, "<div>Invalid user ID</div>")
|
|
}
|
|
|
|
// Fetch target user
|
|
targetUser, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(parsedUUID))
|
|
if err != nil {
|
|
return c.HTML(http.StatusNotFound, "<div>User not found</div>")
|
|
}
|
|
|
|
// Convert to template user
|
|
templateUser := templates.User{
|
|
ID: uuid.UUID(targetUser.ID.Bytes).String(),
|
|
Username: targetUser.Username,
|
|
Email: targetUser.Email,
|
|
Role: targetUser.Role,
|
|
Theme: getText(targetUser.Theme),
|
|
FirstName: getText(targetUser.FirstName),
|
|
LastName: getText(targetUser.LastName),
|
|
CreatedAt: targetUser.CreatedAt.Time,
|
|
}
|
|
|
|
// Render modal
|
|
var buf bytes.Buffer
|
|
err = templates.ProfileModal(templateUser).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}))
|
|
|
|
// ============================================================================
|
|
// LEGACY API ROUTES (for backward compatibility)
|
|
// ============================================================================
|
|
|
|
// Keep legacy routes under /api for existing API consumers
|
|
protected.GET("/devices-page", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
var errorMsg string
|
|
var devices []handlers.DeviceInfo
|
|
var pendingList []templates.PendingRegistrationData
|
|
|
|
devices, err = cfg.DeviceHandler.GetDevicesData(c)
|
|
if err != nil {
|
|
log.Printf("GetDevicesData failed: %v", err)
|
|
devices = []handlers.DeviceInfo{}
|
|
errorMsg = "Error loading devices"
|
|
}
|
|
|
|
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
|
if err != nil {
|
|
log.Printf("GetPendingRegistrationsData failed: %v", err)
|
|
pendingList = []templates.PendingRegistrationData{}
|
|
if errorMsg == "" {
|
|
errorMsg = "Error loading pending registrations"
|
|
}
|
|
} else {
|
|
pendingList = convertPending(pendingMaps)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Devices(user, devices, pendingList, errorMsg, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
protected.GET("/conflicts-page", func(c echo.Context) error {
|
|
user, err := getTemplateUserWithTheme(c, cfg)
|
|
if err != nil {
|
|
return renderErrorPage(c, "Error loading user", "user_load_error")
|
|
}
|
|
|
|
var errorMsg string
|
|
var conflictsData []handlers.ConflictDetailResponse
|
|
var total, unresolved int
|
|
|
|
conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c)
|
|
if err != nil {
|
|
log.Printf("GetConflictsData failed: %v", err)
|
|
conflictsData = []handlers.ConflictDetailResponse{}
|
|
total = 0
|
|
unresolved = 0
|
|
errorMsg = "Error loading conflicts"
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
})
|
|
|
|
// ============================================================================
|
|
// HEALTH CHECK
|
|
// ============================================================================
|
|
|
|
// Health check
|
|
e.GET("/health", func(c echo.Context) error {
|
|
ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
if err := pingDB(cfg, ctx); err != nil {
|
|
return c.JSON(http.StatusServiceUnavailable, map[string]string{
|
|
"status": "unhealthy",
|
|
"error": "database unavailable",
|
|
})
|
|
}
|
|
return c.JSON(http.StatusOK, map[string]string{
|
|
"status": "healthy",
|
|
"database": "connected",
|
|
})
|
|
})
|
|
}
|