diff --git a/pkg/file/service.go b/pkg/file/service.go index d0b867271..7ae9a10c6 100644 --- a/pkg/file/service.go +++ b/pkg/file/service.go @@ -17,51 +17,42 @@ package file import ( "context" "fmt" - "time" "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/gid" ) type Service struct { - pg *pg.Client - fileManager *filemanager.Service + pg *pg.Client + baseURL *baseurl.BaseURL } -func NewService(pgClient *pg.Client, fileManager *filemanager.Service) *Service { +func NewService(pgClient *pg.Client, baseURL *baseurl.BaseURL) *Service { return &Service{ - pg: pgClient, - fileManager: fileManager, + pg: pgClient, + baseURL: baseURL, } } -func (s *Service) GetPublicFileURL( - ctx context.Context, - fileID gid.GID, - expiresIn time.Duration, -) (string, error) { +func (s *Service) GenerateFileURL(ctx context.Context, fileID gid.GID) (string, error) { file := &coredata.File{} - err := s.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - if err := file.LoadPublicByID(ctx, conn, fileID); err != nil { - return fmt.Errorf("cannot load public file: %w", err) - } - - return nil - }, - ) + err := s.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { + if err := file.LoadPublicByID(ctx, conn, fileID); err != nil { + return fmt.Errorf("cannot load public file: %w", err) + } + return nil + }) if err != nil { return "", err } - presignedURL, err := s.fileManager.GenerateFileUrl(ctx, file, expiresIn) + url, err := s.baseURL.AppendPath("/api/files/v1/" + fileID.String()).String() if err != nil { - return "", fmt.Errorf("cannot generate file URL: %w", err) + return "", fmt.Errorf("cannot build file URL: %w", err) } - return presignedURL, nil + return url, nil } diff --git a/pkg/filesign/service.go b/pkg/filesign/service.go new file mode 100644 index 000000000..d98cb8ba9 --- /dev/null +++ b/pkg/filesign/service.go @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 filesign + +import ( + "context" + "fmt" + "time" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/filemanager" + "go.probo.inc/probo/pkg/gid" +) + +type Service struct { + pg *pg.Client + fileManager *filemanager.Service +} + +func NewService(pgClient *pg.Client, fileManager *filemanager.Service) *Service { + return &Service{ + pg: pgClient, + fileManager: fileManager, + } +} + +// GeneratePresignedFileURL returns a short-lived S3 presigned URL for a PUBLIC file. +// Prefer file.Service.GenerateFileURL in most cases — it returns a stable, cacheable +// application URL. Only use this when a direct S3 URL with a controlled TTL is required +// (e.g. the /api/files/v1 HTTP handler that issues the presign-on-redirect). +func (s *Service) GeneratePresignedFileURL( + ctx context.Context, + fileID gid.GID, + expiresIn time.Duration, +) (string, error) { + file := &coredata.File{} + + err := s.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { + if err := file.LoadPublicByID(ctx, conn, fileID); err != nil { + return fmt.Errorf("cannot load public file: %w", err) + } + return nil + }) + if err != nil { + return "", err + } + + return s.fileManager.GenerateFileUrl(ctx, file, expiresIn) +} diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 5639a4025..936a4045c 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -27,6 +27,7 @@ import ( "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/crypto/cipher" "go.probo.inc/probo/pkg/esign" + "go.probo.inc/probo/pkg/file" "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/gid" @@ -79,6 +80,7 @@ type ( html2pdfConverter *html2pdf.Converter acmeService *certmanager.ACMEService fileManager *filemanager.Service + file *file.Service logger *log.Logger slack *slack.Service esign *esign.Service @@ -144,6 +146,7 @@ func NewService( esignService *esign.Service, connectorRegistry *connector.ConnectorRegistry, invitationTokenValidity time.Duration, + fileService *file.Service, ) (*Service, error) { if bucket == "" { return nil, fmt.Errorf("bucket is required") @@ -163,6 +166,7 @@ func NewService( html2pdfConverter: html2pdfConverter, acmeService: acmeService, fileManager: fileManagerService, + file: fileService, logger: logger, slack: slackService, esign: esignService, diff --git a/pkg/probo/trust_center_reference_service.go b/pkg/probo/trust_center_reference_service.go index 37a820c78..bd46f871c 100644 --- a/pkg/probo/trust_center_reference_service.go +++ b/pkg/probo/trust_center_reference_service.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "mime" - "net/url" "path/filepath" "time" @@ -293,49 +292,20 @@ func (s TrustCenterReferenceService) Delete( } func (s TrustCenterReferenceService) GenerateLogoURL( - ctx context.Context, scope coredata.Scoper, + ctx context.Context, + scope coredata.Scoper, referenceID gid.GID, - duration time.Duration, ) (string, error) { reference := &coredata.TrustCenterReference{} - file := &coredata.File{} err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - err := reference.LoadByID(ctx, tx, scope, referenceID) - if err != nil { - return fmt.Errorf("cannot load trust center reference: %w", err) - } - - err = file.LoadByID(ctx, tx, scope, reference.LogoFileID) - if err != nil { - return fmt.Errorf("cannot load logo file: %w", err) - } - - return nil + return reference.LoadByID(ctx, tx, scope, referenceID) }) if err != nil { - return "", nil + return "", fmt.Errorf("cannot load trust center reference: %w", err) } - presignClient := s3.NewPresignClient(s.svc.s3) - - encodedFilename := url.PathEscape(file.FileName) - contentDisposition := fmt.Sprintf("inline; filename=\"%s\"; filename*=UTF-8''%s", - encodedFilename, encodedFilename) - - presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(file.FileKey), - ResponseCacheControl: new("max-age=3600, public"), - ResponseContentDisposition: new(contentDisposition), - }, func(opts *s3.PresignOptions) { - opts.Expires = duration - }) - if err != nil { - return "", fmt.Errorf("cannot presign GetObject request: %w", err) - } - - return presignedReq.URL, nil + return s.svc.file.GenerateFileURL(ctx, reference.LogoFileID) } func (s TrustCenterReferenceService) uploadLogoFile( diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 90b59bb83..9c8192968 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -58,6 +58,7 @@ import ( "go.probo.inc/probo/pkg/evidencedescriber" "go.probo.inc/probo/pkg/file" "go.probo.inc/probo/pkg/filemanager" + "go.probo.inc/probo/pkg/filesign" "go.probo.inc/probo/pkg/geoloc" "go.probo.inc/probo/pkg/html2pdf" "go.probo.inc/probo/pkg/iam" @@ -504,6 +505,9 @@ func (impl *Implm) Run( cookieBannerService := cookiebanner.NewService(pgClient, impl.cfg.Branding) + fileService := file.NewService(pgClient, baseURL) + filesignService := filesign.NewService(pgClient, fileManagerService) + proboService, err := probo.NewService( ctx, encryptionKey, @@ -527,6 +531,7 @@ func (impl *Implm) Run( esignService, defaultConnectorRegistry, time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second, + fileService, ) if err != nil { return fmt.Errorf("cannot create probo service: %w", err) @@ -544,10 +549,9 @@ func (impl *Implm) Run( fileManagerService, l, slackService, + fileService, ) - fileService := file.NewService(pgClient, fileManagerService) - accessReviewService := accessreview.NewService( pgClient, encryptionKey, @@ -564,7 +568,7 @@ func (impl *Implm) Run( AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins, ExtraHeaderFields: impl.cfg.Api.ExtraHeaderFields, Probo: proboService, - File: fileService, + FileSign: filesignService, IAM: iamService, Trust: trustService, ESign: esignService, diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index 2544e62e7..f19b45453 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -31,7 +31,7 @@ import ( "go.probo.inc/probo/pkg/connector/provider" "go.probo.inc/probo/pkg/cookiebanner" "go.probo.inc/probo/pkg/esign" - "go.probo.inc/probo/pkg/file" + "go.probo.inc/probo/pkg/filesign" "go.probo.inc/probo/pkg/geoloc" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/mailman" @@ -55,7 +55,7 @@ type ( BaseURL *baseurl.BaseURL AllowedOrigins []string Probo *probo.Service - File *file.Service + FileSign *filesign.Service IAM *iam.Service Trust *trust.Service ESign *esign.Service @@ -207,7 +207,7 @@ func NewServer(cfg Config) (*Server, error) { ), filesHandler: files_v1.NewMux( cfg.Logger.Named("files.v1"), - cfg.File, + cfg.FileSign, ), mcpHandler: mcp_v1.NewMux( cfg.Logger.Named("mcp.v1"), diff --git a/pkg/server/api/console/v1/common_third_party_resolvers.go b/pkg/server/api/console/v1/common_third_party_resolvers.go index 89c46a363..fd406cfe1 100644 --- a/pkg/server/api/console/v1/common_third_party_resolvers.go +++ b/pkg/server/api/console/v1/common_third_party_resolvers.go @@ -7,7 +7,6 @@ package console_v1 import ( "context" - "time" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/probo" @@ -26,7 +25,7 @@ func (r *commonThirdPartyResolver) LogoURL(ctx context.Context, obj *types.Commo return nil, nil } - logoURL, err := r.thirdParty.GenerateLogoURL(ctx, *obj.LogoFileID, 1*time.Hour) + logoURL, err := r.thirdParty.GenerateLogoURL(ctx, *obj.LogoFileID) if err != nil { r.logger.ErrorCtx(ctx, "cannot generate common third party logo URL", log.Error(err)) return nil, gqlutils.Internal(ctx) diff --git a/pkg/server/api/console/v1/trust_center_resolvers.go b/pkg/server/api/console/v1/trust_center_resolvers.go index 0988b2796..81345b06e 100644 --- a/pkg/server/api/console/v1/trust_center_resolvers.go +++ b/pkg/server/api/console/v1/trust_center_resolvers.go @@ -1183,7 +1183,7 @@ func (r *trustCenterReferenceResolver) LogoURL(ctx context.Context, obj *types.T return "", err } - fileURL, err := r.probo.TrustCenterReferences.GenerateLogoURL(ctx, scope, obj.ID, 1*time.Hour) + fileURL, err := r.probo.TrustCenterReferences.GenerateLogoURL(ctx, scope, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot generate logo URL", log.Error(err)) return "", gqlutils.Internal(ctx) diff --git a/pkg/server/api/files/v1/handler.go b/pkg/server/api/files/v1/handler.go index 3d10474f5..ab3d21e2c 100644 --- a/pkg/server/api/files/v1/handler.go +++ b/pkg/server/api/files/v1/handler.go @@ -22,7 +22,7 @@ import ( "github.com/go-chi/chi/v5" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/file" + "go.probo.inc/probo/pkg/filesign" "go.probo.inc/probo/pkg/gid" ) @@ -30,10 +30,10 @@ const presignedURLExpiry = 1 * time.Hour type Handler struct { logger *log.Logger - fileSvc *file.Service + fileSvc *filesign.Service } -func NewMux(logger *log.Logger, fileSvc *file.Service) *chi.Mux { +func NewMux(logger *log.Logger, fileSvc *filesign.Service) *chi.Mux { h := &Handler{ logger: logger, fileSvc: fileSvc, @@ -55,7 +55,7 @@ func (h *Handler) handleGetPublicFile(w http.ResponseWriter, r *http.Request) { return } - presignedURL, err := h.fileSvc.GetPublicFileURL(r.Context(), fileID, presignedURLExpiry) + presignedURL, err := h.fileSvc.GeneratePresignedFileURL(r.Context(), fileID, presignedURLExpiry) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { http.NotFound(w, r) diff --git a/pkg/server/api/trust/v1/trust_center_resolvers.go b/pkg/server/api/trust/v1/trust_center_resolvers.go index 904ad5fbb..4f83d3db7 100644 --- a/pkg/server/api/trust/v1/trust_center_resolvers.go +++ b/pkg/server/api/trust/v1/trust_center_resolvers.go @@ -975,7 +975,7 @@ func (r *trustCenterReferenceResolver) LogoURL(ctx context.Context, obj *types.T scope := coredata.NewScopeFromObjectID(obj.ID) trustService := r.trust - logoURL, err := trustService.TrustCenterReferences.GenerateLogoURL(ctx, scope, obj.ID, 1*time.Hour) + logoURL, err := trustService.TrustCenterReferences.GenerateLogoURL(ctx, scope, obj.ID) if err != nil { r.logger.ErrorCtx(ctx, "cannot generate logo URL", log.Error(err)) return "", gqlutils.Internal(ctx) diff --git a/pkg/server/server.go b/pkg/server/server.go index e396d14bf..b1d1ba6cf 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -30,7 +30,7 @@ import ( "go.probo.inc/probo/pkg/connector/provider" "go.probo.inc/probo/pkg/cookiebanner" "go.probo.inc/probo/pkg/esign" - "go.probo.inc/probo/pkg/file" + "go.probo.inc/probo/pkg/filesign" "go.probo.inc/probo/pkg/geoloc" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/iam/oauth2server" @@ -54,7 +54,7 @@ type Config struct { AllowedOrigins []string ExtraHeaderFields map[string]string Probo *probo.Service - File *file.Service + FileSign *filesign.Service IAM *iam.Service Trust *trust.Service ESign *esign.Service @@ -92,7 +92,7 @@ func NewServer(cfg Config) (*Server, error) { BaseURL: cfg.BaseURL, AllowedOrigins: cfg.AllowedOrigins, Probo: cfg.Probo, - File: cfg.File, + FileSign: cfg.FileSign, IAM: cfg.IAM, Trust: cfg.Trust, ESign: cfg.ESign, diff --git a/pkg/thirdparty/service.go b/pkg/thirdparty/service.go index 1fd230456..8c1f9ead5 100644 --- a/pkg/thirdparty/service.go +++ b/pkg/thirdparty/service.go @@ -17,7 +17,6 @@ package thirdparty import ( "context" "fmt" - "time" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" @@ -46,9 +45,8 @@ func NewService(pgClient *pg.Client, fileSvc *file.Service, vetter Vetter) *Serv func (s *Service) GenerateLogoURL( ctx context.Context, logoFileID gid.GID, - expiresIn time.Duration, ) (*string, error) { - url, err := s.file.GetPublicFileURL(ctx, logoFileID, expiresIn) + url, err := s.file.GenerateFileURL(ctx, logoFileID) if err != nil { return nil, fmt.Errorf("cannot generate logo URL: %w", err) } diff --git a/pkg/trust/service.go b/pkg/trust/service.go index f6bd0124b..701965210 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -26,6 +26,7 @@ import ( "go.probo.inc/probo/packages/emails" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/esign" + "go.probo.inc/probo/pkg/file" "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/html2pdf" @@ -46,6 +47,7 @@ type ( esign *esign.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service + file *file.Service logger *log.Logger slack *slack.Service TrustCenters *TrustCenterService @@ -75,6 +77,7 @@ func NewService( fileManagerService *filemanager.Service, logger *log.Logger, slack *slack.Service, + fileService *file.Service, ) *Service { svc := &Service{ pg: pgClient, @@ -86,6 +89,7 @@ func NewService( esign: esignSvc, html2pdfConverter: html2pdfConverter, fileManager: fileManagerService, + file: fileService, logger: logger, slack: slack, } diff --git a/pkg/trust/trust_center_reference_service.go b/pkg/trust/trust_center_reference_service.go index 57bccbe37..709a0590a 100644 --- a/pkg/trust/trust_center_reference_service.go +++ b/pkg/trust/trust_center_reference_service.go @@ -17,10 +17,7 @@ package trust import ( "context" "fmt" - "net/url" - "time" - "github.com/aws/aws-sdk-go-v2/service/s3" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" @@ -58,47 +55,17 @@ func (s TrustCenterReferenceService) GenerateLogoURL( ctx context.Context, scope coredata.Scoper, referenceID gid.GID, - duration time.Duration, ) (string, error) { reference := &coredata.TrustCenterReference{} - file := &coredata.File{} err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - err := reference.LoadByID(ctx, tx, scope, referenceID) - if err != nil { - return fmt.Errorf("cannot load trust center reference: %w", err) - } - - err = file.LoadByID(ctx, tx, scope, reference.LogoFileID) - if err != nil { - return fmt.Errorf("cannot load logo file: %w", err) - } - - return nil + return reference.LoadByID(ctx, tx, scope, referenceID) }) if err != nil { - return "", nil + return "", fmt.Errorf("cannot load trust center reference: %w", err) } - presignClient := s3.NewPresignClient(s.svc.s3) - - encodedFilename := url.PathEscape(file.FileName) - contentDisposition := fmt.Sprintf("inline; filename=\"%s\"; filename*=UTF-8''%s", - encodedFilename, encodedFilename) - - presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(file.FileKey), - ResponseCacheControl: new("max-age=3600, public"), - ResponseContentDisposition: new(contentDisposition), - }, func(opts *s3.PresignOptions) { - opts.Expires = duration - }) - if err != nil { - return "", fmt.Errorf("cannot presign GetObject request: %w", err) - } - - return presignedReq.URL, nil + return s.svc.file.GenerateFileURL(ctx, reference.LogoFileID) } func (s TrustCenterReferenceService) Get(