veza/veza-backend-api/internal/services/push_service.go
senke 49e3122e78 feat(notifications): N1.1-N1.3 Web Push subscription, send on events, preferences
- N1.1: POST /notifications/push/subscribe, PushService, migration 090
- N1.2: Send Web Push on follow/like/comment/message via CreateNotification
- N1.3: GET/PUT /notifications/preferences, migration 093
- Shared NotificationService with PushService for profile, track, comment handlers
- Fix MockSocialService GetGlobalFeed, GetTrendingHashtags for tests
2026-02-21 16:41:39 +01:00

93 lines
2.7 KiB
Go

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
}