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:
Ludovic Vielle
2026-06-11 13:54:59 +02:00
parent e06f3e0520
commit eccef41767
60 changed files with 1004 additions and 400 deletions

View File

@@ -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() {

View File

@@ -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),
},

View File

@@ -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.

View File

@@ -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"))

View File

@@ -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,
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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,
},

View File

@@ -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,
)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,
}

View File

@@ -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)

View 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
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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:

View 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,
}
}

View File

@@ -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,

View File

@@ -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)))

View 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
}

View File

@@ -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"

View File

@@ -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

View 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!
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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.

View 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,
}
}