diff --git a/apps/console/src/__generated__/core/ComplianceUpdateFormDialogUpdateMutation.graphql.ts b/apps/console/src/__generated__/core/ComplianceUpdateFormDialogUpdateMutation.graphql.ts index 1193c2800..84c4b255c 100644 --- a/apps/console/src/__generated__/core/ComplianceUpdateFormDialogUpdateMutation.graphql.ts +++ b/apps/console/src/__generated__/core/ComplianceUpdateFormDialogUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<160a29eaaba4b3db2c1b7aca34164dd7>> + * @generated SignedSource<<44dade2df63778a95d78ef9aaf6c8fa2>> * @lightSyntaxTransform * @nogrep */ @@ -11,9 +11,9 @@ import { ConcreteRequest } from 'relay-runtime'; export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT"; export type UpdateMailingListUpdateInput = { - body: string; + body?: string | null | undefined; id: string; - title: string; + title?: string | null | undefined; }; export type ComplianceUpdateFormDialogUpdateMutation$variables = { input: UpdateMailingListUpdateInput; diff --git a/pkg/mailman/service.go b/pkg/mailman/service.go index f5ce08f9e..327b5591e 100644 --- a/pkg/mailman/service.go +++ b/pkg/mailman/service.go @@ -30,6 +30,13 @@ import ( "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/validator" +) + +const ( + updateTitleMaxLength = 200 + updateBodyMaxLength = 50000 + subscriberFullNameMaxLength = 200 ) const ( @@ -51,6 +58,56 @@ func NewService(pgClient *pg.Client, fm *filemanager.Service, tokenSecret string return &Service{pg: pgClient, fm: fm, tokenSecret: tokenSecret, apiBaseURL: apiBaseURL, bucket: bucket, encryptionKey: encryptionKey, logger: logger} } +type ( + CreateMailingListUpdateRequest struct { + MailingListID gid.GID + Title string + Body string + } + + UpdateMailingListUpdateRequest struct { + ID gid.GID + Title *string + Body *string + } + + CreateSubscriberRequest struct { + MailingListID gid.GID + Email mail.Addr + FullName string + } +) + +func (r *CreateMailingListUpdateRequest) Validate() error { + v := validator.New() + + v.Check(r.MailingListID, "mailing_list_id", validator.Required(), validator.GID(coredata.MailingListEntityType)) + v.Check(r.Title, "title", validator.Required(), validator.SafeText(updateTitleMaxLength)) + v.Check(r.Body, "body", validator.Required(), validator.SafeText(updateBodyMaxLength)) + + return v.Error() +} + +func (r *UpdateMailingListUpdateRequest) Validate() error { + v := validator.New() + + v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.MailingListUpdateEntityType)) + v.Check(r.Title, "title", validator.SafeText(updateTitleMaxLength)) + v.Check(r.Body, "body", validator.SafeText(updateBodyMaxLength)) + + return v.Error() +} + +func (r *CreateSubscriberRequest) Validate() error { + v := validator.New() + + v.Check(r.MailingListID, "mailing_list_id", validator.Required(), validator.GID(coredata.MailingListEntityType)) + v.Check(r.Email, "email", validator.Required(), validator.NotEmpty()) + v.Check(r.FullName, "full_name", validator.Required(), validator.SafeText(subscriberFullNameMaxLength)) + + return v.Error() +} + func (s *Service) UpdateMailingList( ctx context.Context, id gid.GID, @@ -116,10 +173,16 @@ func (s *Service) GetSubscriber( func (s *Service) CreateSubscriber( ctx context.Context, - mailingListID gid.GID, - email mail.Addr, - fullName string, + req *CreateSubscriberRequest, ) (*coredata.MailingListSubscriber, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + mailingListID := req.MailingListID + email := req.Email + fullName := req.FullName + scope := coredata.NewScopeFromObjectID(mailingListID) emailRecord, err := s.buildConfirmationMail(ctx, mailingListID, email, fullName) if err != nil { @@ -334,18 +397,21 @@ func (s *Service) ListSubscribers( func (s *Service) CreateMailingListUpdate( ctx context.Context, - mailingListID gid.GID, - title string, - body string, + req *CreateMailingListUpdateRequest, ) (*coredata.MailingListUpdate, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + mailingListID := req.MailingListID scope := coredata.NewScopeFromObjectID(mailingListID) now := time.Now() mlu := &coredata.MailingListUpdate{ ID: gid.New(scope.GetTenantID(), coredata.MailingListUpdateEntityType), MailingListID: mailingListID, - Title: title, - Body: body, + Title: req.Title, + Body: req.Body, Status: coredata.MailingListUpdateStatusDraft, CreatedAt: now, UpdatedAt: now, @@ -406,17 +472,19 @@ func (s *Service) GetMailingListUpdate( func (s *Service) UpdateMailingListUpdate( ctx context.Context, - id gid.GID, - title string, - body string, + req *UpdateMailingListUpdateRequest, ) (*coredata.MailingListUpdate, error) { - scope := coredata.NewScopeFromObjectID(id) + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + scope := coredata.NewScopeFromObjectID(req.ID) var mlu coredata.MailingListUpdate err := s.pg.WithConn( ctx, func(conn pg.Conn) error { - if err := mlu.LoadByID(ctx, conn, scope, id); err != nil { + if err := mlu.LoadByID(ctx, conn, scope, req.ID); err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListUpdateNotFound } @@ -427,8 +495,12 @@ func (s *Service) UpdateMailingListUpdate( return ErrMailingListUpdateAlreadySent } - mlu.Title = title - mlu.Body = body + if req.Title != nil { + mlu.Title = *req.Title + } + if req.Body != nil { + mlu.Body = *req.Body + } mlu.UpdatedAt = time.Now() if err := mlu.Update(ctx, conn, scope); err != nil { diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 07d6389aa..006b1c51d 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -3902,8 +3902,8 @@ input CreateMailingListUpdateInput { input UpdateMailingListUpdateInput { id: ID! - title: String! - body: String! + title: String + body: String } input SendMailingListUpdateInput { diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index f02f28d4a..599f57d22 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -15318,8 +15318,8 @@ input CreateMailingListUpdateInput { input UpdateMailingListUpdateInput { id: ID! - title: String! - body: String! + title: String + body: String } input SendMailingListUpdateInput { @@ -73536,14 +73536,14 @@ func (ec *executionContext) unmarshalInputUpdateMailingListUpdateInput(ctx conte it.ID = data case "title": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("title")) - data, err := ec.unmarshalNString2string(ctx, v) + data, err := ec.unmarshalOString2áš–string(ctx, v) if err != nil { return it, err } it.Title = data case "body": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("body")) - data, err := ec.unmarshalNString2string(ctx, v) + data, err := ec.unmarshalOString2áš–string(ctx, v) if err != nil { return it, err } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 9fae1ba80..2a543c844 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -2227,8 +2227,8 @@ type UpdateMailingListPayload struct { type UpdateMailingListUpdateInput struct { ID gid.GID `json:"id"` - Title string `json:"title"` - Body string `json:"body"` + Title *string `json:"title,omitempty"` + Body *string `json:"body,omitempty"` } type UpdateMailingListUpdatePayload struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index baf520b25..9e8e985ca 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -26,6 +26,7 @@ import ( "go.probo.inc/probo/pkg/server/api/console/v1/types" "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/gqlutils/types/cursor" + "go.probo.inc/probo/pkg/validator" ) // StateOfApplicability is the resolver for the stateOfApplicability field. @@ -2072,8 +2073,18 @@ func (r *mutationResolver) CreateMailingListUpdate(ctx context.Context, input ty return nil, err } - mlu, err := r.mailman.CreateMailingListUpdate(ctx, input.MailingListID, input.Title, input.Body) + mlu, err := r.mailman.CreateMailingListUpdate( + ctx, + &mailman.CreateMailingListUpdateRequest{ + MailingListID: input.MailingListID, + Title: input.Title, + Body: input.Body, + }, + ) if err != nil { + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { + return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) + } r.logger.ErrorCtx(ctx, "cannot create mailing list update", log.Error(err)) return nil, gqlutils.Internal(ctx) } @@ -2089,8 +2100,18 @@ func (r *mutationResolver) UpdateMailingListUpdate(ctx context.Context, input ty return nil, err } - mlu, err := r.mailman.UpdateMailingListUpdate(ctx, input.ID, input.Title, input.Body) + mlu, err := r.mailman.UpdateMailingListUpdate( + ctx, + &mailman.UpdateMailingListUpdateRequest{ + ID: input.ID, + Title: input.Title, + Body: input.Body, + }, + ) if err != nil { + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { + return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) + } if errors.Is(err, mailman.ErrMailingListUpdateAlreadySent) { return nil, gqlutils.Conflictf(ctx, "mailing list update can only be edited when in draft") } @@ -2173,11 +2194,16 @@ func (r *mutationResolver) CreateMailingListSubscriber(ctx context.Context, inpu subscriber, err := r.mailman.CreateSubscriber( ctx, - input.MailingListID, - input.Email, - input.FullName, + &mailman.CreateSubscriberRequest{ + MailingListID: input.MailingListID, + Email: input.Email, + FullName: input.FullName, + }, ) if err != nil { + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { + return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) + } if errors.Is(err, mailman.ErrSubscriberAlreadyExist) { return nil, gqlutils.Conflictf(ctx, "subscriber already exists in this mailing list") } diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 2b4293fc9..303cc45f5 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -28,6 +28,7 @@ import ( "go.probo.inc/probo/pkg/server/api/trust/v1/types" "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/trust" + "go.probo.inc/probo/pkg/validator" ) // Framework is the resolver for the framework field. @@ -706,11 +707,16 @@ func (r *mutationResolver) SubscribeToMailingList(ctx context.Context) (*types.S subscriber, err := r.mailman.CreateSubscriber( ctx, - *trustCenter.MailingListID, - identity.EmailAddress, - identity.FullName, + &mailman.CreateSubscriberRequest{ + MailingListID: *trustCenter.MailingListID, + Email: identity.EmailAddress, + FullName: identity.FullName, + }, ) if err != nil { + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { + return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) + } if errors.Is(err, mailman.ErrSubscriberAlreadyExist) { return nil, gqlutils.Conflictf(ctx, "already subscribed to this mailing list") } diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index f4b74ee5a..13e487a21 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -170,6 +170,14 @@ func Invalidf(ctx context.Context, format string, a ...any) *gqlerror.Error { return Invalid(ctx, fmt.Errorf(format, a...)) } +func InvalidValidationErrors(ctx context.Context, errs validator.ValidationErrors) gqlerror.List { + gqlErrors := make(gqlerror.List, 0, len(errs)) + for _, ve := range errs { + gqlErrors = append(gqlErrors, Invalid(ctx, ve)) + } + return gqlErrors +} + func Internal(ctx context.Context) *gqlerror.Error { return &gqlerror.Error{ Message: "An internal server error occurred. Please try again later.",