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 }