@@ -15,11 +15,13 @@
|
|||||||
package esign
|
package esign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.gearno.de/crypto/uuid"
|
||||||
"go.gearno.de/kit/httpclient"
|
"go.gearno.de/kit/httpclient"
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
@@ -123,12 +125,6 @@ func (s *Service) Run(ctx context.Context, presenterConfigFunc EmailPresenterCon
|
|||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSignatureRequest contains the parameters for creating a PENDING
|
|
||||||
// electronic signature.
|
|
||||||
|
|
||||||
// CreateSignature creates a PENDING electronic signature row. The conn
|
|
||||||
// parameter allows the caller to include this insert inside its own
|
|
||||||
// transaction.
|
|
||||||
func (s *Service) CreateSignature(
|
func (s *Service) CreateSignature(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -142,8 +138,6 @@ func (s *Service) CreateSignature(
|
|||||||
return nil, fmt.Errorf("cannot derive consent text: %w", err)
|
return nil, fmt.Errorf("cannot derive consent text: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Caller provided explicit text; append e-sign process consent
|
|
||||||
// suffix if not already present.
|
|
||||||
if !strings.HasSuffix(consentText, coredata.ESignProcessConsentText) {
|
if !strings.HasSuffix(consentText, coredata.ESignProcessConsentText) {
|
||||||
consentText = consentText + " " + coredata.ESignProcessConsentText
|
consentText = consentText + " " + coredata.ESignProcessConsentText
|
||||||
}
|
}
|
||||||
@@ -152,12 +146,19 @@ func (s *Service) CreateSignature(
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
scope := coredata.NewScopeFromObjectID(req.OrganizationID)
|
scope := coredata.NewScopeFromObjectID(req.OrganizationID)
|
||||||
|
|
||||||
|
signatureID := gid.New(scope.GetTenantID(), coredata.ElectronicSignatureEntityType)
|
||||||
|
|
||||||
|
stampedFileID, err := s.createStampedDocument(ctx, conn, scope, req.OrganizationID, req.FileID, signatureID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create stamped document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
sig := &coredata.ElectronicSignature{
|
sig := &coredata.ElectronicSignature{
|
||||||
ID: gid.New(scope.GetTenantID(), coredata.ElectronicSignatureEntityType),
|
ID: signatureID,
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
Status: coredata.ElectronicSignatureStatusPending,
|
Status: coredata.ElectronicSignatureStatusPending,
|
||||||
DocumentType: req.DocumentType,
|
DocumentType: req.DocumentType,
|
||||||
FileID: req.FileID,
|
FileID: stampedFileID,
|
||||||
SignerEmail: req.SignerEmail.String(),
|
SignerEmail: req.SignerEmail.String(),
|
||||||
ConsentText: consentText,
|
ConsentText: consentText,
|
||||||
SealVersion: 1,
|
SealVersion: 1,
|
||||||
@@ -174,6 +175,63 @@ func (s *Service) CreateSignature(
|
|||||||
return sig, nil
|
return sig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) createStampedDocument(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope coredata.Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
originalFileID gid.GID,
|
||||||
|
signatureID gid.GID,
|
||||||
|
) (gid.GID, error) {
|
||||||
|
var originalFile coredata.File
|
||||||
|
if err := originalFile.LoadByID(ctx, conn, scope, originalFileID); err != nil {
|
||||||
|
return gid.GID{}, fmt.Errorf("cannot load original file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfData, err := s.fileManager.GetFileBytes(ctx, &originalFile)
|
||||||
|
if err != nil {
|
||||||
|
return gid.GID{}, fmt.Errorf("cannot download original file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stampedData, err := StampSignatureID(pdfData, signatureID.String())
|
||||||
|
if err != nil {
|
||||||
|
return gid.GID{}, fmt.Errorf("cannot stamp signature ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
stampedFile := coredata.File{
|
||||||
|
ID: gid.New(scope.GetTenantID(), coredata.FileEntityType),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
BucketName: s.bucket,
|
||||||
|
MimeType: "application/pdf",
|
||||||
|
FileName: originalFile.FileName,
|
||||||
|
FileKey: uuid.MustNewV4().String(),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
stampedSize, err := s.fileManager.PutFile(
|
||||||
|
ctx,
|
||||||
|
&stampedFile,
|
||||||
|
bytes.NewReader(stampedData),
|
||||||
|
map[string]string{
|
||||||
|
"type": "stamped-document",
|
||||||
|
"signature-id": signatureID.String(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return gid.GID{}, fmt.Errorf("cannot upload stamped file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stampedFile.FileSize = stampedSize
|
||||||
|
|
||||||
|
if err := stampedFile.Insert(ctx, conn, scope); err != nil {
|
||||||
|
return gid.GID{}, fmt.Errorf("cannot insert stamped file record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stampedFile.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) AcceptSignature(ctx context.Context, req *AcceptSignatureRequest) (*coredata.ElectronicSignature, error) {
|
func (s *Service) AcceptSignature(ctx context.Context, req *AcceptSignatureRequest) (*coredata.ElectronicSignature, error) {
|
||||||
var (
|
var (
|
||||||
scope = coredata.NewScopeFromObjectID(req.SignatureID)
|
scope = coredata.NewScopeFromObjectID(req.SignatureID)
|
||||||
@@ -323,6 +381,43 @@ func (s *Service) GenerateCertificateFileURL(
|
|||||||
return url, nil
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GenerateSignatureFileURL(
|
||||||
|
ctx context.Context,
|
||||||
|
signatureID gid.GID,
|
||||||
|
expiresIn time.Duration,
|
||||||
|
) (string, error) {
|
||||||
|
var (
|
||||||
|
scope = coredata.NewScopeFromObjectID(signatureID)
|
||||||
|
signature coredata.ElectronicSignature
|
||||||
|
file coredata.File
|
||||||
|
)
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := signature.LoadByID(ctx, conn, scope, signatureID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load electronic signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := file.LoadByID(ctx, conn, scope, signature.FileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load signature file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := s.fileManager.GenerateFileUrl(ctx, &file, expiresIn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot generate signature file URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetEventsBySignatureID(
|
func (s *Service) GetEventsBySignatureID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
signatureID gid.GID,
|
signatureID gid.GID,
|
||||||
|
|||||||
42
pkg/esign/stamp.go
Normal file
42
pkg/esign/stamp.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package esign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||||
|
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StampSignatureID(pdfData []byte, signatureID string) ([]byte, error) {
|
||||||
|
text := fmt.Sprintf("Electronic Signature ID: %s", signatureID)
|
||||||
|
desc := "fontname:Helvetica, points:9, pos:tl, rot:0, op:1.0, scale:1.0 abs, color:0.3 0.3 0.3, offset:10 -10"
|
||||||
|
|
||||||
|
wm, err := api.TextWatermark(text, desc, true, false, types.POINTS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create signature ID stamp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(pdfData)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
if err := api.AddWatermarks(reader, &buf, nil, wm, nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot stamp signature ID on PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
@@ -648,6 +648,21 @@ func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.R
|
|||||||
// FileURL is the resolver for the fileUrl field.
|
// FileURL is the resolver for the fileUrl field.
|
||||||
func (r *nonDisclosureAgreementResolver) FileURL(ctx context.Context, obj *types.NonDisclosureAgreement) (string, error) {
|
func (r *nonDisclosureAgreementResolver) FileURL(ctx context.Context, obj *types.NonDisclosureAgreement) (string, error) {
|
||||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||||
|
|
||||||
|
if identity := authn.IdentityFromContext(ctx); identity != nil && r.esign != nil {
|
||||||
|
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
|
||||||
|
|
||||||
|
access, err := trustService.TrustCenterAccesses.GetAccess(ctx, trustCenter.ID, identity.EmailAddress)
|
||||||
|
if err == nil && access.ElectronicSignatureID != nil {
|
||||||
|
fileURL, err := r.esign.GenerateSignatureFileURL(ctx, *access.ElectronicSignatureID, 15*time.Minute)
|
||||||
|
if err == nil {
|
||||||
|
return fileURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot generate signature file URL, falling back to original NDA", log.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
|
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
|
||||||
|
|
||||||
fileURL, err := trustService.TrustCenters.GenerateNDAFileURL(ctx, trustCenter.ID, 15*time.Minute)
|
fileURL, err := trustService.TrustCenters.GenerateNDAFileURL(ctx, trustCenter.ID, 15*time.Minute)
|
||||||
|
|||||||
Reference in New Issue
Block a user