mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
204 lines
5.5 KiB
Go
204 lines
5.5 KiB
Go
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
|
|
}
|