@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user