Migrate Connect org logos to File type
Replace Organization.logoUrl and horizontalLogoUrl with nested File
objects whose downloadUrl points at /api/files/v1/public/{id}, matching
the Console migration.
Org logos are FileVisibilityPublic and served without HTTP auth, so
Connect File.downloadUrl is built eagerly in NewFile with no field-level
authorize. Logo loading moves to iam.OrganizationService.LogoFile and
HorizontalLogoFile; the old URL generators are removed.
Sync IAM Relay components and n8n organization operations. Add an e2e
test for Connect multipart logo upload and ExecuteConnectWithFile.
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -1372,18 +1372,17 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context,
|
||||
return organization, nil
|
||||
}
|
||||
|
||||
func (s OrganizationService) GenerateLogoURL(
|
||||
func (s OrganizationService) LogoFile(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
expiresIn time.Duration,
|
||||
) (*string, error) {
|
||||
) (*coredata.File, error) {
|
||||
var (
|
||||
errNoLogoFile = errors.New("no logo file found")
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
file = &coredata.File{}
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
if err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
organization := &coredata.Organization{}
|
||||
@@ -1401,35 +1400,28 @@ func (s OrganizationService) GenerateLogoURL(
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err == errNoLogoFile {
|
||||
return nil, nil
|
||||
); err != nil {
|
||||
if errors.Is(err, errNoLogoFile) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot load logo file: %w", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate logo URL: %w", err)
|
||||
}
|
||||
|
||||
downloadURL, err := s.fm.BuildDownloadURL(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate file URL: %w", err)
|
||||
}
|
||||
|
||||
return &downloadURL, nil
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (s OrganizationService) GenerateHorizontalLogoURL(
|
||||
func (s OrganizationService) HorizontalLogoFile(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
expiresIn time.Duration,
|
||||
) (*string, error) {
|
||||
) (*coredata.File, error) {
|
||||
var (
|
||||
errNoLogoFile = errors.New("no logo file found")
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
file = &coredata.File{}
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
if err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
organization := &coredata.Organization{}
|
||||
@@ -1447,21 +1439,15 @@ func (s OrganizationService) GenerateHorizontalLogoURL(
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err == errNoLogoFile {
|
||||
return nil, nil
|
||||
); err != nil {
|
||||
if errors.Is(err, errNoLogoFile) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot load horizontal logo file: %w", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadURL, err := s.fm.BuildDownloadURL(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate file URL: %w", err)
|
||||
}
|
||||
|
||||
return &downloadURL, nil
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (s OrganizationService) DeleteSAMLConfiguration(
|
||||
|
||||
@@ -38,6 +38,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"
|
||||
@@ -11,6 +11,7 @@ directive @goModel(
|
||||
|
||||
directive @goEnum(value: String) on ENUM_VALUE
|
||||
|
||||
scalar BigInt
|
||||
scalar CursorKey
|
||||
scalar Datetime
|
||||
scalar Upload
|
||||
|
||||
12
pkg/server/api/connect/v1/graphql/file.graphql
Normal file
12
pkg/server/api/connect/v1/graphql/file.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
# Connect File is PUBLIC-ONLY: downloadUrl points at /api/files/v1/public/{id}
|
||||
# and carries no authorization. Do not expose a private file through this type
|
||||
# without adding a forceResolver + auth on downloadUrl.
|
||||
type File {
|
||||
id: ID!
|
||||
mimeType: String!
|
||||
fileName: String!
|
||||
size: BigInt!
|
||||
downloadUrl: String!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
type Organization implements Node {
|
||||
id: ID!
|
||||
name: String!
|
||||
logoUrl: String @goField(forceResolver: true)
|
||||
horizontalLogoUrl: String @goField(forceResolver: true)
|
||||
logo: File @goField(forceResolver: true)
|
||||
horizontalLogo: File @goField(forceResolver: true)
|
||||
email: String
|
||||
description: String
|
||||
websiteUrl: String
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"go.probo.inc/probo/pkg/iam/scim/bridge/provider/microsoft365"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/authz"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
@@ -158,34 +156,34 @@ func (r *mutationResolver) DeleteOrganizationHorizontalLogo(ctx context.Context,
|
||||
panic(fmt.Errorf("not implemented: DeleteOrganizationHorizontalLogo - deleteOrganizationHorizontalLogo"))
|
||||
}
|
||||
|
||||
// LogoURL is the resolver for the logoUrl field.
|
||||
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
|
||||
if _, err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
presignedURL, err := r.iam.OrganizationService.GenerateLogoURL(ctx, obj.ID, 1*time.Hour)
|
||||
// Logo is the resolver for the logo field.
|
||||
func (r *organizationResolver) Logo(ctx context.Context, obj *types.Organization) (*types.File, error) {
|
||||
file, err := r.iam.OrganizationService.LogoFile(ctx, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot generate logo URL", log.Error(err))
|
||||
r.logger.ErrorCtx(ctx, "cannot load logo file", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return presignedURL, nil
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return types.NewFile(file, r.baseURL), nil
|
||||
}
|
||||
|
||||
// HorizontalLogoURL is the resolver for the horizontalLogoUrl field.
|
||||
func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
|
||||
if _, err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
presignedURL, err := r.iam.OrganizationService.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour)
|
||||
// HorizontalLogo is the resolver for the horizontalLogo field.
|
||||
func (r *organizationResolver) HorizontalLogo(ctx context.Context, obj *types.Organization) (*types.File, error) {
|
||||
file, err := r.iam.OrganizationService.HorizontalLogoFile(ctx, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot generate horizontal logo URL", log.Error(err))
|
||||
r.logger.ErrorCtx(ctx, "cannot load horizontal logo file", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return presignedURL, nil
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return types.NewFile(file, r.baseURL), nil
|
||||
}
|
||||
|
||||
// Profiles is the resolver for the profiles field.
|
||||
|
||||
35
pkg/server/api/connect/v1/types/file.go
Normal file
35
pkg/server/api/connect/v1/types/file.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 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()
|
||||
|
||||
return &File{
|
||||
ID: r.ID,
|
||||
MimeType: r.MimeType,
|
||||
FileName: r.FileName,
|
||||
Size: r.FileSize,
|
||||
DownloadURL: url,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ type (
|
||||
)
|
||||
|
||||
func NewOrganization(organization *coredata.Organization) *Organization {
|
||||
return &Organization{
|
||||
org := &Organization{
|
||||
ID: organization.ID,
|
||||
Name: organization.Name,
|
||||
Email: organization.Email,
|
||||
@@ -33,4 +33,14 @@ func NewOrganization(organization *coredata.Organization) *Organization {
|
||||
CreatedAt: organization.CreatedAt,
|
||||
UpdatedAt: organization.UpdatedAt,
|
||||
}
|
||||
|
||||
if organization.LogoFileID != nil {
|
||||
org.Logo = &File{ID: *organization.LogoFileID}
|
||||
}
|
||||
|
||||
if organization.HorizontalLogoFileID != nil {
|
||||
org.HorizontalLogo = &File{ID: *organization.HorizontalLogoFileID}
|
||||
}
|
||||
|
||||
return org
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user