Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commite1f27a7

Browse files
authored
feat(site): add webpush notification serviceworker (#17123)
* Improves tests for webpush notifications* Sets subscriber correctly in web push payload (without this,notifications do not work in Safari)* NOTE: for now, I'm using the Coder Access URL. Some push messagingservice don't like it when you use a non-HTTPS URL, so dropping a warnlog about this.* Adds a service worker and context for push notifications* Adds a button beside "Inbox" to enable / disable push notificationsNotes:* ✅ Tested in in Firefox and Safari, and Chrome.
1 parent661ed23 commite1f27a7

File tree

11 files changed

+285
-52
lines changed

11 files changed

+285
-52
lines changed

‎cli/server.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import (
9595
"github.com/coder/coder/v2/coderd/tracing"
9696
"github.com/coder/coder/v2/coderd/unhanger"
9797
"github.com/coder/coder/v2/coderd/updatecheck"
98+
"github.com/coder/coder/v2/coderd/util/ptr"
9899
"github.com/coder/coder/v2/coderd/util/slice"
99100
stringutil"github.com/coder/coder/v2/coderd/util/strings"
100101
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
@@ -779,7 +780,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
779780
// Manage push notifications.
780781
experiments:=coderd.ReadExperiments(options.Logger,options.DeploymentValues.Experiments.Value())
781782
ifexperiments.Enabled(codersdk.ExperimentWebPush) {
782-
webpusher,err:=webpush.New(ctx,&options.Logger,options.Database)
783+
if!strings.HasPrefix(options.AccessURL.String(),"https://") {
784+
options.Logger.Warn(ctx,"access URL is not HTTPS, so web push notifications may not work on some browsers",slog.F("access_url",options.AccessURL.String()))
785+
}
786+
webpusher,err:=webpush.New(ctx,ptr.Ref(options.Logger.Named("webpush")),options.Database,options.AccessURL.String())
783787
iferr!=nil {
784788
options.Logger.Error(ctx,"failed to create web push dispatcher",slog.Error(err))
785789
options.Logger.Warn(ctx,"web push notifications will not work until the VAPID keys are regenerated")

‎coderd/coderdtest/coderdtest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
284284

285285
ifoptions.WebpushDispatcher==nil {
286286
// nolint:gocritic // Gets/sets VAPID keys.
287-
pushNotifier,err:=webpush.New(dbauthz.AsNotifier(context.Background()),options.Logger,options.Database)
287+
pushNotifier,err:=webpush.New(dbauthz.AsNotifier(context.Background()),options.Logger,options.Database,"http://example.com")
288288
iferr!=nil {
289289
panic(xerrors.Errorf("failed to create web push notifier: %w",err))
290290
}

‎coderd/webpush/webpush.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ type Dispatcher interface {
4141
// for updates inside of a workspace, which we want to be immediate.
4242
//
4343
// See: https://github.com/coder/internal/issues/528
44-
funcNew(ctx context.Context,log*slog.Logger,db database.Store) (Dispatcher,error) {
44+
funcNew(ctx context.Context,log*slog.Logger,db database.Store,vapidSubstring) (Dispatcher,error) {
4545
keys,err:=db.GetWebpushVAPIDKeys(ctx)
4646
iferr!=nil {
4747
if!errors.Is(err,sql.ErrNoRows) {
4848
returnnil,xerrors.Errorf("get notification vapid keys: %w",err)
4949
}
5050
}
51+
5152
ifkeys.VapidPublicKey==""||keys.VapidPrivateKey=="" {
5253
// Generate new VAPID keys. This also deletes all existing push
5354
// subscriptions as part of the transaction, as they are no longer
@@ -62,6 +63,7 @@ func New(ctx context.Context, log *slog.Logger, db database.Store) (Dispatcher,
6263
}
6364

6465
return&Webpusher{
66+
vapidSub:vapidSub,
6567
store:db,
6668
log:log,
6769
VAPIDPublicKey:keys.VapidPublicKey,
@@ -72,7 +74,13 @@ func New(ctx context.Context, log *slog.Logger, db database.Store) (Dispatcher,
7274
typeWebpusherstruct {
7375
store database.Store
7476
log*slog.Logger
77+
// VAPID allows us to identify the sender of the message.
78+
// This must be a https:// URL or an email address.
79+
// Some push services (such as Apple's) require this to be set.
80+
vapidSubstring
7581

82+
// public and private keys for VAPID. These are used to sign and encrypt
83+
// the message payload.
7684
VAPIDPublicKeystring
7785
VAPIDPrivateKeystring
7886
}
@@ -148,10 +156,12 @@ func (n *Webpusher) webpushSend(ctx context.Context, msg []byte, endpoint string
148156
Endpoint:endpoint,
149157
Keys:keys,
150158
},&webpush.Options{
159+
Subscriber:n.vapidSub,
151160
VAPIDPublicKey:n.VAPIDPublicKey,
152161
VAPIDPrivateKey:n.VAPIDPrivateKey,
153162
})
154163
iferr!=nil {
164+
n.log.Error(ctx,"failed to send webpush notification",slog.Error(err),slog.F("endpoint",endpoint))
155165
return-1,nil,xerrors.Errorf("send webpush notification: %w",err)
156166
}
157167
deferresp.Body.Close()

‎coderd/webpush/webpush_test.go

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package webpush_test
22

33
import (
44
"context"
5+
"encoding/json"
6+
"io"
57
"net/http"
68
"net/http/httptest"
79
"testing"
@@ -32,7 +34,9 @@ func TestPush(t *testing.T) {
3234
t.Run("SuccessfulDelivery",func(t*testing.T) {
3335
t.Parallel()
3436
ctx:=testutil.Context(t,testutil.WaitShort)
35-
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,_*http.Request) {
37+
msg:=randomWebpushMessage(t)
38+
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,r*http.Request) {
39+
assertWebpushPayload(t,r)
3640
w.WriteHeader(http.StatusOK)
3741
})
3842
user:=dbgen.User(t,store, database.User{})
@@ -45,16 +49,7 @@ func TestPush(t *testing.T) {
4549
})
4650
require.NoError(t,err)
4751

48-
notification:= codersdk.WebpushMessage{
49-
Title:"Test Title",
50-
Body:"Test Body",
51-
Actions: []codersdk.WebpushMessageAction{
52-
{Label:"View",URL:"https://coder.com/view"},
53-
},
54-
Icon:"workspace",
55-
}
56-
57-
err=manager.Dispatch(ctx,user.ID,notification)
52+
err=manager.Dispatch(ctx,user.ID,msg)
5853
require.NoError(t,err)
5954

6055
subscriptions,err:=store.GetWebpushSubscriptionsByUserID(ctx,user.ID)
@@ -66,7 +61,8 @@ func TestPush(t *testing.T) {
6661
t.Run("ExpiredSubscription",func(t*testing.T) {
6762
t.Parallel()
6863
ctx:=testutil.Context(t,testutil.WaitShort)
69-
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,_*http.Request) {
64+
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,r*http.Request) {
65+
assertWebpushPayload(t,r)
7066
w.WriteHeader(http.StatusGone)
7167
})
7268
user:=dbgen.User(t,store, database.User{})
@@ -79,12 +75,8 @@ func TestPush(t *testing.T) {
7975
})
8076
require.NoError(t,err)
8177

82-
notification:= codersdk.WebpushMessage{
83-
Title:"Test Title",
84-
Body:"Test Body",
85-
}
86-
87-
err=manager.Dispatch(ctx,user.ID,notification)
78+
msg:=randomWebpushMessage(t)
79+
err=manager.Dispatch(ctx,user.ID,msg)
8880
require.NoError(t,err)
8981

9082
subscriptions,err:=store.GetWebpushSubscriptionsByUserID(ctx,user.ID)
@@ -95,7 +87,8 @@ func TestPush(t *testing.T) {
9587
t.Run("FailedDelivery",func(t*testing.T) {
9688
t.Parallel()
9789
ctx:=testutil.Context(t,testutil.WaitShort)
98-
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,_*http.Request) {
90+
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,r*http.Request) {
91+
assertWebpushPayload(t,r)
9992
w.WriteHeader(http.StatusBadRequest)
10093
w.Write([]byte("Invalid request"))
10194
})
@@ -110,12 +103,8 @@ func TestPush(t *testing.T) {
110103
})
111104
require.NoError(t,err)
112105

113-
notification:= codersdk.WebpushMessage{
114-
Title:"Test Title",
115-
Body:"Test Body",
116-
}
117-
118-
err=manager.Dispatch(ctx,user.ID,notification)
106+
msg:=randomWebpushMessage(t)
107+
err=manager.Dispatch(ctx,user.ID,msg)
119108
require.Error(t,err)
120109
assert.Contains(t,err.Error(),"Invalid request")
121110

@@ -130,13 +119,15 @@ func TestPush(t *testing.T) {
130119
ctx:=testutil.Context(t,testutil.WaitShort)
131120
varokEndpointCalledbool
132121
vargoneEndpointCalledbool
133-
manager,store,serverOKURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,_*http.Request) {
122+
manager,store,serverOKURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,r*http.Request) {
134123
okEndpointCalled=true
124+
assertWebpushPayload(t,r)
135125
w.WriteHeader(http.StatusOK)
136126
})
137127

138-
serverGone:=httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
128+
serverGone:=httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {
139129
goneEndpointCalled=true
130+
assertWebpushPayload(t,r)
140131
w.WriteHeader(http.StatusGone)
141132
}))
142133
deferserverGone.Close()
@@ -163,15 +154,8 @@ func TestPush(t *testing.T) {
163154
})
164155
require.NoError(t,err)
165156

166-
notification:= codersdk.WebpushMessage{
167-
Title:"Test Title",
168-
Body:"Test Body",
169-
Actions: []codersdk.WebpushMessageAction{
170-
{Label:"View",URL:"https://coder.com/view"},
171-
},
172-
}
173-
174-
err=manager.Dispatch(ctx,user.ID,notification)
157+
msg:=randomWebpushMessage(t)
158+
err=manager.Dispatch(ctx,user.ID,msg)
175159
require.NoError(t,err)
176160
assert.True(t,okEndpointCalled,"The valid endpoint should be called")
177161
assert.True(t,goneEndpointCalled,"The expired endpoint should be called")
@@ -189,8 +173,9 @@ func TestPush(t *testing.T) {
189173

190174
ctx:=testutil.Context(t,testutil.WaitShort)
191175
varrequestReceivedbool
192-
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,_*http.Request) {
176+
manager,store,serverURL:=setupPushTest(ctx,t,func(w http.ResponseWriter,r*http.Request) {
193177
requestReceived=true
178+
assertWebpushPayload(t,r)
194179
w.WriteHeader(http.StatusOK)
195180
})
196181

@@ -205,17 +190,8 @@ func TestPush(t *testing.T) {
205190
})
206191
require.NoError(t,err,"Failed to insert push subscription")
207192

208-
notification:= codersdk.WebpushMessage{
209-
Title:"Test Notification",
210-
Body:"This is a test notification body",
211-
Actions: []codersdk.WebpushMessageAction{
212-
{Label:"View Workspace",URL:"https://coder.com/workspace/123"},
213-
{Label:"Cancel",URL:"https://coder.com/cancel"},
214-
},
215-
Icon:"workspace-icon",
216-
}
217-
218-
err=manager.Dispatch(ctx,user.ID,notification)
193+
msg:=randomWebpushMessage(t)
194+
err=manager.Dispatch(ctx,user.ID,msg)
219195
require.NoError(t,err,"The push notification should be dispatched successfully")
220196
require.True(t,requestReceived,"The push notification request should have been received by the server")
221197
})
@@ -242,15 +218,42 @@ func TestPush(t *testing.T) {
242218
})
243219
}
244220

221+
funcrandomWebpushMessage(t testing.TB) codersdk.WebpushMessage {
222+
t.Helper()
223+
return codersdk.WebpushMessage{
224+
Title:testutil.GetRandomName(t),
225+
Body:testutil.GetRandomName(t),
226+
227+
Actions: []codersdk.WebpushMessageAction{
228+
{Label:"A",URL:"https://example.com/a"},
229+
{Label:"B",URL:"https://example.com/b"},
230+
},
231+
Icon:"https://example.com/icon.png",
232+
}
233+
}
234+
235+
funcassertWebpushPayload(t testing.TB,r*http.Request) {
236+
t.Helper()
237+
assert.Equal(t,http.MethodPost,r.Method)
238+
assert.Equal(t,"application/octet-stream",r.Header.Get("Content-Type"))
239+
assert.Equal(t,r.Header.Get("content-encoding"),"aes128gcm")
240+
assert.Contains(t,r.Header.Get("Authorization"),"vapid")
241+
242+
// Attempting to decode the request body as JSON should fail as it is
243+
// encrypted.
244+
assert.Error(t,json.NewDecoder(r.Body).Decode(io.Discard))
245+
}
246+
245247
// setupPushTest creates a common test setup for webpush notification tests
246248
funcsetupPushTest(ctx context.Context,t*testing.T,handlerFuncfunc(w http.ResponseWriter,r*http.Request)) (webpush.Dispatcher, database.Store,string) {
249+
t.Helper()
247250
logger:=slogtest.Make(t,&slogtest.Options{IgnoreErrors:true}).Leveled(slog.LevelDebug)
248251
db,_:=dbtestutil.NewDB(t)
249252

250253
server:=httptest.NewServer(http.HandlerFunc(handlerFunc))
251254
t.Cleanup(server.Close)
252255

253-
manager,err:=webpush.New(ctx,&logger,db)
256+
manager,err:=webpush.New(ctx,&logger,db,"http://example.com")
254257
require.NoError(t,err,"Failed to create webpush manager")
255258

256259
returnmanager,db,server.URL

‎site/src/api/api.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,6 +2371,28 @@ class ApiMethods {
23712371
awaitthis.axios.post<void>("/api/v2/notifications/test");
23722372
};
23732373

2374+
createWebPushSubscription=async(
2375+
userId:string,
2376+
req:TypesGen.WebpushSubscription,
2377+
)=>{
2378+
awaitthis.axios.post<void>(
2379+
`/api/v2/users/${userId}/webpush/subscription`,
2380+
req,
2381+
);
2382+
};
2383+
2384+
deleteWebPushSubscription=async(
2385+
userId:string,
2386+
req:TypesGen.DeleteWebpushSubscription,
2387+
)=>{
2388+
awaitthis.axios.delete<void>(
2389+
`/api/v2/users/${userId}/webpush/subscription`,
2390+
{
2391+
data:req,
2392+
},
2393+
);
2394+
};
2395+
23742396
requestOneTimePassword=async(
23752397
req:TypesGen.RequestOneTimePasscodeRequest,
23762398
)=>{

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp