Adopt File type for trust logos and MCP
Trust GraphQL and MCP still exposed presigned URL strings for trust-center logos while console and connect already serve stable File.downloadUrl paths. Phase 1 migrates the seven public logo fields on trust GraphQL and the trust-center file references on MCP to the shared File type; trust GraphQL NDA stays on fileUrl for a follow-up. Trust resolvers load public files through filemanager and map them with types.NewFile. The trust app Relay queries and components now read logo.downloadUrl. MCP specification, resolvers, and helpers are updated in sync, including NDA on MCP where callers already have file access. filemanager is split into focused files and its URL surface is narrowed to GenerateFileURL(file) for stable app URLs and GeneratePresignedURL for S3 redirects. GetPublicFile remains the DB entry point when only a file ID is known. Add trust and MCP e2e coverage for public logo download URLs. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -66,8 +66,12 @@ const auditRowFragment = graphql`
|
||||
framework {
|
||||
id
|
||||
name
|
||||
lightLogoURL
|
||||
darkLogoURL
|
||||
lightLogo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -176,8 +180,8 @@ export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
|
||||
<div className="flex flex-col gap-2 items-center w-19">
|
||||
<FrameworkLogo
|
||||
className="size-19"
|
||||
lightLogoURL={audit.framework.lightLogoURL}
|
||||
darkLogoURL={audit.framework.darkLogoURL}
|
||||
lightLogoURL={audit.framework.lightLogo?.downloadUrl}
|
||||
darkLogoURL={audit.framework.darkLogo?.downloadUrl}
|
||||
name={audit.framework.name}
|
||||
/>
|
||||
<div className="txt-primary text-sm max-w-19 overflow-hidden min-w-0 whitespace-nowrap text-ellipsis">
|
||||
@@ -215,8 +219,8 @@ function AuditDialog(
|
||||
<DialogContent className="p-4 lg:p-8 space-y-6">
|
||||
<FrameworkLogo
|
||||
className="size-24 mx-auto"
|
||||
lightLogoURL={audit.framework.lightLogoURL}
|
||||
darkLogoURL={audit.framework.darkLogoURL}
|
||||
lightLogoURL={audit.framework.lightLogo?.downloadUrl}
|
||||
darkLogoURL={audit.framework.darkLogo?.downloadUrl}
|
||||
name={audit.framework.name}
|
||||
/>
|
||||
<h2 className="text-xl font-semibold mb-1">{audit.framework.name}</h2>
|
||||
|
||||
@@ -23,8 +23,12 @@ const frameworkFragment = graphql`
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
id
|
||||
name
|
||||
lightLogoURL
|
||||
darkLogoURL
|
||||
lightLogo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -35,8 +39,8 @@ export function FrameworkBadge(props: { framework: FrameworkBadgeFragment$key })
|
||||
<div className="flex flex-col gap-2 items-center w-19">
|
||||
<FrameworkLogo
|
||||
className="size-19"
|
||||
lightLogoURL={framework.lightLogoURL}
|
||||
darkLogoURL={framework.darkLogoURL}
|
||||
lightLogoURL={framework.lightLogo?.downloadUrl}
|
||||
darkLogoURL={framework.darkLogo?.downloadUrl}
|
||||
name={framework.name}
|
||||
/>
|
||||
<div className="txt-primary text-xs max-w-19 min-w-0 text-center">
|
||||
|
||||
@@ -85,7 +85,9 @@ export function OrganizationSidebar({
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const logoFileUrl = theme === "dark" ? (trustCenter?.darkLogoFileUrl ?? trustCenter?.logoFileUrl) : trustCenter?.logoFileUrl;
|
||||
const logoFileUrl = theme === "dark"
|
||||
? (trustCenter?.darkLogo?.downloadUrl ?? trustCenter?.logo?.downloadUrl)
|
||||
: trustCenter?.logo?.downloadUrl;
|
||||
|
||||
const [requestAllAccesses, isRequestingAccess]
|
||||
= useMutation<OrganizationSidebar_requestAllAccessesMutation>(
|
||||
|
||||
@@ -38,8 +38,8 @@ export function MainLayout(props: Props) {
|
||||
|
||||
useFavicon(
|
||||
theme === "dark"
|
||||
? (trustCenter?.darkLogoFileUrl ?? trustCenter?.logoFileUrl)
|
||||
: trustCenter?.logoFileUrl,
|
||||
? (trustCenter?.darkLogo?.downloadUrl ?? trustCenter?.logo?.downloadUrl)
|
||||
: trustCenter?.logo?.downloadUrl,
|
||||
);
|
||||
useRequestAccessCallback();
|
||||
|
||||
|
||||
@@ -47,8 +47,12 @@ import type { DocumentPageRequestTrustCenterFileAccessMutation } from "./__gener
|
||||
export const documentPageQuery = graphql`
|
||||
query DocumentPageQuery($id: ID!) {
|
||||
currentTrustCenter {
|
||||
logoFileUrl
|
||||
darkLogoFileUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
node(id: $id) @required(action: THROW) {
|
||||
__typename
|
||||
@@ -230,8 +234,8 @@ export function DocumentPage({ queryRef }: Props) {
|
||||
const nodeId = getNodeId(node);
|
||||
|
||||
const logoFileUrl = theme === "dark"
|
||||
? (trustCenter?.darkLogoFileUrl ?? trustCenter?.logoFileUrl)
|
||||
: trustCenter?.logoFileUrl;
|
||||
? (trustCenter?.darkLogo?.downloadUrl ?? trustCenter?.logo?.downloadUrl)
|
||||
: trustCenter?.logo?.downloadUrl;
|
||||
|
||||
const [exportDocument, isExportingDocument]
|
||||
= useMutation<DocumentPageExportDocumentMutation>(exportDocumentMutation);
|
||||
|
||||
@@ -46,7 +46,9 @@ const overviewFragment = graphql`
|
||||
node {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
websiteUrl
|
||||
}
|
||||
}
|
||||
@@ -223,7 +225,7 @@ function Subprocessors({
|
||||
|
||||
type Reference = {
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
logo: { downloadUrl: string } | null;
|
||||
websiteUrl: string;
|
||||
id: string;
|
||||
};
|
||||
@@ -248,7 +250,7 @@ function References({ references }: { references: Reference[] }) {
|
||||
className="flex flex-col justify-center items-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={reference.logoUrl}
|
||||
src={reference.logo?.downloadUrl}
|
||||
alt={reference.name}
|
||||
className="rounded-2xl size-12 block"
|
||||
/>
|
||||
|
||||
@@ -23,8 +23,12 @@ import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
|
||||
export const authLayoutQuery = graphql`
|
||||
query AuthLayoutQuery {
|
||||
currentTrustCenter @required(action: THROW) {
|
||||
logoFileUrl
|
||||
darkLogoFileUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -36,8 +40,8 @@ export function AuthLayout(props: { queryRef: PreloadedQuery<AuthLayoutQuery> })
|
||||
const theme = useSystemTheme();
|
||||
|
||||
const logoFileUrl = theme === "dark"
|
||||
? compliancePage.darkLogoFileUrl ?? compliancePage.logoFileUrl
|
||||
: compliancePage.logoFileUrl;
|
||||
? compliancePage.darkLogo?.downloadUrl ?? compliancePage.logo?.downloadUrl
|
||||
: compliancePage.logo?.downloadUrl;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-txt-primary bg-level-0 flex flex-col items-center justify-center">
|
||||
|
||||
@@ -31,8 +31,12 @@ export const currentTrustGraphQuery = graphql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
logoFileUrl
|
||||
darkLogoFileUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
nonDisclosureAgreement {
|
||||
fileName
|
||||
fileUrl
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
164
e2e/trust/trust_center_logo_test.go
Normal file
164
e2e/trust/trust_center_logo_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
48
pkg/filemanager/load.go
Normal file
48
pkg/filemanager/load.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
167
pkg/filemanager/s3.go
Normal file
167
pkg/filemanager/s3.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
32
pkg/filemanager/url.go
Normal file
32
pkg/filemanager/url.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
42
pkg/server/api/mcp/v1/file_loader.go
Normal file
42
pkg/server/api/mcp/v1/file_loader.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
32
pkg/server/api/mcp/v1/types/file.go
Normal file
32
pkg/server/api/mcp/v1/types/file.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)))
|
||||
|
||||
41
pkg/server/api/trust/v1/file_loader.go
Normal file
41
pkg/server/api/trust/v1/file_loader.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
10
pkg/server/api/trust/v1/graphql/file.graphql
Normal file
10
pkg/server/api/trust/v1/graphql/file.graphql
Normal file
@@ -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!
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
32
pkg/server/api/trust/v1/types/file.go
Normal file
32
pkg/server/api/trust/v1/types/file.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
6
pkg/thirdparty/service.go
vendored
6
pkg/thirdparty/service.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user