Add vendor risk assement

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-04-22 08:03:35 -07:00
parent f038642ece
commit e2e8f38f97
17 changed files with 1275 additions and 582 deletions

View File

@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
### Added
- New "Risk assessments" tab for vendors that allows you to:
- View all risk assessments for a vendor in one place
- Create new risk assessments with data sensitivity and business impact ratings
- Track assessment expiration dates
- Automatic people record creation when accepting invitations
- New vendors in the built-in lists
- Introduced a connector framework enabling integration with external
services:
- Add OAuth2 connector implementation
### Changed
- Completely redesigned vendor detail page with a cleaner, more intuitive layout
- Improved compliance reports table with better file size formatting and date display
- People may be linked to user
## [0.12.0] - 2025-04-20
### Added

View File

@@ -85,7 +85,6 @@ const vendorListFragment = graphql`
description
createdAt
updatedAt
riskTier
}
}
pageInfo {
@@ -111,7 +110,6 @@ const createVendorMutation = graphql`
description
createdAt
updatedAt
riskTier
}
}
}
@@ -322,8 +320,6 @@ function ListVendorContent({
statusPageUrl: vendor.statusPageUrl,
termsOfServiceUrl: vendor.termsOfServiceUrl,
serviceStartAt: new Date().toISOString(),
serviceCriticality: "LOW",
riskTier: "GENERAL",
},
},
onCompleted() {

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<178462391e73ba2adb212005c3813f33>>
* @generated SignedSource<<ccdef4cce3d167b401f6d89dea1dde33>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,8 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
export type CreateVendorInput = {
businessOwnerId?: string | null | undefined;
category?: string | null | undefined;
@@ -22,10 +20,8 @@ export type CreateVendorInput = {
name: string;
organizationId: string;
privacyPolicyUrl?: string | null | undefined;
riskTier: RiskTier;
securityOwnerId?: string | null | undefined;
securityPageUrl?: string | null | undefined;
serviceCriticality: ServiceCriticality;
serviceLevelAgreementUrl?: string | null | undefined;
serviceStartAt: string;
serviceTerminationAt?: string | null | undefined;
@@ -46,7 +42,6 @@ export type ListVendorViewCreateVendorMutation$data = {
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly riskTier: RiskTier;
readonly updatedAt: string;
};
};
@@ -125,13 +120,6 @@ v3 = {
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"storageKey": null
}
],
"storageKey": null
@@ -205,16 +193,16 @@ return {
]
},
"params": {
"cacheID": "6b3c93935ce5b90fead6aadd70992667",
"cacheID": "03c1d61a3a252ca74bd8da1821f1e0b9",
"id": null,
"metadata": {},
"name": "ListVendorViewCreateVendorMutation",
"operationKind": "mutation",
"text": "mutation ListVendorViewCreateVendorMutation(\n $input: CreateVendorInput!\n) {\n createVendor(input: $input) {\n vendorEdge {\n node {\n id\n name\n description\n createdAt\n updatedAt\n riskTier\n }\n }\n }\n}\n"
"text": "mutation ListVendorViewCreateVendorMutation(\n $input: CreateVendorInput!\n) {\n createVendor(input: $input) {\n vendorEdge {\n node {\n id\n name\n description\n createdAt\n updatedAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d75423060ceec238c1cc6b38f79be4e1";
(node as any).hash = "05ca888317537533f7345000f0948c0c";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<49f537ed24daee25a89fb244341ba874>>
* @generated SignedSource<<a59c7eee4b53c2b74df89a2fa693d9cd>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -224,13 +224,6 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"storageKey": null
},
(v10/*: any*/)
],
"storageKey": null
@@ -320,16 +313,16 @@ return {
]
},
"params": {
"cacheID": "f48cd9d88c871edac4c400dc2fa20e9a",
"cacheID": "b4165eeba6363c964b11f14f678d5338",
"id": null,
"metadata": {},
"name": "ListVendorViewPaginationQuery",
"operationKind": "query",
"text": "query ListVendorViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...ListVendorView_vendors_pbnwq\n id\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n riskTier\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
"text": "query ListVendorViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...ListVendorView_vendors_pbnwq\n id\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "6e3badec3308017fd318d4bd1dc41b44";
(node as any).hash = "477e430705f0f3cbadb8dfac89e24629";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<faaaba37922ca105627be876b05672a7>>
* @generated SignedSource<<bdea7e121ea26a5af6985b4399be4bcc>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -226,13 +226,6 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"storageKey": null
},
(v11/*: any*/)
],
"storageKey": null
@@ -322,12 +315,12 @@ return {
]
},
"params": {
"cacheID": "f0f91262680529a635110382be5a2b03",
"cacheID": "d93a40f389769d5895b502cb8e7a2c16",
"id": null,
"metadata": {},
"name": "ListVendorViewQuery",
"operationKind": "query",
"text": "query ListVendorViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...ListVendorView_vendors_pbnwq\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n riskTier\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
"text": "query ListVendorViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...ListVendorView_vendors_pbnwq\n }\n}\n\nfragment ListVendorView_vendors_pbnwq on Organization {\n vendors(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b85fbf05b6b7f76bd8ceb87383bfd4b3>>
* @generated SignedSource<<1ea33fd638cd871d891e3cd41e1f3372>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
import { FragmentRefs } from "relay-runtime";
export type ListVendorView_vendors$data = {
readonly id: string;
@@ -21,7 +20,6 @@ export type ListVendorView_vendors$data = {
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly riskTier: RiskTier;
readonly updatedAt: string;
};
}>;
@@ -169,13 +167,6 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -257,6 +248,6 @@ return {
};
})();
(node as any).hash = "6e3badec3308017fd318d4bd1dc41b44";
(node as any).hash = "477e430705f0f3cbadb8dfac89e24629";
export default node;

View File

@@ -0,0 +1,238 @@
/**
* @generated SignedSource<<8372d50934049d43afb678c894f40dbd>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type BusinessImpact = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM";
export type DataSensitivity = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM" | "NONE";
export type CreateVendorRiskAssessmentInput = {
assessedBy: string;
attachments?: ReadonlyArray<string> | null | undefined;
businessImpact: BusinessImpact;
dataSensitivity: DataSensitivity;
expiresAt: string;
notes?: string | null | undefined;
vendorId: string;
};
export type VendorViewCreateRiskAssessmentMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateVendorRiskAssessmentInput;
};
export type VendorViewCreateRiskAssessmentMutation$data = {
readonly createVendorRiskAssessment: {
readonly vendorRiskAssessmentEdge: {
readonly node: {
readonly assessedAt: string;
readonly assessedBy: {
readonly fullName: string;
readonly id: string;
};
readonly businessImpact: BusinessImpact;
readonly createdAt: string;
readonly dataSensitivity: DataSensitivity;
readonly expiresAt: string;
readonly id: string;
readonly notes: string | null | undefined;
};
};
};
};
export type VendorViewCreateRiskAssessmentMutation = {
response: VendorViewCreateRiskAssessmentMutation$data;
variables: VendorViewCreateRiskAssessmentMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessmentEdge",
"kind": "LinkedField",
"name": "vendorRiskAssessmentEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessment",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "assessedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataSensitivity",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "businessImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "notes",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assessedBy",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "VendorViewCreateRiskAssessmentMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateVendorRiskAssessmentPayload",
"kind": "LinkedField",
"name": "createVendorRiskAssessment",
"plural": false,
"selections": [
(v4/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "VendorViewCreateRiskAssessmentMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateVendorRiskAssessmentPayload",
"kind": "LinkedField",
"name": "createVendorRiskAssessment",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "vendorRiskAssessmentEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "1238320b8db75088917d0e5796652f50",
"id": null,
"metadata": {},
"name": "VendorViewCreateRiskAssessmentMutation",
"operationKind": "mutation",
"text": "mutation VendorViewCreateRiskAssessmentMutation(\n $input: CreateVendorRiskAssessmentInput!\n) {\n createVendorRiskAssessment(input: $input) {\n vendorRiskAssessmentEdge {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n assessedBy {\n id\n fullName\n }\n createdAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "7def769f97c5a728a7331756a6e3c5b2";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<62c3cb3473246b0326ff4cbd4cc4f940>>
* @generated SignedSource<<bbfa9d7b4ab300f8ab96380582d2d9b7>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
export type BusinessImpact = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM";
export type DataSensitivity = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM" | "NONE";
export type VendorViewQuery$variables = {
organizationId: string;
vendorId: string;
@@ -44,13 +44,28 @@ export type VendorViewQuery$data = {
readonly legalName?: string | null | undefined;
readonly name?: string;
readonly privacyPolicyUrl?: string | null | undefined;
readonly riskTier?: RiskTier;
readonly riskAssessments?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly assessedAt: string;
readonly assessedBy: {
readonly fullName: string;
readonly id: string;
};
readonly businessImpact: BusinessImpact;
readonly createdAt: string;
readonly dataSensitivity: DataSensitivity;
readonly expiresAt: string;
readonly id: string;
readonly notes: string | null | undefined;
};
}>;
};
readonly securityOwner?: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly securityPageUrl?: string | null | undefined;
readonly serviceCriticality?: ServiceCriticality;
readonly serviceLevelAgreementUrl?: string | null | undefined;
readonly serviceStartAt?: string;
readonly serviceTerminationAt?: string | null | undefined;
@@ -126,153 +141,139 @@ v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceCriticality",
"name": "statusPageUrl",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"name": "termsOfServiceUrl",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "statusPageUrl",
"name": "privacyPolicyUrl",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "termsOfServiceUrl",
"name": "serviceLevelAgreementUrl",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"name": "dataProcessingAgreementUrl",
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceLevelAgreementUrl",
"name": "securityPageUrl",
"storageKey": null
},
v14 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataProcessingAgreementUrl",
"name": "trustPageUrl",
"storageKey": null
},
v15 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "securityPageUrl",
"name": "certifications",
"storageKey": null
},
v16 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "trustPageUrl",
"name": "headquarterAddress",
"storageKey": null
},
v17 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "certifications",
"name": "legalName",
"storageKey": null
},
v18 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"name": "websiteUrl",
"storageKey": null
},
v19 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "legalName",
"storageKey": null
},
v20 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
v21 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v22 = [
v20 = [
(v3/*: any*/),
(v21/*: any*/)
(v19/*: any*/)
],
v23 = {
v21 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "businessOwner",
"plural": false,
"selections": (v22/*: any*/),
"selections": (v20/*: any*/),
"storageKey": null
},
v24 = {
v22 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "securityOwner",
"plural": false,
"selections": (v22/*: any*/),
"selections": (v20/*: any*/),
"storageKey": null
},
v25 = {
v23 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v26 = {
v24 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
v27 = {
v25 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v28 = {
v26 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v29 = {
v27 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -297,7 +298,7 @@ v29 = {
],
"storageKey": null
},
v30 = [
v28 = [
{
"alias": null,
"args": null,
@@ -350,34 +351,108 @@ v30 = [
"name": "fileSize",
"storageKey": null
},
(v25/*: any*/),
(v27/*: any*/)
(v23/*: any*/),
(v25/*: any*/)
],
"storageKey": null
},
(v28/*: any*/)
(v26/*: any*/)
],
"storageKey": null
},
(v29/*: any*/)
(v27/*: any*/)
],
v31 = [
v29 = [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessmentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorRiskAssessment",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "assessedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "expiresAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataSensitivity",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "businessImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "notes",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assessedBy",
"plural": false,
"selections": (v20/*: any*/),
"storageKey": null
},
(v23/*: any*/),
(v25/*: any*/)
],
"storageKey": null
},
(v26/*: any*/)
],
"storageKey": null
},
(v27/*: any*/)
],
v30 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v32 = {
v31 = {
"kind": "Literal",
"name": "first",
"value": 100
},
v33 = [
(v32/*: any*/)
v32 = [
(v31/*: any*/)
],
v34 = [
(v32/*: any*/),
v33 = [
(v31/*: any*/),
{
"kind": "Literal",
"name": "orderBy",
@@ -424,12 +499,10 @@ return {
(v16/*: any*/),
(v17/*: any*/),
(v18/*: any*/),
(v19/*: any*/),
(v20/*: any*/),
(v21/*: any*/),
(v22/*: any*/),
(v23/*: any*/),
(v24/*: any*/),
(v25/*: any*/),
(v26/*: any*/),
{
"alias": "complianceReports",
"args": null,
@@ -437,7 +510,17 @@ return {
"kind": "LinkedField",
"name": "__VendorView_complianceReports_connection",
"plural": false,
"selections": (v30/*: any*/),
"selections": (v28/*: any*/),
"storageKey": null
},
{
"alias": "riskAssessments",
"args": null,
"concreteType": "VendorRiskAssessmentConnection",
"kind": "LinkedField",
"name": "__VendorView_riskAssessments_connection",
"plural": false,
"selections": (v29/*: any*/),
"storageKey": null
}
],
@@ -449,7 +532,7 @@ return {
},
{
"alias": "organization",
"args": (v31/*: any*/),
"args": (v30/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -484,7 +567,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v27/*: any*/),
(v25/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
@@ -504,30 +587,47 @@ return {
(v16/*: any*/),
(v17/*: any*/),
(v18/*: any*/),
(v19/*: any*/),
(v20/*: any*/),
(v21/*: any*/),
(v22/*: any*/),
(v23/*: any*/),
(v24/*: any*/),
(v25/*: any*/),
(v26/*: any*/),
{
"alias": null,
"args": (v33/*: any*/),
"args": (v32/*: any*/),
"concreteType": "VendorComplianceReportConnection",
"kind": "LinkedField",
"name": "complianceReports",
"plural": false,
"selections": (v30/*: any*/),
"selections": (v28/*: any*/),
"storageKey": "complianceReports(first:100)"
},
{
"alias": null,
"args": (v33/*: any*/),
"args": (v32/*: any*/),
"filters": null,
"handle": "connection",
"key": "VendorView_complianceReports",
"kind": "LinkedHandle",
"name": "complianceReports"
},
{
"alias": null,
"args": (v32/*: any*/),
"concreteType": "VendorRiskAssessmentConnection",
"kind": "LinkedField",
"name": "riskAssessments",
"plural": false,
"selections": (v29/*: any*/),
"storageKey": "riskAssessments(first:100)"
},
{
"alias": null,
"args": (v32/*: any*/),
"filters": null,
"handle": "connection",
"key": "VendorView_riskAssessments",
"kind": "LinkedHandle",
"name": "riskAssessments"
}
],
"type": "Vendor",
@@ -538,20 +638,20 @@ return {
},
{
"alias": "organization",
"args": (v31/*: any*/),
"args": (v30/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v27/*: any*/),
(v25/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v34/*: any*/),
"args": (v33/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
@@ -574,7 +674,7 @@ return {
"plural": false,
"selections": [
(v3/*: any*/),
(v21/*: any*/),
(v19/*: any*/),
{
"alias": null,
"args": null,
@@ -582,21 +682,21 @@ return {
"name": "primaryEmailAddress",
"storageKey": null
},
(v27/*: any*/)
(v25/*: any*/)
],
"storageKey": null
},
(v28/*: any*/)
(v26/*: any*/)
],
"storageKey": null
},
(v29/*: any*/)
(v27/*: any*/)
],
"storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
},
{
"alias": null,
"args": (v34/*: any*/),
"args": (v33/*: any*/),
"filters": [
"orderBy"
],
@@ -615,7 +715,7 @@ return {
]
},
"params": {
"cacheID": "337beb9fa1d1d29bcd974229385d8e7a",
"cacheID": "5df12f781d305bd4bd6b6c0ccb4d947e",
"id": null,
"metadata": {
"connection": [
@@ -627,16 +727,25 @@ return {
"node",
"complianceReports"
]
},
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"node",
"riskAssessments"
]
}
]
},
"name": "VendorViewQuery",
"operationKind": "query",
"text": "query VendorViewQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n securityPageUrl\n trustPageUrl\n certifications\n headquarterAddress\n legalName\n websiteUrl\n businessOwner {\n id\n fullName\n }\n securityOwner {\n id\n fullName\n }\n createdAt\n updatedAt\n complianceReports(first: 100) {\n edges {\n node {\n id\n reportName\n reportDate\n validUntil\n fileUrl\n fileSize\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
"text": "query VendorViewQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n securityPageUrl\n trustPageUrl\n certifications\n headquarterAddress\n legalName\n websiteUrl\n businessOwner {\n id\n fullName\n }\n securityOwner {\n id\n fullName\n }\n createdAt\n updatedAt\n complianceReports(first: 100) {\n edges {\n node {\n id\n reportName\n reportDate\n validUntil\n fileUrl\n fileSize\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n riskAssessments(first: 100) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n assessedBy {\n id\n fullName\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "4a621712e4dfaebf9ad42232ba7f713a";
(node as any).hash = "2b856ea721d1bfb6b853fc281ee1aac6";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e8b13d28eed7587f5fbefcc073c53dc4>>
* @generated SignedSource<<574565aac89550e684dc54b21327b565>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,8 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
export type UpdateVendorInput = {
businessOwnerId?: string | null | undefined;
category?: string | null | undefined;
@@ -22,10 +20,8 @@ export type UpdateVendorInput = {
legalName?: string | null | undefined;
name?: string | null | undefined;
privacyPolicyUrl?: string | null | undefined;
riskTier?: RiskTier | null | undefined;
securityOwnerId?: string | null | undefined;
securityPageUrl?: string | null | undefined;
serviceCriticality?: ServiceCriticality | null | undefined;
serviceLevelAgreementUrl?: string | null | undefined;
serviceStartAt?: string | null | undefined;
serviceTerminationAt?: string | null | undefined;
@@ -52,13 +48,11 @@ export type VendorViewUpdateVendorMutation$data = {
readonly legalName: string | null | undefined;
readonly name: string;
readonly privacyPolicyUrl: string | null | undefined;
readonly riskTier: RiskTier;
readonly securityOwner: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly securityPageUrl: string | null | undefined;
readonly serviceCriticality: ServiceCriticality;
readonly serviceLevelAgreementUrl: string | null | undefined;
readonly serviceStartAt: string;
readonly serviceTerminationAt: string | null | undefined;
@@ -152,20 +146,6 @@ v3 = [
"name": "serviceTerminationAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceCriticality",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -295,16 +275,16 @@ return {
"selections": (v3/*: any*/)
},
"params": {
"cacheID": "d582aaaddaa73ef8e0fdef172364c64e",
"cacheID": "9e2c4c33246b4aa469cdd737be1e690a",
"id": null,
"metadata": {},
"name": "VendorViewUpdateVendorMutation",
"operationKind": "mutation",
"text": "mutation VendorViewUpdateVendorMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n securityPageUrl\n trustPageUrl\n certifications\n headquarterAddress\n legalName\n websiteUrl\n businessOwner {\n id\n fullName\n }\n securityOwner {\n id\n fullName\n }\n updatedAt\n }\n }\n}\n"
"text": "mutation VendorViewUpdateVendorMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n securityPageUrl\n trustPageUrl\n certifications\n headquarterAddress\n legalName\n websiteUrl\n businessOwner {\n id\n fullName\n }\n securityOwner {\n id\n fullName\n }\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "627ef1694ebc04845b019bb65dd72c97";
(node as any).hash = "58c28ca0307e1b27f2ebb72c3c2e7535";
export default node;

View File

@@ -0,0 +1,3 @@
ALTER TABLE risk_assessments RENAME TO vendor_risk_assessments;
ALTER TABLE vendor_risk_assessments ADD COLUMN assessed_at TIMESTAMP WITH TIME ZONE NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE vendor_risk_assessments ADD COLUMN assessed_by TEXT NOT NULL;

View File

@@ -90,7 +90,7 @@ func (r VendorRiskAssessment) Insert(
) error {
q := `
INSERT INTO
risk_assessments (
vendor_risk_assessments (
tenant_id,
id,
vendor_id,
@@ -165,7 +165,7 @@ SELECT
created_at,
updated_at
FROM
risk_assessments
vendor_risk_assessments
WHERE
%s
AND id = @id
@@ -216,7 +216,7 @@ SELECT
created_at,
updated_at
FROM
risk_assessments
vendor_risk_assessments
WHERE
%s
AND vendor_id = @vendor_id
@@ -270,7 +270,7 @@ SELECT
created_at,
updated_at
FROM
risk_assessments
vendor_risk_assessments
WHERE
%s
AND vendor_id = @vendor_id

View File

@@ -19,6 +19,7 @@ import (
"fmt"
"net/http"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/usrmgr"
"go.gearno.de/kit/httpserver"
)
@@ -33,7 +34,7 @@ type (
}
)
func InvitationConfirmationHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc {
func InvitationConfirmationHandler(usrmgrSvc *usrmgr.Service, proboSvc *probo.Service, authCfg AuthConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req InvitationConfirmationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -41,7 +42,7 @@ func InvitationConfirmationHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig
return
}
err := usrmgrSvc.ConfirmInvitation(r.Context(), req.Token, req.Password)
_, err := usrmgrSvc.ConfirmInvitation(r.Context(), req.Token, req.Password)
if err != nil {
httpserver.RenderError(w, http.StatusInternalServerError, err)
return

View File

@@ -80,7 +80,7 @@ func NewMux(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg AuthConf
r.Post("/auth/register", SignUpHandler(usrmgrSvc, authCfg))
r.Post("/auth/login", SignInHandler(usrmgrSvc, authCfg))
r.Delete("/auth/logout", SignOutHandler(usrmgrSvc, authCfg))
r.Post("/auth/invitation", InvitationConfirmationHandler(usrmgrSvc, authCfg))
r.Post("/auth/invitation", InvitationConfirmationHandler(usrmgrSvc, proboSvc, authCfg))
r.Post("/auth/forget-password", ForgetPasswordHandler(usrmgrSvc, authCfg))
r.Post("/auth/reset-password", ResetPasswordHandler(usrmgrSvc, authCfg))

View File

@@ -1585,7 +1585,27 @@ func (r *vendorResolver) ComplianceReports(ctx context.Context, obj *types.Vendo
// RiskAssessments is the resolver for the riskAssessments field.
func (r *vendorResolver) RiskAssessments(ctx context.Context, obj *types.Vendor, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorRiskAssessmentOrder) (*types.VendorRiskAssessmentConnection, error) {
panic(fmt.Errorf("not implemented: RiskAssessments - riskAssessments"))
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.VendorRiskAssessmentOrderField]{
Field: coredata.VendorRiskAssessmentOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.VendorRiskAssessmentOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Vendors.ListRiskAssessments(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("failed to list vendor risk assessments: %w", err))
}
return types.NewVendorRiskAssessmentConnection(page), nil
}
// BusinessOwner is the resolver for the businessOwner field.

View File

@@ -710,31 +710,32 @@ func (s Service) InviteUser(
)
}
func (s Service) ConfirmInvitation(ctx context.Context, tokenString string, password string) error {
func (s Service) ConfirmInvitation(ctx context.Context, tokenString string, password string) (*coredata.User, error) {
token, err := statelesstoken.ValidateToken[InvitationData](
s.tokenSecret,
TokenTypeOrganizationInvitation,
tokenString,
)
if err != nil {
return fmt.Errorf("cannot validate organization invitation token: %w", err)
return nil, fmt.Errorf("cannot validate organization invitation token: %w", err)
}
if len(password) < 8 {
return &ErrInvalidPassword{len(password)}
return nil, &ErrInvalidPassword{len(password)}
}
now := time.Now()
hashedPassword, err := s.hp.HashPassword([]byte(password))
if err != nil {
return fmt.Errorf("cannot hash password: %w", err)
return nil, fmt.Errorf("cannot hash password: %w", err)
}
return s.pg.WithTx(
user := &coredata.User{}
err = s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
user := &coredata.User{}
if err := user.LoadByEmail(ctx, tx, token.Data.Email); err != nil {
var errUserNotFound *coredata.ErrUserNotFound
@@ -766,9 +767,31 @@ func (s Service) ConfirmInvitation(ctx context.Context, tokenString string, pass
return fmt.Errorf("cannot insert user organization: %w", err)
}
people := coredata.People{
OrganizationID: token.Data.OrganizationID,
UserID: &user.ID,
FullName: token.Data.FullName,
PrimaryEmailAddress: token.Data.Email,
Kind: coredata.PeopleKindEmployee,
CreatedAt: now,
UpdatedAt: now,
}
scope := coredata.NewScope(token.Data.OrganizationID.TenantID())
if err := people.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert people: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return user, nil
}
func (s Service) RemoveUser(ctx context.Context, organizationID gid.GID, userID gid.GID) error {