From d757c9604b057a508e5dd2015881c30f30815a62 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 8 Oct 2025 16:04:27 +0200 Subject: [PATCH] Add company name and logo to documents Signed-off-by: Sacha Al Himdani --- .../OrganizationGraph_ViewQuery.graphql.ts | 13 +- .../src/pages/organizations/SettingsPage.tsx | 151 +++++- .../SettingsPageFragment.graphql.ts | 12 +- ...ge_DeleteHorizontalLogoMutation.graphql.ts | 113 +++++ .../SettingsPage_UpdateMutation.graphql.ts | 17 +- .../src/Molecules/FileButton/FileButton.tsx | 9 +- pkg/coredata/migrations/20251009T140000Z.sql | 51 ++ pkg/coredata/organization.go | 82 ++-- pkg/docgen/generator.go | 20 +- pkg/docgen/template.html | 69 ++- pkg/filemanager/service.go | 148 ++++++ pkg/probo/document_service.go | 38 +- pkg/probo/framework_service.go | 1 + pkg/probo/organization_service.go | 270 +++++++---- pkg/probo/service.go | 6 + pkg/probod/probod.go | 5 + pkg/server/api/console/v1/schema.graphql | 13 + pkg/server/api/console/v1/schema/schema.go | 445 +++++++++++++++++- pkg/server/api/console/v1/types/types.go | 10 + pkg/server/api/console/v1/v1_resolver.go | 32 +- pkg/trust/document_service.go | 32 +- pkg/trust/organization_service.go | 17 +- pkg/trust/service.go | 40 +- 23 files changed, 1391 insertions(+), 203 deletions(-) create mode 100644 apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts create mode 100644 pkg/coredata/migrations/20251009T140000Z.sql create mode 100644 pkg/filemanager/service.go diff --git a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts index 5cdac9448..96f92c960 100644 --- a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<267b2fc05603188ad26c38ce1485887b>> + * @generated SignedSource<<5a41cd3709282172e4d5a3ca65d07b96>> * @lightSyntaxTransform * @nogrep */ @@ -151,6 +151,13 @@ return { "name": "logoUrl", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "horizontalLogoUrl", + "storageKey": null + }, { "alias": null, "args": null, @@ -338,12 +345,12 @@ return { ] }, "params": { - "cacheID": "5118b2778835b232009a9d5ef5647aba", + "cacheID": "8b9e1f3b1e93e1823354e77f10764e54", "id": null, "metadata": {}, "name": "OrganizationGraph_ViewQuery", "operationKind": "query", - "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n" + "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n" } }; })(); diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 28ceb34ee..066ecf972 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -4,6 +4,9 @@ import { Badge, Button, Card, + Dialog, + DialogContent, + DialogFooter, DropdownItem, Field, FileButton, @@ -13,6 +16,7 @@ import { Spinner, Textarea, useConfirm, + useDialogRef, useToast, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; @@ -58,6 +62,7 @@ const organizationFragment = graphql` id name logoUrl + horizontalLogoUrl description websiteUrl email @@ -107,6 +112,7 @@ const updateOrganizationMutation = graphql` id name logoUrl + horizontalLogoUrl description websiteUrl email @@ -116,6 +122,17 @@ const updateOrganizationMutation = graphql` } `; +const deleteHorizontalLogoMutation = graphql` + mutation SettingsPage_DeleteHorizontalLogoMutation($input: DeleteOrganizationHorizontalLogoInput!) { + deleteOrganizationHorizontalLogo(input: $input) { + organization { + id + horizontalLogoUrl + } + } + } +`; + export default function SettingsPage({ queryRef }: Props) { const { __ } = useTranslate(); const navigate = useNavigate(); @@ -129,6 +146,13 @@ export default function SettingsPage({ queryRef }: Props) { organizationKey ); const [updateOrganization] = useMutation(updateOrganizationMutation); + const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts( + deleteHorizontalLogoMutation, + { + successMessage: __("Horizontal logo deleted successfully"), + errorMessage: __("Failed to delete horizontal logo"), + } + ); const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation(); const users = organization.users.edges.map((edge) => edge.node); @@ -167,10 +191,10 @@ export default function SettingsPage({ queryRef }: Props) { headquarterAddress: data.headquarterAddress || null, }, }, - onError(error) { + onError() { toast({ - title: __("Failed to update organization"), - description: error.message || __("Please try again."), + title: __("Error"), + description: __("Failed to update organization."), variant: "error", }); }, @@ -201,13 +225,67 @@ export default function SettingsPage({ queryRef }: Props) { uploadables: { "input.logo": file, }, - onError(error) { + onError() { toast({ - title: __("Failed to update organization logo"), - description: error.message || __("Please try again."), + title: __("Error"), + description: __("Failed to update logo"), variant: "error", }); }, + onCompleted() { + toast({ + title: __("Success"), + description: __("Your organization logo has been updated successfully."), + variant: "success", + }); + }, + }); + }; + + const updateHorizontalLogo: ChangeEventHandler = (e) => { + const file = e.target.files?.[0]; + if (!file) { + return; + } + updateOrganization({ + variables: { + input: { + organizationId: organization.id, + horizontalLogoFile: null, + }, + }, + uploadables: { + "input.horizontalLogoFile": file, + }, + onError() { + toast({ + title: __("Error"), + description: __("Failed to update horizontal logo."), + variant: "error", + }); + }, + onCompleted() { + toast({ + title: __("Success"), + description: __("Your organization horizontal logo has been updated successfully."), + variant: "success", + }); + }, + }); + }; + + const deleteDialogRef = useDialogRef(); + + const handleDeleteHorizontalLogo = () => { + deleteHorizontalLogo({ + variables: { + input: { + organizationId: organization.id, + }, + }, + onSuccess: () => { + deleteDialogRef.current?.close(); + }, }); }; @@ -252,11 +330,72 @@ export default function SettingsPage({ queryRef }: Props) { onChange={updateOrganizationLogo} variant="secondary" className="ml-auto" + accept="image/png,image/jpeg,image/jpg" > {__("Change logo")} +
+ +

+ {__("Upload a horizontal version of your logo for use in documents")} +

+
+ {organization.horizontalLogoUrl && ( +
+ {__("Horizontal +
+ )} + + {organization.horizontalLogoUrl ? __("Change horizontal logo") : __("Upload horizontal logo")} + + {organization.horizontalLogoUrl && ( + + } + title={__("Delete Horizontal Logo")} + className="max-w-md" + > + +

+ {__("Are you sure you want to delete the horizontal logo?")} +

+

+ {__("This action cannot be undone.")} +

+
+ + + + +
+ )} +
+
> + * @generated SignedSource<<122f3da12674107565d07159872492db>> * @lightSyntaxTransform * @nogrep */ @@ -40,6 +40,7 @@ export type SettingsPageFragment$data = { readonly description: string | null | undefined; readonly email: string | null | undefined; readonly headquarterAddress: string | null | undefined; + readonly horizontalLogoUrl: string | null | undefined; readonly id: string; readonly logoUrl: string | null | undefined; readonly name: string; @@ -119,6 +120,13 @@ return { "name": "logoUrl", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "horizontalLogoUrl", + "storageKey": null + }, { "alias": null, "args": null, @@ -302,6 +310,6 @@ return { }; })(); -(node as any).hash = "1f76d60a35b4b5116821419432ec4aed"; +(node as any).hash = "6cea6f88fb0d7b2ae7ef9053b9979898"; export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts new file mode 100644 index 000000000..3df9c7037 --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts @@ -0,0 +1,113 @@ +/** + * @generated SignedSource<<4ed3d530d746d8b84b2dba8752e57abc>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteOrganizationHorizontalLogoInput = { + organizationId: string; +}; +export type SettingsPage_DeleteHorizontalLogoMutation$variables = { + input: DeleteOrganizationHorizontalLogoInput; +}; +export type SettingsPage_DeleteHorizontalLogoMutation$data = { + readonly deleteOrganizationHorizontalLogo: { + readonly organization: { + readonly horizontalLogoUrl: string | null | undefined; + readonly id: string; + }; + }; +}; +export type SettingsPage_DeleteHorizontalLogoMutation = { + response: SettingsPage_DeleteHorizontalLogoMutation$data; + variables: SettingsPage_DeleteHorizontalLogoMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "DeleteOrganizationHorizontalLogoPayload", + "kind": "LinkedField", + "name": "deleteOrganizationHorizontalLogo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "horizontalLogoUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SettingsPage_DeleteHorizontalLogoMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SettingsPage_DeleteHorizontalLogoMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "e631480d50c9347050fdc62075a2e3a3", + "id": null, + "metadata": {}, + "name": "SettingsPage_DeleteHorizontalLogoMutation", + "operationKind": "mutation", + "text": "mutation SettingsPage_DeleteHorizontalLogoMutation(\n $input: DeleteOrganizationHorizontalLogoInput!\n) {\n deleteOrganizationHorizontalLogo(input: $input) {\n organization {\n id\n horizontalLogoUrl\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "751c3ff44c59511451095ffc66446c2f"; + +export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts index 536ec90af..d37515962 100644 --- a/apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts +++ b/apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<892a78db951d547b53d595d219b94e75>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -13,6 +13,7 @@ export type UpdateOrganizationInput = { description?: string | null | undefined; email?: string | null | undefined; headquarterAddress?: string | null | undefined; + horizontalLogoFile?: any | null | undefined; logo?: any | null | undefined; name?: string | null | undefined; organizationId: string; @@ -27,6 +28,7 @@ export type SettingsPage_UpdateMutation$data = { readonly description: string | null | undefined; readonly email: string | null | undefined; readonly headquarterAddress: string | null | undefined; + readonly horizontalLogoUrl: string | null | undefined; readonly id: string; readonly logoUrl: string | null | undefined; readonly name: string; @@ -91,6 +93,13 @@ v1 = [ "name": "logoUrl", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "horizontalLogoUrl", + "storageKey": null + }, { "alias": null, "args": null, @@ -144,16 +153,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "3526d270f311db33e0563abc99c96978", + "cacheID": "097a6e519249d6f2b3c2a95963f85a6c", "id": null, "metadata": {}, "name": "SettingsPage_UpdateMutation", "operationKind": "mutation", - "text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n" + "text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n" } }; })(); -(node as any).hash = "54949158defa7f1bb8e1e578db7cd7be"; +(node as any).hash = "c676129018636d84dbca9d8c98962af7"; export default node; diff --git a/packages/ui/src/Molecules/FileButton/FileButton.tsx b/packages/ui/src/Molecules/FileButton/FileButton.tsx index 1f10a03b7..ca4fa1730 100644 --- a/packages/ui/src/Molecules/FileButton/FileButton.tsx +++ b/packages/ui/src/Molecules/FileButton/FileButton.tsx @@ -20,13 +20,20 @@ export function FileButton({ children, icon: IconComponent, ref, + accept, ...props }: Props) { return ( ); } diff --git a/pkg/coredata/migrations/20251009T140000Z.sql b/pkg/coredata/migrations/20251009T140000Z.sql new file mode 100644 index 000000000..6daab1ef1 --- /dev/null +++ b/pkg/coredata/migrations/20251009T140000Z.sql @@ -0,0 +1,51 @@ +ALTER TABLE organizations ALTER COLUMN logo_object_key SET DEFAULT ''; + +ALTER TABLE organizations + ADD COLUMN horizontal_logo_file_id TEXT, + ADD CONSTRAINT organizations_horizontal_logo_file_id_fkey + FOREIGN KEY (horizontal_logo_file_id) + REFERENCES files(id) + ON UPDATE CASCADE + ON DELETE RESTRICT; + +ALTER TABLE organizations + ADD COLUMN logo_file_id TEXT; + +/* 25 is for FileEntityType */ +WITH + logo_files AS ( + SELECT + o.id as organization_id, + generate_gid(decode_base64_unpadded(o.tenant_id), 25) as file_id, + o.tenant_id, + 'probod' as bucket_name, + 'image/png' as mime_type, + 'logo.png' as file_name, + o.logo_object_key, + o.created_at, + o.updated_at + FROM organizations o + WHERE o.logo_object_key IS NOT NULL AND o.logo_object_key != '' + ), + inserted_files AS ( + INSERT INTO files (id, tenant_id, bucket_name, mime_type, file_name, file_key, file_size, created_at, updated_at) + SELECT file_id, tenant_id, bucket_name, mime_type, file_name, logo_object_key::uuid, 0, created_at, updated_at + FROM logo_files + RETURNING id, tenant_id + ) + +SELECT lf.organization_id, lf.file_id +INTO TEMP TABLE file_logo_mapping +FROM logo_files lf; + +UPDATE organizations +SET logo_file_id = fm.file_id +FROM file_logo_mapping fm +WHERE organizations.id = fm.organization_id; + +ALTER TABLE organizations + ADD CONSTRAINT organizations_logo_file_id_fkey + FOREIGN KEY (logo_file_id) + REFERENCES files(id) + ON UPDATE CASCADE + ON DELETE RESTRICT; diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index c9413eee6..6da6606f1 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -28,17 +28,18 @@ import ( type ( Organization struct { - ID gid.GID `db:"id"` - TenantID gid.TenantID `db:"tenant_id"` - Name string `db:"name"` - LogoObjectKey string `db:"logo_object_key"` - Description *string `db:"description"` - WebsiteURL *string `db:"website_url"` - Email *string `db:"email"` - HeadquarterAddress *string `db:"headquarter_address"` - CustomDomainID *gid.GID `db:"custom_domain_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + TenantID gid.TenantID `db:"tenant_id"` + Name string `db:"name"` + LogoFileID *gid.GID `db:"logo_file_id"` + HorizontalLogoFileID *gid.GID `db:"horizontal_logo_file_id"` + Description *string `db:"description"` + WebsiteURL *string `db:"website_url"` + Email *string `db:"email"` + HeadquarterAddress *string `db:"headquarter_address"` + CustomDomainID *gid.GID `db:"custom_domain_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } Organizations []*Organization @@ -68,7 +69,8 @@ SELECT tenant_id, id, name, - logo_object_key, + logo_file_id, + horizontal_logo_file_id, description, website_url, email, @@ -124,7 +126,8 @@ SELECT tenant_id, id, name, - logo_object_key, + logo_file_id, + horizontal_logo_file_id, description, website_url, email, @@ -169,7 +172,8 @@ INSERT INTO organizations ( tenant_id, id, name, - logo_object_key, + logo_file_id, + horizontal_logo_file_id, description, website_url, email, @@ -177,21 +181,22 @@ INSERT INTO organizations ( custom_domain_id, created_at, updated_at -) VALUES (@tenant_id, @id, @name, @logo_object_key, @description, @website_url, @email, @headquarter_address, @custom_domain_id, @created_at, @updated_at) +) VALUES (@tenant_id, @id, @name, @logo_file_id, @horizontal_logo_file_id, @description, @website_url, @email, @headquarter_address, @custom_domain_id, @created_at, @updated_at) ` args := pgx.StrictNamedArgs{ - "tenant_id": o.TenantID, - "id": o.ID, - "name": o.Name, - "logo_object_key": o.LogoObjectKey, - "description": o.Description, - "website_url": o.WebsiteURL, - "email": o.Email, - "headquarter_address": o.HeadquarterAddress, - "custom_domain_id": o.CustomDomainID, - "created_at": o.CreatedAt, - "updated_at": o.UpdatedAt, + "tenant_id": o.TenantID, + "id": o.ID, + "name": o.Name, + "logo_file_id": o.LogoFileID, + "horizontal_logo_file_id": o.HorizontalLogoFileID, + "description": o.Description, + "website_url": o.WebsiteURL, + "email": o.Email, + "headquarter_address": o.HeadquarterAddress, + "custom_domain_id": o.CustomDomainID, + "created_at": o.CreatedAt, + "updated_at": o.UpdatedAt, } _, err := conn.Exec(ctx, q, args) @@ -211,7 +216,8 @@ func (o *Organization) Update( UPDATE organizations SET name = @name, - logo_object_key = @logo_object_key, + logo_file_id = @logo_file_id, + horizontal_logo_file_id = @horizontal_logo_file_id, description = @description, website_url = @website_url, email = @email, @@ -226,15 +232,16 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ - "id": o.ID, - "name": o.Name, - "logo_object_key": o.LogoObjectKey, - "description": o.Description, - "website_url": o.WebsiteURL, - "email": o.Email, - "headquarter_address": o.HeadquarterAddress, - "custom_domain_id": o.CustomDomainID, - "updated_at": o.UpdatedAt, + "id": o.ID, + "name": o.Name, + "logo_file_id": o.LogoFileID, + "horizontal_logo_file_id": o.HorizontalLogoFileID, + "description": o.Description, + "website_url": o.WebsiteURL, + "email": o.Email, + "headquarter_address": o.HeadquarterAddress, + "custom_domain_id": o.CustomDomainID, + "updated_at": o.UpdatedAt, } maps.Copy(args, scope.SQLArguments()) @@ -283,7 +290,8 @@ SELECT tenant_id, id, name, - logo_object_key, + logo_file_id, + horizontal_logo_file_id, description, website_url, email, diff --git a/pkg/docgen/generator.go b/pkg/docgen/generator.go index 49c105827..021a8955e 100644 --- a/pkg/docgen/generator.go +++ b/pkg/docgen/generator.go @@ -53,6 +53,9 @@ var ( } return template.HTML(buf.String()) }, + "imgTag": func(src, alt, class string) template.HTML { + return template.HTML(fmt.Sprintf(`%s`, html.EscapeString(src), html.EscapeString(alt), html.EscapeString(class))) + }, } documentTemplate = template.Must(template.New("document").Funcs(templateFuncs).Parse(htmlTemplateContent)) @@ -62,14 +65,15 @@ type ( Classification string DocumentData struct { - Title string - Content string - Version int - Classification Classification - Approver string - Description string - PublishedAt *time.Time - Signatures []SignatureData + Title string + Content string + Version int + Classification Classification + Approver string + Description string + PublishedAt *time.Time + Signatures []SignatureData + CompanyHorizontalLogoBase64 string } SignatureData struct { diff --git a/pkg/docgen/template.html b/pkg/docgen/template.html index b48414587..09c5dc87e 100644 --- a/pkg/docgen/template.html +++ b/pkg/docgen/template.html @@ -11,17 +11,17 @@ margin: 2.5cm; @bottom-right { content: "Page " counter(page) " of " counter(pages); - font-family: 'Times New Roman', Times, serif; + font-family: Arial, sans-serif; font-size: 9pt; color: #666; } } body { - font-family: 'Times New Roman', Times, serif; + font-family: Arial, sans-serif; font-size: 11pt; - line-height: 1.4; - color: #000; + line-height: 1.6; + color: #333; margin: 0; padding: 20px 0; background: #f5f5f5; @@ -49,46 +49,58 @@ page-break-after: auto; } - - .document-header { - border-bottom: 1px solid #333; - padding-bottom: 15px; + padding-bottom: 0; margin-bottom: 25px; page-break-after: avoid; } + .company-header { + margin-bottom: 25px; + page-break-after: avoid; + } + + .company-logo { + max-height: 50px; + max-width: 250px; + object-fit: contain; + display: block; + } + .document-title { - font-size: 18pt; - font-weight: bold; - color: #000; - margin: 0 0 10px 0; - text-align: center; + font-size: 22pt; + font-weight: normal; + color: #1a1a1a; + margin: 0 0 25px 0; + text-align: left; + } + + .company-header + .document-title { + margin-top: 30px; } .document-meta { - background: #f9f9f9; - padding: 12px; - border: 1px solid #ddd; - margin: 15px 0; + margin: 25px 0 0 0; font-size: 9pt; } .meta-table { width: 100%; border-collapse: collapse; + border: 1px solid #333; } .meta-table td { - padding: 4px 8px; - border-bottom: 1px solid #eee; - vertical-align: top; + padding: 6px 8px; + border: 1px solid #333; + vertical-align: middle; } .meta-table td:first-child { - font-weight: bold; + font-weight: 600; + width: 25%; + background: #f8f8f8; color: #333; - width: 120px; } .classification { @@ -141,7 +153,7 @@ } .document-content p { - margin-bottom: 12px; + margin: 0 0 12px 0; text-align: justify; orphans: 3; widows: 3; @@ -312,18 +324,23 @@
+ {{- if .CompanyHorizontalLogoBase64}} +
+ {{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}} +
+ {{- end}}

{{.Title}}

- + - + @@ -332,7 +349,7 @@ {{- if .PublishedAt}} - + {{- end}} diff --git a/pkg/filemanager/service.go b/pkg/filemanager/service.go new file mode 100644 index 000000000..7747228f3 --- /dev/null +++ b/pkg/filemanager/service.go @@ -0,0 +1,148 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package filemanager + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net/url" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/getprobo/probo/pkg/coredata" +) + +type Service struct { + s3Client *s3.Client +} + +func NewService(s3Client *s3.Client) *Service { + return &Service{ + s3Client: s3Client, + } +} + +func (s *Service) GetFileBase64( + ctx context.Context, + file *coredata.File, +) (base64Data string, mimeType string, err error) { + result, err := s.s3Client.GetObject( + ctx, + &s3.GetObjectInput{ + Bucket: &file.BucketName, + Key: &file.FileKey, + }, + ) + if err != nil { + return "", "", fmt.Errorf("cannot get file from S3: %w", err) + } + defer result.Body.Close() + + fileData, err := io.ReadAll(result.Body) + if err != nil { + return "", "", fmt.Errorf("cannot read file data: %w", err) + } + + if result.ContentType == nil || *result.ContentType == "" { + return "", "", fmt.Errorf("no MIME type available for file %s", file.FileKey) + } + + base64Data = base64.StdEncoding.EncodeToString(fileData) + mimeType = *result.ContentType + + return base64Data, mimeType, nil +} + +func (s *Service) GetFileSize(content io.Reader) (int64, error) { + seeker, ok := content.(io.Seeker) + if !ok { + return 0, fmt.Errorf("cannot determine file size: content is not seekable") + } + + size, err := seeker.Seek(0, io.SeekEnd) + if err != nil { + return 0, fmt.Errorf("cannot determine file size: %w", err) + } + + _, err = seeker.Seek(0, io.SeekStart) + if err != nil { + return 0, fmt.Errorf("cannot reset file position: %w", err) + } + + return size, nil +} + +func (s *Service) PutFile( + ctx context.Context, + file *coredata.File, + content io.Reader, + metadata map[string]string, +) (int64, error) { + _, err := s.s3Client.PutObject( + ctx, + &s3.PutObjectInput{ + Bucket: &file.BucketName, + Key: &file.FileKey, + Body: content, + ContentType: &file.MimeType, + Metadata: metadata, + }) + if err != nil { + return 0, fmt.Errorf("cannot upload file to S3: %w", err) + } + + headOutput, err := s.s3Client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &file.BucketName, + Key: &file.FileKey, + }) + if err != nil { + return 0, fmt.Errorf("cannot get object metadata: %w", err) + } + + return *headOutput.ContentLength, nil +} + +func (s *Service) GenerateFileUrl( + ctx context.Context, + file *coredata.File, + expiresIn time.Duration, +) (string, error) { + presignClient := s3.NewPresignClient(s.s3Client) + + encodedFilename := url.QueryEscape(file.FileName) + contentDisposition := fmt.Sprintf("attachment; filename=%q; filename*=UTF-8''%s", + encodedFilename, encodedFilename) + + presignedReq, err := presignClient.PresignGetObject( + ctx, + &s3.GetObjectInput{ + Bucket: &file.BucketName, + Key: &file.FileKey, + ResponseCacheControl: aws.String("max-age=3600, public"), + ResponseContentDisposition: &contentDisposition, + }, + func(opts *s3.PresignOptions) { + opts.Expires = expiresIn + }, + ) + if err != nil { + return "", fmt.Errorf("cannot presign GetObject request: %w", err) + } + + return presignedReq.URL, nil +} diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index 91a627e2e..4aab33a74 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -1201,7 +1201,7 @@ func (s *DocumentService) ExportPDF( err := s.svc.pg.WithTx( ctx, func(conn pg.Conn) (err error) { - data, err = exportDocumentPDF(ctx, s.html2pdfConverter, conn, s.svc.scope, documentVersionID, options) + data, err = exportDocumentPDF(ctx, s.svc, s.html2pdfConverter, conn, s.svc.scope, documentVersionID, options) if err != nil { return fmt.Errorf("cannot export document PDF: %w", err) } @@ -1333,6 +1333,7 @@ func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID func exportDocumentPDF( ctx context.Context, + svc *TenantService, html2pdfConverter *html2pdf.Converter, conn pg.Conn, scope coredata.Scoper, @@ -1342,6 +1343,7 @@ func exportDocumentPDF( document := &coredata.Document{} version := &coredata.DocumentVersion{} owner := &coredata.People{} + organization := &coredata.Organization{} signatures := coredata.DocumentVersionSignatures{} peopleMap := make(map[gid.GID]*coredata.People) @@ -1357,6 +1359,10 @@ func exportDocumentPDF( return nil, fmt.Errorf("cannot load document owner: %w", err) } + if err := organization.LoadByID(ctx, conn, scope, document.OrganizationID); err != nil { + return nil, fmt.Errorf("cannot load organization: %w", err) + } + var signatureData []docgen.SignatureData if options.WithSignatures { cursor := page.NewCursor( @@ -1403,14 +1409,29 @@ func exportDocumentPDF( classification = docgen.ClassificationSecret } + horizontalLogoBase64 := "" + if organization.HorizontalLogoFileID != nil { + fileRecord := &coredata.File{} + fileErr := svc.pg.WithConn(ctx, func(conn pg.Conn) error { + return fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID) + }) + if fileErr == nil { + base64Data, mimeType, logoErr := svc.fileManager.GetFileBase64(ctx, fileRecord) + if logoErr == nil { + horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) + } + } + } + docData := docgen.DocumentData{ - Title: version.Title, - Content: version.Content, - Version: version.VersionNumber, - Classification: classification, - Approver: owner.FullName, - PublishedAt: version.PublishedAt, - Signatures: signatureData, + Title: version.Title, + Content: version.Content, + Version: version.VersionNumber, + Classification: classification, + Approver: owner.FullName, + PublishedAt: version.PublishedAt, + Signatures: signatureData, + CompanyHorizontalLogoBase64: horizontalLogoBase64, } htmlContent, err := docgen.RenderHTML(docData) @@ -1492,6 +1513,7 @@ func (s *DocumentService) Export( exportedPDF, err := exportDocumentPDF( ctx, + s.svc, s.html2pdfConverter, conn, s.svc.scope, diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 17db88455..da708c334 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -285,6 +285,7 @@ func (s FrameworkService) Export( exportedPDF, err := exportDocumentPDF( ctx, + s.svc, s.html2pdfConverter, conn, s.svc.scope, diff --git a/pkg/probo/organization_service.go b/pkg/probo/organization_service.go index f2579d1e2..e942df8a9 100644 --- a/pkg/probo/organization_service.go +++ b/pkg/probo/organization_service.go @@ -15,18 +15,13 @@ package probo import ( - "bytes" "context" "fmt" - "io" "mime" "net/mail" - "net/url" "path/filepath" "time" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/filevalidation" "github.com/getprobo/probo/pkg/gid" @@ -71,6 +66,7 @@ type ( ID gid.GID Name *string File *File + HorizontalLogoFile *File Description **string WebsiteURL **string Email **string @@ -160,14 +156,15 @@ func (s OrganizationService) Update( ) (*coredata.Organization, error) { organization := &coredata.Organization{} - err := s.svc.pg.WithConn( + err := s.svc.pg.WithTx( ctx, - func(conn pg.Conn) error { - if err := organization.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { + func(tx pg.Conn) error { + if err := organization.LoadByID(ctx, tx, s.svc.scope, req.ID); err != nil { return fmt.Errorf("cannot load organization: %w", err) } - organization.UpdatedAt = time.Now() + now := time.Now() + organization.UpdatedAt = now if req.Name != nil { organization.Name = *req.Name @@ -195,41 +192,15 @@ func (s OrganizationService) Update( } if req.File != nil { + fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) objectKey, err := uuid.NewV7() if err != nil { return fmt.Errorf("cannot generate object key: %w", err) } - var fileSize int64 - var fileContent io.ReadSeeker filename := req.File.Filename contentType := req.File.ContentType - if seeker, ok := req.File.Content.(io.Seeker); ok { - if req.File.Size <= 0 { - size, err := seeker.Seek(0, io.SeekEnd) - if err != nil { - return fmt.Errorf("cannot determine file size: %w", err) - } - fileSize = size - - _, err = seeker.Seek(0, io.SeekStart) - if err != nil { - return fmt.Errorf("cannot reset file position: %w", err) - } - } else { - fileSize = req.File.Size - } - fileContent = req.File.Content.(io.ReadSeeker) - } else { - buf, err := io.ReadAll(req.File.Content) - if err != nil { - return fmt.Errorf("cannot read file: %w", err) - } - fileSize = int64(len(buf)) - fileContent = bytes.NewReader(buf) - } - if contentType == "" { contentType = "application/octet-stream" if filename != "" { @@ -239,29 +210,98 @@ func (s OrganizationService) Update( } } + fileSize, err := s.svc.fileManager.GetFileSize(req.File.Content) + if err != nil { + return fmt.Errorf("cannot get file size: %w", err) + } + if err := s.fileValidator.Validate(filename, contentType, fileSize); err != nil { return err } - _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ - Bucket: aws.String(s.svc.bucket), - Key: aws.String(objectKey.String()), - Body: fileContent, - ContentType: aws.String(contentType), - Metadata: map[string]string{ - "type": "organization-logo", - "organization-id": organization.ID.String(), - }, - }) - - if err != nil { - return fmt.Errorf("cannot upload file to S3: %w", err) + fileRecord := &coredata.File{ + ID: fileID, + BucketName: s.svc.bucket, + MimeType: contentType, + FileName: filename, + FileKey: objectKey.String(), + CreatedAt: now, + UpdatedAt: now, } - organization.LogoObjectKey = objectKey.String() + fileSize, err = s.svc.fileManager.PutFile(ctx, fileRecord, req.File.Content, map[string]string{ + "type": "organization-logo", + "organization-id": organization.ID.String(), + }) + if err != nil { + return fmt.Errorf("cannot upload logo file: %w", err) + } + + fileRecord.FileSize = fileSize + + if err := fileRecord.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert file: %w", err) + } + + organization.LogoFileID = &fileID } - if err := organization.Update(ctx, s.svc.scope, conn); err != nil { + if req.HorizontalLogoFile != nil { + fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) + objectKey, err := uuid.NewV7() + if err != nil { + return fmt.Errorf("cannot generate object key: %w", err) + } + + filename := req.HorizontalLogoFile.Filename + contentType := req.HorizontalLogoFile.ContentType + + if contentType == "" { + contentType = "application/octet-stream" + if filename != "" { + if detectedType := mime.TypeByExtension(filepath.Ext(filename)); detectedType != "" { + contentType = detectedType + } + } + } + + fileSize, err := s.svc.fileManager.GetFileSize(req.HorizontalLogoFile.Content) + if err != nil { + return fmt.Errorf("cannot get file size: %w", err) + } + + if err := s.fileValidator.Validate(filename, contentType, fileSize); err != nil { + return err + } + + fileRecord := &coredata.File{ + ID: fileID, + BucketName: s.svc.bucket, + MimeType: contentType, + FileName: filename, + FileKey: objectKey.String(), + CreatedAt: now, + UpdatedAt: now, + } + + fileSize, err = s.svc.fileManager.PutFile(ctx, fileRecord, req.HorizontalLogoFile.Content, map[string]string{ + "type": "organization-horizontal-logo", + "organization-id": organization.ID.String(), + }) + if err != nil { + return fmt.Errorf("cannot upload horizontal logo file: %w", err) + } + + fileRecord.FileSize = fileSize + + if err := fileRecord.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert file: %w", err) + } + + organization.HorizontalLogoFileID = &fileID + } + + if err := organization.Update(ctx, s.svc.scope, tx); err != nil { return fmt.Errorf("cannot update organization: %w", err) } @@ -281,34 +321,114 @@ func (s OrganizationService) GenerateLogoURL( organizationID gid.GID, expiresIn time.Duration, ) (*string, error) { - organization, err := s.Get(ctx, organizationID) + file := &coredata.File{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + organization := &coredata.Organization{} + if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + if organization.LogoFileID == nil { + return nil + } + + if err := file.LoadByID(ctx, conn, s.svc.scope, *organization.LogoFileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ) if err != nil { - return nil, fmt.Errorf("cannot get organization: %w", err) + return nil, err } - if organization.LogoObjectKey == "" { + if file.FileKey == "" { return nil, nil } - presignClient := s3.NewPresignClient(s.svc.s3) - - encodedFilename := url.QueryEscape(organization.Name) - contentDisposition := fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", - encodedFilename, encodedFilename) - - presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ - Bucket: aws.String(s.svc.bucket), - Key: aws.String(organization.LogoObjectKey), - ResponseCacheControl: aws.String("max-age=3600, public"), - ResponseContentDisposition: aws.String(contentDisposition), - }, func(opts *s3.PresignOptions) { - opts.Expires = expiresIn - }) + presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, file, expiresIn) if err != nil { - return nil, fmt.Errorf("cannot presign GetObject request: %w", err) + return nil, fmt.Errorf("cannot generate file URL: %w", err) } - return &presignedReq.URL, nil + return &presignedURL, nil +} + +func (s OrganizationService) GenerateHorizontalLogoURL( + ctx context.Context, + organizationID gid.GID, + expiresIn time.Duration, +) (*string, error) { + file := &coredata.File{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + organization := &coredata.Organization{} + if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + if organization.HorizontalLogoFileID == nil { + return nil + } + + if err := file.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + if file.FileKey == "" { + return nil, nil + } + + presignedURL, err := s.svc.fileManager.GenerateFileUrl(ctx, file, expiresIn) + if err != nil { + return nil, fmt.Errorf("cannot generate file URL: %w", err) + } + + return &presignedURL, nil +} + +func (s OrganizationService) DeleteHorizontalLogo( + ctx context.Context, + organizationID gid.GID, +) (*coredata.Organization, error) { + organization := &coredata.Organization{} + + err := s.svc.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + organization.HorizontalLogoFileID = nil + organization.UpdatedAt = time.Now() + + if err := organization.Update(ctx, s.svc.scope, tx); err != nil { + return fmt.Errorf("cannot update organization: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return organization, nil } func (s OrganizationService) Delete( @@ -343,16 +463,6 @@ func (s OrganizationService) Delete( return err } - if organization.LogoObjectKey != "" { - _, err := s.svc.s3.DeleteObject(ctx, &s3.DeleteObjectInput{ - Bucket: aws.String(s.svc.bucket), - Key: aws.String(organization.LogoObjectKey), - }) - if err != nil { - return fmt.Errorf("organization deleted but failed to delete logo from S3: %w", err) - } - } - return nil } diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 097db7e28..4d0c32b2e 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -24,6 +24,7 @@ import ( "github.com/getprobo/probo/pkg/certmanager" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/crypto/cipher" + "github.com/getprobo/probo/pkg/filemanager" "github.com/getprobo/probo/pkg/filevalidation" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/html2pdf" @@ -57,6 +58,7 @@ type ( html2pdfConverter *html2pdf.Converter usrmgr *usrmgr.Service acmeService *certmanager.ACMEService + fileManager *filemanager.Service logger *log.Logger } @@ -70,6 +72,7 @@ type ( tokenSecret string trustConfig TrustConfig agent *agents.Agent + fileManager *filemanager.Service Frameworks *FrameworkService Measures *MeasureService Tasks *TaskService @@ -116,6 +119,7 @@ func NewService( html2pdfConverter *html2pdf.Converter, usrmgrService *usrmgr.Service, acmeService *certmanager.ACMEService, + fileManagerService *filemanager.Service, logger *log.Logger, ) (*Service, error) { if bucket == "" { @@ -134,6 +138,7 @@ func NewService( html2pdfConverter: html2pdfConverter, usrmgr: usrmgrService, acmeService: acmeService, + fileManager: fileManagerService, logger: logger, } @@ -151,6 +156,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { tokenSecret: s.tokenSecret, trustConfig: s.trustConfig, agent: agents.NewAgent(nil, s.agentConfig), + fileManager: s.fileManager, } tenantService.Frameworks = &FrameworkService{ diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 7cd7aa5aa..327934d37 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -36,6 +36,7 @@ import ( "github.com/getprobo/probo/pkg/crypto/keys" "github.com/getprobo/probo/pkg/crypto/passwdhash" "github.com/getprobo/probo/pkg/crypto/pem" + "github.com/getprobo/probo/pkg/filemanager" "github.com/getprobo/probo/pkg/html2pdf" "github.com/getprobo/probo/pkg/mailer" "github.com/getprobo/probo/pkg/probo" @@ -269,6 +270,8 @@ func (impl *Implm) Run( return fmt.Errorf("cannot create usrmgr service: %w", err) } + fileManagerService := filemanager.NewService(s3Client) + var accountKey crypto.Signer if impl.cfg.CustomDomains.ACME.AccountKey != "" { accountKey, err = pem.DecodePrivateKey([]byte(impl.cfg.CustomDomains.ACME.AccountKey)) @@ -311,6 +314,7 @@ func (impl *Implm) Run( html2pdfConverter, usrmgrService, acmeService, + fileManagerService, l.Named("probo"), ) if err != nil { @@ -325,6 +329,7 @@ func (impl *Implm) Run( impl.cfg.TrustAuth.TokenSecret, usrmgrService, html2pdfConverter, + fileManagerService, ) serverHandler, err := server.NewServer( diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 3a1b9bf49..02965958d 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1488,6 +1488,7 @@ type Organization implements Node { id: ID! name: String! logoUrl: String @goField(forceResolver: true) + horizontalLogoUrl: String @goField(forceResolver: true) description: String websiteUrl: String @@ -2619,6 +2620,9 @@ type Mutation { updateOrganization( input: UpdateOrganizationInput! ): UpdateOrganizationPayload! + deleteOrganizationHorizontalLogo( + input: DeleteOrganizationHorizontalLogoInput! + ): DeleteOrganizationHorizontalLogoPayload! deleteOrganization( input: DeleteOrganizationInput! ): DeleteOrganizationPayload! @@ -2941,12 +2945,17 @@ input UpdateOrganizationInput { organizationId: ID! name: String logo: Upload + horizontalLogoFile: Upload description: String websiteUrl: String email: String headquarterAddress: String } +input DeleteOrganizationHorizontalLogoInput { + organizationId: ID! +} + input DeleteOrganizationInput { organizationId: ID! } @@ -3628,6 +3637,10 @@ type UpdateOrganizationPayload { organization: Organization! } +type DeleteOrganizationHorizontalLogoPayload { + organization: Organization! +} + type DeleteOrganizationPayload { deletedOrganizationId: ID! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 77488f3aa..e62517bd8 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -516,6 +516,10 @@ type ComplexityRoot struct { DeletedObligationID func(childComplexity int) int } + DeleteOrganizationHorizontalLogoPayload struct { + Organization func(childComplexity int) int + } + DeleteOrganizationPayload struct { DeletedOrganizationID func(childComplexity int) int } @@ -835,6 +839,7 @@ type ComplexityRoot struct { DeleteNonconformity func(childComplexity int, input types.DeleteNonconformityInput) int DeleteObligation func(childComplexity int, input types.DeleteObligationInput) int DeleteOrganization func(childComplexity int, input types.DeleteOrganizationInput) int + DeleteOrganizationHorizontalLogo func(childComplexity int, input types.DeleteOrganizationHorizontalLogoInput) int DeletePeople func(childComplexity int, input types.DeletePeopleInput) int DeleteProcessingActivity func(childComplexity int, input types.DeleteProcessingActivityInput) int DeleteRisk func(childComplexity int, input types.DeleteRiskInput) int @@ -968,6 +973,7 @@ type ComplexityRoot struct { Email func(childComplexity int) int Frameworks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) int HeadquarterAddress func(childComplexity int) int + HorizontalLogoURL func(childComplexity int) int ID func(childComplexity int) int LogoURL func(childComplexity int) int Measures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) int @@ -1673,6 +1679,7 @@ type MeasureConnectionResolver interface { type MutationResolver interface { CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) + DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) UpdateTrustCenter(ctx context.Context, input types.UpdateTrustCenterInput) (*types.UpdateTrustCenterPayload, error) UploadTrustCenterNda(ctx context.Context, input types.UploadTrustCenterNDAInput) (*types.UploadTrustCenterNDAPayload, error) @@ -1809,6 +1816,7 @@ type ObligationConnectionResolver interface { } type OrganizationResolver interface { LogoURL(ctx context.Context, obj *types.Organization) (*string, error) + HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) Users(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) (*types.UserConnection, error) Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error) @@ -3239,6 +3247,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.DeleteObligationPayload.DeletedObligationID(childComplexity), true + case "DeleteOrganizationHorizontalLogoPayload.organization": + if e.complexity.DeleteOrganizationHorizontalLogoPayload.Organization == nil { + break + } + + return e.complexity.DeleteOrganizationHorizontalLogoPayload.Organization(childComplexity), true + case "DeleteOrganizationPayload.deletedOrganizationId": if e.complexity.DeleteOrganizationPayload.DeletedOrganizationID == nil { break @@ -4850,6 +4865,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.DeleteOrganization(childComplexity, args["input"].(types.DeleteOrganizationInput)), true + case "Mutation.deleteOrganizationHorizontalLogo": + if e.complexity.Mutation.DeleteOrganizationHorizontalLogo == nil { + break + } + + args, err := ec.field_Mutation_deleteOrganizationHorizontalLogo_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteOrganizationHorizontalLogo(childComplexity, args["input"].(types.DeleteOrganizationHorizontalLogoInput)), true + case "Mutation.deletePeople": if e.complexity.Mutation.DeletePeople == nil { break @@ -5969,6 +5996,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.HeadquarterAddress(childComplexity), true + case "Organization.horizontalLogoUrl": + if e.complexity.Organization.HorizontalLogoURL == nil { + break + } + + return e.complexity.Organization.HorizontalLogoURL(childComplexity), true + case "Organization.id": if e.complexity.Organization.ID == nil { break @@ -8506,6 +8540,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDeleteMeasureInput, ec.unmarshalInputDeleteNonconformityInput, ec.unmarshalInputDeleteObligationInput, + ec.unmarshalInputDeleteOrganizationHorizontalLogoInput, ec.unmarshalInputDeleteOrganizationInput, ec.unmarshalInputDeletePeopleInput, ec.unmarshalInputDeleteProcessingActivityInput, @@ -10188,6 +10223,7 @@ type Organization implements Node { id: ID! name: String! logoUrl: String @goField(forceResolver: true) + horizontalLogoUrl: String @goField(forceResolver: true) description: String websiteUrl: String @@ -11319,6 +11355,9 @@ type Mutation { updateOrganization( input: UpdateOrganizationInput! ): UpdateOrganizationPayload! + deleteOrganizationHorizontalLogo( + input: DeleteOrganizationHorizontalLogoInput! + ): DeleteOrganizationHorizontalLogoPayload! deleteOrganization( input: DeleteOrganizationInput! ): DeleteOrganizationPayload! @@ -11641,12 +11680,17 @@ input UpdateOrganizationInput { organizationId: ID! name: String logo: Upload + horizontalLogoFile: Upload description: String websiteUrl: String email: String headquarterAddress: String } +input DeleteOrganizationHorizontalLogoInput { + organizationId: ID! +} + input DeleteOrganizationInput { organizationId: ID! } @@ -12328,6 +12372,10 @@ type UpdateOrganizationPayload { organization: Organization! } +type DeleteOrganizationHorizontalLogoPayload { + organization: Organization! +} + type DeleteOrganizationPayload { deletedOrganizationId: ID! } @@ -16056,6 +16104,29 @@ func (ec *executionContext) field_Mutation_deleteObligation_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_deleteOrganizationHorizontalLogo_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_deleteOrganizationHorizontalLogo_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_deleteOrganizationHorizontalLogo_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.DeleteOrganizationHorizontalLogoInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNDeleteOrganizationHorizontalLogoInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoInput(ctx, tmp) + } + + var zeroVal types.DeleteOrganizationHorizontalLogoInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_deleteOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -21512,6 +21583,8 @@ func (ec *executionContext) fieldContext_Asset_organization(_ context.Context, f return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -22119,6 +22192,8 @@ func (ec *executionContext) fieldContext_Audit_organization(_ context.Context, f return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -23846,6 +23921,8 @@ func (ec *executionContext) fieldContext_ContinualImprovement_organization(_ con return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -27604,6 +27681,8 @@ func (ec *executionContext) fieldContext_CustomDomain_organization(_ context.Con return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -28503,6 +28582,8 @@ func (ec *executionContext) fieldContext_Datum_organization(_ context.Context, f return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -29902,6 +29983,112 @@ func (ec *executionContext) fieldContext_DeleteObligationPayload_deletedObligati return fc, nil } +func (ec *executionContext) _DeleteOrganizationHorizontalLogoPayload_organization(ctx context.Context, field graphql.CollectedField, obj *types.DeleteOrganizationHorizontalLogoPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DeleteOrganizationHorizontalLogoPayload_organization(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Organization, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.Organization) + fc.Result = res + return ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DeleteOrganizationHorizontalLogoPayload_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DeleteOrganizationHorizontalLogoPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Organization_id(ctx, field) + case "name": + return ec.fieldContext_Organization_name(ctx, field) + case "logoUrl": + return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) + case "description": + return ec.fieldContext_Organization_description(ctx, field) + case "websiteUrl": + return ec.fieldContext_Organization_websiteUrl(ctx, field) + case "email": + return ec.fieldContext_Organization_email(ctx, field) + case "headquarterAddress": + return ec.fieldContext_Organization_headquarterAddress(ctx, field) + case "users": + return ec.fieldContext_Organization_users(ctx, field) + case "connectors": + return ec.fieldContext_Organization_connectors(ctx, field) + case "frameworks": + return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) + case "vendors": + return ec.fieldContext_Organization_vendors(ctx, field) + case "peoples": + return ec.fieldContext_Organization_peoples(ctx, field) + case "documents": + return ec.fieldContext_Organization_documents(ctx, field) + case "measures": + return ec.fieldContext_Organization_measures(ctx, field) + case "risks": + return ec.fieldContext_Organization_risks(ctx, field) + case "tasks": + return ec.fieldContext_Organization_tasks(ctx, field) + case "assets": + return ec.fieldContext_Organization_assets(ctx, field) + case "data": + return ec.fieldContext_Organization_data(ctx, field) + case "audits": + return ec.fieldContext_Organization_audits(ctx, field) + case "nonconformities": + return ec.fieldContext_Organization_nonconformities(ctx, field) + case "obligations": + return ec.fieldContext_Organization_obligations(ctx, field) + case "continualImprovements": + return ec.fieldContext_Organization_continualImprovements(ctx, field) + case "processingActivities": + return ec.fieldContext_Organization_processingActivities(ctx, field) + case "snapshots": + return ec.fieldContext_Organization_snapshots(ctx, field) + case "trustCenter": + return ec.fieldContext_Organization_trustCenter(ctx, field) + case "customDomain": + return ec.fieldContext_Organization_customDomain(ctx, field) + case "createdAt": + return ec.fieldContext_Organization_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Organization_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _DeleteOrganizationPayload_deletedOrganizationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteOrganizationPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DeleteOrganizationPayload_deletedOrganizationId(ctx, field) if err != nil { @@ -31220,6 +31407,8 @@ func (ec *executionContext) fieldContext_Document_organization(_ context.Context return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -34542,6 +34731,8 @@ func (ec *executionContext) fieldContext_Framework_organization(_ context.Contex return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -36224,6 +36415,65 @@ func (ec *executionContext) fieldContext_Mutation_updateOrganization(ctx context return fc, nil } +func (ec *executionContext) _Mutation_deleteOrganizationHorizontalLogo(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteOrganizationHorizontalLogo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteOrganizationHorizontalLogo(rctx, fc.Args["input"].(types.DeleteOrganizationHorizontalLogoInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.DeleteOrganizationHorizontalLogoPayload) + fc.Result = res + return ec.marshalNDeleteOrganizationHorizontalLogoPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteOrganizationHorizontalLogo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "organization": + return ec.fieldContext_DeleteOrganizationHorizontalLogoPayload_organization(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DeleteOrganizationHorizontalLogoPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteOrganizationHorizontalLogo_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_deleteOrganization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_deleteOrganization(ctx, field) if err != nil { @@ -43173,6 +43423,8 @@ func (ec *executionContext) fieldContext_Nonconformity_organization(_ context.Co return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -44234,6 +44486,8 @@ func (ec *executionContext) fieldContext_Obligation_organization(_ context.Conte return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -45175,6 +45429,47 @@ func (ec *executionContext) fieldContext_Organization_logoUrl(_ context.Context, return fc, nil } +func (ec *executionContext) _Organization_horizontalLogoUrl(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Organization().HorizontalLogoURL(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Organization_horizontalLogoUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Organization", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Organization_description(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Organization_description(ctx, field) if err != nil { @@ -46872,6 +47167,8 @@ func (ec *executionContext) fieldContext_OrganizationEdge_node(_ context.Context return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -47961,6 +48258,8 @@ func (ec *executionContext) fieldContext_ProcessingActivity_organization(_ conte return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -50638,6 +50937,8 @@ func (ec *executionContext) fieldContext_Risk_organization(_ context.Context, fi return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -51538,6 +51839,8 @@ func (ec *executionContext) fieldContext_Snapshot_organization(_ context.Context return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -52451,6 +52754,8 @@ func (ec *executionContext) fieldContext_Task_organization(_ context.Context, fi return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -53335,6 +53640,8 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -56374,6 +56681,8 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization( return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -58414,6 +58723,8 @@ func (ec *executionContext) fieldContext_Vendor_organization(_ context.Context, return ec.fieldContext_Organization_name(ctx, field) case "logoUrl": return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) case "description": return ec.fieldContext_Organization_description(ctx, field) case "websiteUrl": @@ -68401,6 +68712,33 @@ func (ec *executionContext) unmarshalInputDeleteObligationInput(ctx context.Cont return it, nil } +func (ec *executionContext) unmarshalInputDeleteOrganizationHorizontalLogoInput(ctx context.Context, obj any) (types.DeleteOrganizationHorizontalLogoInput, error) { + var it types.DeleteOrganizationHorizontalLogoInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"organizationId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "organizationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OrganizationID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteOrganizationInput(ctx context.Context, obj any) (types.DeleteOrganizationInput, error) { var it types.DeleteOrganizationInput asMap := map[string]any{} @@ -70950,7 +71288,7 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "name", "logo", "description", "websiteUrl", "email", "headquarterAddress"} + fieldsInOrder := [...]string{"organizationId", "name", "logo", "horizontalLogoFile", "description", "websiteUrl", "email", "headquarterAddress"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -70978,6 +71316,13 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co return it, err } it.Logo = data + case "horizontalLogoFile": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("horizontalLogoFile")) + data, err := ec.unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v) + if err != nil { + return it, err + } + it.HorizontalLogoFile = data case "description": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description")) data, err := ec.unmarshalOString2ᚖstring(ctx, v) @@ -76888,6 +77233,45 @@ func (ec *executionContext) _DeleteObligationPayload(ctx context.Context, sel as return out } +var deleteOrganizationHorizontalLogoPayloadImplementors = []string{"DeleteOrganizationHorizontalLogoPayload"} + +func (ec *executionContext) _DeleteOrganizationHorizontalLogoPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteOrganizationHorizontalLogoPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deleteOrganizationHorizontalLogoPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DeleteOrganizationHorizontalLogoPayload") + case "organization": + out.Values[i] = ec._DeleteOrganizationHorizontalLogoPayload_organization(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var deleteOrganizationPayloadImplementors = []string{"DeleteOrganizationPayload"} func (ec *executionContext) _DeleteOrganizationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteOrganizationPayload) graphql.Marshaler { @@ -79771,6 +80155,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "deleteOrganizationHorizontalLogo": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteOrganizationHorizontalLogo(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "deleteOrganization": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_deleteOrganization(ctx, field) @@ -81228,6 +81619,39 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "horizontalLogoUrl": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Organization_horizontalLogoUrl(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "description": out.Values[i] = ec._Organization_description(ctx, field, obj) @@ -91639,6 +92063,25 @@ func (ec *executionContext) marshalNDeleteObligationPayload2ᚖgithubᚗcomᚋge return ec._DeleteObligationPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDeleteOrganizationHorizontalLogoInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoInput(ctx context.Context, v any) (types.DeleteOrganizationHorizontalLogoInput, error) { + res, err := ec.unmarshalInputDeleteOrganizationHorizontalLogoInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDeleteOrganizationHorizontalLogoPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoPayload(ctx context.Context, sel ast.SelectionSet, v types.DeleteOrganizationHorizontalLogoPayload) graphql.Marshaler { + return ec._DeleteOrganizationHorizontalLogoPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDeleteOrganizationHorizontalLogoPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteOrganizationHorizontalLogoPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DeleteOrganizationHorizontalLogoPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNDeleteOrganizationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteOrganizationInput(ctx context.Context, v any) (types.DeleteOrganizationInput, error) { res, err := ec.unmarshalInputDeleteOrganizationInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 045d686c0..987b899ee 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -831,6 +831,14 @@ type DeleteObligationPayload struct { DeletedObligationID gid.GID `json:"deletedObligationId"` } +type DeleteOrganizationHorizontalLogoInput struct { + OrganizationID gid.GID `json:"organizationId"` +} + +type DeleteOrganizationHorizontalLogoPayload struct { + Organization *Organization `json:"organization"` +} + type DeleteOrganizationInput struct { OrganizationID gid.GID `json:"organizationId"` } @@ -1288,6 +1296,7 @@ type Organization struct { ID gid.GID `json:"id"` Name string `json:"name"` LogoURL *string `json:"logoUrl,omitempty"` + HorizontalLogoURL *string `json:"horizontalLogoUrl,omitempty"` Description *string `json:"description,omitempty"` WebsiteURL *string `json:"websiteUrl,omitempty"` Email *string `json:"email,omitempty"` @@ -1798,6 +1807,7 @@ type UpdateOrganizationInput struct { OrganizationID gid.GID `json:"organizationId"` Name *string `json:"name,omitempty"` Logo *graphql.Upload `json:"logo,omitempty"` + HorizontalLogoFile *graphql.Upload `json:"horizontalLogoFile,omitempty"` Description *string `json:"description,omitempty"` WebsiteURL *string `json:"websiteUrl,omitempty"` Email *string `json:"email,omitempty"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 046e871c0..f6d7ea1e2 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1099,9 +1099,18 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.U } } + if input.HorizontalLogoFile != nil { + req.HorizontalLogoFile = &probo.File{ + Filename: input.HorizontalLogoFile.Filename, + ContentType: input.HorizontalLogoFile.ContentType, + Size: input.HorizontalLogoFile.Size, + Content: input.HorizontalLogoFile.File, + } + } + organization, err := prb.Organizations.Update(ctx, req) if err != nil { - return nil, fmt.Errorf("cannot update organization: %w", err) + panic(fmt.Errorf("cannot update organization: %w", err)) } return &types.UpdateOrganizationPayload{ @@ -1109,6 +1118,20 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.U }, nil } +// DeleteOrganizationHorizontalLogo is the resolver for the deleteOrganizationHorizontalLogo field. +func (r *mutationResolver) DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) { + prb := r.ProboService(ctx, input.OrganizationID.TenantID()) + + organization, err := prb.Organizations.DeleteHorizontalLogo(ctx, input.OrganizationID) + if err != nil { + return nil, fmt.Errorf("cannot delete horizontal logo: %w", err) + } + + return &types.DeleteOrganizationHorizontalLogoPayload{ + Organization: types.NewOrganization(organization), + }, nil +} + // DeleteOrganization is the resolver for the deleteOrganization field. func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) { prb := r.ProboService(ctx, input.OrganizationID.TenantID()) @@ -3497,6 +3520,13 @@ func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organizat return prb.Organizations.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) } +// HorizontalLogoURL is the resolver for the horizontalLogoUrl field. +func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) { + prb := r.ProboService(ctx, obj.ID.TenantID()) + + return prb.Organizations.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour) +} + // Users is the resolver for the users field. func (r *organizationResolver) Users(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) (*types.UserConnection, error) { pageOrderBy := page.OrderBy[coredata.UserOrderField]{ diff --git a/pkg/trust/document_service.go b/pkg/trust/document_service.go index 03ec4954d..c3a9bb087 100644 --- a/pkg/trust/document_service.go +++ b/pkg/trust/document_service.go @@ -141,6 +141,7 @@ func (s *DocumentService) exportPDFData( document := &coredata.Document{} version := &coredata.DocumentVersion{} owner := &coredata.People{} + organization := &coredata.Organization{} err := s.svc.pg.WithConn( ctx, @@ -161,6 +162,10 @@ func (s *DocumentService) exportPDFData( return fmt.Errorf("cannot load document owner: %w", err) } + if err := organization.LoadByID(ctx, conn, s.svc.scope, document.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + return nil }, ) @@ -177,13 +182,28 @@ func (s *DocumentService) exportPDFData( classification = docgen.ClassificationSecret } + horizontalLogoBase64 := "" + if organization.HorizontalLogoFileID != nil { + fileRecord := &coredata.File{} + fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID) + }) + if fileErr == nil { + base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord) + if logoErr == nil { + horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) + } + } + } + docData := docgen.DocumentData{ - Title: version.Title, - Content: version.Content, - Version: version.VersionNumber, - Classification: classification, - Approver: owner.FullName, - PublishedAt: version.PublishedAt, + Title: version.Title, + Content: version.Content, + Version: version.VersionNumber, + Classification: classification, + Approver: owner.FullName, + PublishedAt: version.PublishedAt, + CompanyHorizontalLogoBase64: horizontalLogoBase64, } htmlContent, err := docgen.RenderHTML(docData) diff --git a/pkg/trust/organization_service.go b/pkg/trust/organization_service.go index 9576c49e9..5cb37cb7a 100644 --- a/pkg/trust/organization_service.go +++ b/pkg/trust/organization_service.go @@ -71,19 +71,30 @@ func (s OrganizationService) GenerateLogoURL( return nil, fmt.Errorf("cannot get organization: %w", err) } - if organization.LogoObjectKey == "" { + if organization.LogoFileID == nil { return nil, nil } + file := &coredata.File{} + err = s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return file.LoadByID(ctx, conn, s.svc.scope, *organization.LogoFileID) + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot load file: %w", err) + } + presignClient := s3.NewPresignClient(s.svc.s3) - encodedFilename := url.QueryEscape(organization.Name) + encodedFilename := url.QueryEscape(file.FileName) contentDisposition := fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", encodedFilename, encodedFilename) presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(s.svc.bucket), - Key: aws.String(organization.LogoObjectKey), + Key: aws.String(file.FileKey), ResponseCacheControl: aws.String("max-age=3600, public"), ResponseContentDisposition: aws.String(contentDisposition), }, func(opts *s3.PresignOptions) { diff --git a/pkg/trust/service.go b/pkg/trust/service.go index 2b735ebb8..f261b034e 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -18,6 +18,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/crypto/cipher" + "github.com/getprobo/probo/pkg/filemanager" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/html2pdf" "github.com/getprobo/probo/pkg/probo" @@ -35,27 +36,29 @@ type ( tokenSecret string usrmgr *usrmgr.Service html2pdfConverter *html2pdf.Converter + fileManager *filemanager.Service } TenantService struct { - pg *pg.Client - s3 *s3.Client - bucket string - scope coredata.Scoper - proboSvc *probo.Service - encryptionKey cipher.EncryptionKey - tokenSecret string - usrmgr *usrmgr.Service - html2pdfConverter *html2pdf.Converter - TrustCenters *TrustCenterService - Documents *DocumentService - Audits *AuditService - Vendors *VendorService - Frameworks *FrameworkService - TrustCenterAccesses *TrustCenterAccessService + pg *pg.Client + s3 *s3.Client + bucket string + scope coredata.Scoper + proboSvc *probo.Service + encryptionKey cipher.EncryptionKey + tokenSecret string + usrmgr *usrmgr.Service + html2pdfConverter *html2pdf.Converter + fileManager *filemanager.Service + TrustCenters *TrustCenterService + Documents *DocumentService + Audits *AuditService + Vendors *VendorService + Frameworks *FrameworkService + TrustCenterAccesses *TrustCenterAccessService TrustCenterReferences *TrustCenterReferenceService - Reports *ReportService - Organizations *OrganizationService + Reports *ReportService + Organizations *OrganizationService } ) @@ -67,6 +70,7 @@ func NewService( tokenSecret string, usrmgr *usrmgr.Service, html2pdfConverter *html2pdf.Converter, + fileManagerService *filemanager.Service, ) *Service { return &Service{ pg: pgClient, @@ -76,6 +80,7 @@ func NewService( tokenSecret: tokenSecret, usrmgr: usrmgr, html2pdfConverter: html2pdfConverter, + fileManager: fileManagerService, } } @@ -90,6 +95,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { tokenSecret: s.tokenSecret, usrmgr: s.usrmgr, html2pdfConverter: s.html2pdfConverter, + fileManager: s.fileManager, } tenantService.TrustCenters = &TrustCenterService{svc: tenantService}
Classification:Classification {{.Classification | classificationString}}
Approver:Approver {{.Approver}}
Published:Published {{.PublishedAt.Format "January 2, 2006"}}