mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
Initial commit: Beszel fork with Domain Locker integration
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// RegisterRoutes registers notification API routes
|
||||
func RegisterRoutes(app core.App, se *core.ServeEvent) {
|
||||
api := &NotificationAPI{app: app}
|
||||
|
||||
group := se.Router.Group("/api/beszel/notifications")
|
||||
group.Bind(apis.RequireAuth())
|
||||
|
||||
group.GET("/", api.listNotifications)
|
||||
group.POST("/", api.createNotification)
|
||||
group.GET("/{id}", api.getNotification)
|
||||
group.PATCH("/{id}", api.updateNotification)
|
||||
group.DELETE("/{id}", api.deleteNotification)
|
||||
group.POST("/{id}/test", api.testNotification)
|
||||
}
|
||||
|
||||
// NotificationAPI handles notification API requests
|
||||
type NotificationAPI struct {
|
||||
app core.App
|
||||
}
|
||||
|
||||
// CreateNotificationRequest represents a notification creation request
|
||||
type CreateNotificationRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// UpdateNotificationRequest represents a notification update request
|
||||
type UpdateNotificationRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
IsDefault *bool `json:"is_default,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationResponse represents a notification response
|
||||
type NotificationResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Active bool `json:"active"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
// listNotifications lists all notifications for the authenticated user
|
||||
func (api *NotificationAPI) listNotifications(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
records, err := api.app.FindAllRecords("notifications",
|
||||
dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}),
|
||||
)
|
||||
if err != nil {
|
||||
return e.InternalServerError("failed to fetch notifications", err)
|
||||
}
|
||||
|
||||
notifications := make([]NotificationResponse, 0, len(records))
|
||||
for _, record := range records {
|
||||
notifications = append(notifications, api.recordToResponse(record))
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, notifications)
|
||||
}
|
||||
|
||||
// createNotification creates a new notification
|
||||
func (api *NotificationAPI) createNotification(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
var req CreateNotificationRequest
|
||||
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
||||
return e.BadRequestError("invalid request body", err)
|
||||
}
|
||||
|
||||
if req.Name == "" || req.Type == "" {
|
||||
return e.BadRequestError("name and type are required", nil)
|
||||
}
|
||||
|
||||
collection, err := api.app.FindCollectionByNameOrId("notifications")
|
||||
if err != nil {
|
||||
return e.InternalServerError("failed to find collection", err)
|
||||
}
|
||||
|
||||
settingsJSON, _ := json.Marshal(req.Settings)
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("name", req.Name)
|
||||
record.Set("type", req.Type)
|
||||
record.Set("settings", string(settingsJSON))
|
||||
record.Set("is_default", req.IsDefault)
|
||||
record.Set("active", true)
|
||||
record.Set("user", authRecord.Id)
|
||||
|
||||
if err := api.app.Save(record); err != nil {
|
||||
return e.InternalServerError("failed to create notification", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusCreated, api.recordToResponse(record))
|
||||
}
|
||||
|
||||
// getNotification gets a single notification
|
||||
func (api *NotificationAPI) getNotification(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
id := e.Request.PathValue("id")
|
||||
record, err := api.app.FindRecordById("notifications", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("notification not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, api.recordToResponse(record))
|
||||
}
|
||||
|
||||
// updateNotification updates a notification
|
||||
func (api *NotificationAPI) updateNotification(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
id := e.Request.PathValue("id")
|
||||
record, err := api.app.FindRecordById("notifications", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("notification not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
var req UpdateNotificationRequest
|
||||
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
||||
return e.BadRequestError("invalid request body", err)
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
record.Set("name", req.Name)
|
||||
}
|
||||
if req.Settings != nil {
|
||||
settingsJSON, _ := json.Marshal(req.Settings)
|
||||
record.Set("settings", string(settingsJSON))
|
||||
}
|
||||
if req.IsDefault != nil {
|
||||
record.Set("is_default", *req.IsDefault)
|
||||
}
|
||||
if req.Active != nil {
|
||||
record.Set("active", *req.Active)
|
||||
}
|
||||
|
||||
if err := api.app.Save(record); err != nil {
|
||||
return e.InternalServerError("failed to update notification", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, api.recordToResponse(record))
|
||||
}
|
||||
|
||||
// deleteNotification deletes a notification
|
||||
func (api *NotificationAPI) deleteNotification(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
id := e.Request.PathValue("id")
|
||||
record, err := api.app.FindRecordById("notifications", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("notification not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
if err := api.app.Delete(record); err != nil {
|
||||
return e.InternalServerError("failed to delete notification", err)
|
||||
}
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// testNotification sends a test notification
|
||||
func (api *NotificationAPI) testNotification(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
id := e.Request.PathValue("id")
|
||||
record, err := api.app.FindRecordById("notifications", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("notification not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
notif := ¬ification.Notification{
|
||||
ID: record.Id,
|
||||
Name: record.GetString("name"),
|
||||
Type: record.GetString("type"),
|
||||
Active: record.GetBool("active"),
|
||||
}
|
||||
|
||||
if settingsJSON := record.GetString("settings"); settingsJSON != "" {
|
||||
var settings map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(settingsJSON), &settings); err == nil {
|
||||
notif.Settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
dispatcher := NewDispatcher(api.app)
|
||||
msg := ¬ification.NotificationMessage{
|
||||
Title: "Test Notification",
|
||||
Body: "This is a test notification from Beszel.",
|
||||
MonitorName: "Test Monitor",
|
||||
Status: "UP",
|
||||
Message: "Test message",
|
||||
}
|
||||
|
||||
provider, err := dispatcher.getProvider(notif)
|
||||
if err != nil {
|
||||
return e.InternalServerError("failed to create provider", err)
|
||||
}
|
||||
|
||||
if err := provider.Send(msg); err != nil {
|
||||
return e.InternalServerError("failed to send test notification", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]string{"status": "sent"})
|
||||
}
|
||||
|
||||
// recordToResponse converts a record to a response
|
||||
func (api *NotificationAPI) recordToResponse(record *core.Record) NotificationResponse {
|
||||
var settings map[string]interface{}
|
||||
if settingsJSON := record.GetString("settings"); settingsJSON != "" {
|
||||
json.Unmarshal([]byte(settingsJSON), &settings)
|
||||
}
|
||||
|
||||
return NotificationResponse{
|
||||
ID: record.Id,
|
||||
Name: record.GetString("name"),
|
||||
Type: record.GetString("type"),
|
||||
IsDefault: record.GetBool("is_default"),
|
||||
Active: record.GetBool("active"),
|
||||
Settings: settings,
|
||||
Created: record.GetDateTime("created").String(),
|
||||
Updated: record.GetDateTime("updated").String(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/monitor"
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
"github.com/henrygd/beszel/internal/hub/notifications/providers"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// Dispatcher manages notification sending for monitor events
|
||||
type Dispatcher struct {
|
||||
app core.App
|
||||
mu sync.RWMutex
|
||||
providers map[string]notification.Provider
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new notification dispatcher
|
||||
func NewDispatcher(app core.App) *Dispatcher {
|
||||
return &Dispatcher{
|
||||
app: app,
|
||||
providers: make(map[string]notification.Provider),
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification sends a notification for a monitor event
|
||||
func (d *Dispatcher) SendNotification(monitorRecord *monitor.Monitor, heartbeat *monitor.Heartbeat, isRecovery bool) {
|
||||
// Get linked notifications for this monitor
|
||||
notifications, err := d.getMonitorNotifications(monitorRecord.ID)
|
||||
if err != nil {
|
||||
log.Printf("[notification-dispatcher] Failed to get notifications: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(notifications) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the message
|
||||
msg := d.buildMessage(monitorRecord, heartbeat, isRecovery)
|
||||
|
||||
// Send to each notification provider
|
||||
for _, n := range notifications {
|
||||
if !n.Active {
|
||||
continue
|
||||
}
|
||||
|
||||
provider, err := d.getProvider(n)
|
||||
if err != nil {
|
||||
log.Printf("[notification-dispatcher] Failed to get provider: %v", err)
|
||||
d.logNotificationEvent(n.ID, monitorRecord.ID, "failed", "", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if err := provider.Send(msg); err != nil {
|
||||
log.Printf("[notification-dispatcher] Failed to send notification: %v", err)
|
||||
d.logNotificationEvent(n.ID, monitorRecord.ID, "failed", "", err.Error())
|
||||
} else {
|
||||
log.Printf("[notification-dispatcher] Sent notification via %s for monitor %s", n.Type, monitorRecord.Name)
|
||||
d.logNotificationEvent(n.ID, monitorRecord.ID, "sent", "", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getMonitorNotifications retrieves all notifications linked to a monitor
|
||||
func (d *Dispatcher) getMonitorNotifications(monitorID string) ([]*notification.Notification, error) {
|
||||
// Find monitor_notification records for this monitor
|
||||
records, err := d.app.FindAllRecords("monitor_notifications",
|
||||
dbx.HashExp{"monitor": monitorID},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var notifications []*notification.Notification
|
||||
for _, record := range records {
|
||||
notificationID := record.GetString("notification")
|
||||
notifRecord, err := d.app.FindRecordById("notifications", notificationID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
notif := ¬ification.Notification{
|
||||
ID: notifRecord.Id,
|
||||
Name: notifRecord.GetString("name"),
|
||||
Type: notifRecord.GetString("type"),
|
||||
IsDefault: notifRecord.GetBool("is_default"),
|
||||
Active: notifRecord.GetBool("active"),
|
||||
}
|
||||
|
||||
// Parse settings from JSON
|
||||
if settingsJSON := notifRecord.GetString("settings"); settingsJSON != "" {
|
||||
var settings map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(settingsJSON), &settings); err == nil {
|
||||
notif.Settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
notifications = append(notifications, notif)
|
||||
}
|
||||
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
// getProvider creates a provider instance for a notification config
|
||||
func (d *Dispatcher) getProvider(n *notification.Notification) (notification.Provider, error) {
|
||||
d.mu.RLock()
|
||||
if provider, ok := d.providers[n.ID]; ok {
|
||||
d.mu.RUnlock()
|
||||
return provider, nil
|
||||
}
|
||||
d.mu.RUnlock()
|
||||
|
||||
var provider notification.Provider
|
||||
|
||||
switch n.Type {
|
||||
case notification.ProviderEmail:
|
||||
settings := n.GetSettings().(notification.EmailSettings)
|
||||
provider = providers.NewEmailProvider(settings)
|
||||
case notification.ProviderWebhook:
|
||||
settings := n.GetSettings().(notification.WebhookSettings)
|
||||
provider = providers.NewWebhookProvider(settings)
|
||||
case notification.ProviderDiscord:
|
||||
settings := n.GetSettings().(notification.DiscordSettings)
|
||||
provider = providers.NewDiscordProvider(settings)
|
||||
case notification.ProviderSlack:
|
||||
settings := n.GetSettings().(notification.SlackSettings)
|
||||
provider = providers.NewSlackProvider(settings)
|
||||
case notification.ProviderTelegram:
|
||||
settings := n.GetSettings().(notification.TelegramSettings)
|
||||
provider = providers.NewTelegramProvider(settings)
|
||||
case notification.ProviderGotify:
|
||||
settings := n.GetSettings().(notification.GotifySettings)
|
||||
provider = providers.NewGotifyProvider(settings)
|
||||
case notification.ProviderPushover:
|
||||
settings := n.GetSettings().(notification.PushoverSettings)
|
||||
provider = providers.NewPushoverProvider(settings)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown provider type: %s", n.Type)
|
||||
}
|
||||
|
||||
if err := provider.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.providers[n.ID] = provider
|
||||
d.mu.Unlock()
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// buildMessage creates a notification message from monitor data
|
||||
func (d *Dispatcher) buildMessage(m *monitor.Monitor, h *monitor.Heartbeat, isRecovery bool) *notification.NotificationMessage {
|
||||
status := "DOWN"
|
||||
if isRecovery {
|
||||
status = "UP"
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("%s is %s", m.Name, status)
|
||||
body := fmt.Sprintf("Monitor %s is %s.", m.Name, status)
|
||||
|
||||
if !isRecovery && h.Msg != "" {
|
||||
body = fmt.Sprintf("Monitor %s is %s. Error: %s", m.Name, status, h.Msg)
|
||||
}
|
||||
|
||||
return ¬ification.NotificationMessage{
|
||||
Title: title,
|
||||
Body: body,
|
||||
MonitorName: m.Name,
|
||||
MonitorURL: d.getMonitorURL(m),
|
||||
Status: status,
|
||||
Timestamp: h.Time,
|
||||
Ping: h.Ping,
|
||||
Message: h.Msg,
|
||||
}
|
||||
}
|
||||
|
||||
// getMonitorURL returns the URL or hostname for display
|
||||
func (d *Dispatcher) getMonitorURL(m *monitor.Monitor) string {
|
||||
if m.URL != "" {
|
||||
return m.URL
|
||||
}
|
||||
if m.Hostname != "" {
|
||||
if m.Port > 0 {
|
||||
return fmt.Sprintf("%s:%d", m.Hostname, m.Port)
|
||||
}
|
||||
return m.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// logNotificationEvent logs a notification event to the database
|
||||
func (d *Dispatcher) logNotificationEvent(notificationID, monitorID, status, message, errMsg string) {
|
||||
collection, findErr := d.app.FindCollectionByNameOrId("notification_events")
|
||||
if findErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("notification", notificationID)
|
||||
record.Set("monitor", monitorID)
|
||||
record.Set("status", status)
|
||||
record.Set("message", message)
|
||||
record.Set("error", errMsg)
|
||||
|
||||
if saveErr := d.app.Save(record); saveErr != nil {
|
||||
log.Printf("[notification-dispatcher] Failed to log notification event: %v", saveErr)
|
||||
}
|
||||
}
|
||||
|
||||
// ClearCache clears the provider cache (call when settings change)
|
||||
func (d *Dispatcher) ClearCache() {
|
||||
d.mu.Lock()
|
||||
d.providers = make(map[string]notification.Provider)
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
// Check if we need to send notification for this heartbeat
|
||||
func (d *Dispatcher) ShouldNotify(m *monitor.Monitor, heartbeat *monitor.Heartbeat) (bool, bool) {
|
||||
// Check if this is a status change
|
||||
isDown := heartbeat.Status == monitor.StatusDown
|
||||
isRecovery := heartbeat.Status == monitor.StatusUp && m.Status == monitor.StatusDown
|
||||
|
||||
// Only notify on down (after retries) or recovery
|
||||
return isDown && m.Status == monitor.StatusDown, isRecovery
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
type DiscordProvider struct {
|
||||
settings notification.DiscordSettings
|
||||
}
|
||||
|
||||
func NewDiscordProvider(settings notification.DiscordSettings) *DiscordProvider {
|
||||
return &DiscordProvider{settings: settings}
|
||||
}
|
||||
|
||||
func (p *DiscordProvider) Validate() error {
|
||||
if p.settings.WebhookURL == "" {
|
||||
return fmt.Errorf("Discord webhook URL is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DiscordProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color := 0x00ff00 // Green for UP
|
||||
if msg.Status == "DOWN" {
|
||||
color = 0xff0000 // Red for DOWN
|
||||
}
|
||||
|
||||
embed := map[string]interface{}{
|
||||
"title": msg.Title,
|
||||
"description": msg.Body,
|
||||
"color": color,
|
||||
"timestamp": msg.Timestamp.Format(time.RFC3339),
|
||||
"fields": []map[string]interface{}{
|
||||
{
|
||||
"name": "Monitor",
|
||||
"value": msg.MonitorName,
|
||||
"inline": true,
|
||||
},
|
||||
{
|
||||
"name": "Status",
|
||||
"value": msg.Status,
|
||||
"inline": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if msg.MonitorURL != "" {
|
||||
embed["fields"] = append(embed["fields"].([]map[string]interface{}), map[string]interface{}{
|
||||
"name": "URL",
|
||||
"value": msg.MonitorURL,
|
||||
"inline": false,
|
||||
})
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"embeds": []map[string]interface{}{embed},
|
||||
}
|
||||
|
||||
if p.settings.Username != "" {
|
||||
payload["username"] = p.settings.Username
|
||||
}
|
||||
if p.settings.AvatarURL != "" {
|
||||
payload["avatar_url"] = p.settings.AvatarURL
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", p.settings.WebhookURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord webhook request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("discord webhook returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
// EmailProvider implements email notifications via SMTP
|
||||
type EmailProvider struct {
|
||||
settings notification.EmailSettings
|
||||
}
|
||||
|
||||
// NewEmailProvider creates a new email provider
|
||||
func NewEmailProvider(settings notification.EmailSettings) *EmailProvider {
|
||||
return &EmailProvider{settings: settings}
|
||||
}
|
||||
|
||||
// Validate checks if the email settings are valid
|
||||
func (p *EmailProvider) Validate() error {
|
||||
if p.settings.SMTPHost == "" {
|
||||
return fmt.Errorf("SMTP host is required")
|
||||
}
|
||||
if p.settings.SMTPPort == 0 {
|
||||
return fmt.Errorf("SMTP port is required")
|
||||
}
|
||||
if p.settings.FromEmail == "" {
|
||||
return fmt.Errorf("from email is required")
|
||||
}
|
||||
if p.settings.ToEmail == "" {
|
||||
return fmt.Errorf("to email is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send sends an email notification
|
||||
func (p *EmailProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("[%s] %s - %s", msg.Status, msg.MonitorName, msg.Title)
|
||||
body := p.formatBody(msg)
|
||||
|
||||
// Build email content
|
||||
email := fmt.Sprintf(
|
||||
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
|
||||
p.settings.FromEmail,
|
||||
p.settings.ToEmail,
|
||||
subject,
|
||||
body,
|
||||
)
|
||||
|
||||
// Connect to SMTP server
|
||||
addr := fmt.Sprintf("%s:%d", p.settings.SMTPHost, p.settings.SMTPPort)
|
||||
|
||||
var auth smtp.Auth
|
||||
if p.settings.SMTPUser != "" {
|
||||
auth = smtp.PlainAuth("", p.settings.SMTPUser, p.settings.SMTPPassword, p.settings.SMTPHost)
|
||||
}
|
||||
|
||||
// Send email
|
||||
if p.settings.UseTLS {
|
||||
return p.sendTLS(addr, auth, email)
|
||||
}
|
||||
|
||||
return smtp.SendMail(
|
||||
addr,
|
||||
auth,
|
||||
p.settings.FromEmail,
|
||||
[]string{p.settings.ToEmail},
|
||||
[]byte(email),
|
||||
)
|
||||
}
|
||||
|
||||
// sendTLS sends email using TLS
|
||||
func (p *EmailProvider) sendTLS(addr string, auth smtp.Auth, email string) error {
|
||||
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: p.settings.SMTPHost})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect via TLS: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, p.settings.SMTPHost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if auth != nil {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("authentication failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(p.settings.FromEmail); err != nil {
|
||||
return fmt.Errorf("failed to set sender: %w", err)
|
||||
}
|
||||
if err := client.Rcpt(p.settings.ToEmail); err != nil {
|
||||
return fmt.Errorf("failed to set recipient: %w", err)
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get data writer: %w", err)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(email))
|
||||
if err != nil {
|
||||
w.Close()
|
||||
return fmt.Errorf("failed to write email: %w", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close data writer: %w", err)
|
||||
}
|
||||
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
// formatBody formats the email body
|
||||
func (p *EmailProvider) formatBody(msg *notification.NotificationMessage) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(fmt.Sprintf("Monitor: %s\n", msg.MonitorName))
|
||||
if msg.MonitorURL != "" {
|
||||
b.WriteString(fmt.Sprintf("URL: %s\n", msg.MonitorURL))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Status: %s\n", msg.Status))
|
||||
b.WriteString(fmt.Sprintf("Time: %s\n", msg.Timestamp.Format("2006-01-02 15:04:05")))
|
||||
|
||||
if msg.Ping > 0 {
|
||||
b.WriteString(fmt.Sprintf("Response Time: %dms\n", msg.Ping))
|
||||
}
|
||||
|
||||
if msg.Message != "" {
|
||||
b.WriteString(fmt.Sprintf("\nMessage: %s\n", msg.Message))
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("\n%s\n", msg.Body))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
type GotifyProvider struct {
|
||||
settings notification.GotifySettings
|
||||
}
|
||||
|
||||
func NewGotifyProvider(settings notification.GotifySettings) *GotifyProvider {
|
||||
return &GotifyProvider{settings: settings}
|
||||
}
|
||||
|
||||
func (p *GotifyProvider) Validate() error {
|
||||
if p.settings.ServerURL == "" {
|
||||
return fmt.Errorf("Gotify server URL is required")
|
||||
}
|
||||
if p.settings.AppToken == "" {
|
||||
return fmt.Errorf("Gotify app token is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GotifyProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"title": msg.Title,
|
||||
"message": msg.Body,
|
||||
"priority": p.settings.Priority,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/message?token=%s", p.settings.ServerURL, p.settings.AppToken)
|
||||
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gotify request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("gotify returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
type PushoverProvider struct {
|
||||
settings notification.PushoverSettings
|
||||
}
|
||||
|
||||
func NewPushoverProvider(settings notification.PushoverSettings) *PushoverProvider {
|
||||
return &PushoverProvider{settings: settings}
|
||||
}
|
||||
|
||||
func (p *PushoverProvider) Validate() error {
|
||||
if p.settings.AppToken == "" {
|
||||
return fmt.Errorf("Pushover app token is required")
|
||||
}
|
||||
if p.settings.UserKey == "" {
|
||||
return fmt.Errorf("Pushover user key is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PushoverProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("token", p.settings.AppToken)
|
||||
data.Set("user", p.settings.UserKey)
|
||||
data.Set("title", msg.Title)
|
||||
data.Set("message", msg.Body)
|
||||
data.Set("priority", fmt.Sprintf("%d", p.settings.Priority))
|
||||
|
||||
if p.settings.Device != "" {
|
||||
data.Set("device", p.settings.Device)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.PostForm("https://api.pushover.net/1/messages.json", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pushover API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("pushover API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
type SlackProvider struct {
|
||||
settings notification.SlackSettings
|
||||
}
|
||||
|
||||
func NewSlackProvider(settings notification.SlackSettings) *SlackProvider {
|
||||
return &SlackProvider{settings: settings}
|
||||
}
|
||||
|
||||
func (p *SlackProvider) Validate() error {
|
||||
if p.settings.WebhookURL == "" {
|
||||
return fmt.Errorf("Slack webhook URL is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SlackProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color := "good" // Green for UP
|
||||
if msg.Status == "DOWN" {
|
||||
color = "danger" // Red for DOWN
|
||||
}
|
||||
|
||||
fields := []map[string]string{
|
||||
{
|
||||
"title": "Monitor",
|
||||
"value": msg.MonitorName,
|
||||
"short": "true",
|
||||
},
|
||||
{
|
||||
"title": "Status",
|
||||
"value": msg.Status,
|
||||
"short": "true",
|
||||
},
|
||||
}
|
||||
|
||||
if msg.MonitorURL != "" {
|
||||
fields = append(fields, map[string]string{
|
||||
"title": "URL",
|
||||
"value": msg.MonitorURL,
|
||||
"short": "false",
|
||||
})
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"attachments": []map[string]interface{}{
|
||||
{
|
||||
"color": color,
|
||||
"title": msg.Title,
|
||||
"text": msg.Body,
|
||||
"fields": fields,
|
||||
"timestamp": msg.Timestamp.Unix(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if p.settings.Username != "" {
|
||||
payload["username"] = p.settings.Username
|
||||
}
|
||||
if p.settings.Channel != "" {
|
||||
payload["channel"] = p.settings.Channel
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", p.settings.WebhookURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("slack webhook request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("slack webhook returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
type TelegramProvider struct {
|
||||
settings notification.TelegramSettings
|
||||
}
|
||||
|
||||
func NewTelegramProvider(settings notification.TelegramSettings) *TelegramProvider {
|
||||
return &TelegramProvider{settings: settings}
|
||||
}
|
||||
|
||||
func (p *TelegramProvider) Validate() error {
|
||||
if p.settings.BotToken == "" {
|
||||
return fmt.Errorf("Telegram bot token is required")
|
||||
}
|
||||
if p.settings.ChatID == "" {
|
||||
return fmt.Errorf("Telegram chat ID is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *TelegramProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
icon := "✅"
|
||||
if msg.Status == "DOWN" {
|
||||
icon = "❌"
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("%s *%s*\n\n"+
|
||||
"*Monitor:* %s\n"+
|
||||
"*Status:* %s\n"+
|
||||
"*Time:* %s",
|
||||
icon,
|
||||
msg.Title,
|
||||
msg.MonitorName,
|
||||
msg.Status,
|
||||
msg.Timestamp.Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
|
||||
if msg.MonitorURL != "" {
|
||||
text += fmt.Sprintf("\n*URL:* %s", msg.MonitorURL)
|
||||
}
|
||||
|
||||
if msg.Ping > 0 {
|
||||
text += fmt.Sprintf("\n*Response Time:* %dms", msg.Ping)
|
||||
}
|
||||
|
||||
if msg.Message != "" {
|
||||
text += fmt.Sprintf("\n\n*Message:* %s", msg.Message)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", p.settings.BotToken)
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("chat_id", p.settings.ChatID)
|
||||
data.Set("text", text)
|
||||
data.Set("parse_mode", "Markdown")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.PostForm(apiURL, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("telegram API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("telegram API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/notification"
|
||||
)
|
||||
|
||||
type WebhookProvider struct {
|
||||
settings notification.WebhookSettings
|
||||
}
|
||||
|
||||
func NewWebhookProvider(settings notification.WebhookSettings) *WebhookProvider {
|
||||
return &WebhookProvider{settings: settings}
|
||||
}
|
||||
|
||||
func (p *WebhookProvider) Validate() error {
|
||||
if p.settings.URL == "" {
|
||||
return fmt.Errorf("webhook URL is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *WebhookProvider) Send(msg *notification.NotificationMessage) error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method := p.settings.Method
|
||||
if method == "" {
|
||||
method = "POST"
|
||||
}
|
||||
|
||||
body := p.formatBody(msg)
|
||||
req, err := http.NewRequest(method, p.settings.URL, bytes.NewBufferString(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range p.settings.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
if req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *WebhookProvider) formatBody(msg *notification.NotificationMessage) string {
|
||||
if p.settings.BodyTemplate != "" {
|
||||
return p.settings.BodyTemplate
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"title": msg.Title,
|
||||
"body": msg.Body,
|
||||
"monitor": msg.MonitorName,
|
||||
"url": msg.MonitorURL,
|
||||
"status": msg.Status,
|
||||
"timestamp": msg.Timestamp,
|
||||
"ping": msg.Ping,
|
||||
"message": msg.Message,
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(data)
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
webpush "github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// PushNotification represents a push notification message
|
||||
type PushNotification struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Badge string `json:"badge,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Data map[string]string `json:"data,omitempty"`
|
||||
Actions []Action `json:"actions,omitempty"`
|
||||
RequireInteraction bool `json:"requireInteraction,omitempty"`
|
||||
}
|
||||
|
||||
// Action represents a notification action button
|
||||
type Action struct {
|
||||
Action string `json:"action"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// PushSubscription represents a browser push subscription
|
||||
type PushSubscription struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
Endpoint string `json:"endpoint" db:"endpoint"`
|
||||
P256dh string `json:"p256dh" db:"p256dh"`
|
||||
Auth string `json:"auth" db:"auth"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
}
|
||||
|
||||
// PushService handles push notifications
|
||||
type PushService struct {
|
||||
app core.App
|
||||
vapidPriv string
|
||||
vapidPub string
|
||||
}
|
||||
|
||||
// NewPushService creates a new push notification service
|
||||
func NewPushService(app core.App) *PushService {
|
||||
// Generate or load VAPID keys
|
||||
// In production, load from BESZEL_VAPID_PRIVATE_KEY env var
|
||||
privKey, pubKey := generateVAPIDKeys()
|
||||
|
||||
return &PushService{
|
||||
app: app,
|
||||
vapidPriv: privKey,
|
||||
vapidPub: pubKey,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSubscription registers a push subscription for a user
|
||||
func (s *PushService) RegisterSubscription(userID string, sub *webpush.Subscription) error {
|
||||
collection, err := s.app.FindCollectionByNameOrId("push_subscriptions")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if subscription already exists
|
||||
existing, _ := s.app.FindFirstRecordByFilter("push_subscriptions",
|
||||
"user = {:user} && endpoint = {:endpoint}",
|
||||
map[string]interface{}{"user": userID, "endpoint": sub.Endpoint})
|
||||
|
||||
if existing != nil {
|
||||
// Update existing
|
||||
existing.Set("p256dh", sub.Keys.P256dh)
|
||||
existing.Set("auth", sub.Keys.Auth)
|
||||
return s.app.Save(existing)
|
||||
}
|
||||
|
||||
// Create new subscription
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("user", userID)
|
||||
record.Set("endpoint", sub.Endpoint)
|
||||
record.Set("p256dh", sub.Keys.P256dh)
|
||||
record.Set("auth", sub.Keys.Auth)
|
||||
record.Set("created", time.Now())
|
||||
|
||||
return s.app.Save(record)
|
||||
}
|
||||
|
||||
// UnregisterSubscription removes a push subscription
|
||||
func (s *PushService) UnregisterSubscription(userID string, endpoint string) error {
|
||||
record, err := s.app.FindFirstRecordByFilter("push_subscriptions",
|
||||
"user = {:user} && endpoint = {:endpoint}",
|
||||
map[string]interface{}{"user": userID, "endpoint": endpoint})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.app.Delete(record)
|
||||
}
|
||||
|
||||
// SendNotification sends a push notification to a user
|
||||
func (s *PushService) SendNotification(userID string, notification *PushNotification) error {
|
||||
// Get all subscriptions for user
|
||||
records, err := s.app.FindAllRecords("push_subscriptions",
|
||||
dbx.NewExp("user = {:user}", dbx.Params{"user": userID}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
sub := &webpush.Subscription{
|
||||
Endpoint: record.GetString("endpoint"),
|
||||
Keys: webpush.Keys{
|
||||
P256dh: record.GetString("p256dh"),
|
||||
Auth: record.GetString("auth"),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := webpush.SendNotification(payload, sub, &webpush.Options{
|
||||
Subscriber: "beszel@localhost",
|
||||
VAPIDPublicKey: s.vapidPub,
|
||||
VAPIDPrivateKey: s.vapidPriv,
|
||||
TTL: 30,
|
||||
})
|
||||
if err != nil {
|
||||
// Log error but continue trying other subscriptions
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastNotification sends notification to all users
|
||||
func (s *PushService) BroadcastNotification(notification *PushNotification) error {
|
||||
records, err := s.app.FindAllRecords("push_subscriptions")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
sub := &webpush.Subscription{
|
||||
Endpoint: record.GetString("endpoint"),
|
||||
Keys: webpush.Keys{
|
||||
P256dh: record.GetString("p256dh"),
|
||||
Auth: record.GetString("auth"),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := webpush.SendNotification(payload, sub, &webpush.Options{
|
||||
Subscriber: "beszel@localhost",
|
||||
VAPIDPublicKey: s.vapidPub,
|
||||
VAPIDPrivateKey: s.vapidPriv,
|
||||
TTL: 30,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateVAPIDKeys generates or loads VAPID keys for web push
|
||||
func generateVAPIDKeys() (privateKey, publicKey string) {
|
||||
// Check for environment variable first
|
||||
if envKey := os.Getenv("BESZEL_VAPID_PRIVATE_KEY"); envKey != "" {
|
||||
// If private key provided, we need to derive public key
|
||||
// For now, return empty public key - will be handled by webpush lib
|
||||
return envKey, ""
|
||||
}
|
||||
|
||||
// Generate new VAPID key pair
|
||||
privKey, pubKey, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
// Return empty keys if generation fails
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return privKey, pubKey
|
||||
}
|
||||
|
||||
// GetVAPIDPublicKey returns the VAPID public key for client subscription
|
||||
func (s *PushService) GetVAPIDPublicKey() string {
|
||||
return s.vapidPub
|
||||
}
|
||||
Reference in New Issue
Block a user