mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 17:07:47 +02:00
- Validate resolved IPs immediately before connecting. - Guard Shoutrrr requests during initialization and delivery. - Require admins for services without HTTP client support. - Test DNS rebinding, redirects, internal hosts, and authorization. - Upgrade Shoutrrr to 0.20.0 to support custom dialcontext
161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
package alerts
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"slices"
|
|
|
|
"github.com/henrygd/beszel/internal/hub/utils"
|
|
"github.com/nicholas-fedor/shoutrrr"
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
)
|
|
|
|
// UpsertUserAlerts handles API request to create or update alerts for a user
|
|
// across multiple systems (POST /api/beszel/user-alerts)
|
|
func UpsertUserAlerts(e *core.RequestEvent) error {
|
|
userID := e.Auth.Id
|
|
|
|
reqData := struct {
|
|
Min uint8 `json:"min"`
|
|
Value float64 `json:"value"`
|
|
Name string `json:"name"`
|
|
Systems []string `json:"systems"`
|
|
Overwrite bool `json:"overwrite"`
|
|
}{}
|
|
err := e.BindBody(&reqData)
|
|
if err != nil || userID == "" || reqData.Name == "" || len(reqData.Systems) == 0 {
|
|
return e.BadRequestError("Bad data", err)
|
|
}
|
|
|
|
alertsCollection, err := e.App.FindCachedCollectionByNameOrId("alerts")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = e.App.RunInTransaction(func(txApp core.App) error {
|
|
for _, systemId := range reqData.Systems {
|
|
if !userHasSystem(txApp, userID, systemId) {
|
|
continue
|
|
}
|
|
// find existing matching alert
|
|
alertRecord, err := txApp.FindFirstRecordByFilter(alertsCollection,
|
|
"system={:system} && name={:name} && user={:user}",
|
|
dbx.Params{"system": systemId, "name": reqData.Name, "user": userID})
|
|
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
|
|
// skip if alert already exists and overwrite is not set
|
|
if !reqData.Overwrite && alertRecord != nil {
|
|
continue
|
|
}
|
|
|
|
// create new alert if it doesn't exist
|
|
if alertRecord == nil {
|
|
alertRecord = core.NewRecord(alertsCollection)
|
|
alertRecord.Set("user", userID)
|
|
alertRecord.Set("system", systemId)
|
|
alertRecord.Set("name", reqData.Name)
|
|
}
|
|
|
|
alertRecord.Set("value", reqData.Value)
|
|
alertRecord.Set("min", reqData.Min)
|
|
|
|
if err := txApp.SaveNoValidate(alertRecord); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, map[string]any{"success": true})
|
|
}
|
|
|
|
// DeleteUserAlerts handles API request to delete alerts for a user across multiple systems
|
|
// (DELETE /api/beszel/user-alerts)
|
|
func DeleteUserAlerts(e *core.RequestEvent) error {
|
|
userID := e.Auth.Id
|
|
|
|
reqData := struct {
|
|
AlertName string `json:"name"`
|
|
Systems []string `json:"systems"`
|
|
}{}
|
|
err := e.BindBody(&reqData)
|
|
if err != nil || userID == "" || reqData.AlertName == "" || len(reqData.Systems) == 0 {
|
|
return e.BadRequestError("Bad data", err)
|
|
}
|
|
|
|
var numDeleted uint16
|
|
|
|
err = e.App.RunInTransaction(func(txApp core.App) error {
|
|
for _, systemId := range reqData.Systems {
|
|
if !userHasSystem(txApp, userID, systemId) {
|
|
continue
|
|
}
|
|
// Find existing alert to delete
|
|
alertRecord, err := txApp.FindFirstRecordByFilter("alerts",
|
|
"system={:system} && name={:name} && user={:user}",
|
|
dbx.Params{"system": systemId, "name": reqData.AlertName, "user": userID})
|
|
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// alert doesn't exist, continue to next system
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := txApp.Delete(alertRecord); err != nil {
|
|
return err
|
|
}
|
|
numDeleted++
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, map[string]any{"success": true, "count": numDeleted})
|
|
}
|
|
|
|
func userHasSystem(app core.App, userID, systemID string) bool {
|
|
system, err := app.FindRecordById("systems", systemID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
shareAll, _ := utils.GetEnv("SHARE_ALL_SYSTEMS")
|
|
return shareAll == "true" || slices.Contains(system.GetStringSlice("users"), userID)
|
|
}
|
|
|
|
// SendTestNotification handles API request to send a test notification to a specified Shoutrrr URL
|
|
func (am *AlertManager) SendTestNotification(e *core.RequestEvent) error {
|
|
var data struct {
|
|
URL string `json:"url"`
|
|
}
|
|
err := e.BindBody(&data)
|
|
if err != nil || data.URL == "" {
|
|
return e.BadRequestError("URL is required", err)
|
|
}
|
|
send := shoutrrr.Send
|
|
if !e.Auth.IsSuperuser() && e.Auth.GetString("role") != "admin" {
|
|
send = sendPublicNotification
|
|
}
|
|
err = am.sendShoutrrrAlert(data.URL, "Test Alert", "This is a notification from Beszel.", am.hub.Settings().Meta.AppURL, "View Beszel", send)
|
|
if errors.Is(err, errInternalDestination) || errors.Is(err, errUnrestrictedService) {
|
|
return e.ForbiddenError(err.Error(), nil)
|
|
}
|
|
if err != nil {
|
|
return e.JSON(200, map[string]string{"err": err.Error()})
|
|
}
|
|
return e.JSON(200, map[string]bool{"err": false})
|
|
}
|