100 lines
3.2 KiB
Go
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
|
|
}
|