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 {
|
||||
id
|
||||
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 gap-4 hover:text-primary flex-1">
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
src={organization.logo?.downloadUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
|
||||
@@ -41,7 +41,9 @@ const organizationFragment = graphql`
|
||||
fragment MembershipsDropdownMenuItem_organizationFragment on Organization {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -63,7 +65,7 @@ export function MembershipsDropdownMenuItem(props: {
|
||||
return (
|
||||
<DropdownItem key={id} asChild>
|
||||
<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>
|
||||
{isAssuming && (
|
||||
<IconCheckmark1 size={16} className="text-green-600" />
|
||||
|
||||
@@ -41,8 +41,12 @@ const fragment = graphql`
|
||||
fragment OrganizationFormFragment on Organization {
|
||||
id
|
||||
name @required(action: THROW)
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
horizontalLogo {
|
||||
downloadUrl
|
||||
}
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
@@ -57,8 +61,12 @@ const updateOrganizationMutation = graphql`
|
||||
organization {
|
||||
id
|
||||
name
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
horizontalLogo {
|
||||
downloadUrl
|
||||
}
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
@@ -75,7 +83,9 @@ const deleteHorizontalLogoMutation = graphql`
|
||||
deleteOrganizationHorizontalLogo(input: $input) {
|
||||
organization {
|
||||
id
|
||||
horizontalLogoUrl
|
||||
horizontalLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,8 +235,8 @@ export function OrganizationForm(props: {
|
||||
<Label>{__("Organization logo")}</Label>
|
||||
<div className="flex w-max items-center gap-4">
|
||||
<Avatar
|
||||
className={logoPreview || organization.logoUrl ? "bg-transparent" : undefined}
|
||||
src={logoPreview || organization.logoUrl}
|
||||
className={logoPreview || organization.logo?.downloadUrl ? "bg-transparent" : undefined}
|
||||
src={logoPreview || organization.logo?.downloadUrl}
|
||||
name={organization.name}
|
||||
size="xl"
|
||||
/>
|
||||
@@ -253,12 +263,12 @@ export function OrganizationForm(props: {
|
||||
)}
|
||||
</p>
|
||||
<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">
|
||||
<img
|
||||
src={
|
||||
horizontalLogoPreview
|
||||
|| organization.horizontalLogoUrl
|
||||
|| organization.horizontalLogo?.downloadUrl
|
||||
|| undefined
|
||||
}
|
||||
alt={__("Horizontal logo")}
|
||||
@@ -275,12 +285,12 @@ export function OrganizationForm(props: {
|
||||
>
|
||||
{isUpdatingOrganization
|
||||
? __("Uploading...")
|
||||
: horizontalLogoPreview || organization.horizontalLogoUrl
|
||||
: horizontalLogoPreview || organization.horizontalLogo?.downloadUrl
|
||||
? __("Change horizontal logo")
|
||||
: __("Upload horizontal logo")}
|
||||
</FileButton>
|
||||
)}
|
||||
{canUpdate && organization.horizontalLogoUrl && (
|
||||
{canUpdate && organization.horizontalLogo?.downloadUrl && (
|
||||
<Dialog
|
||||
ref={deleteDialogRef}
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -262,7 +266,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("cannot create request: %w", err)
|
||||
}
|
||||
|
||||
@@ -48,8 +48,16 @@ export async function execute(
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
logo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
horizontalLogo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
@@ -48,8 +48,16 @@ export async function execute(
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
logo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
horizontalLogo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
@@ -68,8 +68,16 @@ export async function execute(
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
logo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
horizontalLogo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
@@ -119,8 +119,16 @@ export async function execute(
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
logoUrl
|
||||
horizontalLogoUrl
|
||||
logo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
horizontalLogo {
|
||||
id
|
||||
fileName
|
||||
downloadUrl
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
@@ -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