From 8d3ca0ad3efbc91c8340991e6a3dc8fcc518147a Mon Sep 17 00:00:00 2001 From: defelmnq Date: Tue, 18 Mar 2025 02:51:43 +0000 Subject: [PATCH 01/12] work on inbox notifications mark-all-as-read endpoint --- coderd/apidoc/docs.go | 25 ++++++++++++ coderd/apidoc/swagger.json | 21 ++++++++++ coderd/coderd.go | 1 + coderd/database/dbauthz/dbauthz.go | 8 ++++ coderd/database/dbauthz/dbauthz_test.go | 9 +++++ coderd/database/dbmem/dbmem.go | 18 +++++++++ coderd/database/dbmetrics/querymetrics.go | 7 ++++ coderd/database/dbmock/dbmock.go | 14 +++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 18 +++++++++ .../database/queries/notificationsinbox.sql | 7 ++++ coderd/inboxnotifications.go | 32 ++++++++++++++++ codersdk/inboxnotification.go | 23 +++++++++++ docs/reference/api/notifications.md | 38 +++++++++++++++++++ 14 files changed, 222 insertions(+) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8dbff0fca8274..7a7766bc29ea8 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1705,6 +1705,31 @@ const docTemplate = `{ } } }, + "/notifications/inbox/mark-all-as-read": { + "put": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notifications" + ], + "summary": "Make all unread notifications as read", + "operationId": "make-all-unread-notifications-as-read", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/notifications/inbox/watch": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 3f58bf0d944fd..29681be26a6bd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1486,6 +1486,27 @@ } } }, + "/notifications/inbox/mark-all-as-read": { + "put": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Notifications"], + "summary": "Make all unread notifications as read", + "operationId": "make-all-unread-notifications-as-read", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/notifications/inbox/watch": { "get": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index f5956d7457fe8..296bfa029abe9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1389,6 +1389,7 @@ func New(options *Options) *API { r.Use(apiKeyMiddleware) r.Route("/inbox", func(r chi.Router) { r.Get("/", api.listInboxNotifications) + r.Put("/mark-all-as-read", api.markAllInboxNotificationsAsRead) r.Get("/watch", api.watchInboxNotifications) r.Put("/{id}/read-status", api.updateInboxNotificationReadStatus) }) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9c88e986cbffc..3129b4cb17fb8 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3554,6 +3554,14 @@ func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID return q.db.ListWorkspaceAgentPortShares(ctx, workspaceID) } +func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceInboxNotification); err != nil { + return err + } + + return q.db.MarkAllInboxNotificationsAsRead(ctx, arg) +} + func (q *querier) OIDCClaimFieldValues(ctx context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) { resource := rbac.ResourceIdpsyncSettings if args.OrganizationID != uuid.Nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ec8ced783fa0a..60ded89d708c8 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4640,6 +4640,15 @@ func (s *MethodTestSuite) TestNotifications() { ReadAt: sql.NullTime{Time: readAt, Valid: true}, }).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionUpdate) })) + + s.Run("MarkAllInboxNotificationsAsRead", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + + check.Args(database.MarkAllInboxNotificationsAsReadParams{ + UserID: u.ID, + ReadAt: sql.NullTime{Time: dbtestutil.NowInDefaultTimezone(), Valid: true}, + }).Asserts(rbac.ResourceInboxNotification, policy.ActionUpdate) + })) } func (s *MethodTestSuite) TestOAuth2ProviderApps() { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 1867c91abf837..b71fe9b49880d 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9498,6 +9498,24 @@ func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceI return shares, nil } +func (q *FakeQuerier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + for idx, notif := range q.inboxNotifications { + if notif.UserID == arg.UserID && !notif.ReadAt.Valid { + q.inboxNotifications[idx].ReadAt = sql.NullTime{ + Time: dbtime.Now(), + Valid: true, + } + } + } + + return nil +} + // nolint:forcetypeassert func (q *FakeQuerier) OIDCClaimFieldValues(_ context.Context, args database.OIDCClaimFieldValuesParams) ([]string, error) { orgMembers := q.getOrganizationMemberNoLock(args.OrganizationID) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 407d9e48bfcf8..fac76b69c33f5 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2257,6 +2257,13 @@ func (m queryMetricsStore) ListWorkspaceAgentPortShares(ctx context.Context, wor return r0, r1 } +func (m queryMetricsStore) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { + start := time.Now() + r0 := m.s.MarkAllInboxNotificationsAsRead(ctx, arg) + m.queryLatencies.WithLabelValues("MarkAllInboxNotificationsAsRead").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) OIDCClaimFieldValues(ctx context.Context, organizationID database.OIDCClaimFieldValuesParams) ([]string, error) { start := time.Now() r0, r1 := m.s.OIDCClaimFieldValues(ctx, organizationID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index fbe4d0745fbb0..89c75aba31748 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4763,6 +4763,20 @@ func (mr *MockStoreMockRecorder) ListWorkspaceAgentPortShares(ctx, workspaceID a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWorkspaceAgentPortShares", reflect.TypeOf((*MockStore)(nil).ListWorkspaceAgentPortShares), ctx, workspaceID) } +// MarkAllInboxNotificationsAsRead mocks base method. +func (m *MockStore) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MarkAllInboxNotificationsAsRead", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// MarkAllInboxNotificationsAsRead indicates an expected call of MarkAllInboxNotificationsAsRead. +func (mr *MockStoreMockRecorder) MarkAllInboxNotificationsAsRead(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkAllInboxNotificationsAsRead", reflect.TypeOf((*MockStore)(nil).MarkAllInboxNotificationsAsRead), ctx, arg) +} + // OIDCClaimFieldValues mocks base method. func (m *MockStore) OIDCClaimFieldValues(ctx context.Context, arg database.OIDCClaimFieldValuesParams) ([]string, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d72469650f0ea..4301461b9d426 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -469,6 +469,7 @@ type sqlcQuerier interface { ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) + MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error) // OIDCClaimFields returns a list of distinct keys in the the merged_claims fields. // This query is used to generate the list of available sync fields for idp sync settings. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ff135aaa8f14e..db608ac59718d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4514,6 +4514,24 @@ func (q *sqlQuerier) InsertInboxNotification(ctx context.Context, arg InsertInbo return i, err } +const markAllInboxNotificationsAsRead = `-- name: MarkAllInboxNotificationsAsRead :exec +UPDATE + inbox_notifications +SET + read_at = $1 +WHERE user_id = $2 and read_at IS NULL +` + +type MarkAllInboxNotificationsAsReadParams struct { + ReadAt sql.NullTime `db:"read_at" json:"read_at"` + UserID uuid.UUID `db:"user_id" json:"user_id"` +} + +func (q *sqlQuerier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error { + _, err := q.db.ExecContext(ctx, markAllInboxNotificationsAsRead, arg.ReadAt, arg.UserID) + return err +} + const updateInboxNotificationReadStatus = `-- name: UpdateInboxNotificationReadStatus :exec UPDATE inbox_notifications diff --git a/coderd/database/queries/notificationsinbox.sql b/coderd/database/queries/notificationsinbox.sql index 43ab63ae83652..5d9d8d22a6c45 100644 --- a/coderd/database/queries/notificationsinbox.sql +++ b/coderd/database/queries/notificationsinbox.sql @@ -57,3 +57,10 @@ SET read_at = $1 WHERE id = $2; + +-- name: MarkAllInboxNotificationsAsRead :exec +UPDATE + inbox_notifications +SET + read_at = $1 +WHERE user_id = $2 and read_at IS NULL; diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 5437165bb71a6..363e746dcdb16 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -345,3 +345,35 @@ func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *htt UnreadCount: int(unreadCount), }) } + +// markAllInboxNotificationsAsRead marks as read all unread notifications. +// @Summary Make all unread notifications as read +// @ID make-all-unread-notifications-as-read +// @Security CoderSessionToken +// @Produce json +// @Tags Notifications +// @Success 200 {object} codersdk.Response +// @Router /notifications/inbox/mark-all-as-read [put] +func (api *API) markAllInboxNotificationsAsRead(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + apikey = httpmw.APIKey(r) + ) + + err := api.Database.MarkAllInboxNotificationsAsRead(ctx, database.MarkAllInboxNotificationsAsReadParams{ + UserID: apikey.UserID, + ReadAt: sql.NullTime{Time: dbtime.Now(), Valid: true}, + }) + if err != nil { + api.Logger.Error(ctx, "failed to mark all unread notifications as read", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to mark all unread notifications as read.", + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateInboxNotificationReadStatusResponse{ + // Notification: convertInboxNotificationResponse(ctx, api.Logger, updatedNotification), + // UnreadCount: int(unreadCount), + }) +} diff --git a/codersdk/inboxnotification.go b/codersdk/inboxnotification.go index 845140ea658c7..de17aec87ebdd 100644 --- a/codersdk/inboxnotification.go +++ b/codersdk/inboxnotification.go @@ -109,3 +109,26 @@ func (c *Client) UpdateInboxNotificationReadStatus(ctx context.Context, notifID var resp UpdateInboxNotificationReadStatusResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } + +type MarkAllInboxNotificationsAsReadResponse struct { + UnreadCount int `json:"unread_count"` +} + +func (c *Client) MarkAllInboxNotificationsAsRead(ctx context.Context) (MarkAllInboxNotificationsAsReadResponse, error) { + res, err := c.Request( + ctx, http.MethodPut, + "/api/v2/notifications/inbox/mark-all-as-read", + nil, + ) + if err != nil { + return MarkAllInboxNotificationsAsReadResponse{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return MarkAllInboxNotificationsAsReadResponse{}, ReadBodyAsError(res) + } + + var resp MarkAllInboxNotificationsAsReadResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} diff --git a/docs/reference/api/notifications.md b/docs/reference/api/notifications.md index 9a181cc1d69c5..207875023a6ea 100644 --- a/docs/reference/api/notifications.md +++ b/docs/reference/api/notifications.md @@ -106,6 +106,44 @@ curl -X GET http://coder-server:8080/api/v2/notifications/inbox \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Make all unread notifications as read + +### Code samples + +```shell +# Example request using curl +curl -X PUT http://coder-server:8080/api/v2/notifications/inbox/mark-all-as-read \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PUT /notifications/inbox/mark-all-as-read` + +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Watch for new inbox notifications ### Code samples From e4e2967d3dd6465affba7ee4d852425004dc5eea Mon Sep 17 00:00:00 2001 From: defelmnq Date: Tue, 18 Mar 2025 13:36:34 +0000 Subject: [PATCH 02/12] fix linter --- coderd/database/dbmem/dbmem.go | 2 +- coderd/inboxnotifications.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b71fe9b49880d..463c27ad9bff5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9498,7 +9498,7 @@ func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceI return shares, nil } -func (q *FakeQuerier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { +func (q *FakeQuerier) MarkAllInboxNotificationsAsRead(_ context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { err := validateDatabaseType(arg) if err != nil { return err diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 363e746dcdb16..1ba8af42bbd94 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -373,7 +373,6 @@ func (api *API) markAllInboxNotificationsAsRead(rw http.ResponseWriter, r *http. } httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateInboxNotificationReadStatusResponse{ - // Notification: convertInboxNotificationResponse(ctx, api.Logger, updatedNotification), - // UnreadCount: int(unreadCount), + UnreadCount: 0, }) } From dd8ee20d134cdb33012e0df7dd3d49f4889984b5 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Tue, 18 Mar 2025 13:48:26 +0000 Subject: [PATCH 03/12] fix gen --- site/src/api/typesGenerated.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6cd0f8a6cfd1f..ec35823124bdf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1226,6 +1226,11 @@ export interface LoginWithPasswordResponse { readonly session_token: string; } +// From codersdk/inboxnotification.go +export interface MarkAllInboxNotificationsAsReadResponse { + readonly unread_count: number; +} + // From codersdk/provisionerdaemons.go export interface MatchedProvisioners { readonly count: number; From 8aaace39d8c59ba068637128cca622f480f37a4b Mon Sep 17 00:00:00 2001 From: defelmnq Date: Tue, 18 Mar 2025 14:46:16 +0000 Subject: [PATCH 04/12] fix dbauthz --- coderd/apidoc/docs.go | 4 +- coderd/apidoc/swagger.json | 4 +- coderd/database/dbauthz/dbauthz.go | 5 +- coderd/database/queries.sql.go | 3 +- .../database/queries/notificationsinbox.sql | 3 +- coderd/inboxnotifications.go | 6 +-- coderd/inboxnotifications_test.go | 54 +++++++++++++++++++ docs/reference/api/notifications.md | 2 +- 8 files changed, 70 insertions(+), 11 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7a7766bc29ea8..176ad1050f08b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1718,8 +1718,8 @@ const docTemplate = `{ "tags": [ "Notifications" ], - "summary": "Make all unread notifications as read", - "operationId": "make-all-unread-notifications-as-read", + "summary": "Mark all unread notifications as read", + "operationId": "mark-all-unread-notifications-as-read", "responses": { "200": { "description": "OK", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 29681be26a6bd..738c5605afaa5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1495,8 +1495,8 @@ ], "produces": ["application/json"], "tags": ["Notifications"], - "summary": "Make all unread notifications as read", - "operationId": "make-all-unread-notifications-as-read", + "summary": "Mark all unread notifications as read", + "operationId": "mark-all-unread-notifications-as-read", "responses": { "200": { "description": "OK", diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 3129b4cb17fb8..9d1d45c1b28e0 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3555,7 +3555,10 @@ func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID } func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceInboxNotification); err != nil { + resource := rbac.ResourceInboxNotification + resource.Owner = arg.UserID.String() + + if err := q.authorizeContext(ctx, policy.ActionUpdate, resource); err != nil { return err } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index db608ac59718d..d3235461a9af0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4519,7 +4519,8 @@ UPDATE inbox_notifications SET read_at = $1 -WHERE user_id = $2 and read_at IS NULL +WHERE + user_id = $2 and read_at IS NULL ` type MarkAllInboxNotificationsAsReadParams struct { diff --git a/coderd/database/queries/notificationsinbox.sql b/coderd/database/queries/notificationsinbox.sql index 5d9d8d22a6c45..41b48fe3d9505 100644 --- a/coderd/database/queries/notificationsinbox.sql +++ b/coderd/database/queries/notificationsinbox.sql @@ -63,4 +63,5 @@ UPDATE inbox_notifications SET read_at = $1 -WHERE user_id = $2 and read_at IS NULL; +WHERE + user_id = $2 and read_at IS NULL; diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 1ba8af42bbd94..e3efa83a8f530 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -346,9 +346,9 @@ func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *htt }) } -// markAllInboxNotificationsAsRead marks as read all unread notifications. -// @Summary Make all unread notifications as read -// @ID make-all-unread-notifications-as-read +// markAllInboxNotificationsAsRead marks as read all unread notifications for authenticated user. +// @Summary Mark all unread notifications as read +// @ID mark-all-unread-notifications-as-read // @Security CoderSessionToken // @Produce json // @Tags Notifications diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index 81e119381d281..59272bac94ad3 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -723,3 +723,57 @@ func TestInboxNotifications_ReadStatus(t *testing.T) { require.Empty(t, updatedNotif.Notification) }) } +func TestInboxNotifications_MarkAllAsRead(t *testing.T) { + t.Parallel() + + // I skip these tests specifically on windows as for now they are flaky - only on Windows. + // For now the idea is that the runner takes too long to insert the entries, could be worth + // investigating a manual Tx. + if runtime.GOOS == "windows" { + t.Skip("our runners are randomly taking too long to insert entries") + } + + t.Run("ok", func(t *testing.T) { + t.Parallel() + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{}) + firstUser := coderdtest.CreateFirstUser(t, client) + client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 0, notifs.UnreadCount) + require.Empty(t, notifs.Notifications) + + for i := range 20 { + dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{ + ID: uuid.New(), + UserID: member.ID, + TemplateID: notifications.TemplateWorkspaceOutOfMemory, + Title: fmt.Sprintf("Notification %d", i), + Actions: json.RawMessage("[]"), + Content: fmt.Sprintf("Content of the notif %d", i), + CreatedAt: dbtime.Now(), + }) + } + + notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 20, notifs.UnreadCount) + require.Len(t, notifs.Notifications, 20) + + resp, err := client.MarkAllInboxNotificationsAsRead(ctx) + require.NoError(t, err) + require.Zero(t, resp.UnreadCount) + + notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 0, notifs.UnreadCount) + require.Len(t, notifs.Notifications, 20) + }) +} diff --git a/docs/reference/api/notifications.md b/docs/reference/api/notifications.md index 207875023a6ea..a9469156fc785 100644 --- a/docs/reference/api/notifications.md +++ b/docs/reference/api/notifications.md @@ -106,7 +106,7 @@ curl -X GET http://coder-server:8080/api/v2/notifications/inbox \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Make all unread notifications as read +## Mark all unread notifications as read ### Code samples From 0d98bf6ff4e26075b9892da06df954d718758b10 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Tue, 18 Mar 2025 14:57:27 +0000 Subject: [PATCH 05/12] improve dbauthz --- coderd/database/dbauthz/dbauthz_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 60ded89d708c8..6e6380baffad4 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4647,7 +4647,7 @@ func (s *MethodTestSuite) TestNotifications() { check.Args(database.MarkAllInboxNotificationsAsReadParams{ UserID: u.ID, ReadAt: sql.NullTime{Time: dbtestutil.NowInDefaultTimezone(), Valid: true}, - }).Asserts(rbac.ResourceInboxNotification, policy.ActionUpdate) + }).Asserts(rbac.ResourceInboxNotification.WithOwner(u.ID.String()), policy.ActionUpdate) })) } From 27ad10bfed2a0dda00cb0d1f7135bc1167c3b856 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Tue, 18 Mar 2025 15:06:33 +0000 Subject: [PATCH 06/12] make fmt --- coderd/inboxnotifications_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index 59272bac94ad3..d91904cab6639 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -723,6 +723,7 @@ func TestInboxNotifications_ReadStatus(t *testing.T) { require.Empty(t, updatedNotif.Notification) }) } + func TestInboxNotifications_MarkAllAsRead(t *testing.T) { t.Parallel() From 452e92b4837bea5c6e1a1604096a89f7d4981e44 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Wed, 19 Mar 2025 22:47:47 +0000 Subject: [PATCH 07/12] change endpoint status code and add some more details --- coderd/apidoc/docs.go | 7 ++----- coderd/apidoc/swagger.json | 7 ++----- coderd/database/dbauthz/dbauthz.go | 3 +-- coderd/inboxnotifications.go | 6 ++---- coderd/inboxnotifications_test.go | 22 ++++++++++++++++++++++ docs/reference/api/notifications.md | 24 +++--------------------- 6 files changed, 32 insertions(+), 37 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 176ad1050f08b..562718b86229c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1721,11 +1721,8 @@ const docTemplate = `{ "summary": "Mark all unread notifications as read", "operationId": "mark-all-unread-notifications-as-read", "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } + "204": { + "description": "No Content" } } } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 738c5605afaa5..18ad1651d749e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1498,11 +1498,8 @@ "summary": "Mark all unread notifications as read", "operationId": "mark-all-unread-notifications-as-read", "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } + "204": { + "description": "No Content" } } } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9d1d45c1b28e0..5474cd78b0868 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3555,8 +3555,7 @@ func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID } func (q *querier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg database.MarkAllInboxNotificationsAsReadParams) error { - resource := rbac.ResourceInboxNotification - resource.Owner = arg.UserID.String() + resource := rbac.ResourceInboxNotification.WithOwner(arg.UserID.String()) if err := q.authorizeContext(ctx, policy.ActionUpdate, resource); err != nil { return err diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index e3efa83a8f530..bed0faafeea8e 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -352,7 +352,7 @@ func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *htt // @Security CoderSessionToken // @Produce json // @Tags Notifications -// @Success 200 {object} codersdk.Response +// @Success 204 // @Router /notifications/inbox/mark-all-as-read [put] func (api *API) markAllInboxNotificationsAsRead(rw http.ResponseWriter, r *http.Request) { var ( @@ -372,7 +372,5 @@ func (api *API) markAllInboxNotificationsAsRead(rw http.ResponseWriter, r *http. return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateInboxNotificationReadStatusResponse{ - UnreadCount: 0, - }) + rw.WriteHeader(http.StatusNoContent) } diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index d91904cab6639..043d3e3c16bf2 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -37,6 +37,7 @@ func TestInboxNotification_Watch(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. + // Related issue for fix : https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -305,6 +306,7 @@ func TestInboxNotifications_List(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. + // Related issue for fix : https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -588,6 +590,7 @@ func TestInboxNotifications_ReadStatus(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. + // Related issue for fix : https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -730,6 +733,7 @@ func TestInboxNotifications_MarkAllAsRead(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. + // Related issue for fix : https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -776,5 +780,23 @@ func TestInboxNotifications_MarkAllAsRead(t *testing.T) { require.NotNil(t, notifs) require.Equal(t, 0, notifs.UnreadCount) require.Len(t, notifs.Notifications, 20) + + for i := range 10 { + dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{ + ID: uuid.New(), + UserID: member.ID, + TemplateID: notifications.TemplateWorkspaceOutOfMemory, + Title: fmt.Sprintf("Notification %d", i), + Actions: json.RawMessage("[]"), + Content: fmt.Sprintf("Content of the notif %d", i), + CreatedAt: dbtime.Now(), + }) + } + + notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 10, notifs.UnreadCount) + require.Len(t, notifs.Notifications, 25) }) } diff --git a/docs/reference/api/notifications.md b/docs/reference/api/notifications.md index a9469156fc785..67b61bccb6302 100644 --- a/docs/reference/api/notifications.md +++ b/docs/reference/api/notifications.md @@ -113,34 +113,16 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl curl -X PUT http://coder-server:8080/api/v2/notifications/inbox/mark-all-as-read \ - -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` `PUT /notifications/inbox/mark-all-as-read` -### Example responses - -> 200 Response - -```json -{ - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] -} -``` - ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|--------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | +| Status | Meaning | Description | Schema | +|--------|-----------------------------------------------------------------|-------------|--------| +| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | | To perform this operation, you must be authenticated. [Learn more](authentication.md). From deff124bf1a926a24af221ac6fb1b4d6d17e32a9 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Wed, 19 Mar 2025 23:15:05 +0000 Subject: [PATCH 08/12] fix 204 --- coderd/inboxnotifications_test.go | 3 +-- codersdk/inboxnotification.go | 11 +++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index 043d3e3c16bf2..8d8a3ceb62cfe 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -771,9 +771,8 @@ func TestInboxNotifications_MarkAllAsRead(t *testing.T) { require.Equal(t, 20, notifs.UnreadCount) require.Len(t, notifs.Notifications, 20) - resp, err := client.MarkAllInboxNotificationsAsRead(ctx) + err = client.MarkAllInboxNotificationsAsRead(ctx) require.NoError(t, err) - require.Zero(t, resp.UnreadCount) notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) require.NoError(t, err) diff --git a/codersdk/inboxnotification.go b/codersdk/inboxnotification.go index de17aec87ebdd..d177b7cb0b16a 100644 --- a/codersdk/inboxnotification.go +++ b/codersdk/inboxnotification.go @@ -114,21 +114,20 @@ type MarkAllInboxNotificationsAsReadResponse struct { UnreadCount int `json:"unread_count"` } -func (c *Client) MarkAllInboxNotificationsAsRead(ctx context.Context) (MarkAllInboxNotificationsAsReadResponse, error) { +func (c *Client) MarkAllInboxNotificationsAsRead(ctx context.Context) error { res, err := c.Request( ctx, http.MethodPut, "/api/v2/notifications/inbox/mark-all-as-read", nil, ) if err != nil { - return MarkAllInboxNotificationsAsReadResponse{}, err + return err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return MarkAllInboxNotificationsAsReadResponse{}, ReadBodyAsError(res) + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) } - var resp MarkAllInboxNotificationsAsReadResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + return nil } From 0815e96c9cbe9ec51963030fb91ca5c0a5700fb4 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Wed, 19 Mar 2025 23:25:53 +0000 Subject: [PATCH 09/12] work on swagger --- coderd/apidoc/docs.go | 3 --- coderd/apidoc/swagger.json | 1 - site/src/api/typesGenerated.ts | 5 ----- 3 files changed, 9 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 562718b86229c..0ac151673f115 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1712,9 +1712,6 @@ const docTemplate = `{ "CoderSessionToken": [] } ], - "produces": [ - "application/json" - ], "tags": [ "Notifications" ], diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 18ad1651d749e..059907fad16de 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1493,7 +1493,6 @@ "CoderSessionToken": [] } ], - "produces": ["application/json"], "tags": ["Notifications"], "summary": "Mark all unread notifications as read", "operationId": "mark-all-unread-notifications-as-read", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ec35823124bdf..6cd0f8a6cfd1f 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1226,11 +1226,6 @@ export interface LoginWithPasswordResponse { readonly session_token: string; } -// From codersdk/inboxnotification.go -export interface MarkAllInboxNotificationsAsReadResponse { - readonly unread_count: number; -} - // From codersdk/provisionerdaemons.go export interface MatchedProvisioners { readonly count: number; From a1567a0e389e8f0d926ce5e5411d2a676a464955 Mon Sep 17 00:00:00 2001 From: defelmnq Date: Wed, 19 Mar 2025 23:39:51 +0000 Subject: [PATCH 10/12] work on swagger --- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 1 + codersdk/inboxnotification.go | 4 ---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0ac151673f115..562718b86229c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1712,6 +1712,9 @@ const docTemplate = `{ "CoderSessionToken": [] } ], + "produces": [ + "application/json" + ], "tags": [ "Notifications" ], diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 059907fad16de..18ad1651d749e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1493,6 +1493,7 @@ "CoderSessionToken": [] } ], + "produces": ["application/json"], "tags": ["Notifications"], "summary": "Mark all unread notifications as read", "operationId": "mark-all-unread-notifications-as-read", diff --git a/codersdk/inboxnotification.go b/codersdk/inboxnotification.go index d177b7cb0b16a..056584d6cf359 100644 --- a/codersdk/inboxnotification.go +++ b/codersdk/inboxnotification.go @@ -110,10 +110,6 @@ func (c *Client) UpdateInboxNotificationReadStatus(ctx context.Context, notifID return resp, json.NewDecoder(res.Body).Decode(&resp) } -type MarkAllInboxNotificationsAsReadResponse struct { - UnreadCount int `json:"unread_count"` -} - func (c *Client) MarkAllInboxNotificationsAsRead(ctx context.Context) error { res, err := c.Request( ctx, http.MethodPut, From 7881887822d885f0e0ab0e3c6f3830e7a0fb0b4d Mon Sep 17 00:00:00 2001 From: defelmnq Date: Wed, 19 Mar 2025 23:50:11 +0000 Subject: [PATCH 11/12] work on swagger --- coderd/apidoc/docs.go | 3 --- coderd/apidoc/swagger.json | 1 - coderd/inboxnotifications.go | 1 - 3 files changed, 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 562718b86229c..0ac151673f115 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1712,9 +1712,6 @@ const docTemplate = `{ "CoderSessionToken": [] } ], - "produces": [ - "application/json" - ], "tags": [ "Notifications" ], diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 18ad1651d749e..059907fad16de 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1493,7 +1493,6 @@ "CoderSessionToken": [] } ], - "produces": ["application/json"], "tags": ["Notifications"], "summary": "Mark all unread notifications as read", "operationId": "mark-all-unread-notifications-as-read", diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index bed0faafeea8e..423b1ce0ac034 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -350,7 +350,6 @@ func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *htt // @Summary Mark all unread notifications as read // @ID mark-all-unread-notifications-as-read // @Security CoderSessionToken -// @Produce json // @Tags Notifications // @Success 204 // @Router /notifications/inbox/mark-all-as-read [put] From 9d871fc4a92a9c7bca84c8d865abd12e9f87746c Mon Sep 17 00:00:00 2001 From: defelmnq Date: Thu, 20 Mar 2025 12:16:08 +0000 Subject: [PATCH 12/12] fixes from review --- coderd/database/dbmem/dbmem.go | 5 +---- coderd/inboxnotifications_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 463c27ad9bff5..5d0d0f4af0820 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9506,10 +9506,7 @@ func (q *FakeQuerier) MarkAllInboxNotificationsAsRead(_ context.Context, arg dat for idx, notif := range q.inboxNotifications { if notif.UserID == arg.UserID && !notif.ReadAt.Valid { - q.inboxNotifications[idx].ReadAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } + q.inboxNotifications[idx].ReadAt = arg.ReadAt } } diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index 8d8a3ceb62cfe..1e72f2b51789e 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -37,7 +37,7 @@ func TestInboxNotification_Watch(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. - // Related issue for fix : https://github.com/coder/internal/issues/503 + // see: https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -306,7 +306,7 @@ func TestInboxNotifications_List(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. - // Related issue for fix : https://github.com/coder/internal/issues/503 + // see: https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -590,7 +590,7 @@ func TestInboxNotifications_ReadStatus(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. - // Related issue for fix : https://github.com/coder/internal/issues/503 + // see: https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") } @@ -733,7 +733,7 @@ func TestInboxNotifications_MarkAllAsRead(t *testing.T) { // I skip these tests specifically on windows as for now they are flaky - only on Windows. // For now the idea is that the runner takes too long to insert the entries, could be worth // investigating a manual Tx. - // Related issue for fix : https://github.com/coder/internal/issues/503 + // see: https://github.com/coder/internal/issues/503 if runtime.GOOS == "windows" { t.Skip("our runners are randomly taking too long to insert entries") }