Skip to content

feat(coderd): add webpush package #17091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a980379
wip
kylecarbs Mar 17, 2025
bc02200
fix migration number
johnstcn Mar 25, 2025
2ecae9d
make fmt
johnstcn Mar 25, 2025
84e3ace
fix tests
johnstcn Mar 25, 2025
377eaab
add cli command to regenerate vapid keypair and remove existing subsc…
johnstcn Mar 25, 2025
982acef
remove dbauthz system usage
johnstcn Mar 25, 2025
d82073e
add test endpoint for push notifications
johnstcn Mar 25, 2025
fc59c70
make linter happy
johnstcn Mar 25, 2025
cbff3ca
test push notifications endpoint
johnstcn Mar 25, 2025
9e7e1dc
add deployment config for push notifications
johnstcn Mar 25, 2025
1315a46
add test for push notifications being enabled
johnstcn Mar 25, 2025
85db78c
rename migration
johnstcn Mar 25, 2025
892388a
skip vapid keypair test on non-postgres;
johnstcn Mar 25, 2025
e600b7d
fix some tests
johnstcn Mar 25, 2025
eef2038
hide subscribe button
johnstcn Mar 25, 2025
46f7519
conditionally hide push notification button
johnstcn Mar 25, 2025
4408090
Revert "conditionally hide push notification button"
johnstcn Mar 25, 2025
204ab4a
fix data race caused by webpush-go mutating msg []byte
johnstcn Mar 25, 2025
0535ed6
Remove deployment config for push notifications in favour of Experime…
johnstcn Mar 26, 2025
9f1f4f9
push notification -> webpush
johnstcn Mar 26, 2025
29bba04
notification -> webpush
johnstcn Mar 26, 2025
46c2cd8
move to webpush package
johnstcn Mar 26, 2025
960c5db
webpush: refactor notification test and send logic
johnstcn Mar 26, 2025
aa22161
move endpoints under webpush beside notifications
johnstcn Mar 26, 2025
57d84a9
skip apidocgen for push notification endpoints
johnstcn Mar 26, 2025
ef22b35
make webpush endpoints 404 if experiment not enabled
johnstcn Mar 26, 2025
9a1a605
auth on test notification endpoint
johnstcn Mar 26, 2025
e8b6083
fix ts
johnstcn Mar 26, 2025
5331f9c
ui fixes
johnstcn Mar 26, 2025
449f882
fixup migrations
johnstcn Mar 26, 2025
e5fd00e
fix check_unstaged.sh again
johnstcn Mar 26, 2025
bcf108a
make bee jenn
johnstcn Mar 26, 2025
eb7b102
address comments
johnstcn Mar 26, 2025
9b5ed09
address more comments + renaming
johnstcn Mar 26, 2025
c06558e
move out check_unstaged.sh changes to separate PR
johnstcn Mar 26, 2025
10ac9fb
remove site changes
johnstcn Mar 26, 2025
a114ddb
fixup! remove site changes
johnstcn Mar 26, 2025
ca72676
nits
johnstcn Mar 27, 2025
3b1dc70
make gen
johnstcn Mar 27, 2025
12808f1
gen
johnstcn Mar 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
address comments
  • Loading branch information
johnstcn committed Mar 26, 2025
commit eb7b1028b865c89eb6fdfca430487bbf5ff13ab0
4 changes: 2 additions & 2 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,9 +787,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
Msg: "Web Push notifications are disabled due to a system error. Please contact your Coder administrator.",
}
}
options.WebpushDispatcher = webpusher
options.WebPushDispatcher = webpusher
} else {
options.WebpushDispatcher = &webpush.NoopWebpusher{
options.WebPushDispatcher = &webpush.NoopWebpusher{
// Users will likely not see this message as the endpoints return 404
// if not enabled. Just in case...
Msg: "Web Push notifications are an experimental feature and are disabled by default. Enable the 'web-push' experiment to use this feature.",
Expand Down
6 changes: 3 additions & 3 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ type Options struct {
OIDCConvertKeyCache cryptokeys.SigningKeycache
Clock quartz.Clock

// WebpushDispatcher is a way to send notifications over Web Push.
WebpushDispatcher webpush.Dispatcher
// WebPushDispatcher is a way to send notifications over Web Push.
WebPushDispatcher webpush.Dispatcher
}

// @title Coder API
Expand Down Expand Up @@ -550,7 +550,7 @@ func New(options *Options) *API {
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
AccessControlStore: options.AccessControlStore,
Experiments: experiments,
WebpushDispatcher: options.WebpushDispatcher,
WebpushDispatcher: options.WebPushDispatcher,
healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{},
Acquirer: provisionerdserver.NewAcquirer(
ctx,
Expand Down
4 changes: 2 additions & 2 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
// nolint:gocritic // Gets/sets VAPID keys.
pushNotifier, err := webpush.New(dbauthz.AsNotifier(context.Background()), options.Logger, options.Database)
if err != nil {
panic(xerrors.Errorf("failed to create push notifier: %w", err))
panic(xerrors.Errorf("failed to create web push notifier: %w", err))
}
options.WebpushDispatcher = pushNotifier
}
Expand Down Expand Up @@ -541,7 +541,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
TrialGenerator: options.TrialGenerator,
RefreshEntitlements: options.RefreshEntitlements,
TailnetCoordinator: options.Coordinator,
WebpushDispatcher: options.WebpushDispatcher,
WebPushDispatcher: options.WebpushDispatcher,
BaseDERPMap: derpMap,
DERPMapUpdateFrequency: 150 * time.Millisecond,
CoordinatorResumeTokenProvider: options.CoordinatorResumeTokenProvider,
Expand Down
2 changes: 2 additions & 0 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -2453,6 +2453,8 @@ func (q *FakeQuerier) DeleteWebpushSubscriptionByUserIDAndEndpoint(_ context.Con
}

func (q *FakeQuerier) DeleteWebpushSubscriptions(_ context.Context, ids []uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for i, subscription := range q.webpushSubscriptions {
if slices.Contains(ids, subscription.ID) {
q.webpushSubscriptions[i] = q.webpushSubscriptions[len(q.webpushSubscriptions)-1]
Expand Down
29 changes: 26 additions & 3 deletions coderd/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package coderd

import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"net/http"
"slices"

"github.com/google/uuid"

Expand Down Expand Up @@ -396,11 +399,31 @@ func (api *API) deleteUserWebpushSubscription(rw http.ResponseWriter, r *http.Re
return
}

err := api.Database.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, database.DeleteWebpushSubscriptionByUserIDAndEndpointParams{
// Return NotFound if the subscription does not exist.
if existing, err := api.Database.GetWebpushSubscriptionsByUserID(ctx, user.ID); err != nil && errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: "Webpush subscription not found.",
})
return
} else if idx := slices.IndexFunc(existing, func(s database.WebpushSubscription) bool {
return s.Endpoint == req.Endpoint
}); idx == -1 {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: "Webpush subscription not found.",
})
return
}

if err := api.Database.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, database.DeleteWebpushSubscriptionByUserIDAndEndpointParams{
UserID: user.ID,
Endpoint: req.Endpoint,
})
if err != nil {
}); err != nil {
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: "Webpush subscription not found.",
})
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to delete push notification subscription.",
Detail: err.Error(),
Expand Down
26 changes: 25 additions & 1 deletion coderd/notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ func TestWebpushSubscribeUnsubscribe(t *testing.T) {
})
owner := coderdtest.CreateFirstUser(t, client)
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
_, anotherMember := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

handlerCalled := make(chan bool, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
Expand All @@ -414,11 +415,34 @@ func TestWebpushSubscribeUnsubscribe(t *testing.T) {
require.True(t, <-handlerCalled, "handler should have been called")

err = memberClient.PostTestWebpushMessage(ctx)
require.NoError(t, err, "test web push notification")
require.NoError(t, err, "test webpush message")
require.True(t, <-handlerCalled, "handler should have been called again")

err = memberClient.DeleteWebpushSubscription(ctx, "me", codersdk.DeleteWebpushSubscription{
Endpoint: server.URL,
})
require.NoError(t, err, "delete webpush subscription")

// Deleting the subscription for a non-existent endpoint should return a 404
err = memberClient.DeleteWebpushSubscription(ctx, "me", codersdk.DeleteWebpushSubscription{
Endpoint: server.URL,
})
var sdkError *codersdk.Error
require.Error(t, err)
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
require.Equal(t, http.StatusNotFound, sdkError.StatusCode())

// Creating a subscription for another user should not be allowed.
err = memberClient.PostWebpushSubscription(ctx, anotherMember.ID.String(), codersdk.WebpushSubscription{
Endpoint: server.URL,
AuthKey: validEndpointAuthKey,
P256DHKey: validEndpointP256dhKey,
})
require.Error(t, err, "create webpush subscription for another user")

// Deleting a subscription for another user should not be allowed.
err = memberClient.DeleteWebpushSubscription(ctx, anotherMember.ID.String(), codersdk.DeleteWebpushSubscription{
Endpoint: server.URL,
})
require.Error(t, err, "delete webpush subscription for another user")
}
4 changes: 4 additions & 0 deletions coderd/webpush/webpush.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ func (n *Webpusher) Dispatch(ctx context.Context, userID uuid.UUID, notification
for _, subscription := range subscriptions {
subscription := subscription
eg.Go(func() error {
// TODO: Implement some retry logic here. For now, this is just a
// best-effort attempt.
statusCode, body, err := n.webpushSend(ctx, notificationJSON, subscription.Endpoint, webpush.Keys{
Auth: subscription.EndpointAuthKey,
P256dh: subscription.EndpointP256dhKey,
Expand Down Expand Up @@ -186,6 +188,8 @@ func (n *Webpusher) Test(ctx context.Context, req codersdk.WebpushSubscription)
return nil
}

// PublicKey returns the VAPID public key for the webpush dispatcher.
// Clients need this, so it's exposed via the BuildInfo endpoint.
func (n *Webpusher) PublicKey() string {
return n.VAPIDPublicKey
}
Expand Down