mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): add webpush package (#17091)
* Adds `codersdk.ExperimentWebPush` (`web-push`) * Adds a `coderd/webpush` package that allows sending native push notifications via `github.com/SherClockHolmes/webpush-go` * Adds database tables to store push notification subscriptions. * Adds an API endpoint that allows users to subscribe/unsubscribe, and send a test notification (404 without experiment, excluded from API docs) * Adds server CLI command to regenerate VAPID keys (note: regenerating the VAPID keypair requires deleting all existing subscriptions) --------- Co-authored-by: Kyle Carberry <kyle@carberry.com>
This commit is contained in:
co-authored by
Kyle Carberry
parent
006600ea3e
commit
06e5d9ef21
@@ -3988,6 +3988,19 @@ func (q *sqlQuerier) BulkMarkNotificationMessagesSent(ctx context.Context, arg B
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
const deleteAllWebpushSubscriptions = `-- name: DeleteAllWebpushSubscriptions :exec
|
||||
TRUNCATE TABLE webpush_subscriptions
|
||||
`
|
||||
|
||||
// Deletes all existing webpush subscriptions.
|
||||
// This should be called when the VAPID keypair is regenerated, as the old
|
||||
// keypair will no longer be valid and all existing subscriptions will need to
|
||||
// be recreated.
|
||||
func (q *sqlQuerier) DeleteAllWebpushSubscriptions(ctx context.Context) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAllWebpushSubscriptions)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteOldNotificationMessages = `-- name: DeleteOldNotificationMessages :exec
|
||||
DELETE
|
||||
FROM notification_messages
|
||||
@@ -4003,6 +4016,31 @@ func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteWebpushSubscriptionByUserIDAndEndpoint = `-- name: DeleteWebpushSubscriptionByUserIDAndEndpoint :exec
|
||||
DELETE FROM webpush_subscriptions
|
||||
WHERE user_id = $1 AND endpoint = $2
|
||||
`
|
||||
|
||||
type DeleteWebpushSubscriptionByUserIDAndEndpointParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
Endpoint string `db:"endpoint" json:"endpoint"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteWebpushSubscriptionByUserIDAndEndpoint, arg.UserID, arg.Endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteWebpushSubscriptions = `-- name: DeleteWebpushSubscriptions :exec
|
||||
DELETE FROM webpush_subscriptions
|
||||
WHERE id = ANY($1::uuid[])
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteWebpushSubscriptions, pq.Array(ids))
|
||||
return err
|
||||
}
|
||||
|
||||
const enqueueNotificationMessage = `-- name: EnqueueNotificationMessage :exec
|
||||
INSERT INTO notification_messages (id, notification_template_id, user_id, method, payload, targets, created_by, created_at)
|
||||
VALUES ($1,
|
||||
@@ -4255,6 +4293,76 @@ func (q *sqlQuerier) GetUserNotificationPreferences(ctx context.Context, userID
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWebpushSubscriptionsByUserID = `-- name: GetWebpushSubscriptionsByUserID :many
|
||||
SELECT id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key
|
||||
FROM webpush_subscriptions
|
||||
WHERE user_id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]WebpushSubscription, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWebpushSubscriptionsByUserID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WebpushSubscription
|
||||
for rows.Next() {
|
||||
var i WebpushSubscription
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Endpoint,
|
||||
&i.EndpointP256dhKey,
|
||||
&i.EndpointAuthKey,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWebpushSubscription = `-- name: InsertWebpushSubscription :one
|
||||
INSERT INTO webpush_subscriptions (user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key
|
||||
`
|
||||
|
||||
type InsertWebpushSubscriptionParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
Endpoint string `db:"endpoint" json:"endpoint"`
|
||||
EndpointP256dhKey string `db:"endpoint_p256dh_key" json:"endpoint_p256dh_key"`
|
||||
EndpointAuthKey string `db:"endpoint_auth_key" json:"endpoint_auth_key"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertWebpushSubscription(ctx context.Context, arg InsertWebpushSubscriptionParams) (WebpushSubscription, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertWebpushSubscription,
|
||||
arg.UserID,
|
||||
arg.CreatedAt,
|
||||
arg.Endpoint,
|
||||
arg.EndpointP256dhKey,
|
||||
arg.EndpointAuthKey,
|
||||
)
|
||||
var i WebpushSubscription
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
&i.Endpoint,
|
||||
&i.EndpointP256dhKey,
|
||||
&i.EndpointAuthKey,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateNotificationTemplateMethodByID = `-- name: UpdateNotificationTemplateMethodByID :one
|
||||
UPDATE notification_templates
|
||||
SET method = $1::notification_method
|
||||
@@ -8561,6 +8669,24 @@ func (q *sqlQuerier) GetRuntimeConfig(ctx context.Context, key string) (string,
|
||||
return value, err
|
||||
}
|
||||
|
||||
const getWebpushVAPIDKeys = `-- name: GetWebpushVAPIDKeys :one
|
||||
SELECT
|
||||
COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_public_key'), '') :: text AS vapid_public_key,
|
||||
COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_private_key'), '') :: text AS vapid_private_key
|
||||
`
|
||||
|
||||
type GetWebpushVAPIDKeysRow struct {
|
||||
VapidPublicKey string `db:"vapid_public_key" json:"vapid_public_key"`
|
||||
VapidPrivateKey string `db:"vapid_private_key" json:"vapid_private_key"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWebpushVAPIDKeys(ctx context.Context) (GetWebpushVAPIDKeysRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWebpushVAPIDKeys)
|
||||
var i GetWebpushVAPIDKeysRow
|
||||
err := row.Scan(&i.VapidPublicKey, &i.VapidPrivateKey)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertDERPMeshKey = `-- name: InsertDERPMeshKey :exec
|
||||
INSERT INTO site_configs (key, value) VALUES ('derp_mesh_key', $1)
|
||||
`
|
||||
@@ -8729,6 +8855,25 @@ func (q *sqlQuerier) UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeC
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertWebpushVAPIDKeys = `-- name: UpsertWebpushVAPIDKeys :exec
|
||||
INSERT INTO site_configs (key, value)
|
||||
VALUES
|
||||
('webpush_vapid_public_key', $1 :: text),
|
||||
('webpush_vapid_private_key', $2 :: text)
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
|
||||
`
|
||||
|
||||
type UpsertWebpushVAPIDKeysParams struct {
|
||||
VapidPublicKey string `db:"vapid_public_key" json:"vapid_public_key"`
|
||||
VapidPrivateKey string `db:"vapid_private_key" json:"vapid_private_key"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpsertWebpushVAPIDKeys(ctx context.Context, arg UpsertWebpushVAPIDKeysParams) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertWebpushVAPIDKeys, arg.VapidPublicKey, arg.VapidPrivateKey)
|
||||
return err
|
||||
}
|
||||
|
||||
const cleanTailnetCoordinators = `-- name: CleanTailnetCoordinators :exec
|
||||
DELETE
|
||||
FROM tailnet_coordinators
|
||||
|
||||
Reference in New Issue
Block a user