From af6f4c57d29035bbefd292f5804106c1e94b79a4 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 18 Feb 2026 11:29:08 -0300 Subject: [PATCH 1/3] feat: workflow notification preferences Signed-off-by: Gustavo Carvalho --- docs/docs.go | 8 +++ docs/swagger.json | 8 +++ docs/swagger.yaml | 8 +++ internal/api/handler/users.go | 54 +++++++++++++------ .../api/handler/users_integration_test.go | 26 +++++++-- internal/service/relational/ccf_internal.go | 5 ++ 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 0dafb113..68b3492f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -29044,6 +29044,14 @@ const docTemplate = `{ "lastName": { "type": "string" }, + "taskAvailableEmailSubscribed": { + "description": "TaskAvailableEmailSubscribed indicates if the user wants an email when tasks become available", + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "description": "TaskDailyDigestSubscribed indicates if the user wants to receive a daily task digest email", + "type": "boolean" + }, "updatedAt": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 99b1c0de..95e8cfc1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -29038,6 +29038,14 @@ "lastName": { "type": "string" }, + "taskAvailableEmailSubscribed": { + "description": "TaskAvailableEmailSubscribed indicates if the user wants an email when tasks become available", + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "description": "TaskDailyDigestSubscribed indicates if the user wants to receive a daily task digest email", + "type": "boolean" + }, "updatedAt": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fabc5c31..a31aa0e5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -6248,6 +6248,14 @@ definitions: type: string lastName: type: string + taskAvailableEmailSubscribed: + description: TaskAvailableEmailSubscribed indicates if the user wants an email + when tasks become available + type: boolean + taskDailyDigestSubscribed: + description: TaskDailyDigestSubscribed indicates if the user wants to receive + a daily task digest email + type: boolean updatedAt: type: string userAttributes: diff --git a/internal/api/handler/users.go b/internal/api/handler/users.go index 315afb9c..5544dff5 100644 --- a/internal/api/handler/users.go +++ b/internal/api/handler/users.go @@ -23,6 +23,18 @@ type userResponse struct { AuthProvider *string `json:"authProvider,omitempty"` } +type digestSubscriptionResponse struct { + Subscribed bool `json:"subscribed"` + TaskAvailableEmailSubscribed bool `json:"taskAvailableEmailSubscribed"` + TaskDailyDigestSubscribed bool `json:"taskDailyDigestSubscribed"` +} + +type updateDigestSubscriptionRequest struct { + Subscribed *bool `json:"subscribed"` + TaskAvailableEmailSubscribed *bool `json:"taskAvailableEmailSubscribed"` + TaskDailyDigestSubscribed *bool `json:"taskDailyDigestSubscribed"` +} + func NewUserHandler(sugar *zap.SugaredLogger, db *gorm.DB) *UserHandler { return &UserHandler{ sugar: sugar, @@ -408,10 +420,6 @@ func (h *UserHandler) ChangeLoggedInUserPassword(ctx echo.Context) error { // @Security OAuth2Password // @Router /users/me/digest-subscription [get] func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { - type digestSubscriptionResponse struct { - Subscribed bool `json:"subscribed"` - } - userClaims := ctx.Get("user").(*authn.UserClaims) email := userClaims.Subject @@ -425,7 +433,11 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { } return ctx.JSON(200, GenericDataResponse[digestSubscriptionResponse]{ - Data: digestSubscriptionResponse{Subscribed: user.DigestSubscribed}, + Data: digestSubscriptionResponse{ + Subscribed: user.DigestSubscribed, + TaskAvailableEmailSubscribed: user.TaskAvailableEmailSubscribed, + TaskDailyDigestSubscribed: user.TaskDailyDigestSubscribed, + }, }) } @@ -445,13 +457,6 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { // @Security OAuth2Password // @Router /users/me/digest-subscription [put] func (h *UserHandler) UpdateDigestSubscription(ctx echo.Context) error { - type updateDigestSubscriptionRequest struct { - Subscribed bool `json:"subscribed"` - } - type digestSubscriptionResponse struct { - Subscribed bool `json:"subscribed"` - } - userClaims := ctx.Get("user").(*authn.UserClaims) var req updateDigestSubscriptionRequest @@ -470,16 +475,35 @@ func (h *UserHandler) UpdateDigestSubscription(ctx echo.Context) error { return ctx.JSON(500, api.NewError(err)) } - user.DigestSubscribed = req.Subscribed + if req.Subscribed != nil { + user.DigestSubscribed = *req.Subscribed + } + if req.TaskAvailableEmailSubscribed != nil { + user.TaskAvailableEmailSubscribed = *req.TaskAvailableEmailSubscribed + } + if req.TaskDailyDigestSubscribed != nil { + user.TaskDailyDigestSubscribed = *req.TaskDailyDigestSubscribed + } + if err := h.db.Save(&user).Error; err != nil { h.sugar.Errorw("Failed to update user digest subscription", "error", err) return ctx.JSON(500, api.NewError(err)) } - h.sugar.Debugw("User digest subscription updated", "email", email, "subscribed", req.Subscribed) + h.sugar.Debugw( + "User digest subscription updated", + "email", email, + "subscribed", user.DigestSubscribed, + "taskAvailableEmailSubscribed", user.TaskAvailableEmailSubscribed, + "taskDailyDigestSubscribed", user.TaskDailyDigestSubscribed, + ) return ctx.JSON(200, GenericDataResponse[digestSubscriptionResponse]{ - Data: digestSubscriptionResponse{Subscribed: user.DigestSubscribed}, + Data: digestSubscriptionResponse{ + Subscribed: user.DigestSubscribed, + TaskAvailableEmailSubscribed: user.TaskAvailableEmailSubscribed, + TaskDailyDigestSubscribed: user.TaskDailyDigestSubscribed, + }, }) } diff --git a/internal/api/handler/users_integration_test.go b/internal/api/handler/users_integration_test.go index 503287f4..bdc19fd7 100644 --- a/internal/api/handler/users_integration_test.go +++ b/internal/api/handler/users_integration_test.go @@ -399,7 +399,9 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { var response struct { Data struct { - Subscribed bool `json:"subscribed"` + Subscribed bool `json:"subscribed"` + TaskAvailableEmailSubscribed bool `json:"taskAvailableEmailSubscribed"` + TaskDailyDigestSubscribed bool `json:"taskDailyDigestSubscribed"` } `json:"data"` } err = json.Unmarshal(rec.Body.Bytes(), &response) @@ -407,11 +409,17 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { // The default should be false for new users suite.False(response.Data.Subscribed, "Expected default digest subscription to be false") + suite.False(response.Data.TaskAvailableEmailSubscribed, "Expected task available email subscription to default to false") + suite.False(response.Data.TaskDailyDigestSubscribed, "Expected task daily digest subscription to default to false") }) suite.Run("UpdateDigestSubscription", func() { // Test subscribing to digest - payload := map[string]bool{"subscribed": true} + payload := map[string]interface{}{ + "subscribed": true, + "taskAvailableEmailSubscribed": true, + "taskDailyDigestSubscribed": true, + } payloadJSON, err := json.Marshal(payload) suite.Require().NoError(err, "Failed to marshal update digest subscription request") @@ -425,16 +433,23 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { var response struct { Data struct { - Subscribed bool `json:"subscribed"` + Subscribed bool `json:"subscribed"` + TaskAvailableEmailSubscribed bool `json:"taskAvailableEmailSubscribed"` + TaskDailyDigestSubscribed bool `json:"taskDailyDigestSubscribed"` } `json:"data"` } err = json.Unmarshal(rec.Body.Bytes(), &response) suite.Require().NoError(err, "Failed to unmarshal UpdateDigestSubscription response") suite.True(response.Data.Subscribed, "Expected digest subscription to be updated to true") + suite.True(response.Data.TaskAvailableEmailSubscribed, "Expected task available email subscription to be updated to true") + suite.True(response.Data.TaskDailyDigestSubscribed, "Expected task daily digest subscription to be updated to true") // Test unsubscribing from digest - payload = map[string]bool{"subscribed": false} + payload = map[string]interface{}{ + "subscribed": false, + "taskDailyDigestSubscribed": false, + } payloadJSON, err = json.Marshal(payload) suite.Require().NoError(err, "Failed to marshal unsubscribe digest request") @@ -450,6 +465,8 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { suite.Require().NoError(err, "Failed to unmarshal unsubscribe digest response") suite.False(response.Data.Subscribed, "Expected digest subscription to be updated to false") + suite.True(response.Data.TaskAvailableEmailSubscribed, "Expected task available email subscription to remain unchanged when omitted") + suite.False(response.Data.TaskDailyDigestSubscribed, "Expected task daily digest subscription to be updated to false") }) suite.Run("UpdateDigestSubscriptionInvalidPayload", func() { @@ -466,4 +483,5 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { suite.server.E().ServeHTTP(rec, req) suite.Equal(400, rec.Code, "Expected Bad Request response for invalid payload") }) + } diff --git a/internal/service/relational/ccf_internal.go b/internal/service/relational/ccf_internal.go index b5cb1b48..4ee7dbf5 100644 --- a/internal/service/relational/ccf_internal.go +++ b/internal/service/relational/ccf_internal.go @@ -30,6 +30,11 @@ type User struct { // DigestSubscribed indicates if the user wants to receive evidence digest emails DigestSubscribed bool `json:"digestSubscribed" gorm:"default:false"` + + // TaskAvailableEmailSubscribed indicates if the user wants an email when tasks become available + TaskAvailableEmailSubscribed bool `json:"taskAvailableEmailSubscribed" gorm:"default:false"` + // TaskDailyDigestSubscribed indicates if the user wants to receive a daily task digest email + TaskDailyDigestSubscribed bool `json:"taskDailyDigestSubscribed" gorm:"default:false"` } func (User) TableName() string { From 56e9d38e0f1dc8ec235aa428c5e50f99c839cea6 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 18 Feb 2026 12:09:28 -0300 Subject: [PATCH 2/3] fix: copilot issues Signed-off-by: Gustavo Carvalho --- docs/docs.go | 60 +++++++++++++++++++++++++---------- docs/swagger.json | 60 +++++++++++++++++++++++++---------- docs/swagger.yaml | 50 ++++++++++++++++++++--------- internal/api/handler/users.go | 28 ++++++++-------- 4 files changed, 137 insertions(+), 61 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 68b3492f..d516b6e1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -16505,19 +16505,19 @@ const docTemplate = `{ }, "/users/me/digest-subscription": { "get": { - "description": "Gets the current user's digest email subscription status", + "description": "Gets the current user's digest and workflow notification email preferences", "produces": [ "application/json" ], "tags": [ "Users" ], - "summary": "Get digest subscription status", + "summary": "Get notification preferences", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_UserHandler" + "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" } }, "401": { @@ -16546,7 +16546,7 @@ const docTemplate = `{ ] }, "put": { - "description": "Updates the current user's digest email subscription status", + "description": "Updates the current user's digest and workflow notification email preferences", "consumes": [ "application/json" ], @@ -16556,15 +16556,15 @@ const docTemplate = `{ "tags": [ "Users" ], - "summary": "Update digest subscription status", + "summary": "Update notification preferences", "parameters": [ { - "description": "Subscription status", + "description": "Notification preferences", "name": "subscription", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.UserHandler" + "$ref": "#/definitions/handler.UpdateDigestSubscriptionRequest" } } ], @@ -16572,7 +16572,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_UserHandler" + "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" } }, "400": { @@ -19695,6 +19695,20 @@ const docTemplate = `{ } } }, + "handler.DigestSubscriptionResponse": { + "type": "object", + "properties": { + "subscribed": { + "type": "boolean" + }, + "taskAvailableEmailSubscribed": { + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "type": "boolean" + } + } + }, "handler.EvidenceActivity": { "type": "object", "properties": { @@ -20615,53 +20629,53 @@ const docTemplate = `{ } } }, - "handler.GenericDataResponse-handler_FilterImportResponse": { + "handler.GenericDataResponse-handler_DigestSubscriptionResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterImportResponse" + "$ref": "#/definitions/handler.DigestSubscriptionResponse" } ] } } }, - "handler.GenericDataResponse-handler_FilterWithAssociations": { + "handler.GenericDataResponse-handler_FilterImportResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterWithAssociations" + "$ref": "#/definitions/handler.FilterImportResponse" } ] } } }, - "handler.GenericDataResponse-handler_OscalLikeEvidence": { + "handler.GenericDataResponse-handler_FilterWithAssociations": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.OscalLikeEvidence" + "$ref": "#/definitions/handler.FilterWithAssociations" } ] } } }, - "handler.GenericDataResponse-handler_UserHandler": { + "handler.GenericDataResponse-handler_OscalLikeEvidence": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.UserHandler" + "$ref": "#/definitions/handler.OscalLikeEvidence" } ] } @@ -21567,6 +21581,20 @@ const docTemplate = `{ } } }, + "handler.UpdateDigestSubscriptionRequest": { + "type": "object", + "properties": { + "subscribed": { + "type": "boolean" + }, + "taskAvailableEmailSubscribed": { + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "type": "boolean" + } + } + }, "handler.UserHandler": { "type": "object" }, diff --git a/docs/swagger.json b/docs/swagger.json index 95e8cfc1..995de976 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -16499,19 +16499,19 @@ }, "/users/me/digest-subscription": { "get": { - "description": "Gets the current user's digest email subscription status", + "description": "Gets the current user's digest and workflow notification email preferences", "produces": [ "application/json" ], "tags": [ "Users" ], - "summary": "Get digest subscription status", + "summary": "Get notification preferences", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_UserHandler" + "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" } }, "401": { @@ -16540,7 +16540,7 @@ ] }, "put": { - "description": "Updates the current user's digest email subscription status", + "description": "Updates the current user's digest and workflow notification email preferences", "consumes": [ "application/json" ], @@ -16550,15 +16550,15 @@ "tags": [ "Users" ], - "summary": "Update digest subscription status", + "summary": "Update notification preferences", "parameters": [ { - "description": "Subscription status", + "description": "Notification preferences", "name": "subscription", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.UserHandler" + "$ref": "#/definitions/handler.UpdateDigestSubscriptionRequest" } } ], @@ -16566,7 +16566,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_UserHandler" + "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" } }, "400": { @@ -19689,6 +19689,20 @@ } } }, + "handler.DigestSubscriptionResponse": { + "type": "object", + "properties": { + "subscribed": { + "type": "boolean" + }, + "taskAvailableEmailSubscribed": { + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "type": "boolean" + } + } + }, "handler.EvidenceActivity": { "type": "object", "properties": { @@ -20609,53 +20623,53 @@ } } }, - "handler.GenericDataResponse-handler_FilterImportResponse": { + "handler.GenericDataResponse-handler_DigestSubscriptionResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterImportResponse" + "$ref": "#/definitions/handler.DigestSubscriptionResponse" } ] } } }, - "handler.GenericDataResponse-handler_FilterWithAssociations": { + "handler.GenericDataResponse-handler_FilterImportResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterWithAssociations" + "$ref": "#/definitions/handler.FilterImportResponse" } ] } } }, - "handler.GenericDataResponse-handler_OscalLikeEvidence": { + "handler.GenericDataResponse-handler_FilterWithAssociations": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.OscalLikeEvidence" + "$ref": "#/definitions/handler.FilterWithAssociations" } ] } } }, - "handler.GenericDataResponse-handler_UserHandler": { + "handler.GenericDataResponse-handler_OscalLikeEvidence": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.UserHandler" + "$ref": "#/definitions/handler.OscalLikeEvidence" } ] } @@ -21561,6 +21575,20 @@ } } }, + "handler.UpdateDigestSubscriptionRequest": { + "type": "object", + "properties": { + "subscribed": { + "type": "boolean" + }, + "taskAvailableEmailSubscribed": { + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "type": "boolean" + } + } + }, "handler.UserHandler": { "type": "object" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a31aa0e5..a7d484c8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -117,6 +117,15 @@ definitions: status: type: string type: object + handler.DigestSubscriptionResponse: + properties: + subscribed: + type: boolean + taskAvailableEmailSubscribed: + type: boolean + taskDailyDigestSubscribed: + type: boolean + type: object handler.EvidenceActivity: properties: description: @@ -764,6 +773,13 @@ definitions: - $ref: '#/definitions/digest.EvidenceSummary' description: Items from the list response type: object + handler.GenericDataResponse-handler_DigestSubscriptionResponse: + properties: + data: + allOf: + - $ref: '#/definitions/handler.DigestSubscriptionResponse' + description: Items from the list response + type: object handler.GenericDataResponse-handler_FilterImportResponse: properties: data: @@ -785,13 +801,6 @@ definitions: - $ref: '#/definitions/handler.OscalLikeEvidence' description: Items from the list response type: object - handler.GenericDataResponse-handler_UserHandler: - properties: - data: - allOf: - - $ref: '#/definitions/handler.UserHandler' - description: Items from the list response - type: object handler.GenericDataResponse-oscal_BuildByPropsResponse: properties: data: @@ -1299,6 +1308,15 @@ definitions: $ref: '#/definitions/handler.StatusCount' type: array type: object + handler.UpdateDigestSubscriptionRequest: + properties: + subscribed: + type: boolean + taskAvailableEmailSubscribed: + type: boolean + taskDailyDigestSubscribed: + type: boolean + type: object handler.UserHandler: type: object handler.createFilterRequest: @@ -18008,14 +18026,15 @@ paths: - Users /users/me/digest-subscription: get: - description: Gets the current user's digest email subscription status + description: Gets the current user's digest and workflow notification email + preferences produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handler.GenericDataResponse-handler_UserHandler' + $ref: '#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse' "401": description: Unauthorized schema: @@ -18030,27 +18049,28 @@ paths: $ref: '#/definitions/api.Error' security: - OAuth2Password: [] - summary: Get digest subscription status + summary: Get notification preferences tags: - Users put: consumes: - application/json - description: Updates the current user's digest email subscription status + description: Updates the current user's digest and workflow notification email + preferences parameters: - - description: Subscription status + - description: Notification preferences in: body name: subscription required: true schema: - $ref: '#/definitions/handler.UserHandler' + $ref: '#/definitions/handler.UpdateDigestSubscriptionRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handler.GenericDataResponse-handler_UserHandler' + $ref: '#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse' "400": description: Bad Request schema: @@ -18069,7 +18089,7 @@ paths: $ref: '#/definitions/api.Error' security: - OAuth2Password: [] - summary: Update digest subscription status + summary: Update notification preferences tags: - Users /workflows/control-relationships: diff --git a/internal/api/handler/users.go b/internal/api/handler/users.go index 5544dff5..569ff58c 100644 --- a/internal/api/handler/users.go +++ b/internal/api/handler/users.go @@ -23,13 +23,13 @@ type userResponse struct { AuthProvider *string `json:"authProvider,omitempty"` } -type digestSubscriptionResponse struct { +type DigestSubscriptionResponse struct { Subscribed bool `json:"subscribed"` TaskAvailableEmailSubscribed bool `json:"taskAvailableEmailSubscribed"` TaskDailyDigestSubscribed bool `json:"taskDailyDigestSubscribed"` } -type updateDigestSubscriptionRequest struct { +type UpdateDigestSubscriptionRequest struct { Subscribed *bool `json:"subscribed"` TaskAvailableEmailSubscribed *bool `json:"taskAvailableEmailSubscribed"` TaskDailyDigestSubscribed *bool `json:"taskDailyDigestSubscribed"` @@ -409,11 +409,11 @@ func (h *UserHandler) ChangeLoggedInUserPassword(ctx echo.Context) error { // GetDigestSubscription godoc // -// @Summary Get digest subscription status -// @Description Gets the current user's digest email subscription status +// @Summary Get notification preferences +// @Description Gets the current user's digest and workflow notification email preferences // @Tags Users // @Produce json -// @Success 200 {object} handler.GenericDataResponse[handler.UserHandler.GetDigestSubscription.digestSubscriptionResponse] +// @Success 200 {object} handler.GenericDataResponse[handler.DigestSubscriptionResponse] // @Failure 401 {object} api.Error // @Failure 404 {object} api.Error // @Failure 500 {object} api.Error @@ -432,8 +432,8 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { return ctx.JSON(500, api.NewError(err)) } - return ctx.JSON(200, GenericDataResponse[digestSubscriptionResponse]{ - Data: digestSubscriptionResponse{ + return ctx.JSON(200, GenericDataResponse[DigestSubscriptionResponse]{ + Data: DigestSubscriptionResponse{ Subscribed: user.DigestSubscribed, TaskAvailableEmailSubscribed: user.TaskAvailableEmailSubscribed, TaskDailyDigestSubscribed: user.TaskDailyDigestSubscribed, @@ -443,13 +443,13 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { // UpdateDigestSubscription godoc // -// @Summary Update digest subscription status -// @Description Updates the current user's digest email subscription status +// @Summary Update notification preferences +// @Description Updates the current user's digest and workflow notification email preferences // @Tags Users // @Accept json // @Produce json -// @Param subscription body handler.UserHandler.UpdateDigestSubscription.updateDigestSubscriptionRequest true "Subscription status" -// @Success 200 {object} handler.GenericDataResponse[handler.UserHandler.UpdateDigestSubscription.digestSubscriptionResponse] +// @Param subscription body handler.UpdateDigestSubscriptionRequest true "Notification preferences" +// @Success 200 {object} handler.GenericDataResponse[handler.DigestSubscriptionResponse] // @Failure 400 {object} api.Error // @Failure 401 {object} api.Error // @Failure 404 {object} api.Error @@ -459,7 +459,7 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { func (h *UserHandler) UpdateDigestSubscription(ctx echo.Context) error { userClaims := ctx.Get("user").(*authn.UserClaims) - var req updateDigestSubscriptionRequest + var req UpdateDigestSubscriptionRequest if err := ctx.Bind(&req); err != nil { h.sugar.Errorw("Failed to bind update digest subscription request", "error", err) return ctx.JSON(400, api.NewError(err)) @@ -498,8 +498,8 @@ func (h *UserHandler) UpdateDigestSubscription(ctx echo.Context) error { "taskDailyDigestSubscribed", user.TaskDailyDigestSubscribed, ) - return ctx.JSON(200, GenericDataResponse[digestSubscriptionResponse]{ - Data: digestSubscriptionResponse{ + return ctx.JSON(200, GenericDataResponse[DigestSubscriptionResponse]{ + Data: DigestSubscriptionResponse{ Subscribed: user.DigestSubscribed, TaskAvailableEmailSubscribed: user.TaskAvailableEmailSubscribed, TaskDailyDigestSubscribed: user.TaskDailyDigestSubscribed, From d3f32c332b3c3b48d3d08950fc5fc6fc505c5e1b Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 18 Feb 2026 12:41:01 -0300 Subject: [PATCH 3/3] fix: change methods per copilot review Signed-off-by: Gustavo Carvalho --- docs/docs.go | 54 +++++++++---------- docs/swagger.json | 54 +++++++++---------- docs/swagger.yaml | 42 +++++++-------- internal/api/handler/users.go | 42 +++++++-------- .../api/handler/users_integration_test.go | 32 +++++------ 5 files changed, 112 insertions(+), 112 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index d516b6e1..28d287c2 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -16503,7 +16503,7 @@ const docTemplate = `{ ] } }, - "/users/me/digest-subscription": { + "/users/me/subscriptions": { "get": { "description": "Gets the current user's digest and workflow notification email preferences", "produces": [ @@ -16517,7 +16517,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" + "$ref": "#/definitions/handler.GenericDataResponse-handler_SubscriptionsResponse" } }, "401": { @@ -16564,7 +16564,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.UpdateDigestSubscriptionRequest" + "$ref": "#/definitions/handler.UpdateSubscriptionsRequest" } } ], @@ -16572,7 +16572,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" + "$ref": "#/definitions/handler.GenericDataResponse-handler_SubscriptionsResponse" } }, "400": { @@ -19695,20 +19695,6 @@ const docTemplate = `{ } } }, - "handler.DigestSubscriptionResponse": { - "type": "object", - "properties": { - "subscribed": { - "type": "boolean" - }, - "taskAvailableEmailSubscribed": { - "type": "boolean" - }, - "taskDailyDigestSubscribed": { - "type": "boolean" - } - } - }, "handler.EvidenceActivity": { "type": "object", "properties": { @@ -20629,53 +20615,53 @@ const docTemplate = `{ } } }, - "handler.GenericDataResponse-handler_DigestSubscriptionResponse": { + "handler.GenericDataResponse-handler_FilterImportResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.DigestSubscriptionResponse" + "$ref": "#/definitions/handler.FilterImportResponse" } ] } } }, - "handler.GenericDataResponse-handler_FilterImportResponse": { + "handler.GenericDataResponse-handler_FilterWithAssociations": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterImportResponse" + "$ref": "#/definitions/handler.FilterWithAssociations" } ] } } }, - "handler.GenericDataResponse-handler_FilterWithAssociations": { + "handler.GenericDataResponse-handler_OscalLikeEvidence": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterWithAssociations" + "$ref": "#/definitions/handler.OscalLikeEvidence" } ] } } }, - "handler.GenericDataResponse-handler_OscalLikeEvidence": { + "handler.GenericDataResponse-handler_SubscriptionsResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.OscalLikeEvidence" + "$ref": "#/definitions/handler.SubscriptionsResponse" } ] } @@ -21581,7 +21567,21 @@ const docTemplate = `{ } } }, - "handler.UpdateDigestSubscriptionRequest": { + "handler.SubscriptionsResponse": { + "type": "object", + "properties": { + "subscribed": { + "type": "boolean" + }, + "taskAvailableEmailSubscribed": { + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "type": "boolean" + } + } + }, + "handler.UpdateSubscriptionsRequest": { "type": "object", "properties": { "subscribed": { diff --git a/docs/swagger.json b/docs/swagger.json index 995de976..fa017b39 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -16497,7 +16497,7 @@ ] } }, - "/users/me/digest-subscription": { + "/users/me/subscriptions": { "get": { "description": "Gets the current user's digest and workflow notification email preferences", "produces": [ @@ -16511,7 +16511,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" + "$ref": "#/definitions/handler.GenericDataResponse-handler_SubscriptionsResponse" } }, "401": { @@ -16558,7 +16558,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.UpdateDigestSubscriptionRequest" + "$ref": "#/definitions/handler.UpdateSubscriptionsRequest" } } ], @@ -16566,7 +16566,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse" + "$ref": "#/definitions/handler.GenericDataResponse-handler_SubscriptionsResponse" } }, "400": { @@ -19689,20 +19689,6 @@ } } }, - "handler.DigestSubscriptionResponse": { - "type": "object", - "properties": { - "subscribed": { - "type": "boolean" - }, - "taskAvailableEmailSubscribed": { - "type": "boolean" - }, - "taskDailyDigestSubscribed": { - "type": "boolean" - } - } - }, "handler.EvidenceActivity": { "type": "object", "properties": { @@ -20623,53 +20609,53 @@ } } }, - "handler.GenericDataResponse-handler_DigestSubscriptionResponse": { + "handler.GenericDataResponse-handler_FilterImportResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.DigestSubscriptionResponse" + "$ref": "#/definitions/handler.FilterImportResponse" } ] } } }, - "handler.GenericDataResponse-handler_FilterImportResponse": { + "handler.GenericDataResponse-handler_FilterWithAssociations": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterImportResponse" + "$ref": "#/definitions/handler.FilterWithAssociations" } ] } } }, - "handler.GenericDataResponse-handler_FilterWithAssociations": { + "handler.GenericDataResponse-handler_OscalLikeEvidence": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.FilterWithAssociations" + "$ref": "#/definitions/handler.OscalLikeEvidence" } ] } } }, - "handler.GenericDataResponse-handler_OscalLikeEvidence": { + "handler.GenericDataResponse-handler_SubscriptionsResponse": { "type": "object", "properties": { "data": { "description": "Items from the list response", "allOf": [ { - "$ref": "#/definitions/handler.OscalLikeEvidence" + "$ref": "#/definitions/handler.SubscriptionsResponse" } ] } @@ -21575,7 +21561,21 @@ } } }, - "handler.UpdateDigestSubscriptionRequest": { + "handler.SubscriptionsResponse": { + "type": "object", + "properties": { + "subscribed": { + "type": "boolean" + }, + "taskAvailableEmailSubscribed": { + "type": "boolean" + }, + "taskDailyDigestSubscribed": { + "type": "boolean" + } + } + }, + "handler.UpdateSubscriptionsRequest": { "type": "object", "properties": { "subscribed": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a7d484c8..74240bd6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -117,15 +117,6 @@ definitions: status: type: string type: object - handler.DigestSubscriptionResponse: - properties: - subscribed: - type: boolean - taskAvailableEmailSubscribed: - type: boolean - taskDailyDigestSubscribed: - type: boolean - type: object handler.EvidenceActivity: properties: description: @@ -773,13 +764,6 @@ definitions: - $ref: '#/definitions/digest.EvidenceSummary' description: Items from the list response type: object - handler.GenericDataResponse-handler_DigestSubscriptionResponse: - properties: - data: - allOf: - - $ref: '#/definitions/handler.DigestSubscriptionResponse' - description: Items from the list response - type: object handler.GenericDataResponse-handler_FilterImportResponse: properties: data: @@ -801,6 +785,13 @@ definitions: - $ref: '#/definitions/handler.OscalLikeEvidence' description: Items from the list response type: object + handler.GenericDataResponse-handler_SubscriptionsResponse: + properties: + data: + allOf: + - $ref: '#/definitions/handler.SubscriptionsResponse' + description: Items from the list response + type: object handler.GenericDataResponse-oscal_BuildByPropsResponse: properties: data: @@ -1308,7 +1299,16 @@ definitions: $ref: '#/definitions/handler.StatusCount' type: array type: object - handler.UpdateDigestSubscriptionRequest: + handler.SubscriptionsResponse: + properties: + subscribed: + type: boolean + taskAvailableEmailSubscribed: + type: boolean + taskDailyDigestSubscribed: + type: boolean + type: object + handler.UpdateSubscriptionsRequest: properties: subscribed: type: boolean @@ -18024,7 +18024,7 @@ paths: summary: Change password for logged-in user tags: - Users - /users/me/digest-subscription: + /users/me/subscriptions: get: description: Gets the current user's digest and workflow notification email preferences @@ -18034,7 +18034,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse' + $ref: '#/definitions/handler.GenericDataResponse-handler_SubscriptionsResponse' "401": description: Unauthorized schema: @@ -18063,14 +18063,14 @@ paths: name: subscription required: true schema: - $ref: '#/definitions/handler.UpdateDigestSubscriptionRequest' + $ref: '#/definitions/handler.UpdateSubscriptionsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handler.GenericDataResponse-handler_DigestSubscriptionResponse' + $ref: '#/definitions/handler.GenericDataResponse-handler_SubscriptionsResponse' "400": description: Bad Request schema: diff --git a/internal/api/handler/users.go b/internal/api/handler/users.go index 569ff58c..f95098c8 100644 --- a/internal/api/handler/users.go +++ b/internal/api/handler/users.go @@ -23,13 +23,13 @@ type userResponse struct { AuthProvider *string `json:"authProvider,omitempty"` } -type DigestSubscriptionResponse struct { +type SubscriptionsResponse struct { Subscribed bool `json:"subscribed"` TaskAvailableEmailSubscribed bool `json:"taskAvailableEmailSubscribed"` TaskDailyDigestSubscribed bool `json:"taskDailyDigestSubscribed"` } -type UpdateDigestSubscriptionRequest struct { +type UpdateSubscriptionsRequest struct { Subscribed *bool `json:"subscribed"` TaskAvailableEmailSubscribed *bool `json:"taskAvailableEmailSubscribed"` TaskDailyDigestSubscribed *bool `json:"taskDailyDigestSubscribed"` @@ -54,8 +54,8 @@ func (h *UserHandler) Register(api *echo.Group) { func (h *UserHandler) RegisterSelfRoutes(api *echo.Group) { api.GET("/me", h.GetMe) api.POST("/me/change-password", h.ChangeLoggedInUserPassword) - api.GET("/me/digest-subscription", h.GetDigestSubscription) - api.PUT("/me/digest-subscription", h.UpdateDigestSubscription) + api.GET("/me/subscriptions", h.GetSubscriptions) + api.PUT("/me/subscriptions", h.UpdateSubscriptions) } // ListUsers godoc @@ -407,19 +407,19 @@ func (h *UserHandler) ChangeLoggedInUserPassword(ctx echo.Context) error { return ctx.NoContent(204) } -// GetDigestSubscription godoc +// GetSubscriptions godoc // // @Summary Get notification preferences // @Description Gets the current user's digest and workflow notification email preferences // @Tags Users // @Produce json -// @Success 200 {object} handler.GenericDataResponse[handler.DigestSubscriptionResponse] +// @Success 200 {object} handler.GenericDataResponse[handler.SubscriptionsResponse] // @Failure 401 {object} api.Error // @Failure 404 {object} api.Error // @Failure 500 {object} api.Error // @Security OAuth2Password -// @Router /users/me/digest-subscription [get] -func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { +// @Router /users/me/subscriptions [get] +func (h *UserHandler) GetSubscriptions(ctx echo.Context) error { userClaims := ctx.Get("user").(*authn.UserClaims) email := userClaims.Subject @@ -432,8 +432,8 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { return ctx.JSON(500, api.NewError(err)) } - return ctx.JSON(200, GenericDataResponse[DigestSubscriptionResponse]{ - Data: DigestSubscriptionResponse{ + return ctx.JSON(200, GenericDataResponse[SubscriptionsResponse]{ + Data: SubscriptionsResponse{ Subscribed: user.DigestSubscribed, TaskAvailableEmailSubscribed: user.TaskAvailableEmailSubscribed, TaskDailyDigestSubscribed: user.TaskDailyDigestSubscribed, @@ -441,27 +441,27 @@ func (h *UserHandler) GetDigestSubscription(ctx echo.Context) error { }) } -// UpdateDigestSubscription godoc +// UpdateSubscriptions godoc // // @Summary Update notification preferences // @Description Updates the current user's digest and workflow notification email preferences // @Tags Users // @Accept json // @Produce json -// @Param subscription body handler.UpdateDigestSubscriptionRequest true "Notification preferences" -// @Success 200 {object} handler.GenericDataResponse[handler.DigestSubscriptionResponse] +// @Param subscription body handler.UpdateSubscriptionsRequest true "Notification preferences" +// @Success 200 {object} handler.GenericDataResponse[handler.SubscriptionsResponse] // @Failure 400 {object} api.Error // @Failure 401 {object} api.Error // @Failure 404 {object} api.Error // @Failure 500 {object} api.Error // @Security OAuth2Password -// @Router /users/me/digest-subscription [put] -func (h *UserHandler) UpdateDigestSubscription(ctx echo.Context) error { +// @Router /users/me/subscriptions [put] +func (h *UserHandler) UpdateSubscriptions(ctx echo.Context) error { userClaims := ctx.Get("user").(*authn.UserClaims) - var req UpdateDigestSubscriptionRequest + var req UpdateSubscriptionsRequest if err := ctx.Bind(&req); err != nil { - h.sugar.Errorw("Failed to bind update digest subscription request", "error", err) + h.sugar.Errorw("Failed to bind update subscriptions request", "error", err) return ctx.JSON(400, api.NewError(err)) } @@ -486,20 +486,20 @@ func (h *UserHandler) UpdateDigestSubscription(ctx echo.Context) error { } if err := h.db.Save(&user).Error; err != nil { - h.sugar.Errorw("Failed to update user digest subscription", "error", err) + h.sugar.Errorw("Failed to update user subscriptions", "error", err) return ctx.JSON(500, api.NewError(err)) } h.sugar.Debugw( - "User digest subscription updated", + "User subscriptions updated", "email", email, "subscribed", user.DigestSubscribed, "taskAvailableEmailSubscribed", user.TaskAvailableEmailSubscribed, "taskDailyDigestSubscribed", user.TaskDailyDigestSubscribed, ) - return ctx.JSON(200, GenericDataResponse[DigestSubscriptionResponse]{ - Data: DigestSubscriptionResponse{ + return ctx.JSON(200, GenericDataResponse[SubscriptionsResponse]{ + Data: SubscriptionsResponse{ Subscribed: user.DigestSubscribed, TaskAvailableEmailSubscribed: user.TaskAvailableEmailSubscribed, TaskDailyDigestSubscribed: user.TaskDailyDigestSubscribed, diff --git a/internal/api/handler/users_integration_test.go b/internal/api/handler/users_integration_test.go index bdc19fd7..1be7874d 100644 --- a/internal/api/handler/users_integration_test.go +++ b/internal/api/handler/users_integration_test.go @@ -385,17 +385,17 @@ func (suite *UserApiIntegrationSuite) TestChangePassword() { }) } -func (suite *UserApiIntegrationSuite) TestDigestSubscription() { +func (suite *UserApiIntegrationSuite) TestSubscriptions() { token, err := suite.GetAuthToken() suite.Require().NoError(err) - suite.Run("GetDigestSubscription", func() { + suite.Run("GetSubscriptions", func() { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/api/users/me/digest-subscription", nil) + req := httptest.NewRequest("GET", "/api/users/me/subscriptions", nil) req.Header.Set("Authorization", "Bearer "+*token) suite.server.E().ServeHTTP(rec, req) - suite.Equal(200, rec.Code, "Expected OK response for GetDigestSubscription") + suite.Equal(200, rec.Code, "Expected OK response for GetSubscriptions") var response struct { Data struct { @@ -405,7 +405,7 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { } `json:"data"` } err = json.Unmarshal(rec.Body.Bytes(), &response) - suite.Require().NoError(err, "Failed to unmarshal GetDigestSubscription response") + suite.Require().NoError(err, "Failed to unmarshal GetSubscriptions response") // The default should be false for new users suite.False(response.Data.Subscribed, "Expected default digest subscription to be false") @@ -413,7 +413,7 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { suite.False(response.Data.TaskDailyDigestSubscribed, "Expected task daily digest subscription to default to false") }) - suite.Run("UpdateDigestSubscription", func() { + suite.Run("UpdateSubscriptions", func() { // Test subscribing to digest payload := map[string]interface{}{ "subscribed": true, @@ -421,15 +421,15 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { "taskDailyDigestSubscribed": true, } payloadJSON, err := json.Marshal(payload) - suite.Require().NoError(err, "Failed to marshal update digest subscription request") + suite.Require().NoError(err, "Failed to marshal update subscriptions request") rec := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/api/users/me/digest-subscription", bytes.NewReader(payloadJSON)) + req := httptest.NewRequest("PUT", "/api/users/me/subscriptions", bytes.NewReader(payloadJSON)) req.Header.Set("Authorization", "Bearer "+*token) req.Header.Set("Content-Type", "application/json") suite.server.E().ServeHTTP(rec, req) - suite.Equal(200, rec.Code, "Expected OK response for UpdateDigestSubscription") + suite.Equal(200, rec.Code, "Expected OK response for UpdateSubscriptions") var response struct { Data struct { @@ -439,7 +439,7 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { } `json:"data"` } err = json.Unmarshal(rec.Body.Bytes(), &response) - suite.Require().NoError(err, "Failed to unmarshal UpdateDigestSubscription response") + suite.Require().NoError(err, "Failed to unmarshal UpdateSubscriptions response") suite.True(response.Data.Subscribed, "Expected digest subscription to be updated to true") suite.True(response.Data.TaskAvailableEmailSubscribed, "Expected task available email subscription to be updated to true") @@ -451,10 +451,10 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { "taskDailyDigestSubscribed": false, } payloadJSON, err = json.Marshal(payload) - suite.Require().NoError(err, "Failed to marshal unsubscribe digest request") + suite.Require().NoError(err, "Failed to marshal unsubscribe request") rec = httptest.NewRecorder() - req = httptest.NewRequest("PUT", "/api/users/me/digest-subscription", bytes.NewReader(payloadJSON)) + req = httptest.NewRequest("PUT", "/api/users/me/subscriptions", bytes.NewReader(payloadJSON)) req.Header.Set("Authorization", "Bearer "+*token) req.Header.Set("Content-Type", "application/json") @@ -462,21 +462,21 @@ func (suite *UserApiIntegrationSuite) TestDigestSubscription() { suite.Equal(200, rec.Code, "Expected OK response for unsubscribe digest") err = json.Unmarshal(rec.Body.Bytes(), &response) - suite.Require().NoError(err, "Failed to unmarshal unsubscribe digest response") + suite.Require().NoError(err, "Failed to unmarshal unsubscribe response") suite.False(response.Data.Subscribed, "Expected digest subscription to be updated to false") suite.True(response.Data.TaskAvailableEmailSubscribed, "Expected task available email subscription to remain unchanged when omitted") suite.False(response.Data.TaskDailyDigestSubscribed, "Expected task daily digest subscription to be updated to false") }) - suite.Run("UpdateDigestSubscriptionInvalidPayload", func() { + suite.Run("UpdateSubscriptionsInvalidPayload", func() { // Test with invalid payload payload := map[string]string{"subscribed": "invalid"} payloadJSON, err := json.Marshal(payload) - suite.Require().NoError(err, "Failed to marshal invalid digest subscription request") + suite.Require().NoError(err, "Failed to marshal invalid subscriptions request") rec := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/api/users/me/digest-subscription", bytes.NewReader(payloadJSON)) + req := httptest.NewRequest("PUT", "/api/users/me/subscriptions", bytes.NewReader(payloadJSON)) req.Header.Set("Authorization", "Bearer "+*token) req.Header.Set("Content-Type", "application/json")