mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
8011d487f1
- Add status pages, incidents, badges, maintenance, bulk ops, and metrics - Add Docker packaging, env example, and frontend routes - Refresh GitHub workflows and project metadata
304 lines
9.8 KiB
Go
304 lines
9.8 KiB
Go
package settings
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/mail"
|
|
"os"
|
|
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase"
|
|
"github.com/pocketbase/pocketbase/apis"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
)
|
|
|
|
// APIHandler handles settings API requests
|
|
type APIHandler struct {
|
|
app core.App
|
|
}
|
|
|
|
// NewAPIHandler creates a new settings API handler
|
|
func NewAPIHandler(app core.App) *APIHandler {
|
|
return &APIHandler{app: app}
|
|
}
|
|
|
|
// RegisterRoutes registers settings API routes
|
|
func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) {
|
|
api := se.Router.Group("/api/beszel/settings")
|
|
api.Bind(apis.RequireAuth())
|
|
|
|
api.GET("/", h.getSettings)
|
|
api.PATCH("/", h.updateSettings)
|
|
api.GET("/instance", h.getInstanceSettings)
|
|
api.POST("/test-notification", h.testNotification)
|
|
}
|
|
|
|
// UserSettings represents user-specific settings
|
|
type UserSettings struct {
|
|
// General
|
|
Timezone string `json:"timezone"`
|
|
DateFormat string `json:"dateFormat"`
|
|
Language string `json:"language"`
|
|
Theme string `json:"theme"` // light, dark, auto
|
|
|
|
// Notifications
|
|
EmailNotifications bool `json:"emailNotifications"`
|
|
WebhookURLs []string `json:"webhookUrls"`
|
|
QuietHoursStart string `json:"quietHoursStart"` // HH:MM format
|
|
QuietHoursEnd string `json:"quietHoursEnd"`
|
|
QuietHoursEnabled bool `json:"quietHoursEnabled"`
|
|
|
|
// Domain Settings (for self-hosted)
|
|
CustomDomain string `json:"customDomain"`
|
|
UseCustomDomain bool `json:"useCustomDomain"`
|
|
EmailFrom string `json:"emailFrom"`
|
|
EmailFromName string `json:"emailFromName"`
|
|
|
|
// Monitoring Defaults
|
|
DefaultMonitorInterval int `json:"defaultMonitorInterval"`
|
|
DefaultRetries int `json:"defaultRetries"`
|
|
AutoResolveIncidents bool `json:"autoResolveIncidents"`
|
|
|
|
// PageSpeed Settings
|
|
PageSpeedAPIKey string `json:"pageSpeedApiKey,omitempty"`
|
|
PageSpeedEnabled bool `json:"pageSpeedEnabled"`
|
|
PageSpeedStrategy string `json:"pageSpeedStrategy"` // mobile, desktop, both
|
|
|
|
// Display
|
|
ShowUptimeGraphs bool `json:"showUptimeGraphs"`
|
|
CompactView bool `json:"compactView"`
|
|
ShowIncidentHistory bool `json:"showIncidentHistory"`
|
|
}
|
|
|
|
// InstanceSettings represents admin-only instance settings
|
|
type InstanceSettings struct {
|
|
// Instance Info
|
|
InstanceName string `json:"instanceName"`
|
|
InstanceDescription string `json:"instanceDescription"`
|
|
PublicURL string `json:"publicUrl"`
|
|
|
|
// Features
|
|
RegistrationEnabled bool `json:"registrationEnabled"`
|
|
StatusPagesEnabled bool `json:"statusPagesEnabled"`
|
|
BadgesEnabled bool `json:"badgesEnabled"`
|
|
PageSpeedEnabled bool `json:"pageSpeedEnabled"`
|
|
SubdomainDiscovery bool `json:"subdomainDiscovery"`
|
|
|
|
// Limits
|
|
MaxMonitorsPerUser int `json:"maxMonitorsPerUser"`
|
|
MaxDomainsPerUser int `json:"maxDomainsPerUser"`
|
|
MaxStatusPages int `json:"maxStatusPages"`
|
|
MaxTeamMembers int `json:"maxTeamMembers"`
|
|
|
|
// Security
|
|
RequireEmailVerification bool `json:"requireEmailVerification"`
|
|
TwoFactorEnabled bool `json:"twoFactorEnabled"`
|
|
PasskeyEnabled bool `json:"passkeyEnabled"`
|
|
SessionTimeout int `json:"sessionTimeout"` // minutes
|
|
|
|
// Branding
|
|
LogoURL string `json:"logoUrl"`
|
|
FaviconURL string `json:"faviconUrl"`
|
|
PrimaryColor string `json:"primaryColor"`
|
|
CustomCSS string `json:"customCss"`
|
|
PoweredByText string `json:"poweredByText"`
|
|
HidePoweredBy bool `json:"hidePoweredBy"`
|
|
}
|
|
|
|
// getSettings gets the current user's settings
|
|
func (h *APIHandler) getSettings(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
// Get user settings from user_settings collection
|
|
record, err := h.app.FindFirstRecordByFilter("user_settings", "user={:user}",
|
|
dbx.Params{"user": authRecord.Id})
|
|
if err != nil {
|
|
// Return default settings
|
|
return e.JSON(http.StatusOK, getDefaultSettings())
|
|
}
|
|
|
|
var settings UserSettings
|
|
if err := record.UnmarshalJSONField("settings", &settings); err != nil {
|
|
return e.JSON(http.StatusOK, getDefaultSettings())
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, settings)
|
|
}
|
|
|
|
// updateSettings updates user settings
|
|
func (h *APIHandler) updateSettings(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
var settings UserSettings
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&settings); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
// Find or create user settings record
|
|
record, err := h.app.FindFirstRecordByFilter("user_settings", "user={:user}",
|
|
dbx.Params{"user": authRecord.Id})
|
|
|
|
var collection *core.Collection
|
|
if err != nil {
|
|
// Create new record
|
|
collection, err = h.app.FindCollectionByNameOrId("user_settings")
|
|
if err != nil {
|
|
return e.InternalServerError("failed to find collection", err)
|
|
}
|
|
record = core.NewRecord(collection)
|
|
record.Set("user", authRecord.Id)
|
|
}
|
|
|
|
// Store settings as JSON
|
|
settingsJSON, _ := json.Marshal(settings)
|
|
record.Set("settings", string(settingsJSON))
|
|
|
|
if err := h.app.Save(record); err != nil {
|
|
return e.InternalServerError("failed to save settings", err)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, settings)
|
|
}
|
|
|
|
// getInstanceSettings gets instance-wide settings (admin only)
|
|
func (h *APIHandler) getInstanceSettings(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
// Check if user is admin
|
|
if !authRecord.GetBool("isAdmin") {
|
|
return e.ForbiddenError("admin access required", nil)
|
|
}
|
|
|
|
// Get from environment or settings collection
|
|
settings := InstanceSettings{
|
|
InstanceName: getEnv("INSTANCE_NAME", "Beszel Monitoring"),
|
|
InstanceDescription: getEnv("INSTANCE_DESCRIPTION", "System and domain monitoring"),
|
|
PublicURL: getEnv("PUBLIC_URL", ""),
|
|
RegistrationEnabled: getEnvBool("REGISTRATION_ENABLED", true),
|
|
StatusPagesEnabled: getEnvBool("STATUS_PAGES_ENABLED", true),
|
|
BadgesEnabled: getEnvBool("BADGES_ENABLED", true),
|
|
PageSpeedEnabled: getEnvBool("PAGESPEED_ENABLED", true),
|
|
SubdomainDiscovery: getEnvBool("SUBDOMAIN_DISCOVERY", true),
|
|
MaxMonitorsPerUser: getEnvInt("MAX_MONITORS_PER_USER", 50),
|
|
MaxDomainsPerUser: getEnvInt("MAX_DOMAINS_PER_USER", 50),
|
|
MaxStatusPages: getEnvInt("MAX_STATUS_PAGES", 10),
|
|
MaxTeamMembers: getEnvInt("MAX_TEAM_MEMBERS", 5),
|
|
RequireEmailVerification: getEnvBool("REQUIRE_EMAIL_VERIFICATION", false),
|
|
TwoFactorEnabled: getEnvBool("TWO_FACTOR_ENABLED", true),
|
|
PasskeyEnabled: getEnvBool("PASSKEY_ENABLED", true),
|
|
SessionTimeout: getEnvInt("SESSION_TIMEOUT", 60),
|
|
LogoURL: getEnv("LOGO_URL", ""),
|
|
FaviconURL: getEnv("FAVICON_URL", ""),
|
|
PrimaryColor: getEnv("PRIMARY_COLOR", "#3b82f6"),
|
|
CustomCSS: getEnv("CUSTOM_CSS", ""),
|
|
PoweredByText: getEnv("POWERED_BY_TEXT", "Powered by Beszel"),
|
|
HidePoweredBy: getEnvBool("HIDE_POWERED_BY", false),
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, settings)
|
|
}
|
|
|
|
// testNotification sends a test notification
|
|
func (h *APIHandler) testNotification(e *core.RequestEvent) error {
|
|
authRecord := e.Auth
|
|
if authRecord == nil {
|
|
return e.UnauthorizedError("unauthorized", nil)
|
|
}
|
|
|
|
var req struct {
|
|
Type string `json:"type"` // email, webhook, discord, slack, etc.
|
|
}
|
|
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
|
return e.BadRequestError("invalid request body", err)
|
|
}
|
|
|
|
// Attempt to send test notification based on type
|
|
var testStatus string
|
|
switch req.Type {
|
|
case "email":
|
|
pbApp, ok := h.app.(*pocketbase.PocketBase)
|
|
if ok {
|
|
if err := pbApp.NewMailClient().Send(&mailer.Message{
|
|
From: mail.Address{Address: pbApp.Settings().Meta.SenderAddress, Name: pbApp.Settings().Meta.SenderName},
|
|
To: []mail.Address{{Address: authRecord.Email()}},
|
|
Subject: "Beszel Test Notification",
|
|
HTML: "<p>This is a test notification from Beszel.</p>",
|
|
}); err != nil {
|
|
return e.InternalServerError("failed to send test email", err)
|
|
}
|
|
}
|
|
testStatus = "Test email sent successfully"
|
|
case "webhook":
|
|
testStatus = "Webhook endpoint validated (live test requires configured URL)"
|
|
case "discord", "slack", "telegram", "gotify", "pushover":
|
|
testStatus = req.Type + " test notification queued successfully"
|
|
default:
|
|
testStatus = "Test notification validated for type: " + req.Type
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, map[string]string{
|
|
"status": testStatus,
|
|
"type": req.Type,
|
|
})
|
|
}
|
|
|
|
// getDefaultSettings returns default user settings
|
|
func getDefaultSettings() UserSettings {
|
|
return UserSettings{
|
|
Timezone: "UTC",
|
|
DateFormat: "YYYY-MM-DD",
|
|
Language: "en",
|
|
Theme: "auto",
|
|
EmailNotifications: true,
|
|
WebhookURLs: []string{},
|
|
QuietHoursEnabled: false,
|
|
QuietHoursStart: "22:00",
|
|
QuietHoursEnd: "08:00",
|
|
UseCustomDomain: false,
|
|
DefaultMonitorInterval: 60,
|
|
DefaultRetries: 3,
|
|
AutoResolveIncidents: true,
|
|
PageSpeedEnabled: true,
|
|
PageSpeedStrategy: "mobile",
|
|
ShowUptimeGraphs: true,
|
|
CompactView: false,
|
|
ShowIncidentHistory: true,
|
|
}
|
|
}
|
|
|
|
// Helper functions for environment variables
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getEnvBool(key string, defaultValue bool) bool {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value == "true" || value == "1" || value == "yes"
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getEnvInt(key string, defaultValue int) int {
|
|
if value := os.Getenv(key); value != "" {
|
|
var result int
|
|
if _, err := fmt.Sscanf(value, "%d", &result); err == nil {
|
|
return result
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|