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:
Sacha Al Himdani
2026-04-15 14:00:10 +02:00
parent 524f209df7
commit 754dac9ec6
4 changed files with 58 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
import type { SignatureDocumentsDialogMutation } from "#/__generated__/core/SignatureDocumentsDialogMutation.graphql"; 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 { SignatureDocumentsDialogPeopleQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleQuery.graphql";
import type { SignatureDocumentsDialogPeopleRefetchQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleRefetchQuery.graphql"; import type { SignatureDocumentsDialogPeopleRefetchQuery } from "#/__generated__/core/SignatureDocumentsDialogPeopleRefetchQuery.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
@@ -203,7 +204,10 @@ function PeopleList({
hasNext, hasNext,
loadNext, loadNext,
isLoadingNext, isLoadingNext,
} = usePaginationFragment<SignatureDocumentsDialogPeopleRefetchQuery>( } = usePaginationFragment<
SignatureDocumentsDialogPeopleRefetchQuery,
SignatureDocumentsDialogPeopleFragment$key
>(
signatureDocumentsDialogPeopleFragment, signatureDocumentsDialogPeopleFragment,
data.organization, data.organization,
); );

View File

@@ -106,6 +106,18 @@ func (s *DocumentApprovalService) RequestApproval(
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(ctx context.Context, tx pg.Tx) error { 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{} document := &coredata.Document{}
if err := document.LoadByID(ctx, tx, s.svc.scope, req.DocumentID); err != nil { if err := document.LoadByID(ctx, tx, s.svc.scope, req.DocumentID); err != nil {
return fmt.Errorf("cannot load document: %w", err) return fmt.Errorf("cannot load document: %w", err)

View File

@@ -80,6 +80,10 @@ type (
ErrDocumentVersionSignatureAlreadySigned struct { ErrDocumentVersionSignatureAlreadySigned struct {
} }
ErrProfileContractEnded struct {
ProfileID gid.GID
}
CreateDocumentRequest struct { CreateDocumentRequest struct {
OrganizationID gid.GID OrganizationID gid.GID
Title string Title string
@@ -217,6 +221,10 @@ func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
return "document version signature already signed" 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( func (s *DocumentService) Get(
ctx context.Context, ctx context.Context,
documentID gid.GID, documentID gid.GID,
@@ -883,6 +891,18 @@ func (s *DocumentService) BulkRequestSignatures(
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(
ctx, ctx,
func(ctx context.Context, tx pg.Tx) error { 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 { for _, documentID := range req.DocumentIDs {
documentVersion := &coredata.DocumentVersion{} documentVersion := &coredata.DocumentVersion{}
if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil { 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") 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 var err error
signature, err = s.createSignatureRequestInTx(ctx, tx, req.DocumentVersionID, req.Signatory, false) signature, err = s.createSignatureRequestInTx(ctx, tx, req.DocumentVersionID, req.Signatory, false)
if err != nil { if err != nil {

View File

@@ -1156,6 +1156,10 @@ func (r *mutationResolver) RequestDocumentVersionApproval(ctx context.Context, i
return nil, gqlutils.Conflict(ctx, errNotDraft) 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 { if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) 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) 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)) r.logger.ErrorCtx(ctx, "cannot request signature", log.Error(err))
return nil, gqlutils.Internal(ctx) return nil, gqlutils.Internal(ctx)
} }
@@ -1387,6 +1395,10 @@ func (r *mutationResolver) BulkRequestSignatures(ctx context.Context, input type
}, },
) )
if err != nil { 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)) r.logger.ErrorCtx(ctx, "cannot bulk request signatures", log.Error(err))
return nil, gqlutils.Internal(ctx) return nil, gqlutils.Internal(ctx)
} }