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:
Cian Johnston
2025-03-27 10:03:53 +00:00
committed by GitHub
co-authored by Kyle Carberry
parent 006600ea3e
commit 06e5d9ef21
43 changed files with 2136 additions and 20 deletions
+51
View File
@@ -283,6 +283,8 @@ var (
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceInboxNotification.Type: {policy.ActionCreate},
rbac.ResourceWebpushSubscription.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceDeploymentConfig.Type: {policy.ActionRead, policy.ActionUpdate}, // To read and upsert VAPID keys
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
@@ -1176,6 +1178,13 @@ func (q *querier) DeleteAllTailnetTunnels(ctx context.Context, arg database.Dele
return q.db.DeleteAllTailnetTunnels(ctx, arg)
}
func (q *querier) DeleteAllWebpushSubscriptions(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription); err != nil {
return err
}
return q.db.DeleteAllWebpushSubscriptions(ctx)
}
func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
// TODO: This is not 100% correct because it omits apikey IDs.
err := q.authorizeContext(ctx, policy.ActionDelete,
@@ -1381,6 +1390,20 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa
return q.db.DeleteTailnetTunnel(ctx, arg)
}
func (q *querier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil {
return err
}
return q.db.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, arg)
}
func (q *querier) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil {
return err
}
return q.db.DeleteWebpushSubscriptions(ctx, ids)
}
func (q *querier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
if err != nil {
@@ -2663,6 +2686,20 @@ func (q *querier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]databas
return q.db.GetUsersByIDs(ctx, ids)
}
func (q *querier) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]database.WebpushSubscription, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWebpushSubscription.WithOwner(userID.String())); err != nil {
return nil, err
}
return q.db.GetWebpushSubscriptionsByUserID(ctx, userID)
}
func (q *querier) GetWebpushVAPIDKeys(ctx context.Context) (database.GetWebpushVAPIDKeysRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil {
return database.GetWebpushVAPIDKeysRow{}, err
}
return q.db.GetWebpushVAPIDKeys(ctx)
}
func (q *querier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) {
// This is a system function
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
@@ -3420,6 +3457,13 @@ func (q *querier) InsertVolumeResourceMonitor(ctx context.Context, arg database.
return q.db.InsertVolumeResourceMonitor(ctx, arg)
}
func (q *querier) InsertWebpushSubscription(ctx context.Context, arg database.InsertWebpushSubscriptionParams) (database.WebpushSubscription, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil {
return database.WebpushSubscription{}, err
}
return q.db.InsertWebpushSubscription(ctx, arg)
}
func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) {
obj := rbac.ResourceWorkspace.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID)
tpl, err := q.GetTemplateByID(ctx, arg.TemplateID)
@@ -4670,6 +4714,13 @@ func (q *querier) UpsertTemplateUsageStats(ctx context.Context) error {
return q.db.UpsertTemplateUsageStats(ctx)
}
func (q *querier) UpsertWebpushVAPIDKeys(ctx context.Context, arg database.UpsertWebpushVAPIDKeysParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
}
return q.db.UpsertWebpushVAPIDKeys(ctx, arg)
}
func (q *querier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
if err != nil {
+49
View File
@@ -4531,6 +4531,22 @@ func (s *MethodTestSuite) TestSystemFunctions() {
s.Run("UpsertOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) {
check.Args(true).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
s.Run("GetWebpushVAPIDKeys", s.Subtest(func(db database.Store, check *expects) {
require.NoError(s.T(), db.UpsertWebpushVAPIDKeys(context.Background(), database.UpsertWebpushVAPIDKeysParams{
VapidPublicKey: "test",
VapidPrivateKey: "test",
}))
check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(database.GetWebpushVAPIDKeysRow{
VapidPublicKey: "test",
VapidPrivateKey: "test",
})
}))
s.Run("UpsertWebpushVAPIDKeys", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.UpsertWebpushVAPIDKeysParams{
VapidPublicKey: "test",
VapidPrivateKey: "test",
}).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestNotifications() {
@@ -4568,6 +4584,39 @@ func (s *MethodTestSuite) TestNotifications() {
}).Asserts(rbac.ResourceNotificationMessage, policy.ActionRead)
}))
// webpush subscriptions
s.Run("GetWebpushSubscriptionsByUserID", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
check.Args(user.ID).Asserts(rbac.ResourceWebpushSubscription.WithOwner(user.ID.String()), policy.ActionRead)
}))
s.Run("InsertWebpushSubscription", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
check.Args(database.InsertWebpushSubscriptionParams{
UserID: user.ID,
}).Asserts(rbac.ResourceWebpushSubscription.WithOwner(user.ID.String()), policy.ActionCreate)
}))
s.Run("DeleteWebpushSubscriptions", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
push := dbgen.WebpushSubscription(s.T(), db, database.InsertWebpushSubscriptionParams{
UserID: user.ID,
})
check.Args([]uuid.UUID{push.ID}).Asserts(rbac.ResourceSystem, policy.ActionDelete)
}))
s.Run("DeleteWebpushSubscriptionByUserIDAndEndpoint", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
push := dbgen.WebpushSubscription(s.T(), db, database.InsertWebpushSubscriptionParams{
UserID: user.ID,
})
check.Args(database.DeleteWebpushSubscriptionByUserIDAndEndpointParams{
UserID: user.ID,
Endpoint: push.Endpoint,
}).Asserts(rbac.ResourceWebpushSubscription.WithOwner(user.ID.String()), policy.ActionDelete)
}))
s.Run("DeleteAllWebpushSubscriptions", s.Subtest(func(_ database.Store, check *expects) {
check.Args().
Asserts(rbac.ResourceWebpushSubscription, policy.ActionDelete)
}))
// Notification templates
s.Run("GetNotificationTemplateByID", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
+12
View File
@@ -479,6 +479,18 @@ func NotificationInbox(t testing.TB, db database.Store, orig database.InsertInbo
return notification
}
func WebpushSubscription(t testing.TB, db database.Store, orig database.InsertWebpushSubscriptionParams) database.WebpushSubscription {
subscription, err := db.InsertWebpushSubscription(genCtx, database.InsertWebpushSubscriptionParams{
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UserID: takeFirst(orig.UserID, uuid.New()),
Endpoint: takeFirst(orig.Endpoint, testutil.GetRandomName(t)),
EndpointP256dhKey: takeFirst(orig.EndpointP256dhKey, testutil.GetRandomName(t)),
EndpointAuthKey: takeFirst(orig.EndpointAuthKey, testutil.GetRandomName(t)),
})
require.NoError(t, err, "insert webpush subscription")
return subscription
}
func Group(t testing.TB, db database.Store, orig database.Group) database.Group {
t.Helper()
+106
View File
@@ -246,6 +246,7 @@ type data struct {
templates []database.TemplateTable
templateUsageStats []database.TemplateUsageStat
userConfigs []database.UserConfig
webpushSubscriptions []database.WebpushSubscription
workspaceAgents []database.WorkspaceAgent
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
workspaceAgentLogs []database.WorkspaceAgentLog
@@ -289,6 +290,8 @@ type data struct {
lastLicenseID int32
defaultProxyDisplayName string
defaultProxyIconURL string
webpushVAPIDPublicKey string
webpushVAPIDPrivateKey string
userStatusChanges []database.UserStatusChange
telemetryItems []database.TelemetryItem
presets []database.TemplateVersionPreset
@@ -1853,6 +1856,14 @@ func (*FakeQuerier) DeleteAllTailnetTunnels(_ context.Context, arg database.Dele
return ErrUnimplemented
}
func (q *FakeQuerier) DeleteAllWebpushSubscriptions(_ context.Context) error {
q.mutex.Lock()
defer q.mutex.Unlock()
q.webpushSubscriptions = make([]database.WebpushSubscription, 0)
return nil
}
func (q *FakeQuerier) DeleteApplicationConnectAPIKeysByUserID(_ context.Context, userID uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -2422,6 +2433,38 @@ func (*FakeQuerier) DeleteTailnetTunnel(_ context.Context, arg database.DeleteTa
return database.DeleteTailnetTunnelRow{}, ErrUnimplemented
}
func (q *FakeQuerier) DeleteWebpushSubscriptionByUserIDAndEndpoint(_ context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, subscription := range q.webpushSubscriptions {
if subscription.UserID == arg.UserID && subscription.Endpoint == arg.Endpoint {
q.webpushSubscriptions[i] = q.webpushSubscriptions[len(q.webpushSubscriptions)-1]
q.webpushSubscriptions = q.webpushSubscriptions[:len(q.webpushSubscriptions)-1]
return nil
}
}
return sql.ErrNoRows
}
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]
q.webpushSubscriptions = q.webpushSubscriptions[:len(q.webpushSubscriptions)-1]
return nil
}
}
return sql.ErrNoRows
}
func (q *FakeQuerier) DeleteWorkspaceAgentPortShare(_ context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
err := validateDatabaseType(arg)
if err != nil {
@@ -6717,6 +6760,34 @@ func (q *FakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]datab
return users, nil
}
func (q *FakeQuerier) GetWebpushSubscriptionsByUserID(_ context.Context, userID uuid.UUID) ([]database.WebpushSubscription, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
out := make([]database.WebpushSubscription, 0)
for _, subscription := range q.webpushSubscriptions {
if subscription.UserID == userID {
out = append(out, subscription)
}
}
return out, nil
}
func (q *FakeQuerier) GetWebpushVAPIDKeys(_ context.Context) (database.GetWebpushVAPIDKeysRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
if q.webpushVAPIDPublicKey == "" && q.webpushVAPIDPrivateKey == "" {
return database.GetWebpushVAPIDKeysRow{}, sql.ErrNoRows
}
return database.GetWebpushVAPIDKeysRow{
VapidPublicKey: q.webpushVAPIDPublicKey,
VapidPrivateKey: q.webpushVAPIDPrivateKey,
}, nil
}
func (q *FakeQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(_ context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -9144,6 +9215,27 @@ func (q *FakeQuerier) InsertVolumeResourceMonitor(_ context.Context, arg databas
return monitor, nil
}
func (q *FakeQuerier) InsertWebpushSubscription(_ context.Context, arg database.InsertWebpushSubscriptionParams) (database.WebpushSubscription, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.WebpushSubscription{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
newSub := database.WebpushSubscription{
ID: uuid.New(),
UserID: arg.UserID,
CreatedAt: arg.CreatedAt,
Endpoint: arg.Endpoint,
EndpointP256dhKey: arg.EndpointP256dhKey,
EndpointAuthKey: arg.EndpointAuthKey,
}
q.webpushSubscriptions = append(q.webpushSubscriptions, newSub)
return newSub, nil
}
func (q *FakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) {
if err := validateDatabaseType(arg); err != nil {
return database.WorkspaceTable{}, err
@@ -12458,6 +12550,20 @@ TemplateUsageStatsInsertLoop:
return nil
}
func (q *FakeQuerier) UpsertWebpushVAPIDKeys(_ context.Context, arg database.UpsertWebpushVAPIDKeysParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
q.webpushVAPIDPublicKey = arg.VapidPublicKey
q.webpushVAPIDPrivateKey = arg.VapidPrivateKey
return nil
}
func (q *FakeQuerier) UpsertWorkspaceAgentPortShare(_ context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
err := validateDatabaseType(arg)
if err != nil {
+49
View File
@@ -221,6 +221,13 @@ func (m queryMetricsStore) DeleteAllTailnetTunnels(ctx context.Context, arg data
return r0
}
func (m queryMetricsStore) DeleteAllWebpushSubscriptions(ctx context.Context) error {
start := time.Now()
r0 := m.s.DeleteAllWebpushSubscriptions(ctx)
m.queryLatencies.WithLabelValues("DeleteAllWebpushSubscriptions").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
start := time.Now()
err := m.s.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
@@ -410,6 +417,20 @@ func (m queryMetricsStore) DeleteTailnetTunnel(ctx context.Context, arg database
return r0, r1
}
func (m queryMetricsStore) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
start := time.Now()
r0 := m.s.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteWebpushSubscriptionByUserIDAndEndpoint").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error {
start := time.Now()
r0 := m.s.DeleteWebpushSubscriptions(ctx, ids)
m.queryLatencies.WithLabelValues("DeleteWebpushSubscriptions").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
start := time.Now()
r0 := m.s.DeleteWorkspaceAgentPortShare(ctx, arg)
@@ -1502,6 +1523,20 @@ func (m queryMetricsStore) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) (
return users, err
}
func (m queryMetricsStore) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]database.WebpushSubscription, error) {
start := time.Now()
r0, r1 := m.s.GetWebpushSubscriptionsByUserID(ctx, userID)
m.queryLatencies.WithLabelValues("GetWebpushSubscriptionsByUserID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetWebpushVAPIDKeys(ctx context.Context) (database.GetWebpushVAPIDKeysRow, error) {
start := time.Now()
r0, r1 := m.s.GetWebpushVAPIDKeys(ctx)
m.queryLatencies.WithLabelValues("GetWebpushVAPIDKeys").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, authToken)
@@ -2146,6 +2181,13 @@ func (m queryMetricsStore) InsertVolumeResourceMonitor(ctx context.Context, arg
return r0, r1
}
func (m queryMetricsStore) InsertWebpushSubscription(ctx context.Context, arg database.InsertWebpushSubscriptionParams) (database.WebpushSubscription, error) {
start := time.Now()
r0, r1 := m.s.InsertWebpushSubscription(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWebpushSubscription").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) {
start := time.Now()
workspace, err := m.s.InsertWorkspace(ctx, arg)
@@ -3014,6 +3056,13 @@ func (m queryMetricsStore) UpsertTemplateUsageStats(ctx context.Context) error {
return r0
}
func (m queryMetricsStore) UpsertWebpushVAPIDKeys(ctx context.Context, arg database.UpsertWebpushVAPIDKeysParams) error {
start := time.Now()
r0 := m.s.UpsertWebpushVAPIDKeys(ctx, arg)
m.queryLatencies.WithLabelValues("UpsertWebpushVAPIDKeys").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
start := time.Now()
r0, r1 := m.s.UpsertWorkspaceAgentPortShare(ctx, arg)
+101
View File
@@ -318,6 +318,20 @@ func (mr *MockStoreMockRecorder) DeleteAllTailnetTunnels(ctx, arg any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTailnetTunnels", reflect.TypeOf((*MockStore)(nil).DeleteAllTailnetTunnels), ctx, arg)
}
// DeleteAllWebpushSubscriptions mocks base method.
func (m *MockStore) DeleteAllWebpushSubscriptions(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllWebpushSubscriptions", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAllWebpushSubscriptions indicates an expected call of DeleteAllWebpushSubscriptions.
func (mr *MockStoreMockRecorder) DeleteAllWebpushSubscriptions(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllWebpushSubscriptions", reflect.TypeOf((*MockStore)(nil).DeleteAllWebpushSubscriptions), ctx)
}
// DeleteApplicationConnectAPIKeysByUserID mocks base method.
func (m *MockStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
m.ctrl.T.Helper()
@@ -702,6 +716,34 @@ func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), ctx, arg)
}
// DeleteWebpushSubscriptionByUserIDAndEndpoint mocks base method.
func (m *MockStore) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebpushSubscriptionByUserIDAndEndpoint", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteWebpushSubscriptionByUserIDAndEndpoint indicates an expected call of DeleteWebpushSubscriptionByUserIDAndEndpoint.
func (mr *MockStoreMockRecorder) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebpushSubscriptionByUserIDAndEndpoint", reflect.TypeOf((*MockStore)(nil).DeleteWebpushSubscriptionByUserIDAndEndpoint), ctx, arg)
}
// DeleteWebpushSubscriptions mocks base method.
func (m *MockStore) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebpushSubscriptions", ctx, ids)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteWebpushSubscriptions indicates an expected call of DeleteWebpushSubscriptions.
func (mr *MockStoreMockRecorder) DeleteWebpushSubscriptions(ctx, ids any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebpushSubscriptions", reflect.TypeOf((*MockStore)(nil).DeleteWebpushSubscriptions), ctx, ids)
}
// DeleteWorkspaceAgentPortShare mocks base method.
func (m *MockStore) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
m.ctrl.T.Helper()
@@ -3142,6 +3184,36 @@ func (mr *MockStoreMockRecorder) GetUsersByIDs(ctx, ids any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByIDs", reflect.TypeOf((*MockStore)(nil).GetUsersByIDs), ctx, ids)
}
// GetWebpushSubscriptionsByUserID mocks base method.
func (m *MockStore) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]database.WebpushSubscription, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWebpushSubscriptionsByUserID", ctx, userID)
ret0, _ := ret[0].([]database.WebpushSubscription)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWebpushSubscriptionsByUserID indicates an expected call of GetWebpushSubscriptionsByUserID.
func (mr *MockStoreMockRecorder) GetWebpushSubscriptionsByUserID(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWebpushSubscriptionsByUserID", reflect.TypeOf((*MockStore)(nil).GetWebpushSubscriptionsByUserID), ctx, userID)
}
// GetWebpushVAPIDKeys mocks base method.
func (m *MockStore) GetWebpushVAPIDKeys(ctx context.Context) (database.GetWebpushVAPIDKeysRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWebpushVAPIDKeys", ctx)
ret0, _ := ret[0].(database.GetWebpushVAPIDKeysRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWebpushVAPIDKeys indicates an expected call of GetWebpushVAPIDKeys.
func (mr *MockStoreMockRecorder) GetWebpushVAPIDKeys(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWebpushVAPIDKeys", reflect.TypeOf((*MockStore)(nil).GetWebpushVAPIDKeys), ctx)
}
// GetWorkspaceAgentAndLatestBuildByAuthToken mocks base method.
func (m *MockStore) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error) {
m.ctrl.T.Helper()
@@ -4527,6 +4599,21 @@ func (mr *MockStoreMockRecorder) InsertVolumeResourceMonitor(ctx, arg any) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertVolumeResourceMonitor", reflect.TypeOf((*MockStore)(nil).InsertVolumeResourceMonitor), ctx, arg)
}
// InsertWebpushSubscription mocks base method.
func (m *MockStore) InsertWebpushSubscription(ctx context.Context, arg database.InsertWebpushSubscriptionParams) (database.WebpushSubscription, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWebpushSubscription", ctx, arg)
ret0, _ := ret[0].(database.WebpushSubscription)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWebpushSubscription indicates an expected call of InsertWebpushSubscription.
func (mr *MockStoreMockRecorder) InsertWebpushSubscription(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWebpushSubscription", reflect.TypeOf((*MockStore)(nil).InsertWebpushSubscription), ctx, arg)
}
// InsertWorkspace mocks base method.
func (m *MockStore) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) {
m.ctrl.T.Helper()
@@ -6347,6 +6434,20 @@ func (mr *MockStoreMockRecorder) UpsertTemplateUsageStats(ctx any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTemplateUsageStats", reflect.TypeOf((*MockStore)(nil).UpsertTemplateUsageStats), ctx)
}
// UpsertWebpushVAPIDKeys mocks base method.
func (m *MockStore) UpsertWebpushVAPIDKeys(ctx context.Context, arg database.UpsertWebpushVAPIDKeysParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertWebpushVAPIDKeys", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertWebpushVAPIDKeys indicates an expected call of UpsertWebpushVAPIDKeys.
func (mr *MockStoreMockRecorder) UpsertWebpushVAPIDKeys(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWebpushVAPIDKeys", reflect.TypeOf((*MockStore)(nil).UpsertWebpushVAPIDKeys), ctx, arg)
}
// UpsertWorkspaceAgentPortShare mocks base method.
func (m *MockStore) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
m.ctrl.T.Helper()
+15
View File
@@ -1614,6 +1614,15 @@ CREATE TABLE user_status_changes (
COMMENT ON TABLE user_status_changes IS 'Tracks the history of user status changes';
CREATE TABLE webpush_subscriptions (
id uuid DEFAULT gen_random_uuid() NOT NULL,
user_id uuid NOT NULL,
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
endpoint text NOT NULL,
endpoint_p256dh_key text NOT NULL,
endpoint_auth_key text NOT NULL
);
CREATE TABLE workspace_agent_devcontainers (
id uuid NOT NULL,
workspace_agent_id uuid NOT NULL,
@@ -2305,6 +2314,9 @@ ALTER TABLE ONLY user_status_changes
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE ONLY webpush_subscriptions
ADD CONSTRAINT webpush_subscriptions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY workspace_agent_devcontainers
ADD CONSTRAINT workspace_agent_devcontainers_pkey PRIMARY KEY (id);
@@ -2745,6 +2757,9 @@ ALTER TABLE ONLY user_links
ALTER TABLE ONLY user_status_changes
ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY webpush_subscriptions
ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_agent_devcontainers
ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
@@ -58,6 +58,7 @@ const (
ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyWorkspaceAgentMemoryResourceMonitorsAgentID ForeignKeyConstraint = "workspace_agent_memory_resource_monitors_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS webpush_subscriptions;
@@ -0,0 +1,13 @@
-- webpush_subscriptions is a table that stores push notification
-- subscriptions for users. These are acquired via the Push API in the browser.
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- endpoint is called by coderd to send a push notification to the user.
endpoint TEXT NOT NULL,
-- endpoint_p256dh_key is the public key for the endpoint.
endpoint_p256dh_key TEXT NOT NULL,
-- endpoint_auth_key is the authentication key for the endpoint.
endpoint_auth_key TEXT NOT NULL
);
@@ -0,0 +1,2 @@
-- VAPID keys lited from coderd/notifications_test.go.
INSERT INTO webpush_subscriptions (id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key) VALUES (gen_random_uuid(), (SELECT id FROM users LIMIT 1), NOW(), 'https://example.com', 'BNNL5ZaTfK81qhXOx23+wewhigUeFb632jN6LvRWCFH1ubQr77FE/9qV1FuojuRmHP42zmf34rXgW80OvUVDgTk=', 'zqbxT6JKstKSY9JKibZLSQ==');
+9
View File
@@ -3240,6 +3240,15 @@ type VisibleUser struct {
AvatarURL string `db:"avatar_url" json:"avatar_url"`
}
type WebpushSubscription struct {
ID uuid.UUID `db:"id" json:"id"`
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"`
}
// Joins in the display name information such as username, avatar, and organization name.
type Workspace struct {
ID uuid.UUID `db:"id" json:"id"`
+11
View File
@@ -69,6 +69,11 @@ type sqlcQuerier interface {
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteAllTailnetClientSubscriptions(ctx context.Context, arg DeleteAllTailnetClientSubscriptionsParams) error
DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error
// 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.
DeleteAllWebpushSubscriptions(ctx context.Context) error
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteCoordinator(ctx context.Context, id uuid.UUID) error
DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error)
@@ -104,6 +109,8 @@ type sqlcQuerier interface {
DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error
DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error
DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error
DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error
// Disable foreign keys and triggers for all tables.
@@ -340,6 +347,8 @@ type sqlcQuerier interface {
// to look up references to actions. eg. a user could build a workspace
// for another user, then be deleted... we still want them to appear!
GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error)
GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]WebpushSubscription, error)
GetWebpushVAPIDKeys(ctx context.Context) (GetWebpushVAPIDKeysRow, error)
GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndLatestBuildByAuthTokenRow, error)
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
@@ -453,6 +462,7 @@ type sqlcQuerier interface {
InsertUserGroupsByName(ctx context.Context, arg InsertUserGroupsByNameParams) error
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error)
InsertWebpushSubscription(ctx context.Context, arg InsertWebpushSubscriptionParams) (WebpushSubscription, error)
InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (WorkspaceTable, error)
InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error)
InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error)
@@ -597,6 +607,7 @@ type sqlcQuerier interface {
// used to store the data, and the minutes are summed for each user and template
// combination. The result is stored in the template_usage_stats table.
UpsertTemplateUsageStats(ctx context.Context) error
UpsertWebpushVAPIDKeys(ctx context.Context, arg UpsertWebpushVAPIDKeysParams) error
UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error)
//
// The returned boolean, new_or_stale, can be used to deduce if a new session
+145
View File
@@ -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
+25
View File
@@ -189,3 +189,28 @@ WHERE
INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES (@notification_template_id, @last_generated_at)
ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at
WHERE notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id;
-- name: GetWebpushSubscriptionsByUserID :many
SELECT *
FROM webpush_subscriptions
WHERE user_id = @user_id::uuid;
-- 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 *;
-- name: DeleteWebpushSubscriptions :exec
DELETE FROM webpush_subscriptions
WHERE id = ANY(@ids::uuid[]);
-- name: DeleteWebpushSubscriptionByUserIDAndEndpoint :exec
DELETE FROM webpush_subscriptions
WHERE user_id = @user_id AND endpoint = @endpoint;
-- name: DeleteAllWebpushSubscriptions :exec
-- 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.
TRUNCATE TABLE webpush_subscriptions;
+13
View File
@@ -131,3 +131,16 @@ SET value = CASE
ELSE 'false'
END
WHERE site_configs.key = 'oauth2_github_default_eligible';
-- name: UpsertWebpushVAPIDKeys :exec
INSERT INTO site_configs (key, value)
VALUES
('webpush_vapid_public_key', @vapid_public_key :: text),
('webpush_vapid_private_key', @vapid_private_key :: text)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key;
-- 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;
+1
View File
@@ -71,6 +71,7 @@ const (
UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type);
UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
UniqueWebpushSubscriptionsPkey UniqueConstraint = "webpush_subscriptions_pkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_pkey PRIMARY KEY (id);
UniqueWorkspaceAgentDevcontainersPkey UniqueConstraint = "workspace_agent_devcontainers_pkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_pkey PRIMARY KEY (id);
UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
UniqueWorkspaceAgentMemoryResourceMonitorsPkey UniqueConstraint = "workspace_agent_memory_resource_monitors_pkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_pkey PRIMARY KEY (agent_id);