Skip to content

Commit 4960a1e

Browse files
authored
feat(coderd): add mark-all-as-read endpoint for inbox notifications (coder#16976)
[Resolve this issue](coder/internal#506) Add a mark-all-as-read endpoint which is marking as read all notifications that are not read for the authenticated user. Also adds the DB logic.
1 parent d8d4b9b commit 4960a1e

15 files changed

+262
-0
lines changed

coderd/apidoc/docs.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,7 @@ func New(options *Options) *API {
13951395
r.Use(apiKeyMiddleware)
13961396
r.Route("/inbox", func(r chi.Router) {
13971397
r.Get("/", api.listInboxNotifications)
1398+
r.Put("/mark-all-as-read", api.markAllInboxNotificationsAsRead)
13981399
r.Get("/watch", api.watchInboxNotifications)
13991400
r.Put("/{id}/read-status", api.updateInboxNotificationReadStatus)
14001401
})

coderd/database/dbauthz/dbauthz.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3554,6 +3554,16 @@ func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID
35543554
return q.db.ListWorkspaceAgentPortShares(ctx, workspaceID)
35553555
}
35563556

3557+
func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error {
3558+
resource := rbac.ResourceInboxNotification.WithOwner(arg.UserID.String())
3559+
3560+
if err := q.authorizeContext(ctx, policy.ActionUpdate, resource); err != nil {
3561+
return err
3562+
}
3563+
3564+
return q.db.MarkAllInboxNotificationsAsRead(ctx, arg)
3565+
}
3566+
35573567
func (q *querier) OIDCClaimFieldValues(ctx context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) {
35583568
resource := rbac.ResourceIdpsyncSettings
35593569
if args.OrganizationID != uuid.Nil {

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4653,6 +4653,15 @@ func (s *MethodTestSuite) TestNotifications() {
46534653
ReadAt: sql.NullTime{Time: readAt, Valid: true},
46544654
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionUpdate)
46554655
}))
4656+
4657+
s.Run("MarkAllInboxNotificationsAsRead", s.Subtest(func(db database.Store, check *expects) {
4658+
u := dbgen.User(s.T(), db, database.User{})
4659+
4660+
check.Args(database.MarkAllInboxNotificationsAsReadParams{
4661+
UserID: u.ID,
4662+
ReadAt: sql.NullTime{Time: dbtestutil.NowInDefaultTimezone(), Valid: true},
4663+
}).Asserts(rbac.ResourceInboxNotification.WithOwner(u.ID.String()), policy.ActionUpdate)
4664+
}))
46564665
}
46574666

46584667
func (s *MethodTestSuite) TestOAuth2ProviderApps() {

coderd/database/dbmem/dbmem.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9500,6 +9500,21 @@ func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceI
95009500
return shares, nil
95019501
}
95029502

9503+
func (q *FakeQuerier) MarkAllInboxNotificationsAsRead(_ context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error {
9504+
err := validateDatabaseType(arg)
9505+
if err != nil {
9506+
return err
9507+
}
9508+
9509+
for idx, notif := range q.inboxNotifications {
9510+
if notif.UserID == arg.UserID && !notif.ReadAt.Valid {
9511+
q.inboxNotifications[idx].ReadAt = arg.ReadAt
9512+
}
9513+
}
9514+
9515+
return nil
9516+
}
9517+
95039518
// nolint:forcetypeassert
95049519
func (q *FakeQuerier) OIDCClaimFieldValues(_ context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) {
95059520
orgMembers := q.getOrganizationMemberNoLock(args.OrganizationID)

coderd/database/dbmetrics/querymetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/notificationsinbox.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ SET
5757
read_at = $1
5858
WHERE
5959
id = $2;
60+
61+
-- name: MarkAllInboxNotificationsAsRead :exec
62+
UPDATE
63+
inbox_notifications
64+
SET
65+
read_at = $1
66+
WHERE
67+
user_id = $2 and read_at IS NULL;

coderd/inboxnotifications.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,31 @@ func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *htt
344344
UnreadCount: int(unreadCount),
345345
})
346346
}
347+
348+
// markAllInboxNotificationsAsRead marks as read all unread notifications for authenticated user.
349+
// @Summary Mark all unread notifications as read
350+
// @ID mark-all-unread-notifications-as-read
351+
// @Security CoderSessionToken
352+
// @Tags Notifications
353+
// @Success 204
354+
// @Router /notifications/inbox/mark-all-as-read [put]
355+
func (api *API) markAllInboxNotificationsAsRead(rw http.ResponseWriter, r *http.Request) {
356+
var (
357+
ctx = r.Context()
358+
apikey = httpmw.APIKey(r)
359+
)
360+
361+
err := api.Database.MarkAllInboxNotificationsAsRead(ctx, database.MarkAllInboxNotificationsAsReadParams{
362+
UserID: apikey.UserID,
363+
ReadAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
364+
})
365+
if err != nil {
366+
api.Logger.Error(ctx, "failed to mark all unread notifications as read", slog.Error(err))
367+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
368+
Message: "Failed to mark all unread notifications as read.",
369+
})
370+
return
371+
}
372+
373+
rw.WriteHeader(http.StatusNoContent)
374+
}

coderd/inboxnotifications_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func TestInboxNotification_Watch(t *testing.T) {
3737
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
3838
// For now the idea is that the runner takes too long to insert the entries, could be worth
3939
// investigating a manual Tx.
40+
// see: https://github.com/coder/internal/issues/503
4041
if runtime.GOOS == "windows" {
4142
t.Skip("our runners are randomly taking too long to insert entries")
4243
}
@@ -312,6 +313,7 @@ func TestInboxNotifications_List(t *testing.T) {
312313
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
313314
// For now the idea is that the runner takes too long to insert the entries, could be worth
314315
// investigating a manual Tx.
316+
// see: https://github.com/coder/internal/issues/503
315317
if runtime.GOOS == "windows" {
316318
t.Skip("our runners are randomly taking too long to insert entries")
317319
}
@@ -595,6 +597,7 @@ func TestInboxNotifications_ReadStatus(t *testing.T) {
595597
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
596598
// For now the idea is that the runner takes too long to insert the entries, could be worth
597599
// investigating a manual Tx.
600+
// see: https://github.com/coder/internal/issues/503
598601
if runtime.GOOS == "windows" {
599602
t.Skip("our runners are randomly taking too long to insert entries")
600603
}
@@ -730,3 +733,76 @@ func TestInboxNotifications_ReadStatus(t *testing.T) {
730733
require.Empty(t, updatedNotif.Notification)
731734
})
732735
}
736+
737+
func TestInboxNotifications_MarkAllAsRead(t *testing.T) {
738+
t.Parallel()
739+
740+
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
741+
// For now the idea is that the runner takes too long to insert the entries, could be worth
742+
// investigating a manual Tx.
743+
// see: https://github.com/coder/internal/issues/503
744+
if runtime.GOOS == "windows" {
745+
t.Skip("our runners are randomly taking too long to insert entries")
746+
}
747+
748+
t.Run("ok", func(t *testing.T) {
749+
t.Parallel()
750+
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
751+
firstUser := coderdtest.CreateFirstUser(t, client)
752+
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
753+
754+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
755+
defer cancel()
756+
757+
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
758+
require.NoError(t, err)
759+
require.NotNil(t, notifs)
760+
require.Equal(t, 0, notifs.UnreadCount)
761+
require.Empty(t, notifs.Notifications)
762+
763+
for i := range 20 {
764+
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
765+
ID: uuid.New(),
766+
UserID: member.ID,
767+
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
768+
Title: fmt.Sprintf("Notification %d", i),
769+
Actions: json.RawMessage("[]"),
770+
Content: fmt.Sprintf("Content of the notif %d", i),
771+
CreatedAt: dbtime.Now(),
772+
})
773+
}
774+
775+
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
776+
require.NoError(t, err)
777+
require.NotNil(t, notifs)
778+
require.Equal(t, 20, notifs.UnreadCount)
779+
require.Len(t, notifs.Notifications, 20)
780+
781+
err = client.MarkAllInboxNotificationsAsRead(ctx)
782+
require.NoError(t, err)
783+
784+
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
785+
require.NoError(t, err)
786+
require.NotNil(t, notifs)
787+
require.Equal(t, 0, notifs.UnreadCount)
788+
require.Len(t, notifs.Notifications, 20)
789+
790+
for i := range 10 {
791+
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
792+
ID: uuid.New(),
793+
UserID: member.ID,
794+
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
795+
Title: fmt.Sprintf("Notification %d", i),
796+
Actions: json.RawMessage("[]"),
797+
Content: fmt.Sprintf("Content of the notif %d", i),
798+
CreatedAt: dbtime.Now(),
799+
})
800+
}
801+
802+
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
803+
require.NoError(t, err)
804+
require.NotNil(t, notifs)
805+
require.Equal(t, 10, notifs.UnreadCount)
806+
require.Len(t, notifs.Notifications, 25)
807+
})
808+
}

codersdk/inboxnotification.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,21 @@ func (c *Client) UpdateInboxNotificationReadStatus(ctx context.Context, notifID
109109
var resp UpdateInboxNotificationReadStatusResponse
110110
return resp, json.NewDecoder(res.Body).Decode(&resp)
111111
}
112+
113+
func (c *Client) MarkAllInboxNotificationsAsRead(ctx context.Context) error {
114+
res, err := c.Request(
115+
ctx, http.MethodPut,
116+
"/api/v2/notifications/inbox/mark-all-as-read",
117+
nil,
118+
)
119+
if err != nil {
120+
return err
121+
}
122+
defer res.Body.Close()
123+
124+
if res.StatusCode != http.StatusNoContent {
125+
return ReadBodyAsError(res)
126+
}
127+
128+
return nil
129+
}

docs/reference/api/notifications.md

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)