Add export document pdf options
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -31,7 +31,10 @@ type (
|
||||
ExportJobs []*ExportJob
|
||||
|
||||
DocumentExportArguments struct {
|
||||
DocumentIDs []gid.GID `json:"document_ids"`
|
||||
DocumentIDs []gid.GID `json:"document_ids"`
|
||||
WithWatermark bool `json:"with_watermark"`
|
||||
WatermarkEmail *string `json:"watermark_email"`
|
||||
WithSignatures bool `json:"with_signatures"`
|
||||
}
|
||||
|
||||
FrameworkExportArguments struct {
|
||||
|
||||
@@ -60,7 +60,6 @@ func TestRenderHTML(t *testing.T) {
|
||||
"<td>1</td>",
|
||||
"PUBLIC",
|
||||
"John Doe",
|
||||
"Test document description",
|
||||
"Alice Smith",
|
||||
},
|
||||
},
|
||||
@@ -80,7 +79,6 @@ func TestRenderHTML(t *testing.T) {
|
||||
wantContains: []string{
|
||||
"Test &amp; &lt;Script&gt; Title",
|
||||
"John &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; Doe",
|
||||
"Description with &amp; symbols and &lt;tags&gt;",
|
||||
"Alice &amp; &lt;Bob&gt;",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/getprobo/probo/pkg/html2pdf"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
"github.com/getprobo/probo/pkg/statelesstoken"
|
||||
"github.com/getprobo/probo/pkg/watermarkpdf"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/crypto/uuid"
|
||||
"go.gearno.de/kit/pg"
|
||||
@@ -833,10 +835,20 @@ func (s *DocumentService) RequestExport(
|
||||
documentIDs []gid.GID,
|
||||
recipientEmail string,
|
||||
recipientName string,
|
||||
options BulkExportOptions,
|
||||
) (*coredata.ExportJob, error) {
|
||||
var exportJobID gid.GID
|
||||
exportJob := &coredata.ExportJob{}
|
||||
|
||||
if options.WithWatermark {
|
||||
if options.WatermarkEmail == nil {
|
||||
return nil, fmt.Errorf("watermark email is required when with watermark is true")
|
||||
}
|
||||
if _, err := mail.ParseAddress(*options.WatermarkEmail); err != nil {
|
||||
return nil, fmt.Errorf("invalid email address")
|
||||
}
|
||||
}
|
||||
|
||||
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
|
||||
for _, documentID := range documentIDs {
|
||||
document := &coredata.Document{}
|
||||
@@ -849,7 +861,10 @@ func (s *DocumentService) RequestExport(
|
||||
exportJobID = gid.New(s.svc.scope.GetTenantID(), coredata.ExportJobEntityType)
|
||||
|
||||
args := coredata.DocumentExportArguments{
|
||||
DocumentIDs: documentIDs,
|
||||
DocumentIDs: documentIDs,
|
||||
WithWatermark: options.WithWatermark,
|
||||
WatermarkEmail: options.WatermarkEmail,
|
||||
WithSignatures: options.WithSignatures,
|
||||
}
|
||||
argsJSON, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
@@ -1159,16 +1174,29 @@ func (s *DocumentService) CancelSignatureRequest(
|
||||
)
|
||||
}
|
||||
|
||||
type ExportPDFOptions struct {
|
||||
WithWatermark bool
|
||||
WatermarkEmail *string
|
||||
WithSignatures bool
|
||||
}
|
||||
|
||||
type BulkExportOptions struct {
|
||||
WithWatermark bool
|
||||
WatermarkEmail *string
|
||||
WithSignatures bool
|
||||
}
|
||||
|
||||
func (s *DocumentService) ExportPDF(
|
||||
ctx context.Context,
|
||||
documentVersionID gid.GID,
|
||||
options ExportPDFOptions,
|
||||
) ([]byte, error) {
|
||||
var data []byte
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(conn pg.Conn) (err error) {
|
||||
data, err = exportDocumentPDF(ctx, s.html2pdfConverter, conn, s.svc.scope, documentVersionID)
|
||||
data, err = exportDocumentPDF(ctx, s.html2pdfConverter, conn, s.svc.scope, documentVersionID, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot export document PDF: %w", err)
|
||||
}
|
||||
@@ -1206,7 +1234,18 @@ func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID
|
||||
defer tempFile.Close()
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
err = s.Export(ctx, documentIDs, tempFile)
|
||||
exportArgs, err := exportJob.GetDocumentExportArguments()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get export arguments: %w", err)
|
||||
}
|
||||
|
||||
exportOptions := BulkExportOptions{
|
||||
WithWatermark: exportArgs.WithWatermark,
|
||||
WatermarkEmail: exportArgs.WatermarkEmail,
|
||||
WithSignatures: exportArgs.WithSignatures,
|
||||
}
|
||||
|
||||
err = s.Export(ctx, documentIDs, tempFile, exportOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot export documents: %w", err)
|
||||
}
|
||||
@@ -1281,6 +1320,7 @@ func exportDocumentPDF(
|
||||
conn pg.Conn,
|
||||
scope coredata.Scoper,
|
||||
documentVersionID gid.GID,
|
||||
options ExportPDFOptions,
|
||||
) ([]byte, error) {
|
||||
document := &coredata.Document{}
|
||||
version := &coredata.DocumentVersion{}
|
||||
@@ -1296,32 +1336,45 @@ func exportDocumentPDF(
|
||||
return nil, fmt.Errorf("cannot load document: %w", err)
|
||||
}
|
||||
|
||||
cursor := page.NewCursor(
|
||||
100,
|
||||
nil,
|
||||
page.Head,
|
||||
page.OrderBy[coredata.DocumentVersionSignatureOrderField]{
|
||||
Field: coredata.DocumentVersionSignatureOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionAsc,
|
||||
},
|
||||
)
|
||||
|
||||
if err := signatures.LoadByDocumentVersionID(ctx, conn, scope, documentVersionID, cursor); err != nil {
|
||||
return nil, fmt.Errorf("cannot load document version signatures: %w", err)
|
||||
}
|
||||
|
||||
if err := owner.LoadByID(ctx, conn, scope, document.OwnerID); err != nil {
|
||||
return nil, fmt.Errorf("cannot load document owner: %w", err)
|
||||
}
|
||||
|
||||
// TODO: refactor this to use a single query
|
||||
for _, sig := range signatures {
|
||||
if _, ok := peopleMap[sig.SignedBy]; !ok {
|
||||
people := &coredata.People{}
|
||||
if err := people.LoadByID(ctx, conn, scope, sig.SignedBy); err != nil {
|
||||
return nil, fmt.Errorf("cannot load people %q: %w", sig.SignedBy, err)
|
||||
var signatureData []docgen.SignatureData
|
||||
if options.WithSignatures {
|
||||
cursor := page.NewCursor(
|
||||
100,
|
||||
nil,
|
||||
page.Head,
|
||||
page.OrderBy[coredata.DocumentVersionSignatureOrderField]{
|
||||
Field: coredata.DocumentVersionSignatureOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionAsc,
|
||||
},
|
||||
)
|
||||
|
||||
if err := signatures.LoadByDocumentVersionID(ctx, conn, scope, documentVersionID, cursor); err != nil {
|
||||
return nil, fmt.Errorf("cannot load document version signatures: %w", err)
|
||||
}
|
||||
|
||||
// TODO: refactor this to use a single query
|
||||
for _, sig := range signatures {
|
||||
if _, ok := peopleMap[sig.SignedBy]; !ok {
|
||||
people := &coredata.People{}
|
||||
if err := people.LoadByID(ctx, conn, scope, sig.SignedBy); err != nil {
|
||||
return nil, fmt.Errorf("cannot load people %q: %w", sig.SignedBy, err)
|
||||
}
|
||||
peopleMap[sig.SignedBy] = people
|
||||
}
|
||||
}
|
||||
|
||||
signatureData = make([]docgen.SignatureData, len(signatures))
|
||||
for i, sig := range signatures {
|
||||
signatureData[i] = docgen.SignatureData{
|
||||
SignedBy: peopleMap[sig.SignedBy].FullName,
|
||||
SignedAt: sig.SignedAt,
|
||||
State: sig.State,
|
||||
RequestedAt: sig.RequestedAt,
|
||||
}
|
||||
peopleMap[sig.SignedBy] = people
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1340,16 +1393,7 @@ func exportDocumentPDF(
|
||||
Classification: classification,
|
||||
Approver: owner.FullName,
|
||||
PublishedAt: version.PublishedAt,
|
||||
Signatures: make([]docgen.SignatureData, len(signatures)),
|
||||
}
|
||||
|
||||
for i, sig := range signatures {
|
||||
docData.Signatures[i] = docgen.SignatureData{
|
||||
SignedBy: peopleMap[sig.SignedBy].FullName,
|
||||
SignedAt: sig.SignedAt,
|
||||
State: sig.State,
|
||||
RequestedAt: sig.RequestedAt,
|
||||
}
|
||||
Signatures: signatureData,
|
||||
}
|
||||
|
||||
htmlContent, err := docgen.RenderHTML(docData)
|
||||
@@ -1377,6 +1421,22 @@ func exportDocumentPDF(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read PDF data: %w", err)
|
||||
}
|
||||
|
||||
if options.WithWatermark {
|
||||
if options.WatermarkEmail == nil {
|
||||
return nil, fmt.Errorf("watermark email is required with watermark enabled")
|
||||
}
|
||||
if _, err := mail.ParseAddress(*options.WatermarkEmail); err != nil {
|
||||
return nil, fmt.Errorf("invalid email address")
|
||||
}
|
||||
|
||||
watermarkedPDF, err := watermarkpdf.AddConfidentialWithTimestamp(pdfData, *options.WatermarkEmail)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot add watermark to PDF: %w", err)
|
||||
}
|
||||
return watermarkedPDF, nil
|
||||
}
|
||||
|
||||
return pdfData, nil
|
||||
}
|
||||
|
||||
@@ -1384,6 +1444,7 @@ func (s *DocumentService) Export(
|
||||
ctx context.Context,
|
||||
documentIDs []gid.GID,
|
||||
file io.Writer,
|
||||
options BulkExportOptions,
|
||||
) (err error) {
|
||||
archive := zip.NewWriter(file)
|
||||
defer func() {
|
||||
@@ -1406,12 +1467,19 @@ func (s *DocumentService) Export(
|
||||
return fmt.Errorf("cannot load document version for %q: %w", documentID, err)
|
||||
}
|
||||
|
||||
pdfOptions := ExportPDFOptions{
|
||||
WithWatermark: options.WithWatermark,
|
||||
WatermarkEmail: options.WatermarkEmail,
|
||||
WithSignatures: options.WithSignatures,
|
||||
}
|
||||
|
||||
exportedPDF, err := exportDocumentPDF(
|
||||
ctx,
|
||||
s.html2pdfConverter,
|
||||
conn,
|
||||
s.svc.scope,
|
||||
documentVersion.ID,
|
||||
pdfOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot export document PDF for %q: %w", documentID, err)
|
||||
|
||||
@@ -284,6 +284,7 @@ func (s FrameworkService) Export(
|
||||
conn,
|
||||
s.svc.scope,
|
||||
documentVersion.ID,
|
||||
ExportPDFOptions{WithSignatures: true},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot export document PDF: %w", err)
|
||||
|
||||
@@ -3303,6 +3303,9 @@ input UpdateDocumentInput {
|
||||
|
||||
input ExportDocumentVersionPDFInput {
|
||||
documentVersionId: ID!
|
||||
withWatermark: Boolean!
|
||||
watermarkEmail: String
|
||||
withSignatures: Boolean!
|
||||
}
|
||||
|
||||
input DeleteDocumentInput {
|
||||
@@ -4002,6 +4005,9 @@ input BulkDeleteDocumentsInput {
|
||||
|
||||
input BulkExportDocumentsInput {
|
||||
documentIds: [ID!]!
|
||||
withWatermark: Boolean!
|
||||
watermarkEmail: String
|
||||
withSignatures: Boolean!
|
||||
}
|
||||
|
||||
type BulkDeleteDocumentsPayload {
|
||||
|
||||
@@ -11713,6 +11713,9 @@ input UpdateDocumentInput {
|
||||
|
||||
input ExportDocumentVersionPDFInput {
|
||||
documentVersionId: ID!
|
||||
withWatermark: Boolean!
|
||||
watermarkEmail: String
|
||||
withSignatures: Boolean!
|
||||
}
|
||||
|
||||
input DeleteDocumentInput {
|
||||
@@ -12412,6 +12415,9 @@ input BulkDeleteDocumentsInput {
|
||||
|
||||
input BulkExportDocumentsInput {
|
||||
documentIds: [ID!]!
|
||||
withWatermark: Boolean!
|
||||
watermarkEmail: String
|
||||
withSignatures: Boolean!
|
||||
}
|
||||
|
||||
type BulkDeleteDocumentsPayload {
|
||||
@@ -63447,7 +63453,7 @@ func (ec *executionContext) unmarshalInputBulkExportDocumentsInput(ctx context.C
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
fieldsInOrder := [...]string{"documentIds"}
|
||||
fieldsInOrder := [...]string{"documentIds", "withWatermark", "watermarkEmail", "withSignatures"}
|
||||
for _, k := range fieldsInOrder {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -63461,6 +63467,27 @@ func (ec *executionContext) unmarshalInputBulkExportDocumentsInput(ctx context.C
|
||||
return it, err
|
||||
}
|
||||
it.DocumentIds = data
|
||||
case "withWatermark":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withWatermark"))
|
||||
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.WithWatermark = data
|
||||
case "watermarkEmail":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("watermarkEmail"))
|
||||
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.WatermarkEmail = data
|
||||
case "withSignatures":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withSignatures"))
|
||||
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.WithSignatures = data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66867,7 +66894,7 @@ func (ec *executionContext) unmarshalInputExportDocumentVersionPDFInput(ctx cont
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
fieldsInOrder := [...]string{"documentVersionId"}
|
||||
fieldsInOrder := [...]string{"documentVersionId", "withWatermark", "watermarkEmail", "withSignatures"}
|
||||
for _, k := range fieldsInOrder {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -66881,6 +66908,27 @@ func (ec *executionContext) unmarshalInputExportDocumentVersionPDFInput(ctx cont
|
||||
return it, err
|
||||
}
|
||||
it.DocumentVersionID = data
|
||||
case "withWatermark":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withWatermark"))
|
||||
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.WithWatermark = data
|
||||
case "watermarkEmail":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("watermarkEmail"))
|
||||
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.WatermarkEmail = data
|
||||
case "withSignatures":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withSignatures"))
|
||||
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.WithSignatures = data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,10 @@ type BulkDeleteDocumentsPayload struct {
|
||||
}
|
||||
|
||||
type BulkExportDocumentsInput struct {
|
||||
DocumentIds []gid.GID `json:"documentIds"`
|
||||
DocumentIds []gid.GID `json:"documentIds"`
|
||||
WithWatermark bool `json:"withWatermark"`
|
||||
WatermarkEmail *string `json:"watermarkEmail,omitempty"`
|
||||
WithSignatures bool `json:"withSignatures"`
|
||||
}
|
||||
|
||||
type BulkExportDocumentsPayload struct {
|
||||
@@ -1053,6 +1056,9 @@ type EvidenceEdge struct {
|
||||
|
||||
type ExportDocumentVersionPDFInput struct {
|
||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||
WithWatermark bool `json:"withWatermark"`
|
||||
WatermarkEmail *string `json:"watermarkEmail,omitempty"`
|
||||
WithSignatures bool `json:"withSignatures"`
|
||||
}
|
||||
|
||||
type ExportDocumentVersionPDFPayload struct {
|
||||
|
||||
@@ -2628,7 +2628,13 @@ func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.
|
||||
panic(fmt.Errorf("user not found"))
|
||||
}
|
||||
|
||||
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, user.EmailAddress, user.FullName)
|
||||
options := probo.BulkExportOptions{
|
||||
WithWatermark: input.WithWatermark,
|
||||
WithSignatures: input.WithSignatures,
|
||||
WatermarkEmail: input.WatermarkEmail,
|
||||
}
|
||||
|
||||
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, user.EmailAddress, user.FullName, options)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot request document export: %w", err))
|
||||
}
|
||||
@@ -2775,7 +2781,13 @@ func (r *mutationResolver) CancelSignatureRequest(ctx context.Context, input typ
|
||||
func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error) {
|
||||
prb := r.ProboService(ctx, input.DocumentVersionID.TenantID())
|
||||
|
||||
pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID)
|
||||
options := probo.ExportPDFOptions{
|
||||
WithSignatures: input.WithSignatures,
|
||||
WithWatermark: input.WithWatermark,
|
||||
WatermarkEmail: input.WatermarkEmail,
|
||||
}
|
||||
|
||||
pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID, options)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot export document version PDF: %w", err))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user