Files
probo/pkg/server/api/console/v1/document_resolvers.go
Sacha Al Himdani bf20ca1a90 Add esign to document signatures
Employee document signatures recorded an acknowledgment with no
cryptographic proof, unlike document approvals which already create
and accept an electronic signature on every decision.

Mirror the approval flow on the sign path: generate the signed
document PDF, create-and-accept an esign record, and persist its id
on the document_version_signatures row through a new
electronic_signature_id column. Capture the signer IP and user agent
in the resolver, and re-check the published/archived preconditions
inside the transaction so the seal cannot race document state.

Make the consent wording a single backend source of truth shared by
the text that is sealed and the text shown in the UI. Define
DocumentSignatureConsentText and DocumentApprovalConsentText in the
probo service package and the NDA copy in the trust service, each
owned by the flow that uses it, and stop esign from appending the
generic clause to caller-provided consent text so approvals no
longer seal a duplicated sentence.

Expose the resolved consent text through GraphQL on
EmployeeDocumentVersion and DocumentVersionApprovalDecision, and have
the signing, approval, and NDA pages render it from the API instead
of hard-coded strings, mirroring how the NDA page already worked.

Align the wording with the actual interaction: the buttons read
"Review and sign" and "Review and approve", the clauses reference
those actions, and the inaccurate "typing my full name" phrasing is
removed everywhere.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-06-15 19:18:42 +02:00

1722 lines
58 KiB
Go

package console_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.90
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"github.com/vikstrous/dataloadgen"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
"go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/validator"
)
// Organization is the resolver for the organization field.
func (r *documentResolver) Organization(ctx context.Context, obj *types.Document) (*types.Organization, error) {
if _, err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
return nil, err
}
loaders := dataloader.FromContext(ctx)
organization, err := loaders.Organization.Load(ctx, obj.Organization.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewOrganization(organization), nil
}
// Versions is the resolver for the versions field.
func (r *documentResolver) Versions(ctx context.Context, obj *types.Document, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionOrderBy, filter *types.DocumentVersionFilter) (*types.DocumentVersionConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionList)
if err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.DocumentVersionOrderField]{
Field: coredata.DocumentVersionOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentVersionOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
versionFilter := coredata.NewDocumentVersionFilter()
if filter != nil && len(filter.Statuses) > 0 {
versionFilter = versionFilter.WithStatuses(filter.Statuses...)
}
page, err := r.probo.Documents.ListVersions(ctx, scope, obj.ID, cursor, versionFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list document versions", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersionConnection(page, r, obj.ID), nil
}
// Controls is the resolver for the controls field.
func (r *documentResolver) Controls(ctx context.Context, obj *types.Document, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionControlList)
if err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var controlFilter = coredata.NewControlFilter(nil)
if filter != nil {
controlFilter = coredata.NewControlFilter(filter.Query)
}
page, err := r.probo.Controls.ListForDocumentID(ctx, scope, obj.ID, cursor, controlFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list document controls", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewControlConnection(page, r, obj.ID, controlFilter), nil
}
// DefaultApprovers is the resolver for the defaultApprovers field.
func (r *documentResolver) DefaultApprovers(ctx context.Context, obj *types.Document) ([]*types.Profile, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet)
if err != nil {
return nil, err
}
profiles, err := r.probo.Documents.GetDefaultApprovers(ctx, scope, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get default approvers", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
result := make([]*types.Profile, len(profiles))
for i, p := range profiles {
result[i] = types.NewProfile(p)
}
return result, nil
}
// Permission is the resolver for the permission field.
func (r *documentResolver) Permission(ctx context.Context, obj *types.Document, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *documentConnectionResolver) TotalCount(ctx context.Context, obj *types.DocumentConnection) (int, error) {
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionDocumentList)
if err != nil {
return 0, err
}
switch obj.Resolver.(type) {
case *controlResolver:
count, err := r.probo.Documents.CountForControlID(ctx, scope, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
case *organizationResolver:
count, err := r.probo.Documents.CountForOrganizationID(ctx, scope, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count documents", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
case *riskResolver:
count, err := r.probo.Documents.CountForRiskID(ctx, scope, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count risks", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
case *measureResolver:
count, err := r.probo.Documents.CountForMeasureID(ctx, scope, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count documents", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
r.logger.ErrorCtx(ctx, "unsupported resolver")
return 0, gqlutils.Internal(ctx)
}
// Document is the resolver for the document field.
func (r *documentVersionResolver) Document(ctx context.Context, obj *types.DocumentVersion) (*types.Document, error) {
if _, err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
return nil, err
}
loaders := dataloader.FromContext(ctx)
document, err := loaders.Document.Load(ctx, obj.Document.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocument(document), nil
}
// Approvers is the resolver for the approvers field.
func (r *documentVersionResolver) Approvers(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) (*types.ProfileConnection, error) {
scope, err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList)
if err != nil {
return nil, err
}
if gqlutils.OnlyTotalCountSelected(ctx) {
return &types.ProfileConnection{
Resolver: r,
ParentID: obj.ID,
}, nil
}
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
Field: coredata.MembershipProfileOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy.Field = coredata.MembershipProfileOrderField(orderBy.Field)
pageOrderBy.Direction = page.OrderDirection(orderBy.Direction)
}
c := types.NewCursor(first, after, last, before, pageOrderBy)
p, err := r.probo.Documents.ListVersionApprovers(ctx, scope, obj.ID, c)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list document version approvers", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfileConnection(p, r, obj.ID, nil), nil
}
// Signatures is the resolver for the signatures field.
func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder, filter *types.DocumentVersionSignatureFilter) (*types.DocumentVersionSignatureConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionSignatureList)
if err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.DocumentVersionSignatureOrderField]{
Field: coredata.DocumentVersionSignatureOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentVersionSignatureOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
var (
signatureStates []coredata.DocumentVersionSignatureState
activeContract *bool
profileState *coredata.ProfileState
)
if filter != nil {
if filter.States != nil {
signatureStates = filter.States
}
if filter.ActiveContract != nil {
activeContract = filter.ActiveContract
}
if filter.State != nil {
profileState = filter.State
}
}
signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract, profileState)
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.probo.Documents.ListSignatures(ctx, scope, obj.ID, cursor, signatureFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list document version signatures", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersionSignatureConnection(page, r, obj.ID, signatureFilter), nil
}
// ApprovalQuorums is the resolver for the approvalQuorums field.
func (r *documentVersionResolver) ApprovalQuorums(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionApprovalQuorumOrder) (*types.DocumentVersionApprovalQuorumConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionApprovalList)
if err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.DocumentVersionApprovalQuorumOrderField]{
Field: coredata.DocumentVersionApprovalQuorumOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentVersionApprovalQuorumOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
p, err := r.probo.DocumentApprovals.ListQuorums(ctx, scope, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list approval quorums", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersionApprovalQuorumConnection(p, r, obj.ID), nil
}
// Signed is the resolver for the signed field.
func (r *documentVersionResolver) Signed(ctx context.Context, obj *types.DocumentVersion) (bool, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionGet)
if err != nil {
return false, err
}
identity := authn.IdentityFromContext(ctx)
signed, err := r.probo.Documents.IsVersionSignedByUserEmail(ctx, scope, obj.ID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot check if document version is signed", log.Error(err))
return false, gqlutils.Internal(ctx)
}
return signed, nil
}
// Permission is the resolver for the permission field.
func (r *documentVersionResolver) Permission(ctx context.Context, obj *types.DocumentVersion, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// Quorum is the resolver for the quorum field.
func (r *documentVersionApprovalDecisionResolver) Quorum(ctx context.Context, obj *types.DocumentVersionApprovalDecision) (*types.DocumentVersionApprovalQuorum, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionApprovalList)
if err != nil {
return nil, err
}
quorum, err := r.probo.DocumentApprovals.GetQuorum(ctx, scope, obj.Quorum.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get approval quorum", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersionApprovalQuorum(quorum), nil
}
// DocumentVersion is the resolver for the documentVersion field.
func (r *documentVersionApprovalDecisionResolver) DocumentVersion(ctx context.Context, obj *types.DocumentVersionApprovalDecision) (*types.DocumentVersion, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionGet)
if err != nil {
return nil, err
}
quorum, err := r.probo.DocumentApprovals.GetQuorum(ctx, scope, obj.Quorum.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get approval quorum", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
documentVersion, err := r.probo.Documents.GetVersion(ctx, scope, quorum.VersionID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersion(documentVersion), nil
}
// Approver is the resolver for the approver field.
func (r *documentVersionApprovalDecisionResolver) Approver(ctx context.Context, obj *types.DocumentVersionApprovalDecision) (*types.Profile, error) {
if _, err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
return nil, err
}
profile, err := r.iam.OrganizationService.GetProfile(ctx, obj.Approver.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get approver profile", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfile(profile), nil
}
// ConsentText is the resolver for the consentText field.
func (r *documentVersionApprovalDecisionResolver) ConsentText(ctx context.Context, obj *types.DocumentVersionApprovalDecision) (string, error) {
return probo.DocumentApprovalConsentText, nil
}
// Permission is the resolver for the permission field.
func (r *documentVersionApprovalDecisionResolver) Permission(ctx context.Context, obj *types.DocumentVersionApprovalDecision, action string) (bool, error) {
// Approve and reject actions are only allowed for the viewer's own decision.
if action == probo.ActionDocumentVersionApprove || action == probo.ActionDocumentVersionReject {
identity := authn.IdentityFromContext(ctx)
profile, err := r.iam.OrganizationService.GetProfile(ctx, obj.Approver.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return false, nil
}
return false, gqlutils.Internal(ctx)
}
if profile.IdentityID != identity.ID {
return false, nil
}
}
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *documentVersionApprovalDecisionConnectionResolver) TotalCount(ctx context.Context, obj *types.DocumentVersionApprovalDecisionConnection) (int, error) {
if obj.ParentID.EntityType() != coredata.DocumentVersionApprovalQuorumEntityType {
return 0, nil
}
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionDocumentVersionApprovalList)
if err != nil {
return 0, err
}
filter := coredata.NewDocumentVersionApprovalDecisionFilter(nil)
if obj.Filters != nil {
filter = obj.Filters
}
count, err := r.probo.DocumentApprovals.CountDecisions(ctx, scope, obj.ParentID, filter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count approval decisions", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
// DocumentVersion is the resolver for the documentVersion field.
func (r *documentVersionApprovalQuorumResolver) DocumentVersion(ctx context.Context, obj *types.DocumentVersionApprovalQuorum) (*types.DocumentVersion, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionGet)
if err != nil {
return nil, err
}
documentVersion, err := r.probo.Documents.GetVersion(ctx, scope, obj.DocumentVersion.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersion(documentVersion), nil
}
// Decisions is the resolver for the decisions field.
func (r *documentVersionApprovalQuorumResolver) Decisions(ctx context.Context, obj *types.DocumentVersionApprovalQuorum, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionApprovalDecisionOrder, filter *types.DocumentVersionApprovalDecisionFilter) (*types.DocumentVersionApprovalDecisionConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionApprovalList)
if err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.DocumentVersionApprovalDecisionOrderField]{
Field: coredata.DocumentVersionApprovalDecisionOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentVersionApprovalDecisionOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
var approvalStates []coredata.DocumentVersionApprovalDecisionState
if filter != nil && filter.States != nil {
approvalStates = filter.States
}
approvalFilter := coredata.NewDocumentVersionApprovalDecisionFilter(approvalStates)
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
p, err := r.probo.DocumentApprovals.ListDecisions(ctx, scope, obj.ID, cursor, approvalFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list approval decisions", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersionApprovalDecisionConnection(p, r, obj.ID, approvalFilter), nil
}
// Permission is the resolver for the permission field.
func (r *documentVersionApprovalQuorumResolver) Permission(ctx context.Context, obj *types.DocumentVersionApprovalQuorum, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *documentVersionApprovalQuorumConnectionResolver) TotalCount(ctx context.Context, obj *types.DocumentVersionApprovalQuorumConnection) (int, error) {
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionDocumentVersionApprovalList)
if err != nil {
return 0, err
}
count, err := r.probo.DocumentApprovals.CountQuorums(ctx, scope, obj.ParentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count approval quorums", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
// TotalCount is the resolver for the totalCount field.
func (r *documentVersionConnectionResolver) TotalCount(ctx context.Context, obj *types.DocumentVersionConnection) (int, error) {
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionDocumentVersionList)
if err != nil {
return 0, err
}
switch obj.Resolver.(type) {
case *documentResolver:
filter := &coredata.DocumentVersionFilter{}
if obj.Filters != nil {
filter = obj.Filters
}
count, err := r.probo.Documents.CountVersionsForDocumentID(ctx, scope, obj.ParentID, filter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count document versions", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
r.logger.ErrorCtx(ctx, "unsupported resolver")
return 0, gqlutils.Internal(ctx)
}
// DocumentVersion is the resolver for the documentVersion field.
func (r *documentVersionSignatureResolver) DocumentVersion(ctx context.Context, obj *types.DocumentVersionSignature) (*types.DocumentVersion, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionDocumentVersionGet)
if err != nil {
return nil, err
}
documentVersion, err := r.probo.Documents.GetVersion(ctx, scope, obj.DocumentVersion.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersion(documentVersion), nil
}
// SignedBy is the resolver for the signedBy field.
func (r *documentVersionSignatureResolver) SignedBy(ctx context.Context, obj *types.DocumentVersionSignature) (*types.Profile, error) {
if _, err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
return nil, err
}
loaders := dataloader.FromContext(ctx)
signatory, err := loaders.Profile.Load(ctx, obj.SignedBy.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get people", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewProfile(signatory), nil
}
// Permission is the resolver for the permission field.
func (r *documentVersionSignatureResolver) Permission(ctx context.Context, obj *types.DocumentVersionSignature, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *documentVersionSignatureConnectionResolver) TotalCount(ctx context.Context, obj *types.DocumentVersionSignatureConnection) (int, error) {
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionDocumentVersionSignatureList)
if err != nil {
return 0, err
}
switch obj.Resolver.(type) {
case *documentVersionResolver:
filter := &coredata.DocumentVersionSignatureFilter{}
if obj.Filters != nil {
filter = obj.Filters
}
count, err := r.probo.Documents.CountSignaturesForVersionID(ctx, scope, obj.ParentID, filter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count signatures", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
r.logger.ErrorCtx(ctx, "unsupported resolver")
return 0, gqlutils.Internal(ctx)
}
// Signed is the resolver for the signed field.
func (r *employeeDocumentResolver) Signed(ctx context.Context, obj *types.EmployeeDocument) (*bool, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
signed, err := r.probo.Documents.IsSigned(ctx, scope, obj.ID, identity.EmailAddress)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, nil
}
r.logger.ErrorCtx(ctx, "cannot check if document is signed", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &signed, nil
}
// ApprovalState is the resolver for the approvalState field.
func (r *employeeDocumentResolver) ApprovalState(ctx context.Context, obj *types.EmployeeDocument) (*coredata.DocumentVersionApprovalDecisionState, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
state, err := r.probo.Documents.GetViewerApprovalState(ctx, scope, obj.ID, identity.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, nil
}
r.logger.ErrorCtx(ctx, "cannot get viewer approval state", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &state, nil
}
// Versions is the resolver for the versions field.
func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.EmployeeDocument, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionOrderBy) (*types.EmployeeDocumentVersionConnection, error) {
scope, err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet)
if err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.DocumentVersionOrderField]{
Field: coredata.DocumentVersionOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentVersionOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
identity := authn.IdentityFromContext(ctx)
var filterMode coredata.EmployeeFilterMode
switch obj.FilterMode {
case types.EmployeeDocumentFilterModeSignature:
filterMode = coredata.EmployeeFilterModeSignature
case types.EmployeeDocumentFilterModeApproval:
filterMode = coredata.EmployeeFilterModeApproval
default:
r.logger.ErrorCtx(ctx, "unsupported employee document filter mode", log.String("filter_mode", string(obj.FilterMode)))
return nil, gqlutils.Internal(ctx)
}
versionFilter := coredata.NewDocumentVersionFilter().
WithEmployeeIdentityID(&identity.ID, filterMode)
versionsPage, err := r.probo.Documents.ListVersions(ctx, scope, obj.ID, cursor, versionFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list employee document versions", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
employeeVersions := make([]*types.EmployeeDocumentVersion, len(versionsPage.Data))
for i, v := range versionsPage.Data {
employeeVersions[i] = &types.EmployeeDocumentVersion{
ID: v.ID,
DocumentID: obj.ID,
OrganizationID: v.OrganizationID,
Major: v.Major,
Minor: v.Minor,
Status: v.Status,
Classification: v.Classification,
DocumentType: v.DocumentType,
PublishedAt: v.PublishedAt,
CreatedAt: v.CreatedAt,
UpdatedAt: v.UpdatedAt,
}
}
p := page.NewPage(employeeVersions, versionsPage.Cursor)
return types.NewEmployeeDocumentVersionConnection(p), nil
}
// Signed is the resolver for the signed field.
func (r *employeeDocumentVersionResolver) Signed(ctx context.Context, obj *types.EmployeeDocumentVersion) (bool, error) {
scope, err := r.authorize(ctx, obj.DocumentID, probo.ActionEmployeeDocumentGet)
if err != nil {
return false, err
}
identity := authn.IdentityFromContext(ctx)
signed, err := r.probo.Documents.IsVersionSignedByUserEmail(ctx, scope, obj.ID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot check if version is signed", log.Error(err))
return false, gqlutils.Internal(ctx)
}
return signed, nil
}
// ConsentText is the resolver for the consentText field.
func (r *employeeDocumentVersionResolver) ConsentText(ctx context.Context, obj *types.EmployeeDocumentVersion) (string, error) {
return probo.DocumentSignatureConsentText, nil
}
// ApprovalDecision is the resolver for the approvalDecision field.
func (r *employeeDocumentVersionResolver) ApprovalDecision(ctx context.Context, obj *types.EmployeeDocumentVersion) (*types.DocumentVersionApprovalDecision, error) {
scope, err := r.authorize(ctx, obj.DocumentID, probo.ActionEmployeeDocumentGet)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
decision, err := r.probo.DocumentApprovals.GetViewerDecision(ctx, scope, obj.ID, identity.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, nil
}
r.logger.ErrorCtx(ctx, "cannot get viewer approval decision", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentVersionApprovalDecision(decision), nil
}
// CreateDocument is the resolver for the createDocument field.
func (r *mutationResolver) CreateDocument(ctx context.Context, input types.CreateDocumentInput) (*types.CreateDocumentPayload, error) {
scope, err := r.authorize(ctx, input.OrganizationID, probo.ActionDocumentCreate)
if err != nil {
return nil, err
}
var content string
if input.Content != nil {
content = *input.Content
}
document, documentVersion, err := r.probo.Documents.Create(
ctx, scope,
probo.CreateDocumentRequest{
OrganizationID: input.OrganizationID,
Title: input.Title,
Content: content,
Classification: input.Classification,
DocumentType: input.DocumentType,
TrustCenterVisibility: input.TrustCenterVisibility,
DefaultApproverIDs: input.DefaultApproverIds,
},
)
if err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return nil, gqlutils.Conflict(ctx, err)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot create document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CreateDocumentPayload{
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldTitle),
DocumentVersionEdge: types.NewDocumentVersionEdge(documentVersion, coredata.DocumentVersionOrderFieldCreatedAt),
}, nil
}
// UpdateDocument is the resolver for the updateDocument field.
func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.UpdateDocumentInput) (*types.UpdateDocumentPayload, error) {
scope, err := r.authorize(ctx, input.ID, probo.ActionDocumentUpdate)
if err != nil {
return nil, err
}
var defaultApproverIDs *[]gid.GID
if input.DefaultApproverIds != nil {
defaultApproverIDs = &input.DefaultApproverIds
}
document, documentVersion, draftCreated, err := r.probo.Documents.Update(
ctx, scope,
probo.UpdateDocumentRequest{
DocumentID: input.ID,
Title: input.Title,
Content: input.Content,
Classification: input.Classification,
DocumentType: input.DocumentType,
TrustCenterVisibility: input.TrustCenterVisibility,
DefaultApproverIDs: defaultApproverIDs,
},
)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errGenerated, ok := errors.AsType[*probo.ErrDocumentVersionGenerated](err); ok {
return nil, gqlutils.Conflict(ctx, errGenerated)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot update document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
payload := &types.UpdateDocumentPayload{
Document: types.NewDocument(document),
}
if documentVersion != nil {
payload.DocumentVersion = types.NewDocumentVersion(documentVersion)
}
if draftCreated {
payload.DocumentVersionEdge = types.NewDocumentVersionEdge(
documentVersion,
coredata.DocumentVersionOrderFieldCreatedAt,
)
}
return payload, nil
}
// DeleteDocumentDraft is the resolver for the deleteDocumentDraft field.
func (r *mutationResolver) DeleteDocumentDraft(ctx context.Context, input types.DeleteDocumentDraftInput) (*types.DeleteDocumentDraftPayload, error) {
scope, err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentDeleteDraft)
if err != nil {
return nil, err
}
document, err := r.probo.Documents.DeleteDraft(ctx, scope, input.DocumentID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errNotDeletable, ok := errors.AsType[*probo.ErrDocumentDraftNotDeletable](err); ok {
return nil, gqlutils.Conflict(ctx, errNotDeletable)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
r.logger.ErrorCtx(ctx, "cannot delete document draft", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteDocumentDraftPayload{
Document: types.NewDocument(document),
}, nil
}
// ArchiveDocument is the resolver for the archiveDocument field.
func (r *mutationResolver) ArchiveDocument(ctx context.Context, input types.ArchiveDocumentInput) (*types.ArchiveDocumentPayload, error) {
scope, err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentArchive)
if err != nil {
return nil, err
}
document, err := r.probo.Documents.Archive(ctx, scope, input.DocumentID)
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
r.logger.ErrorCtx(ctx, "cannot archive document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.ArchiveDocumentPayload{
Document: types.NewDocument(document),
}, nil
}
// UnarchiveDocument is the resolver for the unarchiveDocument field.
func (r *mutationResolver) UnarchiveDocument(ctx context.Context, input types.UnarchiveDocumentInput) (*types.UnarchiveDocumentPayload, error) {
scope, err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentUnarchive)
if err != nil {
return nil, err
}
document, err := r.probo.Documents.Unarchive(ctx, scope, input.DocumentID)
if err != nil {
if errNotArchived, ok := errors.AsType[*probo.ErrDocumentNotArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errNotArchived)
}
r.logger.ErrorCtx(ctx, "cannot unarchive document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UnarchiveDocumentPayload{
Document: types.NewDocument(document),
}, nil
}
// DeleteDocument is the resolver for the deleteDocument field.
func (r *mutationResolver) DeleteDocument(ctx context.Context, input types.DeleteDocumentInput) (*types.DeleteDocumentPayload, error) {
scope, err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentDelete)
if err != nil {
return nil, err
}
if err := r.probo.Documents.SoftDelete(ctx, scope, input.DocumentID); err != nil {
r.logger.ErrorCtx(ctx, "cannot soft delete document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteDocumentPayload{
DeletedDocumentID: input.DocumentID,
}, nil
}
// PublishDocument is the resolver for the publishDocument field.
func (r *mutationResolver) PublishDocument(ctx context.Context, input types.PublishDocumentInput) (*types.PublishDocumentPayload, error) {
action := probo.ActionDocumentVersionPublish
if !input.Minor && len(input.ApproverIds) > 0 {
action = probo.ActionDocumentVersionRequestApproval
}
scope, err := r.authorize(ctx, input.DocumentID, action)
if err != nil {
return nil, err
}
result, err := r.probo.Documents.PublishVersion(ctx, scope, probo.PublishDocumentRequest{
DocumentID: input.DocumentID,
Minor: input.Minor,
ApproverIDs: input.ApproverIds,
Changelog: input.Changelog,
})
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok {
return nil, gqlutils.Invalid(ctx, errNotDraft)
}
if errPending, ok := errors.AsType[*probo.ErrDocumentVersionPendingApproval](err); ok {
return nil, gqlutils.Conflict(ctx, errPending)
}
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)
}
r.logger.ErrorCtx(ctx, "cannot publish document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
payload := &types.PublishDocumentPayload{
Document: types.NewDocument(result.Document),
DocumentVersion: types.NewDocumentVersion(result.Version),
}
if result.Quorum != nil {
payload.ApprovalQuorum = types.NewDocumentVersionApprovalQuorum(result.Quorum)
}
return payload, nil
}
// BulkPublishDocuments is the resolver for the bulkPublishDocuments field.
func (r *mutationResolver) BulkPublishDocuments(ctx context.Context, input types.BulkPublishDocumentsInput) (*types.BulkPublishDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkPublishDocumentsPayload{
DocumentVersions: []*types.DocumentVersion{},
Documents: []*types.Document{},
}, nil
}
for _, documentID := range input.DocumentIds {
if _, err := r.authorize(ctx, documentID, probo.ActionDocumentVersionPublish); err != nil {
return nil, err
}
}
scope := coredata.NewScopeFromObjectID(input.DocumentIds[0])
versions, documents, err := r.probo.DocumentApprovals.BulkPublishVersions(ctx, scope, probo.BulkPublishVersionsRequest{
DocumentIDs: input.DocumentIds,
Minor: input.Minor,
Changelog: input.Changelog,
})
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok {
return nil, gqlutils.Invalid(ctx, errNotDraft)
}
r.logger.ErrorCtx(ctx, "cannot bulk publish documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
typesVersions := make([]*types.DocumentVersion, len(versions))
for i, v := range versions {
typesVersions[i] = types.NewDocumentVersion(v)
}
typesDocuments := make([]*types.Document, len(documents))
for i, d := range documents {
typesDocuments[i] = types.NewDocument(d)
}
return &types.BulkPublishDocumentsPayload{
DocumentVersions: typesVersions,
Documents: typesDocuments,
}, nil
}
// VoidDocumentVersionApproval is the resolver for the voidDocumentVersionApproval field.
func (r *mutationResolver) VoidDocumentVersionApproval(ctx context.Context, input types.VoidDocumentVersionApprovalInput) (*types.VoidDocumentVersionApprovalPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionVoidApproval)
if err != nil {
return nil, err
}
quorum, documentVersion, err := r.probo.DocumentApprovals.VoidApproval(ctx, scope, input.DocumentVersionID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotPending, ok := errors.AsType[*probo.ErrDocumentVersionNotPendingApproval](err); ok {
return nil, gqlutils.Conflict(ctx, errNotPending)
}
r.logger.ErrorCtx(ctx, "cannot void document version approval", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.VoidDocumentVersionApprovalPayload{
ApprovalQuorum: types.NewDocumentVersionApprovalQuorum(quorum),
DocumentVersion: types.NewDocumentVersion(documentVersion),
}, nil
}
// BulkDeleteDocuments is the resolver for the bulkDeleteDocuments field.
func (r *mutationResolver) BulkDeleteDocuments(ctx context.Context, input types.BulkDeleteDocumentsInput) (*types.BulkDeleteDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkDeleteDocumentsPayload{
DeletedDocumentIds: []gid.GID{},
}, nil
}
for _, documentID := range input.DocumentIds {
if _, err := r.authorize(ctx, documentID, probo.ActionDocumentDelete); err != nil {
return nil, err
}
}
scope := coredata.NewScopeFromObjectID(input.DocumentIds[0])
if err := r.probo.Documents.BulkSoftDelete(ctx, scope, input.DocumentIds); err != nil {
r.logger.ErrorCtx(ctx, "cannot bulk delete documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.BulkDeleteDocumentsPayload{
DeletedDocumentIds: input.DocumentIds,
}, nil
}
// BulkArchiveDocuments is the resolver for the bulkArchiveDocuments field.
func (r *mutationResolver) BulkArchiveDocuments(ctx context.Context, input types.BulkArchiveDocumentsInput) (*types.BulkArchiveDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkArchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
for _, documentID := range input.DocumentIds {
if _, err := r.authorize(ctx, documentID, probo.ActionDocumentArchive); err != nil {
return nil, err
}
}
scope := coredata.NewScopeFromObjectID(input.DocumentIds[0])
if err := r.probo.Documents.BulkArchive(ctx, scope, input.DocumentIds); err != nil {
r.logger.ErrorCtx(ctx, "cannot bulk archive documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.BulkArchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
// BulkUnarchiveDocuments is the resolver for the bulkUnarchiveDocuments field.
func (r *mutationResolver) BulkUnarchiveDocuments(ctx context.Context, input types.BulkUnarchiveDocumentsInput) (*types.BulkUnarchiveDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkUnarchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
for _, documentID := range input.DocumentIds {
if _, err := r.authorize(ctx, documentID, probo.ActionDocumentUnarchive); err != nil {
return nil, err
}
}
scope := coredata.NewScopeFromObjectID(input.DocumentIds[0])
if err := r.probo.Documents.BulkUnarchive(ctx, scope, input.DocumentIds); err != nil {
r.logger.ErrorCtx(ctx, "cannot bulk unarchive documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.BulkUnarchiveDocumentsPayload{
Documents: []*types.Document{},
}, nil
}
// BulkExportDocuments is the resolver for the bulkExportDocuments field.
func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.BulkExportDocumentsInput) (*types.BulkExportDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
r.logger.ErrorCtx(ctx, "no document ids provided")
return nil, gqlutils.Internal(ctx)
}
// TODO have a way to batch authorize for resources
for _, documentID := range input.DocumentIds {
if _, err := r.authorize(ctx, documentID, probo.ActionDocumentVersionExport); err != nil {
return nil, err
}
}
scope := coredata.NewScopeFromObjectID(input.DocumentIds[0])
identity := authn.IdentityFromContext(ctx)
options := probo.ExportPDFOptions{
WithWatermark: input.WithWatermark,
WithSignatures: input.WithSignatures,
WatermarkEmail: input.WatermarkEmail,
}
documentExport, exportErr := r.probo.Documents.RequestExport(ctx, scope, input.DocumentIds, identity.EmailAddress, identity.FullName, options)
if exportErr != nil {
r.logger.ErrorCtx(ctx, "cannot request document export", log.Error(exportErr))
return nil, gqlutils.Internal(ctx)
}
return &types.BulkExportDocumentsPayload{
ExportJobID: documentExport.ID,
}, nil
}
// GenerateDocumentChangelog is the resolver for the generateDocumentChangelog field.
func (r *mutationResolver) GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error) {
scope, err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentChangelogGenerate)
if err != nil {
return nil, err
}
changelog, err := r.probo.Documents.GenerateChangelog(ctx, scope, input.DocumentID)
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
r.logger.ErrorCtx(ctx, "cannot generate document changelog", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.GenerateDocumentChangelogPayload{
Changelog: *changelog,
}, nil
}
// RequestSignature is the resolver for the requestSignature field.
func (r *mutationResolver) RequestSignature(ctx context.Context, input types.RequestSignatureInput) (*types.RequestSignaturePayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionSignatureRequest)
if err != nil {
return nil, err
}
documentVersionSignature, err := r.probo.Documents.RequestSignature(
ctx, scope,
probo.RequestSignatureRequest{
DocumentVersionID: input.DocumentVersionID,
Signatory: input.SignatoryID,
},
)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotPublished, ok := errors.AsType[*probo.ErrDocumentVersionNotPublished](err); ok {
return nil, gqlutils.Conflict(ctx, errNotPublished)
}
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)
}
return &types.RequestSignaturePayload{
DocumentVersionSignatureEdge: types.NewDocumentVersionSignatureEdge(documentVersionSignature, coredata.DocumentVersionSignatureOrderFieldCreatedAt),
}, nil
}
// BulkRequestSignatures is the resolver for the bulkRequestSignatures field.
func (r *mutationResolver) BulkRequestSignatures(ctx context.Context, input types.BulkRequestSignaturesInput) (*types.BulkRequestSignaturesPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkRequestSignaturesPayload{
DocumentVersionSignatureEdges: []*types.DocumentVersionSignatureEdge{},
}, nil
}
for _, documentID := range input.DocumentIds {
if _, err := r.authorize(ctx, documentID, probo.ActionDocumentVersionSignatureRequest); err != nil {
return nil, err
}
}
scope := coredata.NewScopeFromObjectID(input.DocumentIds[0])
documentVersionSignatures, err := r.probo.Documents.BulkRequestSignatures(
ctx, scope,
probo.BulkRequestSignaturesRequest{
DocumentIDs: input.DocumentIds,
SignatoryIDs: input.SignatoryIds,
},
)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
if errNotPublished, ok := errors.AsType[*probo.ErrDocumentVersionNotPublished](err); ok {
return nil, gqlutils.Conflict(ctx, errNotPublished)
}
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)
}
return &types.BulkRequestSignaturesPayload{
DocumentVersionSignatureEdges: types.NewDocumentVersionSignatureEdges(documentVersionSignatures, coredata.DocumentVersionSignatureOrderFieldCreatedAt),
}, nil
}
// SendSigningNotifications is the resolver for the sendSigningNotifications field.
func (r *mutationResolver) SendSigningNotifications(ctx context.Context, input types.SendSigningNotificationsInput) (*types.SendSigningNotificationsPayload, error) {
scope, err := r.authorize(ctx, input.OrganizationID, probo.ActionDocumentSendSigningNotifications)
if err != nil {
return nil, err
}
if err := r.probo.Documents.SendSigningNotifications(ctx, scope, input.OrganizationID); err != nil {
r.logger.ErrorCtx(ctx, "cannot send signing notifications", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.SendSigningNotificationsPayload{
Success: true,
}, nil
}
// CancelSignatureRequest is the resolver for the cancelSignatureRequest field.
func (r *mutationResolver) CancelSignatureRequest(ctx context.Context, input types.CancelSignatureRequestInput) (*types.CancelSignatureRequestPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionSignatureID, probo.ActionDocumentVersionCancelSignature)
if err != nil {
return nil, err
}
if err := r.probo.Documents.CancelSignatureRequest(ctx, scope, input.DocumentVersionSignatureID); err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
r.logger.ErrorCtx(ctx, "cannot cancel signature request", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CancelSignatureRequestPayload{
DeletedDocumentVersionSignatureID: input.DocumentVersionSignatureID,
}, nil
}
// SignDocument is the resolver for the signDocument field.
func (r *mutationResolver) SignDocument(ctx context.Context, input types.SignDocumentInput) (*types.SignDocumentPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionSign)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
httpReq := gqlutils.HTTPRequestFromContext(ctx)
signerIP, _, _ := net.SplitHostPort(httpReq.RemoteAddr)
if signerIP == "" {
signerIP = httpReq.RemoteAddr
}
documentVersionSignature, err := r.probo.Documents.SignDocumentVersionByIdentity(
ctx,
scope,
probo.SignDocumentVersionRequest{
DocumentVersionID: input.DocumentVersionID,
IdentityID: identity.ID,
SignerFullName: identity.FullName,
SignerEmail: identity.EmailAddress,
SignerIPAddr: signerIP,
SignerUA: httpReq.UserAgent(),
},
)
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotPublished, ok := errors.AsType[*probo.ErrDocumentVersionNotPublished](err); ok {
return nil, gqlutils.Invalid(ctx, errNotPublished)
}
if errAlreadySigned, ok := errors.AsType[*probo.ErrDocumentVersionSignatureAlreadySigned](err); ok {
return nil, gqlutils.Conflict(ctx, errAlreadySigned)
}
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return nil, gqlutils.Conflict(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot sign document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.SignDocumentPayload{
DocumentVersionSignature: types.NewDocumentVersionSignature(documentVersionSignature),
}, nil
}
// ApproveDocumentVersion is the resolver for the approveDocumentVersion field.
func (r *mutationResolver) ApproveDocumentVersion(ctx context.Context, input types.ApproveDocumentVersionInput) (*types.ApproveDocumentVersionPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionApprove)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
httpReq := gqlutils.HTTPRequestFromContext(ctx)
signerIP, _, _ := net.SplitHostPort(httpReq.RemoteAddr)
if signerIP == "" {
signerIP = httpReq.RemoteAddr
}
decision, err := r.probo.DocumentApprovals.Approve(ctx, scope, probo.ApproveDocumentVersionRequest{
DocumentVersionID: input.DocumentVersionID,
IdentityID: identity.ID,
Comment: input.Comment,
SignerFullName: identity.FullName,
SignerEmail: identity.EmailAddress,
SignerIPAddr: signerIP,
SignerUA: httpReq.UserAgent(),
})
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotPending, ok := errors.AsType[*probo.ErrDocumentVersionNotPendingApproval](err); ok {
return nil, gqlutils.Invalid(ctx, errNotPending)
}
if errAlready, ok := errors.AsType[*probo.ErrApprovalDecisionAlreadyMade](err); ok {
return nil, gqlutils.Conflict(ctx, errAlready)
}
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot approve document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.ApproveDocumentVersionPayload{
ApprovalDecision: types.NewDocumentVersionApprovalDecision(decision),
}, nil
}
// RejectDocumentVersion is the resolver for the rejectDocumentVersion field.
func (r *mutationResolver) RejectDocumentVersion(ctx context.Context, input types.RejectDocumentVersionInput) (*types.RejectDocumentVersionPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionReject)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
decision, err := r.probo.DocumentApprovals.Reject(ctx, scope, probo.RejectDocumentVersionRequest{
DocumentVersionID: input.DocumentVersionID,
IdentityID: identity.ID,
Comment: input.Comment,
})
if err != nil {
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
return nil, gqlutils.Conflict(ctx, errArchived)
}
if errNotPending, ok := errors.AsType[*probo.ErrDocumentVersionNotPendingApproval](err); ok {
return nil, gqlutils.Invalid(ctx, errNotPending)
}
if errAlready, ok := errors.AsType[*probo.ErrApprovalDecisionAlreadyMade](err); ok {
return nil, gqlutils.Conflict(ctx, errAlready)
}
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot reject document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.RejectDocumentVersionPayload{
ApprovalDecision: types.NewDocumentVersionApprovalDecision(decision),
}, nil
}
// ExportDocumentVersionPDF is the resolver for the exportDocumentVersionPDF field.
func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionExportPDF)
if err != nil {
return nil, err
}
watermarkEmail := input.WatermarkEmail
if input.WithWatermark && watermarkEmail == nil {
identity := authn.IdentityFromContext(ctx)
watermarkEmail = &identity.EmailAddress
}
options := probo.ExportPDFOptions{
WithSignatures: input.WithSignatures,
WithWatermark: input.WithWatermark,
WatermarkEmail: watermarkEmail,
}
pdf, err := r.probo.Documents.ExportPDF(ctx, scope, input.DocumentVersionID, options)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export document version PDF", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.ExportDocumentVersionPDFPayload{
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
}, nil
}
// ExportEmployeeDocumentVersionPDF is the resolver for the exportEmployeeDocumentVersionPDF field.
func (r *mutationResolver) ExportEmployeeDocumentVersionPDF(ctx context.Context, input types.ExportEmployeeDocumentVersionPDFInput) (*types.ExportEmployeeDocumentVersionPDFPayload, error) {
scope, err := r.authorize(ctx, input.DocumentVersionID, probo.ActionEmployeeDocumentVersionExportPDF)
if err != nil {
return nil, err
}
documentVersion, err := r.probo.Documents.GetVersion(ctx, scope, input.DocumentVersionID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
identity := authn.IdentityFromContext(ctx)
documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(
&identity.ID,
coredata.EmployeeFilterModeSignature,
coredata.EmployeeFilterModeApproval,
)
_, err = r.probo.Documents.GetWithFilter(ctx, scope, documentVersion.DocumentID, documentFilter)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get employee document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
options := probo.ExportPDFOptions{
WithSignatures: false,
WithWatermark: true,
WatermarkEmail: &identity.EmailAddress,
}
pdf, err := r.probo.Documents.ExportPDF(ctx, scope, input.DocumentVersionID, options)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export employee document PDF", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.ExportEmployeeDocumentVersionPDFPayload{
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
}, nil
}
// Document returns schema.DocumentResolver implementation.
func (r *Resolver) Document() schema.DocumentResolver { return &documentResolver{r} }
// DocumentConnection returns schema.DocumentConnectionResolver implementation.
func (r *Resolver) DocumentConnection() schema.DocumentConnectionResolver {
return &documentConnectionResolver{r}
}
// DocumentVersion returns schema.DocumentVersionResolver implementation.
func (r *Resolver) DocumentVersion() schema.DocumentVersionResolver {
return &documentVersionResolver{r}
}
// DocumentVersionApprovalDecision returns schema.DocumentVersionApprovalDecisionResolver implementation.
func (r *Resolver) DocumentVersionApprovalDecision() schema.DocumentVersionApprovalDecisionResolver {
return &documentVersionApprovalDecisionResolver{r}
}
// DocumentVersionApprovalDecisionConnection returns schema.DocumentVersionApprovalDecisionConnectionResolver implementation.
func (r *Resolver) DocumentVersionApprovalDecisionConnection() schema.DocumentVersionApprovalDecisionConnectionResolver {
return &documentVersionApprovalDecisionConnectionResolver{r}
}
// DocumentVersionApprovalQuorum returns schema.DocumentVersionApprovalQuorumResolver implementation.
func (r *Resolver) DocumentVersionApprovalQuorum() schema.DocumentVersionApprovalQuorumResolver {
return &documentVersionApprovalQuorumResolver{r}
}
// DocumentVersionApprovalQuorumConnection returns schema.DocumentVersionApprovalQuorumConnectionResolver implementation.
func (r *Resolver) DocumentVersionApprovalQuorumConnection() schema.DocumentVersionApprovalQuorumConnectionResolver {
return &documentVersionApprovalQuorumConnectionResolver{r}
}
// DocumentVersionConnection returns schema.DocumentVersionConnectionResolver implementation.
func (r *Resolver) DocumentVersionConnection() schema.DocumentVersionConnectionResolver {
return &documentVersionConnectionResolver{r}
}
// DocumentVersionSignature returns schema.DocumentVersionSignatureResolver implementation.
func (r *Resolver) DocumentVersionSignature() schema.DocumentVersionSignatureResolver {
return &documentVersionSignatureResolver{r}
}
// DocumentVersionSignatureConnection returns schema.DocumentVersionSignatureConnectionResolver implementation.
func (r *Resolver) DocumentVersionSignatureConnection() schema.DocumentVersionSignatureConnectionResolver {
return &documentVersionSignatureConnectionResolver{r}
}
// EmployeeDocument returns schema.EmployeeDocumentResolver implementation.
func (r *Resolver) EmployeeDocument() schema.EmployeeDocumentResolver {
return &employeeDocumentResolver{r}
}
// EmployeeDocumentVersion returns schema.EmployeeDocumentVersionResolver implementation.
func (r *Resolver) EmployeeDocumentVersion() schema.EmployeeDocumentVersionResolver {
return &employeeDocumentVersionResolver{r}
}
type documentResolver struct{ *Resolver }
type documentConnectionResolver struct{ *Resolver }
type documentVersionResolver struct{ *Resolver }
type documentVersionApprovalDecisionResolver struct{ *Resolver }
type documentVersionApprovalDecisionConnectionResolver struct{ *Resolver }
type documentVersionApprovalQuorumResolver struct{ *Resolver }
type documentVersionApprovalQuorumConnectionResolver struct{ *Resolver }
type documentVersionConnectionResolver struct{ *Resolver }
type documentVersionSignatureResolver struct{ *Resolver }
type documentVersionSignatureConnectionResolver struct{ *Resolver }
type employeeDocumentResolver struct{ *Resolver }
type employeeDocumentVersionResolver struct{ *Resolver }