Files

100 lines
3.2 KiB
Go

package repository
import (
"context"
"fmt"
"github.com/prabhupadasays/spsays-bff/internal/model"
)
// PushRepo stores Web Push subscriptions (one per browser/device endpoint).
type PushRepo struct{ pg *Postgres }
func NewPushRepo(pg *Postgres) *PushRepo { return &PushRepo{pg: pg} }
// Upsert records (or refreshes) a subscription. The endpoint is unique, so a
// browser re-subscribing — possibly under a different user — rebinds cleanly.
func (r *PushRepo) Upsert(ctx context.Context, userID, endpoint, p256dh, auth, userAgent string) error {
_, err := r.pg.Pool.Exec(ctx, `
INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth, user_agent)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (endpoint) DO UPDATE
SET user_id = EXCLUDED.user_id,
p256dh = EXCLUDED.p256dh,
auth = EXCLUDED.auth,
user_agent = EXCLUDED.user_agent,
last_used_at = now()`,
userID, endpoint, p256dh, auth, userAgent,
)
if err != nil {
return fmt.Errorf("upsert push subscription: %w", err)
}
return nil
}
// ListByUser returns the user's active subscriptions.
func (r *PushRepo) ListByUser(ctx context.Context, userID string) ([]model.PushSubscription, error) {
rows, err := r.pg.Pool.Query(ctx, `
SELECT id, user_id, endpoint, p256dh, auth
FROM push_subscriptions WHERE user_id = $1`, userID)
if err != nil {
return nil, fmt.Errorf("list push subscriptions: %w", err)
}
defer rows.Close()
var out []model.PushSubscription
for rows.Next() {
var s model.PushSubscription
if err := rows.Scan(&s.ID, &s.UserID, &s.Endpoint, &s.P256dh, &s.Auth); err != nil {
return nil, fmt.Errorf("scan push subscription: %w", err)
}
out = append(out, s)
}
return out, rows.Err()
}
// PruneToMostRecent keeps only the `keep` most-recently-used subscriptions for
// a user, deleting older ones — a soft per-user device cap.
func (r *PushRepo) PruneToMostRecent(ctx context.Context, userID string, keep int) error {
if keep < 1 {
keep = 1
}
_, err := r.pg.Pool.Exec(ctx, `
DELETE FROM push_subscriptions
WHERE user_id = $1 AND id NOT IN (
SELECT id FROM push_subscriptions
WHERE user_id = $1
ORDER BY last_used_at DESC NULLS LAST
LIMIT $2
)`,
userID, keep,
)
if err != nil {
return fmt.Errorf("prune push subscriptions: %w", err)
}
return nil
}
// DeleteByEndpoint removes a subscription (when the push service reports it
// gone). Server-internal — user-initiated unsubscribes must go through
// DeleteByEndpointForUser so one user can't remove another's subscription.
func (r *PushRepo) DeleteByEndpoint(ctx context.Context, endpoint string) error {
_, err := r.pg.Pool.Exec(ctx,
`DELETE FROM push_subscriptions WHERE endpoint = $1`, endpoint)
if err != nil {
return fmt.Errorf("delete push subscription: %w", err)
}
return nil
}
// DeleteByEndpointForUser removes a subscription only if it belongs to the
// given user — the ownership-checked variant for the unsubscribe endpoint.
func (r *PushRepo) DeleteByEndpointForUser(ctx context.Context, userID, endpoint string) error {
_, err := r.pg.Pool.Exec(ctx,
`DELETE FROM push_subscriptions WHERE endpoint = $1 AND user_id = $2`,
endpoint, userID)
if err != nil {
return fmt.Errorf("delete push subscription: %w", err)
}
return nil
}