feat: add health check and restore frontend routes
Health check endpoint:
- Add /health endpoint that pings database with 2-second timeout
- Returns 200 when DB connected, 503 when unavailable
- Provides true end-to-end health verification
Frontend routes restoration (routes removed in c5f327b):
- Add public routes: /, /login, /register with smart auth detection
- Add redirect routes: /bookshelf, /dashboard
- Add admin routes: /admin, /admin/profile, /admin/library
- Add SSR routes: /api/devices-page, /api/conflicts-page
- Add 'FRONTEND ROUTES - DO NOT DELETE' comment block to prevent future removal
Docker Compose healthcheck:
- Update to use curl on /health endpoint (pg_isready not in Alpine)
- Add 10s start_period for app initialization
- Accurately reflects app + database health status
All changes maintain backward compatibility and existing API behavior.
This commit is contained in:
@@ -623,6 +623,228 @@ func main() {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 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.
|
||||
// ============================================================================
|
||||
|
||||
// Public routes for login and registration pages (no auth required)
|
||||
e.GET("/login", func(c echo.Context) error {
|
||||
var buf bytes.Buffer
|
||||
err := templates.Login().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
|
||||
|
||||
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.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())
|
||||
})
|
||||
|
||||
// Public redirect routes - convenience shortcuts to authenticated routes
|
||||
e.GET("/bookshelf", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf")
|
||||
})
|
||||
|
||||
e.GET("/dashboard", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf")
|
||||
})
|
||||
|
||||
// Admin area routes (authenticated, admin role required, SSR)
|
||||
e.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
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())
|
||||
}))
|
||||
|
||||
e.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
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())
|
||||
}))
|
||||
|
||||
e.GET("/admin/profile", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.AdminProfile(user).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
}))
|
||||
|
||||
e.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.AdminLibrary(user).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
}))
|
||||
|
||||
// Devices management page (authenticated SSR route)
|
||||
protected.GET("/devices-page", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
deviceData, err := deviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
||||
}
|
||||
|
||||
pendingData, err := deviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading pending registrations")
|
||||
}
|
||||
|
||||
devicesList := make([]templates.DeviceData, len(deviceData))
|
||||
for i, d := range deviceData {
|
||||
lastSync := ""
|
||||
if d.LastSync != nil {
|
||||
lastSync = d.LastSync.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
lastSeen := ""
|
||||
if d.LastSeen != nil {
|
||||
lastSeen = d.LastSeen.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
|
||||
devicesList[i] = templates.DeviceData{
|
||||
ID: d.ID.String(),
|
||||
DeviceName: d.DeviceName,
|
||||
DeviceType: d.DeviceType,
|
||||
SyncEnabled: d.SyncEnabled,
|
||||
LastSync: lastSync,
|
||||
LastSeen: lastSeen,
|
||||
}
|
||||
}
|
||||
|
||||
pendingList := make([]templates.PendingRegistrationData, len(pendingData))
|
||||
for i, p := range pendingData {
|
||||
pendingList[i] = templates.PendingRegistrationData{
|
||||
RegistrationID: p["registration_id"].(string),
|
||||
DeviceName: p["device_name"].(string),
|
||||
DeviceType: p["device_type"].(string),
|
||||
ExpiresAt: p["expires_at"].(string),
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Devices(user, devicesList, pendingList).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// Conflicts management page (authenticated SSR route)
|
||||
protected.GET("/conflicts-page", func(c echo.Context) error {
|
||||
user, err := getTemplateUserWithTheme(c, queries)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
|
||||
conflictsData, total, unresolved, err := conflictHandler.GetConflictsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading conflicts")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.Conflicts(user, conflictsData, total, unresolved).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// HEALTH CHECK (public - no authentication required)
|
||||
// ============================================================================
|
||||
|
||||
e.GET("/health", func(c echo.Context) error {
|
||||
ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := dbPool.Ping(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",
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// DOCUMENTATION ROUTES (public - no authentication required)
|
||||
// ============================================================================
|
||||
|
||||
// Documentation routes (no authentication required)
|
||||
docsHandler := docs.NewHTTPHandler("docs")
|
||||
e.GET("/docs", docsHandler.DocsHome)
|
||||
|
||||
Reference in New Issue
Block a user