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:
@@ -46,7 +46,9 @@ const organizationFragment = graphql`
|
|||||||
fragment MembershipCard_organizationFragment on Organization {
|
fragment MembershipCard_organizationFragment on Organization {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
logoUrl
|
logo {
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ export function MembershipCard(props: MembershipCardProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4 hover:text-primary flex-1">
|
<div className="flex items-center gap-4 hover:text-primary flex-1">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={organization.logoUrl}
|
src={organization.logo?.downloadUrl}
|
||||||
name={organization.name}
|
name={organization.name}
|
||||||
size="l"
|
size="l"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ const organizationFragment = graphql`
|
|||||||
fragment MembershipsDropdownMenuItem_organizationFragment on Organization {
|
fragment MembershipsDropdownMenuItem_organizationFragment on Organization {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
logoUrl
|
logo {
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export function MembershipsDropdownMenuItem(props: {
|
|||||||
return (
|
return (
|
||||||
<DropdownItem key={id} asChild>
|
<DropdownItem key={id} asChild>
|
||||||
<Link to={`/organizations/${organization.id}`}>
|
<Link to={`/organizations/${organization.id}`}>
|
||||||
<Avatar name={organization.name} src={organization.logoUrl} />
|
<Avatar name={organization.name} src={organization.logo?.downloadUrl} />
|
||||||
<span className="flex-1">{organization.name}</span>
|
<span className="flex-1">{organization.name}</span>
|
||||||
{isAssuming && (
|
{isAssuming && (
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
<IconCheckmark1 size={16} className="text-green-600" />
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ const fragment = graphql`
|
|||||||
fragment OrganizationFormFragment on Organization {
|
fragment OrganizationFormFragment on Organization {
|
||||||
id
|
id
|
||||||
name @required(action: THROW)
|
name @required(action: THROW)
|
||||||
logoUrl
|
logo {
|
||||||
horizontalLogoUrl
|
downloadUrl
|
||||||
|
}
|
||||||
|
horizontalLogo {
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
description
|
description
|
||||||
websiteUrl
|
websiteUrl
|
||||||
email
|
email
|
||||||
@@ -57,8 +61,12 @@ const updateOrganizationMutation = graphql`
|
|||||||
organization {
|
organization {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
logoUrl
|
logo {
|
||||||
horizontalLogoUrl
|
downloadUrl
|
||||||
|
}
|
||||||
|
horizontalLogo {
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
description
|
description
|
||||||
websiteUrl
|
websiteUrl
|
||||||
email
|
email
|
||||||
@@ -75,7 +83,9 @@ const deleteHorizontalLogoMutation = graphql`
|
|||||||
deleteOrganizationHorizontalLogo(input: $input) {
|
deleteOrganizationHorizontalLogo(input: $input) {
|
||||||
organization {
|
organization {
|
||||||
id
|
id
|
||||||
horizontalLogoUrl
|
horizontalLogo {
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,8 +235,8 @@ export function OrganizationForm(props: {
|
|||||||
<Label>{__("Organization logo")}</Label>
|
<Label>{__("Organization logo")}</Label>
|
||||||
<div className="flex w-max items-center gap-4">
|
<div className="flex w-max items-center gap-4">
|
||||||
<Avatar
|
<Avatar
|
||||||
className={logoPreview || organization.logoUrl ? "bg-transparent" : undefined}
|
className={logoPreview || organization.logo?.downloadUrl ? "bg-transparent" : undefined}
|
||||||
src={logoPreview || organization.logoUrl}
|
src={logoPreview || organization.logo?.downloadUrl}
|
||||||
name={organization.name}
|
name={organization.name}
|
||||||
size="xl"
|
size="xl"
|
||||||
/>
|
/>
|
||||||
@@ -253,12 +263,12 @@ export function OrganizationForm(props: {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{(horizontalLogoPreview || organization.horizontalLogoUrl) && (
|
{(horizontalLogoPreview || organization.horizontalLogo?.downloadUrl) && (
|
||||||
<div className="border border-border-solid rounded-md p-4 bg-surface-secondary">
|
<div className="border border-border-solid rounded-md p-4 bg-surface-secondary">
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
horizontalLogoPreview
|
horizontalLogoPreview
|
||||||
|| organization.horizontalLogoUrl
|
|| organization.horizontalLogo?.downloadUrl
|
||||||
|| undefined
|
|| undefined
|
||||||
}
|
}
|
||||||
alt={__("Horizontal logo")}
|
alt={__("Horizontal logo")}
|
||||||
@@ -275,12 +285,12 @@ export function OrganizationForm(props: {
|
|||||||
>
|
>
|
||||||
{isUpdatingOrganization
|
{isUpdatingOrganization
|
||||||
? __("Uploading...")
|
? __("Uploading...")
|
||||||
: horizontalLogoPreview || organization.horizontalLogoUrl
|
: horizontalLogoPreview || organization.horizontalLogo?.downloadUrl
|
||||||
? __("Change horizontal logo")
|
? __("Change horizontal logo")
|
||||||
: __("Upload horizontal logo")}
|
: __("Upload horizontal logo")}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
)}
|
)}
|
||||||
{canUpdate && organization.horizontalLogoUrl && (
|
{canUpdate && organization.horizontalLogo?.downloadUrl && (
|
||||||
<Dialog
|
<Dialog
|
||||||
ref={deleteDialogRef}
|
ref={deleteDialogRef}
|
||||||
trigger={(
|
trigger={(
|
||||||
|
|||||||
136
e2e/console/connect_organization_logo_test.go
Normal file
136
e2e/console/connect_organization_logo_test.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// 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 console_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.probo.inc/probo/e2e/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConnectOrganization_LogoUpload(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
organizationID := owner.GetOrganizationID().String()
|
||||||
|
|
||||||
|
const uploadMutation = `
|
||||||
|
mutation UpdateOrganization($input: UpdateOrganizationInput!) {
|
||||||
|
updateOrganization(input: $input) {
|
||||||
|
organization {
|
||||||
|
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 {
|
||||||
|
UpdateOrganization struct {
|
||||||
|
Organization struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Logo *struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
DownloadURL string `json:"downloadUrl"`
|
||||||
|
} `json:"logo"`
|
||||||
|
} `json:"organization"`
|
||||||
|
} `json:"updateOrganization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.ExecuteConnectWithFile(uploadMutation, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": organizationID,
|
||||||
|
"logoFile": nil,
|
||||||
|
},
|
||||||
|
}, "input.logoFile", testutil.UploadFile{
|
||||||
|
Filename: "org-logo.png",
|
||||||
|
ContentType: "image/png",
|
||||||
|
Content: pngContent,
|
||||||
|
}, &uploadResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, organizationID, uploadResult.UpdateOrganization.Organization.ID)
|
||||||
|
require.NotNil(t, uploadResult.UpdateOrganization.Organization.Logo)
|
||||||
|
assert.Equal(t, "org-logo.png", uploadResult.UpdateOrganization.Organization.Logo.FileName)
|
||||||
|
assert.NotEmpty(t, uploadResult.UpdateOrganization.Organization.Logo.DownloadURL)
|
||||||
|
assert.True(
|
||||||
|
t,
|
||||||
|
strings.Contains(uploadResult.UpdateOrganization.Organization.Logo.DownloadURL, "/api/files/v1/public/"),
|
||||||
|
"downloadUrl must route through the public files API, got %q",
|
||||||
|
uploadResult.UpdateOrganization.Organization.Logo.DownloadURL,
|
||||||
|
)
|
||||||
|
|
||||||
|
const queryOrganization = `
|
||||||
|
query GetOrganization($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on Organization {
|
||||||
|
logo {
|
||||||
|
downloadUrl
|
||||||
|
fileName
|
||||||
|
mimeType
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var queryResult struct {
|
||||||
|
Node struct {
|
||||||
|
Logo *struct {
|
||||||
|
DownloadURL string `json:"downloadUrl"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
MimeType string `json:"mimeType"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
} `json:"logo"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.ExecuteConnect(queryOrganization, map[string]any{
|
||||||
|
"id": organizationID,
|
||||||
|
}, &queryResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotNil(t, queryResult.Node.Logo)
|
||||||
|
assert.Equal(t, "org-logo.png", queryResult.Node.Logo.FileName)
|
||||||
|
assert.Equal(t, "image/png", queryResult.Node.Logo.MimeType)
|
||||||
|
assert.Equal(t, int64(len(pngContent)), queryResult.Node.Logo.Size)
|
||||||
|
assert.True(
|
||||||
|
t,
|
||||||
|
strings.Contains(queryResult.Node.Logo.DownloadURL, "/api/files/v1/public/"),
|
||||||
|
"downloadUrl must route through the public files API, got %q",
|
||||||
|
queryResult.Node.Logo.DownloadURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -185,14 +185,18 @@ type UploadFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ExecuteWithFile(query string, variables map[string]any, variablePath string, file UploadFile, result any) error {
|
func (c *Client) ExecuteWithFile(query string, variables map[string]any, variablePath string, file UploadFile, result any) error {
|
||||||
return c.executeMultipart(query, variables, map[string]UploadFile{variablePath: file}, result)
|
return c.executeMultipart("/api/console/v1/graphql", query, variables, map[string]UploadFile{variablePath: file}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExecuteConnectWithFile(query string, variables map[string]any, variablePath string, file UploadFile, result any) error {
|
||||||
|
return c.executeMultipart("/api/connect/v1/graphql", query, variables, map[string]UploadFile{variablePath: file}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ExecuteWithFiles(query string, variables map[string]any, files map[string]UploadFile, result any) error {
|
func (c *Client) ExecuteWithFiles(query string, variables map[string]any, files map[string]UploadFile, result any) error {
|
||||||
return c.executeMultipart(query, variables, files, result)
|
return c.executeMultipart("/api/console/v1/graphql", query, variables, files, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) executeMultipart(query string, variables map[string]any, files map[string]UploadFile, result any) error {
|
func (c *Client) executeMultipart(endpoint string, query string, variables map[string]any, files map[string]UploadFile, result any) error {
|
||||||
// Create multipart writer using standard library
|
// Create multipart writer using standard library
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
@@ -262,7 +266,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create request
|
// Create request
|
||||||
req, err := http.NewRequest("POST", c.baseURL+"/api/console/v1/graphql", &buf)
|
req, err := http.NewRequest("POST", c.baseURL+endpoint, &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create request: %w", err)
|
return fmt.Errorf("cannot create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,16 @@ export async function execute(
|
|||||||
websiteUrl
|
websiteUrl
|
||||||
email
|
email
|
||||||
headquarterAddress
|
headquarterAddress
|
||||||
logoUrl
|
logo {
|
||||||
horizontalLogoUrl
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
|
horizontalLogo {
|
||||||
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,16 @@ export async function execute(
|
|||||||
websiteUrl
|
websiteUrl
|
||||||
email
|
email
|
||||||
headquarterAddress
|
headquarterAddress
|
||||||
logoUrl
|
logo {
|
||||||
horizontalLogoUrl
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
|
horizontalLogo {
|
||||||
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,16 @@ export async function execute(
|
|||||||
websiteUrl
|
websiteUrl
|
||||||
email
|
email
|
||||||
headquarterAddress
|
headquarterAddress
|
||||||
logoUrl
|
logo {
|
||||||
horizontalLogoUrl
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
|
horizontalLogo {
|
||||||
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,8 +119,16 @@ export async function execute(
|
|||||||
websiteUrl
|
websiteUrl
|
||||||
email
|
email
|
||||||
headquarterAddress
|
headquarterAddress
|
||||||
logoUrl
|
logo {
|
||||||
horizontalLogoUrl
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
|
horizontalLogo {
|
||||||
|
id
|
||||||
|
fileName
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1372,18 +1372,17 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context,
|
|||||||
return organization, nil
|
return organization, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s OrganizationService) GenerateLogoURL(
|
func (s OrganizationService) LogoFile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
expiresIn time.Duration,
|
) (*coredata.File, error) {
|
||||||
) (*string, error) {
|
|
||||||
var (
|
var (
|
||||||
errNoLogoFile = errors.New("no logo file found")
|
errNoLogoFile = errors.New("no logo file found")
|
||||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||||
file = &coredata.File{}
|
file = &coredata.File{}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
if err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
organization := &coredata.Organization{}
|
organization := &coredata.Organization{}
|
||||||
@@ -1401,35 +1400,28 @@ func (s OrganizationService) GenerateLogoURL(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
); err != nil {
|
||||||
if err == errNoLogoFile {
|
if errors.Is(err, errNoLogoFile) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
return nil, fmt.Errorf("cannot load logo file: %w", err)
|
||||||
return nil, fmt.Errorf("cannot generate logo URL: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadURL, err := s.fm.BuildDownloadURL(file)
|
return file, nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot generate file URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &downloadURL, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s OrganizationService) GenerateHorizontalLogoURL(
|
func (s OrganizationService) HorizontalLogoFile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
expiresIn time.Duration,
|
) (*coredata.File, error) {
|
||||||
) (*string, error) {
|
|
||||||
var (
|
var (
|
||||||
errNoLogoFile = errors.New("no logo file found")
|
errNoLogoFile = errors.New("no logo file found")
|
||||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||||
file = &coredata.File{}
|
file = &coredata.File{}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
if err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
organization := &coredata.Organization{}
|
organization := &coredata.Organization{}
|
||||||
@@ -1447,21 +1439,15 @@ func (s OrganizationService) GenerateHorizontalLogoURL(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
); err != nil {
|
||||||
if err == errNoLogoFile {
|
if errors.Is(err, errNoLogoFile) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
return nil, fmt.Errorf("cannot load horizontal logo file: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadURL, err := s.fm.BuildDownloadURL(file)
|
return file, nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot generate file URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &downloadURL, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s OrganizationService) DeleteSAMLConfiguration(
|
func (s OrganizationService) DeleteSAMLConfiguration(
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ models:
|
|||||||
CursorKey:
|
CursorKey:
|
||||||
model:
|
model:
|
||||||
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
|
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
|
||||||
|
BigInt:
|
||||||
|
model:
|
||||||
|
- "go.probo.inc/probo/pkg/server/gqlutils/types/bigint.BigIntScalar"
|
||||||
EmailAddr:
|
EmailAddr:
|
||||||
model:
|
model:
|
||||||
- "go.probo.inc/probo/pkg/server/gqlutils/types/mail.AddrScalar"
|
- "go.probo.inc/probo/pkg/server/gqlutils/types/mail.AddrScalar"
|
||||||
@@ -11,6 +11,7 @@ directive @goModel(
|
|||||||
|
|
||||||
directive @goEnum(value: String) on ENUM_VALUE
|
directive @goEnum(value: String) on ENUM_VALUE
|
||||||
|
|
||||||
|
scalar BigInt
|
||||||
scalar CursorKey
|
scalar CursorKey
|
||||||
scalar Datetime
|
scalar Datetime
|
||||||
scalar Upload
|
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 {
|
type Organization implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
logoUrl: String @goField(forceResolver: true)
|
logo: File @goField(forceResolver: true)
|
||||||
horizontalLogoUrl: String @goField(forceResolver: true)
|
horizontalLogo: File @goField(forceResolver: true)
|
||||||
email: String
|
email: String
|
||||||
description: String
|
description: String
|
||||||
websiteUrl: String
|
websiteUrl: String
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"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/iam/scim/bridge/provider/microsoft365"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/server/api/authn"
|
"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/schema"
|
||||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
"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"))
|
panic(fmt.Errorf("not implemented: DeleteOrganizationHorizontalLogo - deleteOrganizationHorizontalLogo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoURL is the resolver for the logoUrl field.
|
// Logo is the resolver for the logo field.
|
||||||
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
|
func (r *organizationResolver) Logo(ctx context.Context, obj *types.Organization) (*types.File, error) {
|
||||||
if _, err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil {
|
file, err := r.iam.OrganizationService.LogoFile(ctx, obj.ID)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
presignedURL, err := r.iam.OrganizationService.GenerateLogoURL(ctx, obj.ID, 1*time.Hour)
|
|
||||||
if err != nil {
|
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 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.
|
// HorizontalLogo is the resolver for the horizontalLogo field.
|
||||||
func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
|
func (r *organizationResolver) HorizontalLogo(ctx context.Context, obj *types.Organization) (*types.File, error) {
|
||||||
if _, err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet); err != nil {
|
file, err := r.iam.OrganizationService.HorizontalLogoFile(ctx, obj.ID)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
presignedURL, err := r.iam.OrganizationService.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour)
|
|
||||||
if err != nil {
|
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 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.
|
// 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 {
|
func NewOrganization(organization *coredata.Organization) *Organization {
|
||||||
return &Organization{
|
org := &Organization{
|
||||||
ID: organization.ID,
|
ID: organization.ID,
|
||||||
Name: organization.Name,
|
Name: organization.Name,
|
||||||
Email: organization.Email,
|
Email: organization.Email,
|
||||||
@@ -33,4 +33,14 @@ func NewOrganization(organization *coredata.Organization) *Organization {
|
|||||||
CreatedAt: organization.CreatedAt,
|
CreatedAt: organization.CreatedAt,
|
||||||
UpdatedAt: organization.UpdatedAt,
|
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