Reject signature and approval requests for people with ended contracts
Add server-side validation in BulkRequestSignatures, RequestSignature, and RequestApproval to load the referenced profiles and verify none have an ended contract before proceeding. Returns ErrProfileContractEnded if a profile's contract_end_date is in the past, surfaced as a CONFLICT GraphQL error in all three resolvers. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -37,6 +37,7 @@ import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { SignatureDocumentsDialogMutation } from "#/__generated__/core/SignatureDocumentsDialogMutation.graphql";
|
||||
import type { SignatureDocumentsDialogPeopleFragment$key } from "#/__generated__/core/SignatureDocumentsDialogPeopleFragment.graphql";
|
||||
import type { SignatureDocumentsDialogPeopleQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleQuery.graphql";
|
||||
import type { SignatureDocumentsDialogPeopleRefetchQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleRefetchQuery.graphql";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
@@ -203,7 +204,10 @@ function PeopleList({
|
||||
hasNext,
|
||||
loadNext,
|
||||
isLoadingNext,
|
||||
} = usePaginationFragment<SignatureDocumentsDialogPeopleRefetchQuery>(
|
||||
} = usePaginationFragment<
|
||||
SignatureDocumentsDialogPeopleRefetchQuery,
|
||||
SignatureDocumentsDialogPeopleFragment$key
|
||||
>(
|
||||
signatureDocumentsDialogPeopleFragment,
|
||||
data.organization,
|
||||
);
|
||||
|
||||
@@ -106,6 +106,18 @@ func (s *DocumentApprovalService) RequestApproval(
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
profiles := &coredata.MembershipProfiles{}
|
||||
if err := profiles.LoadByIDs(ctx, tx, s.svc.scope, req.ApproverIDs); err != nil {
|
||||
return fmt.Errorf("cannot load approver profiles: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, p := range *profiles {
|
||||
if p.ContractEndDate != nil && p.ContractEndDate.Before(now) {
|
||||
return &ErrProfileContractEnded{ProfileID: p.ID}
|
||||
}
|
||||
}
|
||||
|
||||
document := &coredata.Document{}
|
||||
if err := document.LoadByID(ctx, tx, s.svc.scope, req.DocumentID); err != nil {
|
||||
return fmt.Errorf("cannot load document: %w", err)
|
||||
|
||||
@@ -80,6 +80,10 @@ type (
|
||||
ErrDocumentVersionSignatureAlreadySigned struct {
|
||||
}
|
||||
|
||||
ErrProfileContractEnded struct {
|
||||
ProfileID gid.GID
|
||||
}
|
||||
|
||||
CreateDocumentRequest struct {
|
||||
OrganizationID gid.GID
|
||||
Title string
|
||||
@@ -217,6 +221,10 @@ func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
|
||||
return "document version signature already signed"
|
||||
}
|
||||
|
||||
func (e ErrProfileContractEnded) Error() string {
|
||||
return fmt.Sprintf("cannot use profile %q: contract has ended", e.ProfileID)
|
||||
}
|
||||
|
||||
func (s *DocumentService) Get(
|
||||
ctx context.Context,
|
||||
documentID gid.GID,
|
||||
@@ -883,6 +891,18 @@ func (s *DocumentService) BulkRequestSignatures(
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
profiles := &coredata.MembershipProfiles{}
|
||||
if err := profiles.LoadByIDs(ctx, tx, s.svc.scope, req.SignatoryIDs); err != nil {
|
||||
return fmt.Errorf("cannot load signatory profiles: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, p := range *profiles {
|
||||
if p.ContractEndDate != nil && p.ContractEndDate.Before(now) {
|
||||
return &ErrProfileContractEnded{ProfileID: p.ID}
|
||||
}
|
||||
}
|
||||
|
||||
for _, documentID := range req.DocumentIDs {
|
||||
documentVersion := &coredata.DocumentVersion{}
|
||||
if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil {
|
||||
@@ -984,6 +1004,15 @@ func (s *DocumentService) RequestSignature(
|
||||
return fmt.Errorf("cannot request signature for unpublished version")
|
||||
}
|
||||
|
||||
profile := &coredata.MembershipProfile{}
|
||||
if err := profile.LoadByID(ctx, tx, s.svc.scope, req.Signatory); err != nil {
|
||||
return fmt.Errorf("cannot load signatory profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.ContractEndDate != nil && profile.ContractEndDate.Before(time.Now()) {
|
||||
return &ErrProfileContractEnded{ProfileID: profile.ID}
|
||||
}
|
||||
|
||||
var err error
|
||||
signature, err = s.createSignatureRequestInTx(ctx, tx, req.DocumentVersionID, req.Signatory, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -1156,6 +1156,10 @@ func (r *mutationResolver) RequestDocumentVersionApproval(ctx context.Context, i
|
||||
return nil, gqlutils.Conflict(ctx, errNotDraft)
|
||||
}
|
||||
|
||||
if errContractEnded, ok := errors.AsType[*probo.ErrProfileContractEnded](err); ok {
|
||||
return nil, gqlutils.Conflict(ctx, errContractEnded)
|
||||
}
|
||||
|
||||
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
|
||||
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
|
||||
}
|
||||
@@ -1354,6 +1358,10 @@ func (r *mutationResolver) RequestSignature(ctx context.Context, input types.Req
|
||||
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||
}
|
||||
|
||||
if errContractEnded, ok := errors.AsType[*probo.ErrProfileContractEnded](err); ok {
|
||||
return nil, gqlutils.Conflict(ctx, errContractEnded)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot request signature", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
@@ -1387,6 +1395,10 @@ func (r *mutationResolver) BulkRequestSignatures(ctx context.Context, input type
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errContractEnded, ok := errors.AsType[*probo.ErrProfileContractEnded](err); ok {
|
||||
return nil, gqlutils.Conflict(ctx, errContractEnded)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot bulk request signatures", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user