Add export pdf func

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-07-03 11:58:01 +02:00
parent 5574c68ee9
commit ed5226ca57
3 changed files with 168 additions and 16 deletions

View File

@@ -3,11 +3,14 @@ package probo
import (
"context"
"fmt"
"io"
"net/url"
"time"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/docgen"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/statelesstoken"
"github.com/jackc/pgx/v5"
@@ -16,7 +19,8 @@ import (
type (
DocumentService struct {
svc *TenantService
svc *TenantService
html2pdfConverter *html2pdf.Converter
}
CreateDocumentRequest struct {
@@ -859,3 +863,134 @@ func (s *DocumentService) Update(
return document, nil
}
func (s *DocumentService) ExportPDF(
ctx context.Context,
documentVersionID gid.GID,
) ([]byte, error) {
document := &coredata.Document{}
version := &coredata.DocumentVersion{}
owner := &coredata.People{}
publishedBy := &coredata.People{}
signatures := coredata.DocumentVersionSignatures{}
peopleMap := make(map[gid.GID]*coredata.People)
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := version.LoadByID(ctx, conn, s.svc.scope, documentVersionID); err != nil {
return fmt.Errorf("cannot load document version: %w", err)
}
if err := document.LoadByID(ctx, conn, s.svc.scope, version.DocumentID); err != nil {
return fmt.Errorf("cannot load document: %w", err)
}
if version.PublishedBy != nil {
if err := publishedBy.LoadByID(ctx, conn, s.svc.scope, *version.PublishedBy); err != nil {
return fmt.Errorf("cannot load published by person: %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, s.svc.scope, documentVersionID, cursor); err != nil {
return fmt.Errorf("cannot load document version signatures: %w", err)
}
if err := owner.LoadByID(ctx, conn, s.svc.scope, document.OwnerID); err != nil {
return 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, s.svc.scope, sig.SignedBy); err != nil {
return fmt.Errorf("cannot load people %q: %w", sig.SignedBy, err)
}
peopleMap[sig.SignedBy] = people
}
if _, ok := peopleMap[sig.RequestedBy]; !ok {
people := &coredata.People{}
if err := people.LoadByID(ctx, conn, s.svc.scope, sig.RequestedBy); err != nil {
return fmt.Errorf("cannot load people %q: %w", sig.RequestedBy, err)
}
peopleMap[sig.RequestedBy] = people
}
}
return nil
},
)
if err != nil {
return nil, err
}
classification := docgen.ClassificationInternal
switch document.DocumentType {
case coredata.DocumentTypePolicy:
classification = docgen.ClassificationConfidential
case coredata.DocumentTypeISMS:
classification = docgen.ClassificationSecret
}
docData := docgen.DocumentData{
Title: version.Title,
Content: version.Content,
Version: version.VersionNumber,
Classification: classification,
Approver: owner.FullName,
Description: version.Changelog,
PublishedAt: version.PublishedAt,
PublishedBy: publishedBy.FullName,
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,
RequestedBy: peopleMap[sig.RequestedBy].FullName,
}
}
htmlContent, err := docgen.RenderHTML(docData)
if err != nil {
return nil, fmt.Errorf("cannot generate HTML: %w", err)
}
cfg := html2pdf.RenderConfig{
PageFormat: html2pdf.PageFormatA4,
Orientation: html2pdf.OrientationPortrait,
MarginTop: html2pdf.NewMarginInches(1.0),
MarginBottom: html2pdf.NewMarginInches(1.0),
MarginLeft: html2pdf.NewMarginInches(1.0),
MarginRight: html2pdf.NewMarginInches(1.0),
PrintBackground: true,
Scale: 1.0,
}
pdfReader, err := s.html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
if err != nil {
return nil, fmt.Errorf("cannot generate PDF: %w", err)
}
pdfData, err := io.ReadAll(pdfReader)
if err != nil {
return nil, fmt.Errorf("cannot read PDF data: %w", err)
}
return pdfData, nil
}

View File

@@ -24,18 +24,20 @@ import (
"github.com/getprobo/probo/pkg/crypto/cipher"
"github.com/getprobo/probo/pkg/filevalidation"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"go.gearno.de/kit/pg"
)
type (
Service struct {
pg *pg.Client
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
hostname string
tokenSecret string
agentConfig agents.Config
pg *pg.Client
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
hostname string
tokenSecret string
agentConfig agents.Config
html2pdfConverter *html2pdf.Converter
}
TenantService struct {
@@ -73,19 +75,21 @@ func NewService(
hostname string,
tokenSecret string,
agentConfig agents.Config,
html2pdfConverter *html2pdf.Converter,
) (*Service, error) {
if bucket == "" {
return nil, fmt.Errorf("bucket is required")
}
svc := &Service{
pg: pgClient,
s3: s3Client,
bucket: bucket,
encryptionKey: encryptionKey,
hostname: hostname,
tokenSecret: tokenSecret,
agentConfig: agentConfig,
pg: pgClient,
s3: s3Client,
bucket: bucket,
encryptionKey: encryptionKey,
hostname: hostname,
tokenSecret: tokenSecret,
agentConfig: agentConfig,
html2pdfConverter: html2pdfConverter,
}
return svc, nil
@@ -120,7 +124,10 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
}
tenantService.Peoples = &PeopleService{svc: tenantService}
tenantService.Vendors = &VendorService{svc: tenantService}
tenantService.Documents = &DocumentService{svc: tenantService}
tenantService.Documents = &DocumentService{
svc: tenantService,
html2pdfConverter: s.html2pdfConverter,
}
tenantService.Organizations = &OrganizationService{
svc: tenantService,
fileValidator: filevalidation.NewValidator(

View File

@@ -30,6 +30,7 @@ import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/crypto/cipher"
"github.com/getprobo/probo/pkg/crypto/passwdhash"
"github.com/getprobo/probo/pkg/html2pdf"
"github.com/getprobo/probo/pkg/mailer"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/saferedirect"
@@ -61,6 +62,7 @@ type (
Mailer mailerConfig `json:"mailer"`
Connectors []connectorConfig `json:"connectors"`
OpenAI openaiConfig `json:"openai"`
ChromeDPAddr string `json:"chrome-dp-addr"`
}
)
@@ -83,6 +85,7 @@ func New() *Implm {
Database: "probod",
PoolSize: 100,
},
ChromeDPAddr: "localhost:9222",
Auth: authConfig{
Password: passwordConfig{
Pepper: "this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes",
@@ -168,6 +171,12 @@ func (impl *Implm) Run(
},
)
html2pdfConverter := html2pdf.NewConverter(
impl.cfg.ChromeDPAddr,
html2pdf.WithLogger(l),
html2pdf.WithTracerProvider(tp),
)
s3Client := s3.NewFromConfig(awsConfig)
err = migrator.NewMigrator(pgClient, coredata.Migrations, l.Named("migrations")).Run(ctx, "migrations")
@@ -216,6 +225,7 @@ func (impl *Implm) Run(
impl.cfg.Hostname,
impl.cfg.Auth.Cookie.Secret,
agentConfig,
html2pdfConverter,
)
if err != nil {
return fmt.Errorf("cannot create probo service: %w", err)