})
const theme = useSystemTheme();
const logoFileUrl = theme === "dark"
- ? compliancePage.darkLogoFileUrl ?? compliancePage.logoFileUrl
- : compliancePage.logoFileUrl;
+ ? compliancePage.darkLogo?.downloadUrl ?? compliancePage.logo?.downloadUrl
+ : compliancePage.logo?.downloadUrl;
return (
diff --git a/apps/trust/src/queries/TrustGraph.ts b/apps/trust/src/queries/TrustGraph.ts
index 66dafa0fc..d745d3a8a 100644
--- a/apps/trust/src/queries/TrustGraph.ts
+++ b/apps/trust/src/queries/TrustGraph.ts
@@ -31,8 +31,12 @@ export const currentTrustGraphQuery = graphql`
createdAt
updatedAt
}
- logoFileUrl
- darkLogoFileUrl
+ logo {
+ downloadUrl
+ }
+ darkLogo {
+ downloadUrl
+ }
nonDisclosureAgreement {
fileName
fileUrl
diff --git a/e2e/internal/testutil/graphql.go b/e2e/internal/testutil/graphql.go
index c15efaaa2..2de0eec55 100644
--- a/e2e/internal/testutil/graphql.go
+++ b/e2e/internal/testutil/graphql.go
@@ -126,6 +126,10 @@ func (c *Client) DoConnect(query string, variables map[string]any) (*GraphQLResp
return c.doWithEndpoint("/api/connect/v1/graphql", query, variables)
}
+func (c *Client) DoTrust(trustCenterID string, query string, variables map[string]any) (*GraphQLResponse, error) {
+ return c.doWithEndpoint(fmt.Sprintf("/trust/%s/api/trust/v1/graphql", trustCenterID), query, variables)
+}
+
func (c *Client) Execute(query string, variables map[string]any, result any) error {
resp, err := c.Do(query, variables)
if err != nil {
@@ -156,6 +160,21 @@ func (c *Client) ExecuteConnect(query string, variables map[string]any, result a
return nil
}
+func (c *Client) ExecuteTrust(trustCenterID string, query string, variables map[string]any, result any) error {
+ resp, err := c.DoTrust(trustCenterID, query, variables)
+ if err != nil {
+ return err
+ }
+
+ if result != nil && resp.Data != nil {
+ if err := json.Unmarshal(resp.Data, result); err != nil {
+ return fmt.Errorf("cannot unmarshal data: %w", err)
+ }
+ }
+
+ return nil
+}
+
func (c *Client) MustExecute(query string, variables map[string]any, result any) {
c.T.Helper()
err := c.Execute(query, variables, result)
diff --git a/e2e/mcp/trust_center_test.go b/e2e/mcp/trust_center_test.go
index 58ff6a224..8714e981b 100644
--- a/e2e/mcp/trust_center_test.go
+++ b/e2e/mcp/trust_center_test.go
@@ -15,6 +15,7 @@
package mcp_test
import (
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -23,11 +24,16 @@ import (
"go.probo.inc/probo/e2e/internal/testutil"
)
+type mcpFile struct {
+ DownloadURL string `json:"download_url"`
+}
+
type trustCenter struct {
- ID string `json:"id"`
- CompanyName string `json:"companyName"`
- PageTitle string `json:"pageTitle"`
- TrustCenterVisible bool `json:"trustCenterVisible"`
+ ID string `json:"id"`
+ CompanyName string `json:"companyName"`
+ PageTitle string `json:"pageTitle"`
+ TrustCenterVisible bool `json:"trustCenterVisible"`
+ Logo *mcpFile `json:"logo,omitempty"`
}
type trustCenterReference struct {
@@ -49,6 +55,85 @@ func TestMCP_GetTrustCenter(t *testing.T) {
mc := testutil.NewMCPClient(t, owner)
orgID := owner.GetOrganizationID().String()
+ const trustCenterQuery = `
+ query($organizationId: ID!) {
+ node(id: $organizationId) {
+ ... on Organization {
+ trustCenter {
+ id
+ }
+ }
+ }
+ }
+ `
+
+ var trustCenterLookup struct {
+ Node struct {
+ TrustCenter struct {
+ ID string `json:"id"`
+ } `json:"trustCenter"`
+ } `json:"node"`
+ }
+
+ err := owner.Execute(trustCenterQuery, map[string]any{
+ "organizationId": orgID,
+ }, &trustCenterLookup)
+ require.NoError(t, err)
+ require.NotEmpty(t, trustCenterLookup.Node.TrustCenter.ID)
+
+ trustCenterID := trustCenterLookup.Node.TrustCenter.ID
+
+ const uploadMutation = `
+ mutation UpdateTrustCenterBrand($input: UpdateTrustCenterBrandInput!) {
+ updateTrustCenterBrand(input: $input) {
+ trustCenter {
+ id
+ logo {
+ id
+ downloadUrl
+ }
+ }
+ }
+ }
+ `
+
+ pngContent := []byte{
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
+ 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
+ 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
+ 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
+ 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
+ 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
+ 0x42, 0x60, 0x82,
+ }
+
+ var uploadResult struct {
+ UpdateTrustCenterBrand struct {
+ TrustCenter struct {
+ ID string `json:"id"`
+ Logo *struct {
+ ID string `json:"id"`
+ DownloadURL string `json:"downloadUrl"`
+ } `json:"logo"`
+ } `json:"trustCenter"`
+ } `json:"updateTrustCenterBrand"`
+ }
+
+ err = owner.ExecuteWithFile(uploadMutation, map[string]any{
+ "input": map[string]any{
+ "trustCenterId": trustCenterID,
+ "logoFile": nil,
+ },
+ }, "input.logoFile", testutil.UploadFile{
+ Filename: "mcp-trust-center-logo.png",
+ ContentType: "image/png",
+ Content: pngContent,
+ }, &uploadResult)
+ require.NoError(t, err)
+ require.NotNil(t, uploadResult.UpdateTrustCenterBrand.TrustCenter.Logo)
+
var result struct {
TrustCenter trustCenter `json:"trustCenter"`
}
@@ -57,6 +142,13 @@ func TestMCP_GetTrustCenter(t *testing.T) {
}, &result)
assert.NotEmpty(t, result.TrustCenter.ID)
+ require.NotNil(t, result.TrustCenter.Logo)
+ assert.True(
+ t,
+ strings.Contains(result.TrustCenter.Logo.DownloadURL, "/api/files/v1/public/"),
+ "download_url must route through the public files API, got %q",
+ result.TrustCenter.Logo.DownloadURL,
+ )
}
func TestMCP_UpdateTrustCenter(t *testing.T) {
diff --git a/e2e/trust/trust_center_logo_test.go b/e2e/trust/trust_center_logo_test.go
new file mode 100644
index 000000000..e9e8685c0
--- /dev/null
+++ b/e2e/trust/trust_center_logo_test.go
@@ -0,0 +1,164 @@
+// 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 trust_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.probo.inc/probo/e2e/internal/testutil"
+)
+
+func TestTrustCenter_LogoFileDownloadURL(t *testing.T) {
+ t.Parallel()
+
+ owner := testutil.NewClient(t, testutil.RoleOwner)
+ organizationID := owner.GetOrganizationID().String()
+
+ const trustCenterQuery = `
+ query($organizationId: ID!) {
+ node(id: $organizationId) {
+ ... on Organization {
+ trustCenter {
+ id
+ }
+ }
+ }
+ }
+ `
+
+ var trustCenterLookup struct {
+ Node struct {
+ TrustCenter struct {
+ ID string `json:"id"`
+ } `json:"trustCenter"`
+ } `json:"node"`
+ }
+
+ err := owner.Execute(trustCenterQuery, map[string]any{
+ "organizationId": organizationID,
+ }, &trustCenterLookup)
+ require.NoError(t, err)
+ require.NotEmpty(t, trustCenterLookup.Node.TrustCenter.ID)
+
+ trustCenterID := trustCenterLookup.Node.TrustCenter.ID
+
+ const activateMutation = `
+ mutation($input: UpdateTrustCenterInput!) {
+ updateTrustCenter(input: $input) {
+ trustCenter {
+ id
+ active
+ }
+ }
+ }
+ `
+
+ err = owner.Execute(activateMutation, map[string]any{
+ "input": map[string]any{
+ "trustCenterId": trustCenterID,
+ "active": true,
+ },
+ }, nil)
+ require.NoError(t, err)
+
+ const uploadMutation = `
+ mutation UpdateTrustCenterBrand($input: UpdateTrustCenterBrandInput!) {
+ updateTrustCenterBrand(input: $input) {
+ trustCenter {
+ id
+ logo {
+ id
+ fileName
+ downloadUrl
+ }
+ }
+ }
+ }
+ `
+
+ pngContent := []byte{
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
+ 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
+ 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
+ 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
+ 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
+ 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
+ 0x42, 0x60, 0x82,
+ }
+
+ var uploadResult struct {
+ UpdateTrustCenterBrand struct {
+ TrustCenter struct {
+ ID string `json:"id"`
+ Logo *struct {
+ ID string `json:"id"`
+ FileName string `json:"fileName"`
+ DownloadURL string `json:"downloadUrl"`
+ } `json:"logo"`
+ } `json:"trustCenter"`
+ } `json:"updateTrustCenterBrand"`
+ }
+
+ err = owner.ExecuteWithFile(uploadMutation, map[string]any{
+ "input": map[string]any{
+ "trustCenterId": trustCenterID,
+ "logoFile": nil,
+ },
+ }, "input.logoFile", testutil.UploadFile{
+ Filename: "trust-center-logo.png",
+ ContentType: "image/png",
+ Content: pngContent,
+ }, &uploadResult)
+ require.NoError(t, err)
+ require.NotNil(t, uploadResult.UpdateTrustCenterBrand.TrustCenter.Logo)
+
+ const trustGraphQLQuery = `
+ query {
+ currentTrustCenter {
+ logo {
+ id
+ fileName
+ downloadUrl
+ }
+ }
+ }
+ `
+
+ var trustResult struct {
+ CurrentTrustCenter struct {
+ Logo *struct {
+ ID string `json:"id"`
+ FileName string `json:"fileName"`
+ DownloadURL string `json:"downloadUrl"`
+ } `json:"logo"`
+ } `json:"currentTrustCenter"`
+ }
+
+ err = owner.ExecuteTrust(trustCenterID, trustGraphQLQuery, nil, &trustResult)
+ require.NoError(t, err)
+ require.NotNil(t, trustResult.CurrentTrustCenter.Logo)
+ assert.Equal(t, uploadResult.UpdateTrustCenterBrand.TrustCenter.Logo.ID, trustResult.CurrentTrustCenter.Logo.ID)
+ assert.True(
+ t,
+ strings.Contains(trustResult.CurrentTrustCenter.Logo.DownloadURL, "/api/files/v1/public/"),
+ "downloadUrl must route through the public files API, got %q",
+ trustResult.CurrentTrustCenter.Logo.DownloadURL,
+ )
+}
diff --git a/pkg/esign/service.go b/pkg/esign/service.go
index befb124ec..0619a27e5 100644
--- a/pkg/esign/service.go
+++ b/pkg/esign/service.go
@@ -464,7 +464,7 @@ func (s *Service) GenerateCertificateFileURL(
return "", err
}
- url, err := s.fileManager.GeneratePresignedFileURL(ctx, &file, expiresIn)
+ url, err := s.fileManager.GeneratePresignedURL(ctx, &file, expiresIn)
if err != nil {
return "", fmt.Errorf("cannot generate certificate file URL: %w", err)
}
@@ -501,7 +501,7 @@ func (s *Service) GenerateSignatureFileURL(
return "", err
}
- url, err := s.fileManager.GeneratePresignedFileURL(ctx, &file, expiresIn)
+ url, err := s.fileManager.GeneratePresignedURL(ctx, &file, expiresIn)
if err != nil {
return "", fmt.Errorf("cannot generate signature file URL: %w", err)
}
diff --git a/pkg/filemanager/load.go b/pkg/filemanager/load.go
new file mode 100644
index 000000000..4503b355c
--- /dev/null
+++ b/pkg/filemanager/load.go
@@ -0,0 +1,48 @@
+// Copyright (c) 2025-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 filemanager
+
+import (
+ "context"
+ "fmt"
+
+ "go.gearno.de/kit/pg"
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/gid"
+)
+
+// GetPublicFile loads a public file record by ID.
+func (s *Service) GetPublicFile(
+ ctx context.Context,
+ fileID gid.GID,
+) (*coredata.File, 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 nil, err
+ }
+
+ return file, nil
+}
diff --git a/pkg/filemanager/s3.go b/pkg/filemanager/s3.go
new file mode 100644
index 000000000..ebebe5c5f
--- /dev/null
+++ b/pkg/filemanager/s3.go
@@ -0,0 +1,167 @@
+// Copyright (c) 2025-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 filemanager
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "net/url"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func (s *Service) GetFileBase64(
+ ctx context.Context,
+ file *coredata.File,
+) (base64Data string, mimeType string, err error) {
+ result, err := s.s3Client.GetObject(
+ ctx,
+ &s3.GetObjectInput{
+ Bucket: new(file.BucketName),
+ Key: new(file.FileKey),
+ },
+ )
+ if err != nil {
+ return "", "", fmt.Errorf("cannot get file from S3: %w", err)
+ }
+
+ defer func() { _ = result.Body.Close() }()
+
+ fileData, err := io.ReadAll(result.Body)
+ if err != nil {
+ return "", "", fmt.Errorf("cannot read file data: %w", err)
+ }
+
+ return base64.StdEncoding.EncodeToString(fileData), file.MimeType, nil
+}
+
+func (s *Service) GetFileBytes(
+ ctx context.Context,
+ file *coredata.File,
+) ([]byte, error) {
+ result, err := s.s3Client.GetObject(
+ ctx,
+ &s3.GetObjectInput{
+ Bucket: new(file.BucketName),
+ Key: new(file.FileKey),
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot get file from S3: %w", err)
+ }
+
+ defer func() { _ = result.Body.Close() }()
+
+ data, err := io.ReadAll(result.Body)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read file data: %w", err)
+ }
+
+ return data, nil
+}
+
+func (s *Service) PutFile(
+ ctx context.Context,
+ file *coredata.File,
+ content io.Reader,
+ metadata map[string]string,
+) (int64, error) {
+ _, err := s.s3Client.PutObject(
+ ctx,
+ &s3.PutObjectInput{
+ Bucket: new(file.BucketName),
+ Key: new(file.FileKey),
+ Body: content,
+ ContentType: new(file.MimeType),
+ CacheControl: new("private, max-age=3600"),
+ Metadata: metadata,
+ },
+ )
+ if err != nil {
+ return 0, fmt.Errorf("cannot upload file to S3: %w", err)
+ }
+
+ headOutput, err := s.s3Client.HeadObject(
+ ctx,
+ &s3.HeadObjectInput{
+ Bucket: new(file.BucketName),
+ Key: new(file.FileKey),
+ },
+ )
+ if err != nil {
+ return 0, fmt.Errorf("cannot get object metadata: %w", err)
+ }
+
+ return *headOutput.ContentLength, nil
+}
+
+func (s *Service) GeneratePresignedURL(
+ ctx context.Context,
+ file *coredata.File,
+ expiresIn time.Duration,
+) (string, error) {
+ presignClient := s3.NewPresignClient(s.s3Client)
+
+ encodedFilename := url.QueryEscape(file.FileName)
+ contentDisposition := fmt.Sprintf(
+ "attachment; filename=%q; filename*=UTF-8''%s",
+ encodedFilename,
+ encodedFilename,
+ )
+
+ presignedReq, err := presignClient.PresignGetObject(
+ ctx,
+ &s3.GetObjectInput{
+ Bucket: new(file.BucketName),
+ Key: new(file.FileKey),
+ ResponseCacheControl: new("max-age=3600, public"),
+ ResponseContentType: new(file.MimeType),
+ ResponseContentDisposition: &contentDisposition,
+ },
+ func(opts *s3.PresignOptions) {
+ opts.Expires = expiresIn
+ },
+ )
+ if err != nil {
+ return "", fmt.Errorf("cannot presign GetObject request: %w", err)
+ }
+
+ return presignedReq.URL, nil
+}
+
+// GetFileSize determines the byte size of a seekable io.Reader by seeking to
+// the end and back. Returns an error if content is not seekable.
+func GetFileSize(content io.Reader) (int64, error) {
+ seeker, ok := content.(io.Seeker)
+ if !ok {
+ return 0, fmt.Errorf("cannot determine file size: content is not seekable")
+ }
+
+ size, err := seeker.Seek(0, io.SeekEnd)
+ if err != nil {
+ return 0, fmt.Errorf("cannot determine file size: %w", err)
+ }
+
+ _, err = seeker.Seek(0, io.SeekStart)
+ if err != nil {
+ return 0, fmt.Errorf("cannot reset file position: %w", err)
+ }
+
+ return size, nil
+}
diff --git a/pkg/filemanager/service.go b/pkg/filemanager/service.go
index 18f0b7ad6..2195a82fd 100644
--- a/pkg/filemanager/service.go
+++ b/pkg/filemanager/service.go
@@ -15,18 +15,9 @@
package filemanager
import (
- "context"
- "encoding/base64"
- "fmt"
- "io"
- "net/url"
- "time"
-
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/baseurl"
- "go.probo.inc/probo/pkg/coredata"
- "go.probo.inc/probo/pkg/gid"
)
type Service struct {
@@ -46,213 +37,3 @@ func NewService(
s3Client: s3Client,
}
}
-
-func (s *Service) GetFileBase64(
- ctx context.Context,
- file *coredata.File,
-) (base64Data string, mimeType string, err error) {
- result, err := s.s3Client.GetObject(
- ctx,
- &awss3.GetObjectInput{
- Bucket: new(file.BucketName),
- Key: new(file.FileKey),
- },
- )
- if err != nil {
- return "", "", fmt.Errorf("cannot get file from S3: %w", err)
- }
-
- defer func() { _ = result.Body.Close() }()
-
- fileData, err := io.ReadAll(result.Body)
- if err != nil {
- return "", "", fmt.Errorf("cannot read file data: %w", err)
- }
-
- return base64.StdEncoding.EncodeToString(fileData), file.MimeType, nil
-}
-
-func (s *Service) GetFileBytes(
- ctx context.Context,
- file *coredata.File,
-) ([]byte, error) {
- result, err := s.s3Client.GetObject(
- ctx,
- &awss3.GetObjectInput{
- Bucket: new(file.BucketName),
- Key: new(file.FileKey),
- },
- )
- if err != nil {
- return nil, fmt.Errorf("cannot get file from S3: %w", err)
- }
-
- defer func() { _ = result.Body.Close() }()
-
- data, err := io.ReadAll(result.Body)
- if err != nil {
- return nil, fmt.Errorf("cannot read file data: %w", err)
- }
-
- return data, nil
-}
-
-func (s *Service) PutFile(
- ctx context.Context,
- file *coredata.File,
- content io.Reader,
- metadata map[string]string,
-) (int64, error) {
- _, err := s.s3Client.PutObject(
- ctx,
- &awss3.PutObjectInput{
- Bucket: new(file.BucketName),
- Key: new(file.FileKey),
- Body: content,
- ContentType: new(file.MimeType),
- CacheControl: new("private, max-age=3600"),
- Metadata: metadata,
- },
- )
- if err != nil {
- return 0, fmt.Errorf("cannot upload file to S3: %w", err)
- }
-
- headOutput, err := s.s3Client.HeadObject(
- ctx,
- &awss3.HeadObjectInput{
- Bucket: new(file.BucketName),
- Key: new(file.FileKey),
- },
- )
- if err != nil {
- return 0, fmt.Errorf("cannot get object metadata: %w", err)
- }
-
- return *headOutput.ContentLength, nil
-}
-
-func (s *Service) GeneratePresignedFileURL(
- ctx context.Context,
- file *coredata.File,
- expiresIn time.Duration,
-) (string, error) {
- presignClient := awss3.NewPresignClient(s.s3Client)
-
- encodedFilename := url.QueryEscape(file.FileName)
- contentDisposition := fmt.Sprintf(
- "attachment; filename=%q; filename*=UTF-8''%s",
- encodedFilename,
- encodedFilename,
- )
-
- presignedReq, err := presignClient.PresignGetObject(
- ctx,
- &awss3.GetObjectInput{
- Bucket: new(file.BucketName),
- Key: new(file.FileKey),
- ResponseCacheControl: new("max-age=3600, public"),
- ResponseContentType: new(file.MimeType),
- ResponseContentDisposition: &contentDisposition,
- },
- func(opts *awss3.PresignOptions) {
- opts.Expires = expiresIn
- },
- )
- if err != nil {
- return "", fmt.Errorf("cannot presign GetObject request: %w", err)
- }
-
- return presignedReq.URL, nil
-}
-
-// DownloadAPIPath returns the stable files API path for a stored file.
-func DownloadAPIPath(file *coredata.File) string {
- if file.Visibility == coredata.FileVisibilityPublic {
- return "/api/files/v1/public/" + file.ID.String()
- }
-
- return "/api/files/v1/" + file.ID.String()
-}
-
-// BuildDownloadURL returns the absolute app URL that routes through the files API.
-func (s *Service) BuildDownloadURL(file *coredata.File) (string, error) {
- url, err := s.baseURL.AppendPath(DownloadAPIPath(file)).String()
- if err != nil {
- return "", fmt.Errorf("cannot build file URL: %w", err)
- }
-
- return url, nil
-}
-
-// GenerateFileURL loads a public file from DB and returns the stable app URL
-// /api/files/v1/public/{id}. Used when a long-lived embeddable URL is needed
-// (e.g. trust center logos).
-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
- })
- if err != nil {
- return "", err
- }
-
- return s.BuildDownloadURL(file)
-}
-
-// GeneratePublicPresignedFileURL loads a public file from DB and returns a
-// short-lived S3 presigned URL. Used by the public HTTP handler.
-func (s *Service) GeneratePublicPresignedFileURL(
- 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.GeneratePresignedFileURL(ctx, file, expiresIn)
-}
-
-// GetFileSize determines the byte size of a seekable io.Reader by seeking to
-// the end and back. Returns an error if content is not seekable.
-func GetFileSize(content io.Reader) (int64, error) {
- seeker, ok := content.(io.Seeker)
- if !ok {
- return 0, fmt.Errorf("cannot determine file size: content is not seekable")
- }
-
- size, err := seeker.Seek(0, io.SeekEnd)
- if err != nil {
- return 0, fmt.Errorf("cannot determine file size: %w", err)
- }
-
- _, err = seeker.Seek(0, io.SeekStart)
- if err != nil {
- return 0, fmt.Errorf("cannot reset file position: %w", err)
- }
-
- return size, nil
-}
diff --git a/pkg/filemanager/url.go b/pkg/filemanager/url.go
new file mode 100644
index 000000000..8870376cd
--- /dev/null
+++ b/pkg/filemanager/url.go
@@ -0,0 +1,32 @@
+// Copyright (c) 2025-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 filemanager
+
+import (
+ "go.probo.inc/probo/pkg/coredata"
+)
+
+func apiPath(file *coredata.File) string {
+ if file.Visibility == coredata.FileVisibilityPublic {
+ return "/api/files/v1/public/" + file.ID.String()
+ }
+
+ return "/api/files/v1/" + file.ID.String()
+}
+
+// GenerateFileURL returns the stable app URL routing through the files API.
+func (s *Service) GenerateFileURL(file *coredata.File) string {
+ return s.baseURL.WithPath(apiPath(file)).MustString()
+}
diff --git a/pkg/filemanager/service_test.go b/pkg/filemanager/url_test.go
similarity index 65%
rename from pkg/filemanager/service_test.go
rename to pkg/filemanager/url_test.go
index 10cf30c82..bae2606a0 100644
--- a/pkg/filemanager/service_test.go
+++ b/pkg/filemanager/url_test.go
@@ -24,29 +24,7 @@ import (
"go.probo.inc/probo/pkg/gid"
)
-func TestDownloadAPIPath_IncludesPublicSegmentForPublicFiles(t *testing.T) {
- t.Parallel()
-
- file := &coredata.File{
- ID: gid.New(gid.NilTenant, coredata.FileEntityType),
- Visibility: coredata.FileVisibilityPublic,
- }
-
- assert.Equal(t, "/api/files/v1/public/"+file.ID.String(), filemanager.DownloadAPIPath(file))
-}
-
-func TestDownloadAPIPath_UsesPrivateSegmentForPrivateFiles(t *testing.T) {
- t.Parallel()
-
- file := &coredata.File{
- ID: gid.New(gid.NilTenant, coredata.FileEntityType),
- Visibility: coredata.FileVisibilityPrivate,
- }
-
- assert.Equal(t, "/api/files/v1/"+file.ID.String(), filemanager.DownloadAPIPath(file))
-}
-
-func TestGenerateFileURL_PathIncludesPublicSegment(t *testing.T) {
+func TestGenerateFileURL_PublicFile(t *testing.T) {
t.Parallel()
base, err := baseurl.Parse("https://app.example.com")
@@ -54,15 +32,36 @@ func TestGenerateFileURL_PathIncludesPublicSegment(t *testing.T) {
t.Fatalf("cannot parse base URL: %v", err)
}
+ svc := filemanager.NewService(nil, base, nil)
file := &coredata.File{
ID: gid.New(gid.NilTenant, coredata.FileEntityType),
Visibility: coredata.FileVisibilityPublic,
}
- url, err := base.AppendPath(filemanager.DownloadAPIPath(file)).String()
+ assert.Equal(
+ t,
+ "https://app.example.com/api/files/v1/public/"+file.ID.String(),
+ svc.GenerateFileURL(file),
+ )
+}
+
+func TestGenerateFileURL_PrivateFile(t *testing.T) {
+ t.Parallel()
+
+ base, err := baseurl.Parse("https://app.example.com")
if err != nil {
- t.Fatalf("cannot build URL: %v", err)
+ t.Fatalf("cannot parse base URL: %v", err)
}
- assert.Equal(t, "https://app.example.com/api/files/v1/public/"+file.ID.String(), url)
+ svc := filemanager.NewService(nil, base, nil)
+ file := &coredata.File{
+ ID: gid.New(gid.NilTenant, coredata.FileEntityType),
+ Visibility: coredata.FileVisibilityPrivate,
+ }
+
+ assert.Equal(
+ t,
+ "https://app.example.com/api/files/v1/"+file.ID.String(),
+ svc.GenerateFileURL(file),
+ )
}
diff --git a/pkg/iam/compliance_page_service.go b/pkg/iam/compliance_page_service.go
index e1ca456d1..17ca5ef34 100644
--- a/pkg/iam/compliance_page_service.go
+++ b/pkg/iam/compliance_page_service.go
@@ -78,7 +78,7 @@ func (s *CompliancePageService) GenerateLogoURL(
return nil, nil
}
- presignedURL, err := s.fm.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.fm.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/probo/file_service.go b/pkg/probo/file_service.go
index 786b497bf..143294e99 100644
--- a/pkg/probo/file_service.go
+++ b/pkg/probo/file_service.go
@@ -202,7 +202,7 @@ func (s FileService) GenerateFileURL(
return "", fmt.Errorf("cannot get file: %w", err)
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return "", fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go
index 405398dc1..661841407 100644
--- a/pkg/probo/framework_service.go
+++ b/pkg/probo/framework_service.go
@@ -866,7 +866,7 @@ func (s FrameworkService) GenerateLightLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -908,7 +908,7 @@ func (s FrameworkService) GenerateDarkLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/probo/organization_service.go b/pkg/probo/organization_service.go
index 113d8d81a..6c89f516f 100644
--- a/pkg/probo/organization_service.go
+++ b/pkg/probo/organization_service.go
@@ -451,7 +451,7 @@ func (s OrganizationService) GenerateLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -493,7 +493,7 @@ func (s OrganizationService) GenerateHorizontalLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/probo/trust_center_file_service.go b/pkg/probo/trust_center_file_service.go
index d41028cf3..ebccdc43f 100644
--- a/pkg/probo/trust_center_file_service.go
+++ b/pkg/probo/trust_center_file_service.go
@@ -314,7 +314,7 @@ func (s TrustCenterFileService) GenerateFileURL(
return "", err
}
- fileURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, storedFile, duration)
+ fileURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, storedFile, duration)
if err != nil {
return "", fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/probo/trust_center_reference_service.go b/pkg/probo/trust_center_reference_service.go
index c6b387f69..0fad80f86 100644
--- a/pkg/probo/trust_center_reference_service.go
+++ b/pkg/probo/trust_center_reference_service.go
@@ -305,7 +305,12 @@ func (s TrustCenterReferenceService) GenerateLogoURL(
return "", fmt.Errorf("cannot load trust center reference: %w", err)
}
- return s.svc.fileManager.GenerateFileURL(ctx, reference.LogoFileID)
+ file, err := s.svc.fileManager.GetPublicFile(ctx, reference.LogoFileID)
+ if err != nil {
+ return "", err
+ }
+
+ return s.svc.fileManager.GenerateFileURL(file), nil
}
func (s TrustCenterReferenceService) uploadLogoFile(
diff --git a/pkg/probo/trust_center_service.go b/pkg/probo/trust_center_service.go
index 56b934d33..1506135ec 100644
--- a/pkg/probo/trust_center_service.go
+++ b/pkg/probo/trust_center_service.go
@@ -498,7 +498,7 @@ func (s TrustCenterService) GenerateNDAFileURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -544,7 +544,7 @@ func (s TrustCenterService) GenerateLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -590,7 +590,7 @@ func (s TrustCenterService) GenerateDarkLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go
index 603d8d21a..d25f1e9f6 100644
--- a/pkg/server/api/api.go
+++ b/pkg/server/api/api.go
@@ -179,6 +179,7 @@ func NewServer(cfg Config) (*Server, error) {
cfg.Logger.Named("trust.v1"),
cfg.IAM,
cfg.Trust,
+ cfg.File,
cfg.ESign,
cfg.Mailman,
cfg.Cookie,
@@ -198,6 +199,7 @@ func NewServer(cfg Config) (*Server, error) {
cfg.TokenSecret,
cfg.ConnectorRegistry,
cfg.ProviderRegistry,
+ cfg.File,
cfg.BaseURL,
cfg.CustomDomainCname,
cfg.ThirdParty,
@@ -225,6 +227,8 @@ func NewServer(cfg Config) (*Server, error) {
cfg.CookieBanner,
cfg.RiskManagement,
cfg.TokenSecret,
+ cfg.File,
+ cfg.BaseURL,
),
slackHandler: slack_v1.NewMux(
cfg.Logger.Named("slack.v1"),
@@ -236,6 +240,7 @@ func NewServer(cfg Config) (*Server, error) {
cfg.IAM,
cfg.Cookie,
cfg.TokenSecret,
+ cfg.File,
cfg.BaseURL,
func(ctx context.Context, host string) bool {
if host == cfg.BaseURL.Host() {
diff --git a/pkg/server/api/connect/v1/graphql_handler.go b/pkg/server/api/connect/v1/graphql_handler.go
index 316a28733..778c67b8c 100644
--- a/pkg/server/api/connect/v1/graphql_handler.go
+++ b/pkg/server/api/connect/v1/graphql_handler.go
@@ -19,6 +19,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
@@ -29,13 +30,14 @@ import (
"go.probo.inc/probo/pkg/server/gqlutils/directives/session"
)
-func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler {
+func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, fileManagerSvc *filemanager.Service, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
authorize: authz.NewAuthorizeFunc(svc, logger),
batchAuthorize: authz.NewBatchAuthorizeFunc(svc, logger),
logger: logger,
iam: svc,
+ fileManager: fileManagerSvc,
baseURL: baseURL,
sessionCookie: authn.NewCookie(&cookieConfig),
},
diff --git a/pkg/server/api/connect/v1/organization_resolvers.go b/pkg/server/api/connect/v1/organization_resolvers.go
index d5a552a1f..4547e2611 100644
--- a/pkg/server/api/connect/v1/organization_resolvers.go
+++ b/pkg/server/api/connect/v1/organization_resolvers.go
@@ -168,7 +168,7 @@ func (r *organizationResolver) Logo(ctx context.Context, obj *types.Organization
return nil, nil
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
// HorizontalLogo is the resolver for the horizontalLogo field.
@@ -183,7 +183,7 @@ func (r *organizationResolver) HorizontalLogo(ctx context.Context, obj *types.Or
return nil, nil
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
// Profiles is the resolver for the profiles field.
diff --git a/pkg/server/api/connect/v1/resolver.go b/pkg/server/api/connect/v1/resolver.go
index 66031a402..7d1170abb 100644
--- a/pkg/server/api/connect/v1/resolver.go
+++ b/pkg/server/api/connect/v1/resolver.go
@@ -37,6 +37,7 @@ import (
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/saferedirect"
@@ -52,6 +53,7 @@ type (
batchAuthorize authz.BatchAuthorizeFunc
logger *log.Logger
iam *iam.Service
+ fileManager *filemanager.Service
baseURL *baseurl.BaseURL
sessionCookie *authn.Cookie
}
@@ -62,6 +64,7 @@ func NewMux(
svc *iam.Service,
cookieConfig securecookie.Config,
tokenSecret string,
+ fileManagerSvc *filemanager.Service,
baseURL *baseurl.BaseURL,
allowedRedirectHost saferedirect.AllowedHostFunc,
isTrustCenterDomain IsTrustCenterDomainFunc,
@@ -71,7 +74,7 @@ func NewMux(
sessionMiddleware := authn.NewSessionMiddleware(svc, cookieConfig)
apiKeyMiddleware := authn.NewAPIKeyMiddleware(svc, tokenSecret)
oauth2Middleware := authn.NewOAuth2AccessTokenMiddleware(svc)
- graphqlHandler := NewGraphQLHandler(svc, logger, baseURL, cookieConfig)
+ graphqlHandler := NewGraphQLHandler(svc, logger, fileManagerSvc, baseURL, cookieConfig)
samlHandler := NewSAMLHandler(svc, cookieConfig, baseURL, logger)
scimHandler := NewSCIMHandler(svc, logger.Named("scim"))
diff --git a/pkg/server/api/connect/v1/types/file.go b/pkg/server/api/connect/v1/types/file.go
index 44cdb8020..224b5ee68 100644
--- a/pkg/server/api/connect/v1/types/file.go
+++ b/pkg/server/api/connect/v1/types/file.go
@@ -15,20 +15,17 @@
package types
import (
- "go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filemanager"
)
-func NewFile(r *coredata.File, base *baseurl.BaseURL) *File {
- url := base.WithPath(filemanager.DownloadAPIPath(r)).MustString()
-
+func NewFile(r *coredata.File, files *filemanager.Service) *File {
return &File{
ID: r.ID,
MimeType: r.MimeType,
FileName: r.FileName,
Size: r.FileSize,
- DownloadURL: url,
+ DownloadURL: files.GenerateFileURL(r),
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
diff --git a/pkg/server/api/console/v1/audit_resolvers.go b/pkg/server/api/console/v1/audit_resolvers.go
index f4ad043c4..1fae3fb8c 100644
--- a/pkg/server/api/console/v1/audit_resolvers.go
+++ b/pkg/server/api/console/v1/audit_resolvers.go
@@ -90,7 +90,7 @@ func (r *auditResolver) ReportFile(ctx context.Context, obj *types.Audit) (*type
return nil, gqlutils.Internal(ctx)
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
// Controls is the resolver for the controls field.
diff --git a/pkg/server/api/console/v1/evidence_resolvers.go b/pkg/server/api/console/v1/evidence_resolvers.go
index cd5edcd9e..bb3d1a694 100644
--- a/pkg/server/api/console/v1/evidence_resolvers.go
+++ b/pkg/server/api/console/v1/evidence_resolvers.go
@@ -43,7 +43,7 @@ func (r *evidenceResolver) File(ctx context.Context, obj *types.Evidence) (*type
return nil, gqlutils.Internal(ctx)
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
// Task is the resolver for the task field.
diff --git a/pkg/server/api/console/v1/file_loader.go b/pkg/server/api/console/v1/file_loader.go
index 89a3c41b0..c55d0449b 100644
--- a/pkg/server/api/console/v1/file_loader.go
+++ b/pkg/server/api/console/v1/file_loader.go
@@ -40,5 +40,5 @@ func (r *Resolver) loadFile(ctx context.Context, fileID gid.GID) (*types.File, e
return nil, gqlutils.Internal(ctx)
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
diff --git a/pkg/server/api/console/v1/graphql_handler.go b/pkg/server/api/console/v1/graphql_handler.go
index 0ea4b52d5..89d9ab36b 100644
--- a/pkg/server/api/console/v1/graphql_handler.go
+++ b/pkg/server/api/console/v1/graphql_handler.go
@@ -25,6 +25,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/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
@@ -50,6 +51,7 @@ func NewGraphQLHandler(
logger *log.Logger,
thirdPartySvc *thirdparty.Service,
riskManagementSvc *riskmanagement.Service,
+ fileManagerSvc *filemanager.Service,
baseURL *baseurl.BaseURL,
) http.Handler {
config := schema.Config{
@@ -68,6 +70,7 @@ func NewGraphQLHandler(
riskManagement: riskManagementSvc,
thirdParty: thirdPartySvc,
customDomainCname: customDomainCname,
+ fileManager: fileManagerSvc,
baseURL: baseURL,
logger: logger,
},
diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go
index 3b2b51839..6d4204b8f 100644
--- a/pkg/server/api/console/v1/resolver.go
+++ b/pkg/server/api/console/v1/resolver.go
@@ -34,6 +34,7 @@ import (
"go.probo.inc/probo/pkg/cookiebanner"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/esign"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
@@ -64,6 +65,7 @@ type (
riskManagement *riskmanagement.Service
thirdParty *thirdparty.Service
logger *log.Logger
+ fileManager *filemanager.Service
baseURL *baseurl.BaseURL
customDomainCname string
}
@@ -82,6 +84,7 @@ func NewMux(
tokenSecret string,
connectorRegistry *connector.ConnectorRegistry,
providerRegistry *provider.Registry,
+ fileManagerSvc *filemanager.Service,
baseURL *baseurl.BaseURL,
customDomainCname string,
thirdPartySvc *thirdparty.Service,
@@ -105,6 +108,7 @@ func NewMux(
logger,
thirdPartySvc,
riskManagementSvc,
+ fileManagerSvc,
baseURL,
)
diff --git a/pkg/server/api/console/v1/third_party_resolvers.go b/pkg/server/api/console/v1/third_party_resolvers.go
index ec15790c0..b0c893f43 100644
--- a/pkg/server/api/console/v1/third_party_resolvers.go
+++ b/pkg/server/api/console/v1/third_party_resolvers.go
@@ -1086,7 +1086,7 @@ func (r *thirdPartyComplianceReportResolver) File(ctx context.Context, obj *type
return nil, gqlutils.Internal(ctx)
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
// Permission is the resolver for the permission field.
diff --git a/pkg/server/api/console/v1/trust_center_resolvers.go b/pkg/server/api/console/v1/trust_center_resolvers.go
index 6a4db6dd6..2adabf7f7 100644
--- a/pkg/server/api/console/v1/trust_center_resolvers.go
+++ b/pkg/server/api/console/v1/trust_center_resolvers.go
@@ -1064,7 +1064,7 @@ func (r *trustCenterDocumentAccessResolver) ReportFile(ctx context.Context, obj
return nil, gqlutils.Internal(ctx)
}
- return types.NewFile(file, r.baseURL), nil
+ return types.NewFile(file, r.fileManager), nil
}
// Audit is the resolver for the audit field.
diff --git a/pkg/server/api/console/v1/types/file.go b/pkg/server/api/console/v1/types/file.go
index 44cdb8020..224b5ee68 100644
--- a/pkg/server/api/console/v1/types/file.go
+++ b/pkg/server/api/console/v1/types/file.go
@@ -15,20 +15,17 @@
package types
import (
- "go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/filemanager"
)
-func NewFile(r *coredata.File, base *baseurl.BaseURL) *File {
- url := base.WithPath(filemanager.DownloadAPIPath(r)).MustString()
-
+func NewFile(r *coredata.File, files *filemanager.Service) *File {
return &File{
ID: r.ID,
MimeType: r.MimeType,
FileName: r.FileName,
Size: r.FileSize,
- DownloadURL: url,
+ DownloadURL: files.GenerateFileURL(r),
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
diff --git a/pkg/server/api/files/v1/handler.go b/pkg/server/api/files/v1/handler.go
index b23f0275c..6adcb280f 100644
--- a/pkg/server/api/files/v1/handler.go
+++ b/pkg/server/api/files/v1/handler.go
@@ -94,7 +94,7 @@ func (h *Handler) handleGetPublicFile(w http.ResponseWriter, r *http.Request) {
return
}
- presignedURL, err := h.fileSvc.GeneratePublicPresignedFileURL(r.Context(), fileID, presignedURLExpiry)
+ file, err := h.fileSvc.GetPublicFile(r.Context(), fileID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
jsonutil.RenderNotFound(w, fmt.Errorf("file not found"))
@@ -112,6 +112,19 @@ func (h *Handler) handleGetPublicFile(w http.ResponseWriter, r *http.Request) {
return
}
+ presignedURL, err := h.fileSvc.GeneratePresignedURL(r.Context(), file, presignedURLExpiry)
+ if err != nil {
+ h.logger.ErrorCtx(
+ r.Context(),
+ "cannot get public file URL",
+ log.Error(err),
+ log.String("file_id", fileIDStr),
+ )
+ jsonutil.RenderInternalServerError(w)
+
+ return
+ }
+
http.Redirect(w, r, presignedURL, http.StatusTemporaryRedirect)
}
@@ -157,7 +170,7 @@ func (h *Handler) handleGetFile(w http.ResponseWriter, r *http.Request) {
return
}
- presignedURL, err := h.fileSvc.GeneratePresignedFileURL(ctx, f, presignedURLExpiry)
+ presignedURL, err := h.fileSvc.GeneratePresignedURL(ctx, f, presignedURLExpiry)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot generate file URL", log.Error(err), log.String("file_id", fileIDStr))
jsonutil.RenderInternalServerError(w)
diff --git a/pkg/server/api/mcp/v1/file_loader.go b/pkg/server/api/mcp/v1/file_loader.go
new file mode 100644
index 000000000..6a5251c6c
--- /dev/null
+++ b/pkg/server/api/mcp/v1/file_loader.go
@@ -0,0 +1,42 @@
+// 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 mcp_v1
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/gid"
+ "go.probo.inc/probo/pkg/server/api/mcp/v1/types"
+)
+
+func (r *Resolver) loadFile(
+ ctx context.Context,
+ scope *coredata.Scope,
+ fileID gid.GID,
+) (*types.File, error) {
+ file, err := r.proboSvc.Files.Get(ctx, scope, fileID)
+ if err != nil {
+ if errors.Is(err, coredata.ErrResourceNotFound) {
+ return nil, fmt.Errorf("file not found")
+ }
+
+ return nil, fmt.Errorf("cannot load file: %w", err)
+ }
+
+ return types.NewFile(file, r.fileManager), nil
+}
diff --git a/pkg/server/api/mcp/v1/resolver.go b/pkg/server/api/mcp/v1/resolver.go
index ad57ad17c..a643b2169 100644
--- a/pkg/server/api/mcp/v1/resolver.go
+++ b/pkg/server/api/mcp/v1/resolver.go
@@ -24,8 +24,10 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/accessreview"
+ "go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/cookiebanner"
"go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
@@ -43,6 +45,8 @@ type Resolver struct {
cookieBanner *cookiebanner.Service
riskManagement *riskmanagement.Service
logger *log.Logger
+ fileManager *filemanager.Service
+ baseURL *baseurl.BaseURL
}
func markdownToProseMirrorJSON(markdown string) (string, error) {
diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go
index 69578b552..02ccb76ce 100644
--- a/pkg/server/api/mcp/v1/schema.resolvers.go
+++ b/pkg/server/api/mcp/v1/schema.resolvers.go
@@ -4910,19 +4910,31 @@ func (r *Resolver) GetTrustCenterTool(ctx context.Context, req *mcp.CallToolRequ
tc := types.NewTrustCenter(trustCenter)
- logoURL, err := prb.TrustCenters.GenerateLogoURL(ctx, scope, trustCenter.ID, 1*time.Hour)
- if err == nil {
- tc.LogoFileURL = logoURL
+ if trustCenter.LogoFileID != nil {
+ logo, err := r.loadFile(ctx, scope, *trustCenter.LogoFileID)
+ if err != nil {
+ return nil, types.GetTrustCenterOutput{}, err
+ }
+
+ tc.Logo = logo
}
- darkLogoURL, err := prb.TrustCenters.GenerateDarkLogoURL(ctx, scope, trustCenter.ID, 1*time.Hour)
- if err == nil {
- tc.DarkLogoFileURL = darkLogoURL
+ if trustCenter.DarkLogoFileID != nil {
+ darkLogo, err := r.loadFile(ctx, scope, *trustCenter.DarkLogoFileID)
+ if err != nil {
+ return nil, types.GetTrustCenterOutput{}, err
+ }
+
+ tc.DarkLogo = darkLogo
}
- ndaFileURL, err := prb.TrustCenters.GenerateNDAFileURL(ctx, scope, trustCenter.ID, 15*time.Minute)
- if err == nil {
- tc.NdaFileURL = ndaFileURL
+ if trustCenter.NonDisclosureAgreementFileID != nil {
+ nda, err := r.loadFile(ctx, scope, *trustCenter.NonDisclosureAgreementFileID)
+ if err != nil {
+ return nil, types.GetTrustCenterOutput{}, err
+ }
+
+ tc.Nda = nda
}
return nil, types.GetTrustCenterOutput{TrustCenter: tc}, nil
@@ -4987,7 +4999,21 @@ func (r *Resolver) ListTrustCenterReferencesTool(ctx context.Context, req *mcp.C
return nil, types.ListTrustCenterReferencesOutput{}, fmt.Errorf("cannot list trust center references: %w", err)
}
- return nil, types.NewListTrustCenterReferencesOutput(p), nil
+ refs := make([]*types.TrustCenterReference, 0, len(p.Data))
+ for _, reference := range p.Data {
+ ref := types.NewTrustCenterReference(reference)
+
+ logo, err := r.loadFile(ctx, scope, reference.LogoFileID)
+ if err != nil {
+ return nil, types.ListTrustCenterReferencesOutput{}, err
+ }
+
+ ref.Logo = logo
+
+ refs = append(refs, ref)
+ }
+
+ return nil, types.NewListTrustCenterReferencesOutput(refs, p), nil
}
// AddTrustCenterReferenceTool handles the addTrustCenterReference tool
@@ -5106,12 +5132,12 @@ func (r *Resolver) ListTrustCenterFilesTool(ctx context.Context, req *mcp.CallTo
files := make([]*types.TrustCenterFile, 0, len(p.Data))
for _, f := range p.Data {
- fileURL, err := prb.TrustCenterFiles.GenerateFileURL(ctx, scope, f.ID, 1*time.Hour)
+ file, err := r.loadFile(ctx, scope, f.FileID)
if err != nil {
- return nil, types.ListTrustCenterFilesOutput{}, fmt.Errorf("cannot generate file URL: %w", err)
+ return nil, types.ListTrustCenterFilesOutput{}, err
}
- files = append(files, types.NewTrustCenterFile(f, fileURL))
+ files = append(files, types.NewTrustCenterFile(f, file))
}
return nil, types.NewListTrustCenterFilesOutput(files, p), nil
diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml
index 348b70670..699d1f803 100644
--- a/pkg/server/api/mcp/v1/specification.yaml
+++ b/pkg/server/api/mcp/v1/specification.yaml
@@ -8846,6 +8846,35 @@ components:
direction:
$ref: "#/components/schemas/OrderDirection"
+ File:
+ type: object
+ required:
+ - id
+ - mime_type
+ - file_name
+ - size
+ - download_url
+ - created_at
+ - updated_at
+ properties:
+ id:
+ $ref: "#/components/schemas/GID"
+ mime_type:
+ type: string
+ file_name:
+ type: string
+ size:
+ type: integer
+ format: int64
+ download_url:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+
TrustCenter:
type: object
required:
@@ -8864,22 +8893,12 @@ components:
type: boolean
search_engine_indexing:
$ref: "#/components/schemas/SearchEngineIndexing"
- logo_file_url:
- type:
- - string
- - "null"
- dark_logo_file_url:
- type:
- - string
- - "null"
- nda_file_name:
- type:
- - string
- - "null"
- nda_file_url:
- type:
- - string
- - "null"
+ logo:
+ $ref: "#/components/schemas/File"
+ dark_logo:
+ $ref: "#/components/schemas/File"
+ nda:
+ $ref: "#/components/schemas/File"
created_at:
type: string
format: date-time
@@ -8908,10 +8927,8 @@ components:
type:
- string
- "null"
- logo_url:
- type:
- - string
- - "null"
+ logo:
+ $ref: "#/components/schemas/File"
rank:
type: integer
created_at:
@@ -8927,7 +8944,7 @@ components:
- id
- name
- category
- - file_url
+ - file
- trust_center_visibility
- organization_id
- created_at
@@ -8939,8 +8956,8 @@ components:
type: string
category:
type: string
- file_url:
- type: string
+ file:
+ $ref: "#/components/schemas/File"
trust_center_visibility:
$ref: "#/components/schemas/TrustCenterVisibility"
organization_id:
diff --git a/pkg/server/api/mcp/v1/types/file.go b/pkg/server/api/mcp/v1/types/file.go
new file mode 100644
index 000000000..093024de0
--- /dev/null
+++ b/pkg/server/api/mcp/v1/types/file.go
@@ -0,0 +1,32 @@
+// 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 types
+
+import (
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/filemanager"
+)
+
+func NewFile(r *coredata.File, files *filemanager.Service) *File {
+ return &File{
+ ID: r.ID,
+ MimeType: r.MimeType,
+ FileName: r.FileName,
+ Size: int(r.FileSize),
+ DownloadURL: files.GenerateFileURL(r),
+ CreatedAt: r.CreatedAt,
+ UpdatedAt: r.UpdatedAt,
+ }
+}
diff --git a/pkg/server/api/mcp/v1/types/trust_center.go b/pkg/server/api/mcp/v1/types/trust_center.go
index 648f12003..b12ff6dc1 100644
--- a/pkg/server/api/mcp/v1/types/trust_center.go
+++ b/pkg/server/api/mcp/v1/types/trust_center.go
@@ -42,12 +42,10 @@ func NewTrustCenterReference(r *coredata.TrustCenterReference) *TrustCenterRefer
}
}
-func NewListTrustCenterReferencesOutput(p *page.Page[*coredata.TrustCenterReference, coredata.TrustCenterReferenceOrderField]) ListTrustCenterReferencesOutput {
- refs := make([]*TrustCenterReference, 0, len(p.Data))
- for _, r := range p.Data {
- refs = append(refs, NewTrustCenterReference(r))
- }
-
+func NewListTrustCenterReferencesOutput(
+ refs []*TrustCenterReference,
+ p *page.Page[*coredata.TrustCenterReference, coredata.TrustCenterReferenceOrderField],
+) ListTrustCenterReferencesOutput {
var nextCursor *page.CursorKey
if len(p.Data) > 0 {
@@ -61,13 +59,13 @@ func NewListTrustCenterReferencesOutput(p *page.Page[*coredata.TrustCenterRefere
}
}
-func NewTrustCenterFile(f *coredata.TrustCenterFile, fileURL string) *TrustCenterFile {
+func NewTrustCenterFile(f *coredata.TrustCenterFile, file *File) *TrustCenterFile {
return &TrustCenterFile{
ID: f.ID,
OrganizationID: f.OrganizationID,
Name: f.Name,
Category: f.Category,
- FileURL: fileURL,
+ File: file,
TrustCenterVisibility: f.TrustCenterVisibility,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
diff --git a/pkg/server/api/mcp/v1/v1_handler.go b/pkg/server/api/mcp/v1/v1_handler.go
index 236e9f714..d1deb1510 100644
--- a/pkg/server/api/mcp/v1/v1_handler.go
+++ b/pkg/server/api/mcp/v1/v1_handler.go
@@ -22,7 +22,9 @@ import (
"go.gearno.de/kit/log"
mcpgenmcp "go.probo.inc/mcpgen/mcp"
"go.probo.inc/probo/pkg/accessreview"
+ "go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/cookiebanner"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/riskmanagement"
@@ -41,6 +43,8 @@ func NewMux(
cookieBannerSvc *cookiebanner.Service,
riskManagementSvc *riskmanagement.Service,
tokenSecret string,
+ fileManagerSvc *filemanager.Service,
+ baseURL *baseurl.BaseURL,
) *chi.Mux {
logger = logger.Named("mcp.v1")
@@ -54,6 +58,8 @@ func NewMux(
cookieBanner: cookieBannerSvc,
riskManagement: riskManagementSvc,
logger: logger,
+ fileManager: fileManagerSvc,
+ baseURL: baseURL,
}
mcpServer := server.New(resolver, mcpgenmcp.WithRecoverFunc(mcputils.NewRecoverFunc(logger)))
diff --git a/pkg/server/api/trust/v1/file_loader.go b/pkg/server/api/trust/v1/file_loader.go
new file mode 100644
index 000000000..e8254eaa8
--- /dev/null
+++ b/pkg/server/api/trust/v1/file_loader.go
@@ -0,0 +1,41 @@
+// 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 trust_v1
+
+import (
+ "context"
+ "errors"
+
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/gid"
+ "go.probo.inc/probo/pkg/server/api/trust/v1/types"
+ "go.probo.inc/probo/pkg/server/gqlutils"
+)
+
+func (r *Resolver) loadPublicFile(ctx context.Context, fileID gid.GID) (*types.File, error) {
+ file, err := r.fileManager.GetPublicFile(ctx, fileID)
+ if err != nil {
+ if errors.Is(err, coredata.ErrResourceNotFound) {
+ return nil, gqlutils.NotFound(ctx, err)
+ }
+
+ r.logger.ErrorCtx(ctx, "cannot load public file", log.Error(err))
+
+ return nil, gqlutils.Internal(ctx)
+ }
+
+ return types.NewFile(file, r.fileManager), nil
+}
diff --git a/pkg/server/api/trust/v1/gqlgen.yaml b/pkg/server/api/trust/v1/gqlgen.yaml
index b76968590..ee5cbe567 100644
--- a/pkg/server/api/trust/v1/gqlgen.yaml
+++ b/pkg/server/api/trust/v1/gqlgen.yaml
@@ -30,6 +30,9 @@ models:
CursorKey:
model:
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
+ BigInt:
+ model:
+ - "go.probo.inc/probo/pkg/server/gqlutils/types/bigint.BigIntScalar"
EmailAddr:
model:
- "go.probo.inc/probo/pkg/server/gqlutils/types/mail.AddrScalar"
diff --git a/pkg/server/api/trust/v1/graphql/base.graphql b/pkg/server/api/trust/v1/graphql/base.graphql
index 4ad0c3c78..309d8ff65 100644
--- a/pkg/server/api/trust/v1/graphql/base.graphql
+++ b/pkg/server/api/trust/v1/graphql/base.graphql
@@ -13,6 +13,7 @@ directive @goEnum(value: String) on ENUM_VALUE
directive @nda on FIELD_DEFINITION | OBJECT
+scalar BigInt
scalar CursorKey
scalar Datetime
scalar EmailAddr
diff --git a/pkg/server/api/trust/v1/graphql/file.graphql b/pkg/server/api/trust/v1/graphql/file.graphql
new file mode 100644
index 000000000..caa4ed3e8
--- /dev/null
+++ b/pkg/server/api/trust/v1/graphql/file.graphql
@@ -0,0 +1,10 @@
+# Trust File: public assets use /api/files/v1/public/{id} (no auth).
+type File {
+ id: ID!
+ mimeType: String!
+ fileName: String!
+ size: BigInt!
+ downloadUrl: String!
+ createdAt: Datetime!
+ updatedAt: Datetime!
+}
diff --git a/pkg/server/api/trust/v1/graphql/organization.graphql b/pkg/server/api/trust/v1/graphql/organization.graphql
index 155897fc7..b286a0119 100644
--- a/pkg/server/api/trust/v1/graphql/organization.graphql
+++ b/pkg/server/api/trust/v1/graphql/organization.graphql
@@ -1,7 +1,7 @@
type Organization implements Node {
id: ID!
name: String!
- logoUrl: String @goField(forceResolver: true)
+ logo: File @goField(forceResolver: true)
description: String
websiteUrl: String
diff --git a/pkg/server/api/trust/v1/graphql/trust_center.graphql b/pkg/server/api/trust/v1/graphql/trust_center.graphql
index 760cca8dc..0193b29b5 100644
--- a/pkg/server/api/trust/v1/graphql/trust_center.graphql
+++ b/pkg/server/api/trust/v1/graphql/trust_center.graphql
@@ -2,8 +2,8 @@ type TrustCenter implements Node {
id: ID!
active: Boolean!
slug: String!
- logoFileUrl: String @goField(forceResolver: true)
- darkLogoFileUrl: String @goField(forceResolver: true)
+ logo: File @goField(forceResolver: true)
+ darkLogo: File @goField(forceResolver: true)
nonDisclosureAgreement: NonDisclosureAgreement @goField(forceResolver: true)
@@ -110,8 +110,8 @@ type DocumentEdge @nda {
type Framework implements Node @nda {
id: ID!
name: String!
- lightLogoURL: String @goField(forceResolver: true)
- darkLogoURL: String @goField(forceResolver: true)
+ lightLogo: File @goField(forceResolver: true)
+ darkLogo: File @goField(forceResolver: true)
}
type AuditReport implements Node @nda {
@@ -262,7 +262,7 @@ type TrustCenterReference implements Node @nda {
name: String!
description: String
websiteUrl: String!
- logoUrl: String! @goField(forceResolver: true)
+ logo: File! @goField(forceResolver: true)
}
type TrustCenterReferenceConnection @nda {
diff --git a/pkg/server/api/trust/v1/graphql_handler.go b/pkg/server/api/trust/v1/graphql_handler.go
index 8e2d190c9..406d0d204 100644
--- a/pkg/server/api/trust/v1/graphql_handler.go
+++ b/pkg/server/api/trust/v1/graphql_handler.go
@@ -20,6 +20,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/esign"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/securecookie"
@@ -31,11 +32,12 @@ import (
"go.probo.inc/probo/pkg/trust"
)
-func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, esignSvc *esign.Service, mailmanSvc *mailman.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config, tokenSecret string) http.Handler {
+func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, fileManagerSvc *filemanager.Service, esignSvc *esign.Service, mailmanSvc *mailman.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config, tokenSecret string) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
iam: iamSvc,
trust: trustSvc,
+ fileManager: fileManagerSvc,
esign: esignSvc,
mailman: mailmanSvc,
logger: logger,
diff --git a/pkg/server/api/trust/v1/organization_resolvers.go b/pkg/server/api/trust/v1/organization_resolvers.go
index 04decb5df..aa3150af1 100644
--- a/pkg/server/api/trust/v1/organization_resolvers.go
+++ b/pkg/server/api/trust/v1/organization_resolvers.go
@@ -7,19 +7,27 @@ package trust_v1
import (
"context"
- "time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
+ "go.probo.inc/probo/pkg/server/gqlutils"
)
-// LogoURL is the resolver for the logoUrl field.
-func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
+// Logo is the resolver for the logo field.
+func (r *organizationResolver) Logo(ctx context.Context, obj *types.Organization) (*types.File, error) {
scope := coredata.NewScopeFromObjectID(obj.ID)
- trustService := r.trust
- return trustService.Organizations.GenerateLogoURL(ctx, scope, obj.ID, 1*time.Hour)
+ organization, err := r.trust.Organizations.Get(ctx, scope, obj.ID)
+ if err != nil {
+ return nil, gqlutils.NotFoundf(ctx, "organization %q not found", obj.ID)
+ }
+
+ if organization.LogoFileID == nil {
+ return nil, nil
+ }
+
+ return r.loadPublicFile(ctx, *organization.LogoFileID)
}
// Organization returns schema.OrganizationResolver implementation.
diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go
index e179b04fc..f8d1959d0 100644
--- a/pkg/server/api/trust/v1/resolver.go
+++ b/pkg/server/api/trust/v1/resolver.go
@@ -39,6 +39,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/esign"
+ "go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/securecookie"
@@ -61,6 +62,7 @@ type (
Resolver struct {
trust *trust.Service
+ fileManager *filemanager.Service
esign *esign.Service
mailman *mailman.Service
logger *log.Logger
@@ -74,6 +76,7 @@ func NewMux(
logger *log.Logger,
iamSvc *iam.Service,
trustSvc *trust.Service,
+ fileManagerSvc *filemanager.Service,
esignSvc *esign.Service,
mailmanSvc *mailman.Service,
cookieConfig securecookie.Config,
@@ -95,7 +98,7 @@ func NewMux(
)
r.Method(http.MethodGet, "/session-transfer", sessionTransferHandler)
- graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, mailmanSvc, logger, baseURL, cookieConfig, tokenSecret)
+ graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, fileManagerSvc, esignSvc, mailmanSvc, logger, baseURL, cookieConfig, tokenSecret)
r.Group(
func(r chi.Router) {
diff --git a/pkg/server/api/trust/v1/trust_center_resolvers.go b/pkg/server/api/trust/v1/trust_center_resolvers.go
index c1e969413..33fecca0c 100644
--- a/pkg/server/api/trust/v1/trust_center_resolvers.go
+++ b/pkg/server/api/trust/v1/trust_center_resolvers.go
@@ -10,7 +10,6 @@ import (
"encoding/base64"
"errors"
"fmt"
- "time"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
@@ -261,20 +260,36 @@ func (r *documentResolver) Access(ctx context.Context, obj *types.Document) (*ty
}, nil
}
-// LightLogoURL is the resolver for the lightLogoURL field.
-func (r *frameworkResolver) LightLogoURL(ctx context.Context, obj *types.Framework) (*string, error) {
+// LightLogo is the resolver for the lightLogo field.
+func (r *frameworkResolver) LightLogo(ctx context.Context, obj *types.Framework) (*types.File, error) {
scope := coredata.NewScopeFromObjectID(obj.ID)
- trustService := r.trust
- return trustService.Frameworks.GenerateLightLogoURL(ctx, scope, obj.ID, 1*time.Hour)
+ framework, err := r.trust.Frameworks.Get(ctx, scope, obj.ID)
+ if err != nil {
+ return nil, gqlutils.NotFoundf(ctx, "framework %q not found", obj.ID)
+ }
+
+ if framework.LightLogoFileID == nil {
+ return nil, nil
+ }
+
+ return r.loadPublicFile(ctx, *framework.LightLogoFileID)
}
-// DarkLogoURL is the resolver for the darkLogoURL field.
-func (r *frameworkResolver) DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) {
+// DarkLogo is the resolver for the darkLogo field.
+func (r *frameworkResolver) DarkLogo(ctx context.Context, obj *types.Framework) (*types.File, error) {
scope := coredata.NewScopeFromObjectID(obj.ID)
- trustService := r.trust
- return trustService.Frameworks.GenerateDarkLogoURL(ctx, scope, obj.ID, 1*time.Hour)
+ framework, err := r.trust.Frameworks.Get(ctx, scope, obj.ID)
+ if err != nil {
+ return nil, gqlutils.NotFoundf(ctx, "framework %q not found", obj.ID)
+ }
+
+ if framework.DarkLogoFileID == nil {
+ return nil, nil
+ }
+
+ return r.loadPublicFile(ctx, *framework.DarkLogoFileID)
}
// RequestAllAccesses is the resolver for the requestAllAccesses field.
@@ -650,20 +665,24 @@ func (r *subprocessorConnectionResolver) TotalCount(ctx context.Context, obj *ty
return 0, gqlutils.Internal(ctx)
}
-// LogoFileURL is the resolver for the logoFileUrl field.
-func (r *trustCenterResolver) LogoFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error) {
- scope := coredata.NewScopeFromObjectID(obj.ID)
- trustService := r.trust
+// Logo is the resolver for the logo field.
+func (r *trustCenterResolver) Logo(ctx context.Context, obj *types.TrustCenter) (*types.File, error) {
+ trustCenter := compliancepage.CompliancePageFromContext(ctx)
+ if trustCenter.LogoFileID == nil {
+ return nil, nil
+ }
- return trustService.TrustCenters.GenerateLogoURL(ctx, scope, obj.ID, 1*time.Hour)
+ return r.loadPublicFile(ctx, *trustCenter.LogoFileID)
}
-// DarkLogoFileURL is the resolver for the darkLogoFileUrl field.
-func (r *trustCenterResolver) DarkLogoFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error) {
- scope := coredata.NewScopeFromObjectID(obj.ID)
- trustService := r.trust
+// DarkLogo is the resolver for the darkLogo field.
+func (r *trustCenterResolver) DarkLogo(ctx context.Context, obj *types.TrustCenter) (*types.File, error) {
+ trustCenter := compliancepage.CompliancePageFromContext(ctx)
+ if trustCenter.DarkLogoFileID == nil {
+ return nil, nil
+ }
- return trustService.TrustCenters.GenerateDarkLogoURL(ctx, scope, obj.ID, 1*time.Hour)
+ return r.loadPublicFile(ctx, *trustCenter.DarkLogoFileID)
}
// NonDisclosureAgreement is the resolver for the nonDisclosureAgreement field.
@@ -975,18 +994,16 @@ func (r *trustCenterFileResolver) Access(ctx context.Context, obj *types.TrustCe
}, nil
}
-// LogoURL is the resolver for the logoUrl field.
-func (r *trustCenterReferenceResolver) LogoURL(ctx context.Context, obj *types.TrustCenterReference) (string, error) {
+// Logo is the resolver for the logo field.
+func (r *trustCenterReferenceResolver) Logo(ctx context.Context, obj *types.TrustCenterReference) (*types.File, error) {
scope := coredata.NewScopeFromObjectID(obj.ID)
- trustService := r.trust
- logoURL, err := trustService.TrustCenterReferences.GenerateLogoURL(ctx, scope, obj.ID)
+ reference, err := r.trust.TrustCenterReferences.Get(ctx, scope, obj.ID)
if err != nil {
- r.logger.ErrorCtx(ctx, "cannot generate logo URL", log.Error(err))
- return "", gqlutils.Internal(ctx)
+ return nil, gqlutils.NotFoundf(ctx, "trust center reference %q not found", obj.ID)
}
- return logoURL, nil
+ return r.loadPublicFile(ctx, reference.LogoFileID)
}
// Audit returns schema.AuditResolver implementation.
diff --git a/pkg/server/api/trust/v1/types/file.go b/pkg/server/api/trust/v1/types/file.go
new file mode 100644
index 000000000..775341e35
--- /dev/null
+++ b/pkg/server/api/trust/v1/types/file.go
@@ -0,0 +1,32 @@
+// 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 types
+
+import (
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/filemanager"
+)
+
+func NewFile(r *coredata.File, files *filemanager.Service) *File {
+ return &File{
+ ID: r.ID,
+ MimeType: r.MimeType,
+ FileName: r.FileName,
+ Size: r.FileSize,
+ DownloadURL: files.GenerateFileURL(r),
+ CreatedAt: r.CreatedAt,
+ UpdatedAt: r.UpdatedAt,
+ }
+}
diff --git a/pkg/thirdparty/service.go b/pkg/thirdparty/service.go
index 3aeff391d..6c32070d8 100644
--- a/pkg/thirdparty/service.go
+++ b/pkg/thirdparty/service.go
@@ -46,11 +46,13 @@ func (s *Service) GenerateLogoURL(
ctx context.Context,
logoFileID gid.GID,
) (*string, error) {
- url, err := s.file.GenerateFileURL(ctx, logoFileID)
+ file, err := s.file.GetPublicFile(ctx, logoFileID)
if err != nil {
- return nil, fmt.Errorf("cannot generate logo URL: %w", err)
+ return nil, fmt.Errorf("cannot load logo file: %w", err)
}
+ url := s.file.GenerateFileURL(file)
+
return &url, nil
}
diff --git a/pkg/trust/framework_service.go b/pkg/trust/framework_service.go
index 11739d85b..8cfe1e15b 100644
--- a/pkg/trust/framework_service.go
+++ b/pkg/trust/framework_service.go
@@ -85,7 +85,7 @@ func (s FrameworkService) GenerateLightLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -128,7 +128,7 @@ func (s FrameworkService) GenerateDarkLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
diff --git a/pkg/trust/trust_center_reference_service.go b/pkg/trust/trust_center_reference_service.go
index 963e74508..f95e80591 100644
--- a/pkg/trust/trust_center_reference_service.go
+++ b/pkg/trust/trust_center_reference_service.go
@@ -65,7 +65,12 @@ func (s TrustCenterReferenceService) GenerateLogoURL(
return "", fmt.Errorf("cannot load trust center reference: %w", err)
}
- return s.svc.fileManager.GenerateFileURL(ctx, reference.LogoFileID)
+ file, err := s.svc.fileManager.GetPublicFile(ctx, reference.LogoFileID)
+ if err != nil {
+ return "", err
+ }
+
+ return s.svc.fileManager.GenerateFileURL(file), nil
}
func (s TrustCenterReferenceService) Get(
diff --git a/pkg/trust/trust_center_service.go b/pkg/trust/trust_center_service.go
index 7aacc6b46..4d40f3ac2 100644
--- a/pkg/trust/trust_center_service.go
+++ b/pkg/trust/trust_center_service.go
@@ -148,7 +148,7 @@ func (s TrustCenterService) GenerateNDAFileURL(
return "", err
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return "", fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -195,7 +195,7 @@ func (s TrustCenterService) GenerateLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
@@ -242,7 +242,7 @@ func (s TrustCenterService) GenerateDarkLogoURL(
return nil, nil
}
- presignedURL, err := s.svc.fileManager.GeneratePresignedFileURL(ctx, file, expiresIn)
+ presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn)
if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}