package services import ( "context" "encoding/json" "fmt" "github.com/SherClockHolmes/webpush-go" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) // PushSubscription represents a Web Push subscription (v0.302 N1) type PushSubscription struct { ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` Endpoint string `gorm:"type:text;not null" json:"endpoint"` P256dh string `gorm:"type:text;not null" json:"p256dh"` Auth string `gorm:"type:text;not null" json:"auth"` CreatedAt string `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` } func (PushSubscription) TableName() string { return "push_subscriptions" } // PushService handles Web Push subscriptions and sending type PushService struct { db *gorm.DB logger *zap.Logger vapidPublicKey string vapidPrivateKey string } // NewPushService creates a new PushService func NewPushService(db *gorm.DB, logger *zap.Logger, vapidPublic, vapidPrivate string) *PushService { return &PushService{ db: db, logger: logger, vapidPublicKey: vapidPublic, vapidPrivateKey: vapidPrivate, } } // SubscribePush stores a Web Push subscription for a user func (s *PushService) SubscribePush(ctx context.Context, userID uuid.UUID, endpoint, p256dh, auth string) error { sub := &PushSubscription{ UserID: userID, Endpoint: endpoint, P256dh: p256dh, Auth: auth, } if err := s.db.WithContext(ctx).Create(sub).Error; err != nil { return fmt.Errorf("failed to store subscription: %w", err) } s.logger.Info("Push subscription stored", zap.String("user_id", userID.String())) return nil } // SendPushToUser sends a Web Push notification to all subscriptions of a user func (s *PushService) SendPushToUser(ctx context.Context, userID uuid.UUID, title, body, link string) error { if s.vapidPrivateKey == "" { return nil } var subs []PushSubscription if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&subs).Error; err != nil { return err } payload, _ := json.Marshal(map[string]string{ "title": title, "body": body, "link": link, }) for _, ps := range subs { subscription := webpush.Subscription{ Endpoint: ps.Endpoint, Keys: webpush.Keys{ P256dh: ps.P256dh, Auth: ps.Auth, }, } _, err := webpush.SendNotification(payload, &subscription, &webpush.Options{ VAPIDPublicKey: s.vapidPublicKey, VAPIDPrivateKey: s.vapidPrivateKey, TTL: 30, }) if err != nil { s.logger.Warn("Failed to send push", zap.Error(err), zap.String("user_id", userID.String())) } } return nil }