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 ( import (
"context" "context"
"fmt" "fmt"
"io"
"net/url" "net/url"
"time" "time"
"github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/docgen"
"github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/statelesstoken" "github.com/getprobo/probo/pkg/statelesstoken"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
@@ -16,7 +19,8 @@ import (
type ( type (
DocumentService struct { DocumentService struct {
svc *TenantService svc *TenantService
html2pdfConverter *html2pdf.Converter
} }
CreateDocumentRequest struct { CreateDocumentRequest struct {
@@ -859,3 +863,134 @@ func (s *DocumentService) Update(
return document, nil 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/crypto/cipher"
"github.com/getprobo/probo/pkg/filevalidation" "github.com/getprobo/probo/pkg/filevalidation"
"github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
type ( type (
Service struct { Service struct {
pg *pg.Client pg *pg.Client
s3 *s3.Client s3 *s3.Client
bucket string bucket string
encryptionKey cipher.EncryptionKey encryptionKey cipher.EncryptionKey
hostname string hostname string
tokenSecret string tokenSecret string
agentConfig agents.Config agentConfig agents.Config
html2pdfConverter *html2pdf.Converter
} }
TenantService struct { TenantService struct {
@@ -73,19 +75,21 @@ func NewService(
hostname string, hostname string,
tokenSecret string, tokenSecret string,
agentConfig agents.Config, agentConfig agents.Config,
html2pdfConverter *html2pdf.Converter,
) (*Service, error) { ) (*Service, error) {
if bucket == "" { if bucket == "" {
return nil, fmt.Errorf("bucket is required") return nil, fmt.Errorf("bucket is required")
} }
svc := &Service{ svc := &Service{
pg: pgClient, pg: pgClient,
s3: s3Client, s3: s3Client,
bucket: bucket, bucket: bucket,
encryptionKey: encryptionKey, encryptionKey: encryptionKey,
hostname: hostname, hostname: hostname,
tokenSecret: tokenSecret, tokenSecret: tokenSecret,
agentConfig: agentConfig, agentConfig: agentConfig,
html2pdfConverter: html2pdfConverter,
} }
return svc, nil return svc, nil
@@ -120,7 +124,10 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
} }
tenantService.Peoples = &PeopleService{svc: tenantService} tenantService.Peoples = &PeopleService{svc: tenantService}
tenantService.Vendors = &VendorService{svc: tenantService} tenantService.Vendors = &VendorService{svc: tenantService}
tenantService.Documents = &DocumentService{svc: tenantService} tenantService.Documents = &DocumentService{
svc: tenantService,
html2pdfConverter: s.html2pdfConverter,
}
tenantService.Organizations = &OrganizationService{ tenantService.Organizations = &OrganizationService{
svc: tenantService, svc: tenantService,
fileValidator: filevalidation.NewValidator( fileValidator: filevalidation.NewValidator(

View File

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