mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
435 lines
13 KiB
Go
435 lines
13 KiB
Go
package statuspages
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/henrygd/beszel/internal/entities/monitor"
|
|
"github.com/henrygd/beszel/internal/entities/statuspage"
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/apis"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
)
|
|
|
|
// APIHandler handles status page API requests
|
|
type APIHandler struct {
|
|
app core.App
|
|
}
|
|
|
|
// NewAPIHandler creates a new status page API handler
|
|
func NewAPIHandler(app core.App) *APIHandler {
|
|
return &APIHandler{app: app}
|
|
}
|
|
|
|
// RegisterRoutes registers status page API routes
|
|
func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) {
|
|
// Public status page (no auth required)
|
|
se.Router.GET("/status/:slug", h.getPublicStatusPage)
|
|
|
|
// Protected routes
|
|
api := se.Router.Group("/api/beszel/status-pages")
|
|
api.Bind(apis.RequireAuth())
|
|
|
|
api.GET("/", h.listStatusPages)
|
|
api.POST("/", h.createStatusPage)
|
|
api.GET("/{id}", h.getStatusPage)
|
|
api.PATCH("/{id}", h.updateStatusPage)
|
|
api.DELETE("/{id}", h.deleteStatusPage)
|
|
api.POST("/{id}/monitors", h.addMonitor)
|
|
api.DELETE("/{id}/monitors/{monitorId}", h.removeMonitor)
|
|
api.GET("/{id}/monitors", h.listMonitors)
|
|
}
|
|
|
|
// listStatusPages lists all status pages for the authenticated user
|
|
func (h *APIHandler) listStatusPages(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
records, err := h.app.FindAllRecords("status_pages",
|
|
dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}),
|
|
)
|
|
if err != nil {
|
|
return e.InternalServerError("failed to fetch status pages", err)
|
|
}
|
|
|
|
pages := make([]statuspage.StatusPageResponse, 0, len(records))
|
|
for _, record := range records {
|
|
pages = append(pages, h.recordToResponse(record))
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, pages)
|
|
}
|
|
|
|
// createStatusPage creates a new status page
|
|
func (h *APIHandler) createStatusPage(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
var req statuspage.CreateStatusPageRequest
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
if req.Name == "" || req.Slug == "" {
|
|
return e.BadRequestError("name and slug are required", nil)
|
|
}
|
|
|
|
// Check if slug is unique
|
|
existing, _ := h.app.FindFirstRecordByFilter("status_pages", "slug = {:slug}",
|
|
dbx.Params{"slug": req.Slug})
|
|
if existing != nil {
|
|
return e.BadRequestError("slug already exists", nil)
|
|
}
|
|
|
|
collection, err := h.app.FindCollectionByNameOrId("status_pages")
|
|
if err != nil {
|
|
return e.InternalServerError("failed to find collection", err)
|
|
}
|
|
|
|
record := core.NewRecord(collection)
|
|
record.Set("name", req.Name)
|
|
record.Set("slug", strings.ToLower(req.Slug))
|
|
record.Set("title", req.Title)
|
|
record.Set("description", req.Description)
|
|
record.Set("logo", req.Logo)
|
|
record.Set("favicon", req.Favicon)
|
|
record.Set("theme", statuspage.ValidateTheme(req.Theme))
|
|
record.Set("custom_css", req.CustomCSS)
|
|
record.Set("public", req.Public)
|
|
record.Set("show_uptime", req.ShowUptime)
|
|
record.Set("user", authRecord.Id)
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to create status page", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusCreated, h.recordToResponse(record))
|
|
}
|
|
|
|
// getStatusPage gets a single status page
|
|
func (h *APIHandler) getStatusPage(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("status_pages", id)
|
|
if err != nil {
|
|
return e.NotFoundError("status page not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
|
}
|
|
|
|
// updateStatusPage updates a status page
|
|
func (h *APIHandler) updateStatusPage(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("status_pages", id)
|
|
if err != nil {
|
|
return e.NotFoundError("status page not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
var req statuspage.UpdateStatusPageRequest
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
if req.Name != nil {
|
|
record.Set("name", *req.Name)
|
|
}
|
|
if req.Title != nil {
|
|
record.Set("title", *req.Title)
|
|
}
|
|
if req.Description != nil {
|
|
record.Set("description", *req.Description)
|
|
}
|
|
if req.Logo != nil {
|
|
record.Set("logo", *req.Logo)
|
|
}
|
|
if req.Favicon != nil {
|
|
record.Set("favicon", *req.Favicon)
|
|
}
|
|
if req.Theme != nil {
|
|
record.Set("theme", statuspage.ValidateTheme(*req.Theme))
|
|
}
|
|
if req.CustomCSS != nil {
|
|
record.Set("custom_css", *req.CustomCSS)
|
|
}
|
|
if req.Public != nil {
|
|
record.Set("public", *req.Public)
|
|
}
|
|
if req.ShowUptime != nil {
|
|
record.Set("show_uptime", *req.ShowUptime)
|
|
}
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to update status page", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
|
}
|
|
|
|
// deleteStatusPage deletes a status page
|
|
func (h *APIHandler) deleteStatusPage(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
id := e.Request.PathValue("id")
|
|
record, err := h.app.FindRecordById("status_pages", id)
|
|
if err != nil {
|
|
return e.NotFoundError("status page not found", err)
|
|
}
|
|
|
|
if record.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
if err := h.app.Delete(record); err != nil {
|
|
return e.InternalServerError("failed to delete status page", err)
|
|
}
|
|
|
|
return e.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
// addMonitor adds a monitor to a status page
|
|
func (h *APIHandler) addMonitor(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
statusPageID := e.Request.PathValue("id")
|
|
statusPage, err := h.app.FindRecordById("status_pages", statusPageID)
|
|
if err != nil {
|
|
return e.NotFoundError("status page not found", err)
|
|
}
|
|
|
|
if statusPage.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
var req statuspage.StatusPageMonitorRequest
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
// Verify monitor exists and belongs to user
|
|
monitorRecord, err := h.app.FindRecordById("monitors", req.MonitorID)
|
|
if err != nil {
|
|
return e.NotFoundError("monitor not found", err)
|
|
}
|
|
|
|
if monitorRecord.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized for this monitor", nil)
|
|
}
|
|
|
|
collection, err := h.app.FindCollectionByNameOrId("status_page_monitors")
|
|
if err != nil {
|
|
return e.InternalServerError("failed to find collection", err)
|
|
}
|
|
|
|
record := core.NewRecord(collection)
|
|
record.Set("status_page", statusPageID)
|
|
record.Set("monitor", req.MonitorID)
|
|
record.Set("display_name", req.DisplayName)
|
|
record.Set("group", req.Group)
|
|
record.Set("sort_order", req.SortOrder)
|
|
record.Set("user", authRecord.Id)
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to add monitor", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusCreated, map[string]string{"status": "added"})
|
|
}
|
|
|
|
// removeMonitor removes a monitor from a status page
|
|
func (h *APIHandler) removeMonitor(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
statusPageID := e.Request.PathValue("id")
|
|
monitorID := e.Request.PathValue("monitorId")
|
|
|
|
// Find the link record
|
|
records, err := h.app.FindAllRecords("status_page_monitors",
|
|
dbx.HashExp{
|
|
"status_page": statusPageID,
|
|
"monitor": monitorID,
|
|
"user": authRecord.Id,
|
|
},
|
|
)
|
|
if err != nil || len(records) == 0 {
|
|
return e.NotFoundError("monitor link not found", err)
|
|
}
|
|
|
|
if err := h.app.Delete(records[0]); err != nil {
|
|
return e.InternalServerError("failed to remove monitor", err)
|
|
}
|
|
|
|
return e.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
// listMonitors lists monitors on a status page
|
|
func (h *APIHandler) listMonitors(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
statusPageID := e.Request.PathValue("id")
|
|
statusPage, err := h.app.FindRecordById("status_pages", statusPageID)
|
|
if err != nil {
|
|
return e.NotFoundError("status page not found", err)
|
|
}
|
|
|
|
if statusPage.GetString("user") != authRecord.Id {
|
|
return e.ForbiddenError("not authorized", nil)
|
|
}
|
|
|
|
records, err := h.app.FindAllRecords("status_page_monitors",
|
|
dbx.NewExp("status_page = {:statusPage}", dbx.Params{"statusPage": statusPageID}),
|
|
)
|
|
if err != nil {
|
|
return e.InternalServerError("failed to fetch monitors", err)
|
|
}
|
|
|
|
monitors := make([]map[string]interface{}, 0, len(records))
|
|
for _, record := range records {
|
|
monitors = append(monitors, map[string]interface{}{
|
|
"id": record.Id,
|
|
"monitor_id": record.GetString("monitor"),
|
|
"display_name": record.GetString("display_name"),
|
|
"group": record.GetString("group"),
|
|
"sort_order": record.GetInt("sort_order"),
|
|
})
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, monitors)
|
|
}
|
|
|
|
// getPublicStatusPage gets a public status page by slug
|
|
func (h *APIHandler) getPublicStatusPage(e *core.RequestEvent) error {
|
|
slug := e.Request.PathValue("slug")
|
|
|
|
record, err := h.app.FindFirstRecordByFilter("status_pages", "slug = {:slug} && public = true",
|
|
dbx.Params{"slug": slug})
|
|
if err != nil {
|
|
return e.NotFoundError("status page not found", err)
|
|
}
|
|
|
|
// Build public status page
|
|
publicPage := h.buildPublicStatusPage(record)
|
|
|
|
return e.JSON(http.StatusOK, publicPage)
|
|
}
|
|
|
|
// buildPublicStatusPage builds a public status page from a record
|
|
func (h *APIHandler) buildPublicStatusPage(record *core.Record) *statuspage.PublicStatusPage {
|
|
statusPageID := record.Id
|
|
|
|
// Get linked monitors
|
|
links, err := h.app.FindAllRecords("status_page_monitors",
|
|
dbx.NewExp("status_page = {:statusPage}", dbx.Params{"statusPage": statusPageID}),
|
|
)
|
|
if err != nil {
|
|
links = []*core.Record{}
|
|
}
|
|
|
|
publicMonitors := make([]statuspage.PublicMonitorStatus, 0, len(links))
|
|
overallStatus := statuspage.StatusOperational
|
|
|
|
for _, link := range links {
|
|
monitorID := link.GetString("monitor")
|
|
monitorRecord, err := h.app.FindRecordById("monitors", monitorID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
status := monitorRecord.GetString("status")
|
|
if status == string(monitor.StatusDown) && overallStatus == statuspage.StatusOperational {
|
|
overallStatus = statuspage.StatusMajor
|
|
}
|
|
|
|
uptimeStats := make(map[string]float64)
|
|
if statsJSON := monitorRecord.GetString("uptime_stats"); statsJSON != "" {
|
|
json.Unmarshal([]byte(statsJSON), &uptimeStats)
|
|
}
|
|
|
|
publicMonitors = append(publicMonitors, statuspage.PublicMonitorStatus{
|
|
ID: monitorID,
|
|
Name: monitorRecord.GetString("name"),
|
|
DisplayName: link.GetString("display_name"),
|
|
Group: link.GetString("group"),
|
|
Status: status,
|
|
Uptime24h: uptimeStats["24h"],
|
|
Uptime7d: uptimeStats["7d"],
|
|
Uptime30d: uptimeStats["30d"],
|
|
LastCheck: monitorRecord.GetDateTime("last_check").Time(),
|
|
})
|
|
}
|
|
|
|
return &statuspage.PublicStatusPage{
|
|
ID: record.Id,
|
|
Name: record.GetString("name"),
|
|
Title: record.GetString("title"),
|
|
Description: record.GetString("description"),
|
|
Logo: record.GetString("logo"),
|
|
Favicon: record.GetString("favicon"),
|
|
Theme: record.GetString("theme"),
|
|
CustomCSS: record.GetString("custom_css"),
|
|
Monitors: publicMonitors,
|
|
OverallStatus: overallStatus,
|
|
UpdatedAt: record.GetDateTime("updated").Time(),
|
|
}
|
|
}
|
|
|
|
// recordToResponse converts a record to a response
|
|
func (h *APIHandler) recordToResponse(record *core.Record) statuspage.StatusPageResponse {
|
|
// Count monitors
|
|
count := 0
|
|
links, _ := h.app.FindAllRecords("status_page_monitors",
|
|
dbx.NewExp("status_page = {:statusPage}", dbx.Params{"statusPage": record.Id}),
|
|
)
|
|
count = len(links)
|
|
|
|
return statuspage.StatusPageResponse{
|
|
ID: record.Id,
|
|
Name: record.GetString("name"),
|
|
Slug: record.GetString("slug"),
|
|
Title: record.GetString("title"),
|
|
Description: record.GetString("description"),
|
|
Logo: record.GetString("logo"),
|
|
Favicon: record.GetString("favicon"),
|
|
Theme: record.GetString("theme"),
|
|
Public: record.GetBool("public"),
|
|
ShowUptime: record.GetBool("show_uptime"),
|
|
MonitorCount: count,
|
|
Created: record.GetDateTime("created").String(),
|
|
Updated: record.GetDateTime("updated").String(),
|
|
}
|
|
}
|