Add company name and logo to documents

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-10-08 16:04:27 +02:00
parent 9bdecdf232
commit d757c9604b
23 changed files with 1391 additions and 203 deletions

View File

@@ -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"
}
};
})();

View File

@@ -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<HTMLInputElement> = (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")}
</FileButton>
</div>
</div>
<div>
<Label>{__("Horizontal logo")}</Label>
<p className="text-sm text-txt-tertiary mb-2">
{__("Upload a horizontal version of your logo for use in documents")}
</p>
<div className="flex items-center gap-4">
{organization.horizontalLogoUrl && (
<div className="border border-border-solid rounded-md p-4 bg-surface-secondary">
<img
src={organization.horizontalLogoUrl}
alt={__("Horizontal logo")}
className="h-12 max-w-xs object-contain"
/>
</div>
)}
<FileButton
disabled={formState.isSubmitting}
onChange={updateHorizontalLogo}
variant="secondary"
accept="image/png,image/jpeg,image/jpg"
>
{organization.horizontalLogoUrl ? __("Change horizontal logo") : __("Upload horizontal logo")}
</FileButton>
{organization.horizontalLogoUrl && (
<Dialog
ref={deleteDialogRef}
trigger={
<Button
variant="quaternary"
icon={IconTrashCan}
aria-label={__("Delete horizontal logo")}
className="text-red-600 hover:text-red-700"
/>
}
title={__("Delete Horizontal Logo")}
className="max-w-md"
>
<DialogContent padded>
<p className="text-txt-secondary">
{__("Are you sure you want to delete the horizontal logo?")}
</p>
<p className="text-txt-secondary mt-2">
{__("This action cannot be undone.")}
</p>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
onClick={handleDeleteHorizontalLogo}
disabled={isDeletingHorizontalLogo}
icon={isDeletingHorizontalLogo ? Spinner : IconTrashCan}
>
{isDeletingHorizontalLogo ? __("Deleting...") : __("Delete")}
</Button>
</DialogFooter>
</Dialog>
)}
</div>
</div>
<Field
{...register("name")}
readOnly={formState.isSubmitting}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<daff4e27d1bdd110777f99826abd4e17>>
* @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;

View File

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

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<892a78db951d547b53d595d219b94e75>>
* @generated SignedSource<<ce571f7246b6f7500a132f28c8081f84>>
* @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;

View File

@@ -20,13 +20,20 @@ export function FileButton({
children,
icon: IconComponent,
ref,
accept,
...props
}: Props) {
return (
<label className={button({ ...props })}>
{IconComponent && <IconComponent size={16} className="flex-none" />}
{children}
<input type="file" onChange={onChange} hidden ref={ref} />
<input
type="file"
onChange={onChange}
hidden
ref={ref}
{...(accept && { accept })}
/>
</label>
);
}

View File

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

View File

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

View File

@@ -53,6 +53,9 @@ var (
}
return template.HTML(buf.String())
},
"imgTag": func(src, alt, class string) template.HTML {
return template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" class="%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 {

View File

@@ -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 @@
<div class="document-container">
<div class="page-section">
<div class="document-header">
{{- if .CompanyHorizontalLogoBase64}}
<div class="company-header">
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
</div>
{{- end}}
<h1 class="document-title">{{.Title}}</h1>
<div class="document-meta">
<table class="meta-table">
<tr>
<td>Classification:</td>
<td>Classification</td>
<td>
<span class="classification">{{.Classification | classificationString}}</span>
</td>
</tr>
<tr>
<td>Approver:</td>
<td>Approver</td>
<td>{{.Approver}}</td>
</tr>
<tr>
@@ -332,7 +349,7 @@
</tr>
{{- if .PublishedAt}}
<tr>
<td>Published:</td>
<td>Published</td>
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
</tr>
{{- end}}

148
pkg/filemanager/service.go Normal file
View File

@@ -0,0 +1,148 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.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 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
}

View File

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

View File

@@ -285,6 +285,7 @@ func (s FrameworkService) Export(
exportedPDF, err := exportDocumentPDF(
ctx,
s.svc,
s.html2pdfConverter,
conn,
s.svc.scope,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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