Files
observatory/internal/storage/push.go
T
gamertan 92a66db3df docs: publish Preview 19 dogfood evidence
Export the reviewed allowlisted snapshot from private source commit 05928cebd01b586cf9e9d4b8c8537a7605a6068c. This records the exact candidate, bounded capacity result, stateful migration scratch requirement, authenticated batch identity proof, and immediate live acceptance evidence.

AI-Assisted: OpenAI Codex
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-18 21:47:08 -04:00

239 lines
11 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only
package storage
import (
"context"
"crypto/sha256"
"database/sql"
"errors"
"time"
"unicode/utf8"
"gamertan.com/observatory/internal/model"
)
const (
MaxPushSubscriptionsPerUser = 8
MaxPushSubscriptionsPerPass = 256
)
type PushSubscription struct {
OrganizationID string
ID string
UserID string
Endpoint string
P256DH []byte
Auth []byte
FailureCount int
CreatedAt time.Time
UpdatedAt time.Time
LastSentAt *time.Time
}
type PushSubscriptionInput struct {
OrganizationID string
UserID string
Endpoint string
P256DH []byte
Auth []byte
}
func (s *Store) SavePushSubscription(ctx context.Context, input PushSubscriptionInput, now time.Time) (PushSubscription, error) {
if err := validatePushSubscription(input); err != nil {
return PushSubscription{}, err
}
digest := sha256.Sum256([]byte(input.Endpoint))
tx, err := s.control.BeginTx(ctx, nil)
if err != nil {
return PushSubscription{}, errors.New("save push subscription")
}
defer tx.Rollback()
var endpointID, endpointUser string
err = tx.QueryRowContext(ctx, `SELECT id,user_id FROM push_endpoints WHERE endpoint_digest=?`, digest[:]).Scan(&endpointID, &endpointUser)
switch {
case err == nil:
if endpointUser != input.UserID {
return PushSubscription{}, errors.New("push subscription is already registered")
}
_, err = tx.ExecContext(ctx, `UPDATE push_endpoints SET endpoint=?,p256dh=?,auth_secret=?,active=1,failure_count=0,updated_at=? WHERE id=?`, input.Endpoint, input.P256DH, input.Auth, now.UTC().Format(time.RFC3339Nano), endpointID)
case errors.Is(err, sql.ErrNoRows):
endpointID, err = storageID("endpoint")
if err == nil {
stamp := now.UTC().Format(time.RFC3339Nano)
_, err = tx.ExecContext(ctx, `INSERT INTO push_endpoints(id,user_id,endpoint,endpoint_digest,p256dh,auth_secret,active,failure_count,created_at,updated_at) VALUES(?,?,?,?,?,?,1,0,?,?)`, endpointID, input.UserID, input.Endpoint, digest[:], input.P256DH, input.Auth, stamp, stamp)
}
default:
return PushSubscription{}, errors.New("save push subscription")
}
if err != nil {
return PushSubscription{}, errors.New("save push subscription")
}
var subscriptionID string
err = tx.QueryRowContext(ctx, `SELECT id FROM push_subscriptions WHERE organization_id=? AND user_id=? AND endpoint_id=?`, input.OrganizationID, input.UserID, endpointID).Scan(&subscriptionID)
if errors.Is(err, sql.ErrNoRows) {
var count int
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM push_subscriptions WHERE organization_id=? AND user_id=?`, input.OrganizationID, input.UserID).Scan(&count); err != nil {
return PushSubscription{}, errors.New("save push subscription")
}
if count >= MaxPushSubscriptionsPerUser {
return PushSubscription{}, errors.New("push subscription limit reached")
}
subscriptionID, err = storageID("push")
if err == nil {
_, err = tx.ExecContext(ctx, `INSERT INTO push_subscriptions(organization_id,id,user_id,endpoint_id,created_at) VALUES(?,?,?,?,?)`, input.OrganizationID, subscriptionID, input.UserID, endpointID, now.UTC().Format(time.RFC3339Nano))
}
}
if err != nil {
return PushSubscription{}, errors.New("save push subscription")
}
if err = tx.Commit(); err != nil {
return PushSubscription{}, errors.New("save push subscription")
}
return s.PushSubscription(ctx, input.OrganizationID, subscriptionID)
}
func (s *Store) HasPushSubscription(ctx context.Context, organizationID, userID, endpoint string) (bool, error) {
if model.ValidateSourceID(organizationID) != nil || model.ValidateSourceID(userID) != nil || !utf8.ValidString(endpoint) || len(endpoint) < 1 || len(endpoint) > 2048 {
return false, errors.New("push subscription lookup is invalid")
}
digest := sha256.Sum256([]byte(endpoint))
var count int
err := s.control.QueryRowContext(ctx, `SELECT COUNT(*) FROM push_subscriptions s JOIN push_endpoints e ON e.id=s.endpoint_id WHERE s.organization_id=? AND s.user_id=? AND e.endpoint_digest=? AND e.active=1`, organizationID, userID, digest[:]).Scan(&count)
if err != nil {
return false, errors.New("lookup push subscription")
}
return count == 1, nil
}
// DeletePushSubscription removes one organization mapping. The returned
// value reports whether the browser endpoint remains mapped elsewhere for
// the same user and therefore must remain subscribed in the user agent.
func (s *Store) DeletePushSubscription(ctx context.Context, organizationID, userID, endpoint string) (bool, error) {
if model.ValidateSourceID(organizationID) != nil || model.ValidateSourceID(userID) != nil || !utf8.ValidString(endpoint) || len(endpoint) < 1 || len(endpoint) > 2048 {
return false, errors.New("push subscription deletion is invalid")
}
digest := sha256.Sum256([]byte(endpoint))
tx, err := s.control.BeginTx(ctx, nil)
if err != nil {
return false, errors.New("delete push subscription")
}
defer tx.Rollback()
var endpointID string
err = tx.QueryRowContext(ctx, `SELECT e.id FROM push_subscriptions s JOIN push_endpoints e ON e.id=s.endpoint_id WHERE s.organization_id=? AND s.user_id=? AND e.endpoint_digest=?`, organizationID, userID, digest[:]).Scan(&endpointID)
if errors.Is(err, sql.ErrNoRows) {
return false, errors.New("push subscription not found")
}
if err != nil {
return false, errors.New("delete push subscription")
}
if _, err = tx.ExecContext(ctx, `DELETE FROM push_subscriptions WHERE organization_id=? AND user_id=? AND endpoint_id=?`, organizationID, userID, endpointID); err != nil {
return false, errors.New("delete push subscription")
}
var remaining int
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM push_subscriptions WHERE endpoint_id=?`, endpointID).Scan(&remaining); err != nil {
return false, errors.New("delete push subscription")
}
if remaining == 0 {
if _, err = tx.ExecContext(ctx, `DELETE FROM push_endpoints WHERE id=?`, endpointID); err != nil {
return false, errors.New("delete push subscription")
}
}
if err = tx.Commit(); err != nil {
return false, errors.New("delete push subscription")
}
return remaining > 0, nil
}
func (s *Store) PushSubscription(ctx context.Context, organizationID, id string) (PushSubscription, error) {
if model.ValidateSourceID(organizationID) != nil || model.ValidateSourceID(id) != nil {
return PushSubscription{}, errors.New("push subscription identity is invalid")
}
return scanPushSubscription(s.control.QueryRowContext(ctx, `SELECT s.organization_id,s.id,s.user_id,e.endpoint,e.p256dh,e.auth_secret,e.failure_count,s.created_at,e.updated_at,e.last_sent_at FROM push_subscriptions s JOIN push_endpoints e ON e.id=s.endpoint_id WHERE s.organization_id=? AND s.id=? AND e.active=1`, organizationID, id))
}
func (s *Store) PushSubscriptions(ctx context.Context, organizationID string) ([]PushSubscription, error) {
if model.ValidateSourceID(organizationID) != nil {
return nil, errors.New("organization identity is invalid")
}
rows, err := s.control.QueryContext(ctx, `SELECT s.organization_id,s.id,s.user_id,e.endpoint,e.p256dh,e.auth_secret,e.failure_count,s.created_at,e.updated_at,e.last_sent_at FROM push_subscriptions s JOIN push_endpoints e ON e.id=s.endpoint_id WHERE s.organization_id=? AND e.active=1 ORDER BY s.id LIMIT ?`, organizationID, MaxPushSubscriptionsPerPass)
if err != nil {
return nil, errors.New("list push subscriptions")
}
defer rows.Close()
var subscriptions []PushSubscription
for rows.Next() {
subscription, scanErr := scanPushSubscription(rows)
if scanErr != nil {
return nil, scanErr
}
subscriptions = append(subscriptions, subscription)
}
if err = rows.Err(); err != nil {
return nil, errors.New("list push subscriptions")
}
return subscriptions, nil
}
func (s *Store) RecordPushResult(ctx context.Context, organizationID, id string, outcome string, now time.Time) error {
if model.ValidateSourceID(organizationID) != nil || model.ValidateSourceID(id) != nil {
return errors.New("push subscription identity is invalid")
}
var result sql.Result
var err error
switch outcome {
case "sent":
result, err = s.control.ExecContext(ctx, `UPDATE push_endpoints SET failure_count=0,last_sent_at=?,updated_at=? WHERE id=(SELECT endpoint_id FROM push_subscriptions WHERE organization_id=? AND id=?) AND active=1`, now.UTC().Format(time.RFC3339Nano), now.UTC().Format(time.RFC3339Nano), organizationID, id)
case "gone":
result, err = s.control.ExecContext(ctx, `UPDATE push_endpoints SET active=0,updated_at=? WHERE id=(SELECT endpoint_id FROM push_subscriptions WHERE organization_id=? AND id=?) AND active=1`, now.UTC().Format(time.RFC3339Nano), organizationID, id)
case "failed":
result, err = s.control.ExecContext(ctx, `UPDATE push_endpoints SET failure_count=failure_count+1,active=CASE WHEN failure_count+1>=5 THEN 0 ELSE 1 END,updated_at=? WHERE id=(SELECT endpoint_id FROM push_subscriptions WHERE organization_id=? AND id=?) AND active=1`, now.UTC().Format(time.RFC3339Nano), organizationID, id)
default:
return errors.New("push delivery outcome is invalid")
}
if err != nil {
return errors.New("record push delivery result")
}
if changed, _ := result.RowsAffected(); changed != 1 {
return errors.New("push subscription not found")
}
return nil
}
func scanPushSubscription(row rowScanner) (PushSubscription, error) {
var subscription PushSubscription
var createdAt, updatedAt string
var lastSent sql.NullString
if err := row.Scan(&subscription.OrganizationID, &subscription.ID, &subscription.UserID, &subscription.Endpoint, &subscription.P256DH, &subscription.Auth, &subscription.FailureCount, &createdAt, &updatedAt, &lastSent); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return PushSubscription{}, errors.New("push subscription not found")
}
return PushSubscription{}, errors.New("read push subscription")
}
var err error
if subscription.CreatedAt, err = time.Parse(time.RFC3339Nano, createdAt); err != nil {
return PushSubscription{}, errors.New("read push subscription")
}
if subscription.UpdatedAt, err = time.Parse(time.RFC3339Nano, updatedAt); err != nil {
return PushSubscription{}, errors.New("read push subscription")
}
if lastSent.Valid {
parsed, parseErr := time.Parse(time.RFC3339Nano, lastSent.String)
if parseErr != nil {
return PushSubscription{}, errors.New("read push subscription")
}
subscription.LastSentAt = &parsed
}
return subscription, nil
}
func validatePushSubscription(input PushSubscriptionInput) error {
if model.ValidateSourceID(input.OrganizationID) != nil || model.ValidateSourceID(input.UserID) != nil {
return errors.New("push subscription scope is invalid")
}
if !utf8.ValidString(input.Endpoint) || len(input.Endpoint) < 1 || len(input.Endpoint) > 2048 || len(input.P256DH) != 65 || len(input.Auth) != 16 {
return errors.New("push subscription material is invalid")
}
return nil
}