Add policy sign email notif
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import AuthLayout from "./layouts/AuthLayout";
|
|||||||
import { RelayEnvironment } from "./RelayEnvironment";
|
import { RelayEnvironment } from "./RelayEnvironment";
|
||||||
import { AuthenticationRoutes } from "./pages/authentication/Routes";
|
import { AuthenticationRoutes } from "./pages/authentication/Routes";
|
||||||
import { OrganizationsRoutes } from "./pages/organizations/Routes";
|
import { OrganizationsRoutes } from "./pages/organizations/Routes";
|
||||||
|
import SigningRequestsPage from "./pages/SigningRequestsPage";
|
||||||
|
|
||||||
posthog.init(process.env.POSTHOG_KEY!, {
|
posthog.init(process.env.POSTHOG_KEY!, {
|
||||||
api_host: process.env.POSTHOG_HOST,
|
api_host: process.env.POSTHOG_HOST,
|
||||||
@@ -53,6 +54,11 @@ function App() {
|
|||||||
element={<OrganizationsRoutes />}
|
element={<OrganizationsRoutes />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="policies/signing-requests"
|
||||||
|
element={<SigningRequestsPage />}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
235
apps/console/src/pages/SigningRequestsPage.tsx
Normal file
235
apps/console/src/pages/SigningRequestsPage.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useSearchParams } from "react-router";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { buildEndpoint } from "../utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
|
||||||
|
type Document = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
signed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SigningResponse = {
|
||||||
|
documents: Document[];
|
||||||
|
requesterName: string;
|
||||||
|
requesterOrganization: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SigningRequestsPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [signingData, setSigningData] = useState<SigningResponse | null>(null);
|
||||||
|
const [currentDocIndex, setCurrentDocIndex] = useState(0);
|
||||||
|
|
||||||
|
// Fetch documents to sign using the token
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
setError("Missing signing token. Please check your URL and try again.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDocuments() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(buildEndpoint("/api/signing-requests"), {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch signing documents");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: SigningResponse = await response.json();
|
||||||
|
setSigningData(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "An unknown error occurred");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDocuments();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
// Handle document signing
|
||||||
|
const handleSignDocument = async () => {
|
||||||
|
if (!signingData || !token) return;
|
||||||
|
|
||||||
|
const docToSign = signingData.documents[currentDocIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(buildEndpoint(`/api/signing-requests/${docToSign.id}/sign`), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to sign document");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
const updatedDocs = [...signingData.documents];
|
||||||
|
updatedDocs[currentDocIndex] = {
|
||||||
|
...updatedDocs[currentDocIndex],
|
||||||
|
signed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
setSigningData({
|
||||||
|
...signingData,
|
||||||
|
documents: updatedDocs,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to next document if available
|
||||||
|
if (currentDocIndex < updatedDocs.length - 1) {
|
||||||
|
setCurrentDocIndex(currentDocIndex + 1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to sign document");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle going to next document
|
||||||
|
const handleNextDocument = () => {
|
||||||
|
if (signingData && currentDocIndex < signingData.documents.length - 1) {
|
||||||
|
setCurrentDocIndex(currentDocIndex + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate progress
|
||||||
|
const getSignedCount = () => {
|
||||||
|
if (!signingData) return 0;
|
||||||
|
return signingData.documents.filter(doc => doc.signed).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressPercentage = () => {
|
||||||
|
if (!signingData || signingData.documents.length === 0) return 0;
|
||||||
|
return (getSignedCount() / signingData.documents.length) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10 flex justify-center">
|
||||||
|
<Card className="w-full max-w-3xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Loading Signing Requests</CardTitle>
|
||||||
|
<CardDescription>Please wait while we fetch your documents...</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10 flex justify-center">
|
||||||
|
<Card className="w-full max-w-3xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Error</CardTitle>
|
||||||
|
<CardDescription>{error}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter>
|
||||||
|
<Button onClick={() => window.location.reload()}>Try Again</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No documents or missing data
|
||||||
|
if (!signingData || signingData.documents.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10 flex justify-center">
|
||||||
|
<Card className="w-full max-w-3xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>No Documents to Sign</CardTitle>
|
||||||
|
<CardDescription>There are no documents requiring your signature at this time.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current document
|
||||||
|
const currentDoc = signingData.documents[currentDocIndex];
|
||||||
|
const isLastDocument = currentDocIndex === signingData.documents.length - 1;
|
||||||
|
const allSigned = getSignedCount() === signingData.documents.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10">
|
||||||
|
<Helmet>
|
||||||
|
<title>Document Signing - Probo</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-2">Document Signing Request</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
From {signingData.requesterName} at {signingData.requesterOrganization}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{getSignedCount()} of {signingData.documents.length} documents signed
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">{Math.round(getProgressPercentage())}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={getProgressPercentage()} className="h-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{currentDoc.title}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Document {currentDocIndex + 1} of {signingData.documents.length}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<div className="border rounded-md p-4 min-h-[400px] bg-muted/20">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: currentDoc.content }} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
{currentDoc.signed ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-sm text-green-600 font-medium">✓ Signed</div>
|
||||||
|
{!isLastDocument && (
|
||||||
|
<Button onClick={handleNextDocument}>
|
||||||
|
Next Document
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleSignDocument}>
|
||||||
|
Sign Document
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allSigned && (
|
||||||
|
<div className="text-green-600 font-medium">
|
||||||
|
All documents have been signed
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { format } from "date-fns";
|
|||||||
import type { PolicyListViewQuery, PolicyListViewQuery$data } from "./__generated__/PolicyListViewQuery.graphql";
|
import type { PolicyListViewQuery, PolicyListViewQuery$data } from "./__generated__/PolicyListViewQuery.graphql";
|
||||||
import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql";
|
import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql";
|
||||||
import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql";
|
import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql";
|
||||||
|
import type { PolicyListViewSendSigningNotificationsMutation } from "./__generated__/PolicyListViewSendSigningNotificationsMutation.graphql";
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
import { PolicyListViewSkeleton } from "./PolicyListPage";
|
import { PolicyListViewSkeleton } from "./PolicyListPage";
|
||||||
import {
|
import {
|
||||||
@@ -117,6 +118,13 @@ const createPolicyMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const sendSigningNotificationsMutation = graphql`
|
||||||
|
mutation PolicyListViewSendSigningNotificationsMutation($input: SendSigningNotificationsInput!) {
|
||||||
|
sendSigningNotifications(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
function PolicyTableRow({
|
function PolicyTableRow({
|
||||||
policy,
|
policy,
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -360,7 +368,10 @@ function CreatePolicyModal({
|
|||||||
className={`text-4xl leading-tight font-bold outline-none focus:outline-none ${!title ? 'text-gray-400' : 'text-black'}`}
|
className={`text-4xl leading-tight font-bold outline-none focus:outline-none ${!title ? 'text-gray-400' : 'text-black'}`}
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
onInput={(e) => setTitle(e.currentTarget.textContent || "")}
|
onInput={(e) => {
|
||||||
|
const newText = e.currentTarget.textContent || "";
|
||||||
|
setTitle(newText);
|
||||||
|
}}
|
||||||
style={{ WebkitTapHighlightColor: 'transparent' }}
|
style={{ WebkitTapHighlightColor: 'transparent' }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
@@ -368,12 +379,22 @@ function CreatePolicyModal({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
if (!title) {
|
if (e.currentTarget.textContent === "Enter policy title...") {
|
||||||
e.currentTarget.textContent = '';
|
e.currentTarget.textContent = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!e.currentTarget.textContent?.trim()) {
|
||||||
|
e.currentTarget.textContent = "Enter policy title...";
|
||||||
|
setTitle("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el && !el.textContent) {
|
||||||
|
el.textContent = title || "Enter policy title...";
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{title || "Enter policy title..."}
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -452,6 +473,8 @@ function PolicyListViewContent({
|
|||||||
|
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [sendSigningNotifications, _] = useMutation<PolicyListViewSendSigningNotificationsMutation>(sendSigningNotificationsMutation);
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
const handleOpenModal = () => {
|
||||||
setCreateModalOpen(true);
|
setCreateModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -460,14 +483,29 @@ function PolicyListViewContent({
|
|||||||
setCreateModalOpen(open);
|
setCreateModalOpen(open);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSendSigningNotifications = () => {
|
||||||
|
sendSigningNotifications({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId!,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTemplate
|
<PageTemplate
|
||||||
title="Policies"
|
title="Policies"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleOpenModal}>
|
<div>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Button onClick={handleOpenModal}>
|
||||||
New policy
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
New policy
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={handleSendSigningNotifications}>
|
||||||
|
Send signing notifications
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Policy table */}
|
{/* Policy table */}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<ec7d0ef29b8366117ddf75e75669f5fa>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type SendSigningNotificationsInput = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type PolicyListViewSendSigningNotificationsMutation$variables = {
|
||||||
|
input: SendSigningNotificationsInput;
|
||||||
|
};
|
||||||
|
export type PolicyListViewSendSigningNotificationsMutation$data = {
|
||||||
|
readonly sendSigningNotifications: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type PolicyListViewSendSigningNotificationsMutation = {
|
||||||
|
response: PolicyListViewSendSigningNotificationsMutation$data;
|
||||||
|
variables: PolicyListViewSendSigningNotificationsMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "SendSigningNotificationsPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "sendSigningNotifications",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "PolicyListViewSendSigningNotificationsMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "PolicyListViewSendSigningNotificationsMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "a6b25b9e4abf4243131b90b47f4e2ef4",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "PolicyListViewSendSigningNotificationsMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation PolicyListViewSendSigningNotificationsMutation(\n $input: SendSigningNotificationsInput!\n) {\n sendSigningNotifications(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "71b0a1e7d209ed71a9bb6552eaf95cee";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -293,3 +293,52 @@ WHERE %s
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Peoples) LoadAwaitingSigning(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH signatories AS (
|
||||||
|
SELECT
|
||||||
|
signed_by
|
||||||
|
FROM
|
||||||
|
policy_version_signatures
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND state = 'REQUESTED'
|
||||||
|
GROUP BY
|
||||||
|
signed_by
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
kind,
|
||||||
|
user_id,
|
||||||
|
full_name,
|
||||||
|
primary_email_address,
|
||||||
|
additional_email_addresses,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
peoples
|
||||||
|
INNER JOIN signatories ON peoples.id = signatories.signed_by
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, scope.SQLArguments())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query people: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peoples, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[People])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect people: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = peoples
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,6 +53,53 @@ func (pvs PolicyVersionSignature) CursorKey(orderBy PolicyVersionSignatureOrderF
|
|||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pvs *PolicyVersionSignature) LoadByPolicyVersionIDAndSignatory(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
policyVersionID gid.GID,
|
||||||
|
signatory gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
policy_version_id,
|
||||||
|
state,
|
||||||
|
signed_by,
|
||||||
|
signed_at,
|
||||||
|
requested_at,
|
||||||
|
requested_by,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
policy_version_signatures
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND policy_version_id = @policy_version_id
|
||||||
|
AND signed_by = @signatory
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"policy_version_id": policyVersionID, "signatory": signatory}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query policy version signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
policyVersionSignature, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[PolicyVersionSignature])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect policy version signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*pvs = policyVersionSignature
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (pvs *PolicyVersionSignature) LoadByID(
|
func (pvs *PolicyVersionSignature) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -195,3 +242,40 @@ WHERE
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pvs *PolicyVersionSignature) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE policy_version_signatures
|
||||||
|
SET
|
||||||
|
state = @state,
|
||||||
|
signed_by = @signed_by,
|
||||||
|
signed_at = @signed_at,
|
||||||
|
updated_at = @updated_at
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": pvs.ID,
|
||||||
|
"state": pvs.State,
|
||||||
|
"signed_by": pvs.SignedBy,
|
||||||
|
"signed_at": pvs.SignedAt,
|
||||||
|
"updated_at": pvs.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot update policy version signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,19 +3,21 @@ package probo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
|
"github.com/getprobo/probo/pkg/statelesstoken"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PolicyService struct {
|
|
||||||
svc *TenantService
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
PolicyService struct {
|
||||||
|
svc *TenantService
|
||||||
|
}
|
||||||
|
|
||||||
CreatePolicyRequest struct {
|
CreatePolicyRequest struct {
|
||||||
OrganizationID gid.GID
|
OrganizationID gid.GID
|
||||||
Title string
|
Title string
|
||||||
@@ -34,6 +36,15 @@ type (
|
|||||||
RequestedBy gid.GID
|
RequestedBy gid.GID
|
||||||
Signatory gid.GID
|
Signatory gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SigningRequestData struct {
|
||||||
|
OrganizationID gid.GID `json:"organization_id"`
|
||||||
|
PeopleID gid.GID `json:"people_id"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenTypeSigningRequest = "signing_request"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *PolicyService) Get(
|
func (s *PolicyService) Get(
|
||||||
@@ -163,6 +174,125 @@ func (s *PolicyService) Create(
|
|||||||
return policy, policyVersion, nil
|
return policy, policyVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PolicyService) SendSigningNotifications(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) error {
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
var peoples coredata.Peoples
|
||||||
|
if err := peoples.LoadAwaitingSigning(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot load people: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, people := range peoples {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
emailID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.EmailEntityType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create email global id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := statelesstoken.NewToken(
|
||||||
|
s.svc.tokenSecret,
|
||||||
|
TokenTypeSigningRequest,
|
||||||
|
time.Hour*24*7,
|
||||||
|
SigningRequestData{
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
PeopleID: people.ID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create signing request token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signRequestURL := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: s.svc.hostname,
|
||||||
|
Path: "/policies/signing-requests",
|
||||||
|
RawQuery: url.Values{
|
||||||
|
"token": []string{token},
|
||||||
|
}.Encode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
email := &coredata.Email{
|
||||||
|
ID: emailID,
|
||||||
|
RecipientEmail: people.PrimaryEmailAddress,
|
||||||
|
RecipientName: people.FullName,
|
||||||
|
Subject: "Probo - Policies Signing Request",
|
||||||
|
TextBody: fmt.Sprintf("Hi,\nYou have documents awaiting your signature. Please follow this link to sign them: %s", signRequestURL.String()),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert email: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot send signing notifications: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PolicyService) SignPolicyVersion(
|
||||||
|
ctx context.Context,
|
||||||
|
policyVersionID gid.GID,
|
||||||
|
signatory gid.GID,
|
||||||
|
) error {
|
||||||
|
policyVersion := &coredata.PolicyVersion{}
|
||||||
|
policyVersionSignature := &coredata.PolicyVersionSignature{}
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := policyVersion.LoadByID(ctx, conn, s.svc.scope, policyVersionID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load policy version %q: %w", policyVersionID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyVersion.Status != coredata.PolicyStatusPublished {
|
||||||
|
return fmt.Errorf("cannot sign unpublished version")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := policyVersionSignature.LoadByPolicyVersionIDAndSignatory(ctx, conn, s.svc.scope, policyVersionID, signatory); err != nil {
|
||||||
|
return fmt.Errorf("cannot load policy version signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyVersionSignature.State == coredata.PolicyVersionSignatureStateSigned {
|
||||||
|
return fmt.Errorf("policy version already signed")
|
||||||
|
}
|
||||||
|
|
||||||
|
policyVersionSignature.State = coredata.PolicyVersionSignatureStateSigned
|
||||||
|
policyVersionSignature.SignedAt = &now
|
||||||
|
policyVersionSignature.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := policyVersion.Update(ctx, conn, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update policy version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := policyVersionSignature.Update(ctx, conn, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update policy version signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot sign policy version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PolicyService) UpdateVersion(
|
func (s *PolicyService) UpdateVersion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req UpdatePolicyVersionRequest,
|
req UpdatePolicyVersionRequest,
|
||||||
|
|||||||
@@ -31,15 +31,18 @@ type (
|
|||||||
s3 *s3.Client
|
s3 *s3.Client
|
||||||
bucket string
|
bucket string
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
|
hostname string
|
||||||
|
tokenSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
TenantService struct {
|
TenantService struct {
|
||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
s3 *s3.Client
|
s3 *s3.Client
|
||||||
bucket string
|
bucket string
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
scope coredata.Scoper
|
scope coredata.Scoper
|
||||||
|
hostname string
|
||||||
|
tokenSecret string
|
||||||
Frameworks *FrameworkService
|
Frameworks *FrameworkService
|
||||||
Mesures *MesureService
|
Mesures *MesureService
|
||||||
Tasks *TaskService
|
Tasks *TaskService
|
||||||
@@ -61,6 +64,8 @@ func NewService(
|
|||||||
pgClient *pg.Client,
|
pgClient *pg.Client,
|
||||||
s3Client *s3.Client,
|
s3Client *s3.Client,
|
||||||
bucket string,
|
bucket string,
|
||||||
|
hostname string,
|
||||||
|
tokenSecret string,
|
||||||
) (*Service, error) {
|
) (*Service, error) {
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
return nil, fmt.Errorf("bucket is required")
|
return nil, fmt.Errorf("bucket is required")
|
||||||
@@ -71,6 +76,8 @@ func NewService(
|
|||||||
s3: s3Client,
|
s3: s3Client,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
encryptionKey: encryptionKey,
|
encryptionKey: encryptionKey,
|
||||||
|
hostname: hostname,
|
||||||
|
tokenSecret: tokenSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
@@ -82,7 +89,9 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
|
|||||||
s3: s.s3,
|
s3: s.s3,
|
||||||
bucket: s.bucket,
|
bucket: s.bucket,
|
||||||
encryptionKey: s.encryptionKey,
|
encryptionKey: s.encryptionKey,
|
||||||
|
hostname: s.hostname,
|
||||||
scope: coredata.NewScope(tenantID),
|
scope: coredata.NewScope(tenantID),
|
||||||
|
tokenSecret: s.tokenSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
tenantService.Frameworks = &FrameworkService{svc: tenantService}
|
tenantService.Frameworks = &FrameworkService{svc: tenantService}
|
||||||
|
|||||||
@@ -197,7 +197,15 @@ func (impl *Implm) Run(
|
|||||||
return fmt.Errorf("cannot create usrmgr service: %w", err)
|
return fmt.Errorf("cannot create usrmgr service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
proboService, err := probo.NewService(ctx, impl.cfg.EncryptionKey, pgClient, s3Client, impl.cfg.AWS.Bucket)
|
proboService, err := probo.NewService(
|
||||||
|
ctx,
|
||||||
|
impl.cfg.EncryptionKey,
|
||||||
|
pgClient,
|
||||||
|
s3Client,
|
||||||
|
impl.cfg.AWS.Bucket,
|
||||||
|
impl.cfg.Hostname,
|
||||||
|
impl.cfg.Auth.Cookie.Secret,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create probo service: %w", err)
|
return fmt.Errorf("cannot create probo service: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ directive @goModel(
|
|||||||
|
|
||||||
directive @goEnum(value: String) on ENUM_VALUE
|
directive @goEnum(value: String) on ENUM_VALUE
|
||||||
|
|
||||||
|
|
||||||
# Scalars
|
# Scalars
|
||||||
scalar CursorKey
|
scalar CursorKey
|
||||||
scalar Void
|
scalar Void
|
||||||
@@ -1006,6 +1007,7 @@ type Mutation {
|
|||||||
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
|
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
|
||||||
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
|
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
|
||||||
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
|
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
|
||||||
|
sendSigningNotifications(input: SendSigningNotificationsInput!): SendSigningNotificationsPayload!
|
||||||
|
|
||||||
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
|
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
|
||||||
}
|
}
|
||||||
@@ -1648,3 +1650,11 @@ input UpdatePolicyVersionInput {
|
|||||||
type UpdatePolicyVersionPayload {
|
type UpdatePolicyVersionPayload {
|
||||||
policyVersion: PolicyVersion!
|
policyVersion: PolicyVersion!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input SendSigningNotificationsInput {
|
||||||
|
organizationId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendSigningNotificationsPayload {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
@@ -351,6 +351,7 @@ type ComplexityRoot struct {
|
|||||||
RemoveUser func(childComplexity int, input types.RemoveUserInput) int
|
RemoveUser func(childComplexity int, input types.RemoveUserInput) int
|
||||||
RequestEvidence func(childComplexity int, input types.RequestEvidenceInput) int
|
RequestEvidence func(childComplexity int, input types.RequestEvidenceInput) int
|
||||||
RequestSignature func(childComplexity int, input types.RequestSignatureInput) int
|
RequestSignature func(childComplexity int, input types.RequestSignatureInput) int
|
||||||
|
SendSigningNotifications func(childComplexity int, input types.SendSigningNotificationsInput) int
|
||||||
UnassignTask func(childComplexity int, input types.UnassignTaskInput) int
|
UnassignTask func(childComplexity int, input types.UnassignTaskInput) int
|
||||||
UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int
|
UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int
|
||||||
UpdateMesure func(childComplexity int, input types.UpdateMesureInput) int
|
UpdateMesure func(childComplexity int, input types.UpdateMesureInput) int
|
||||||
@@ -537,6 +538,10 @@ type ComplexityRoot struct {
|
|||||||
Node func(childComplexity int) int
|
Node func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SendSigningNotificationsPayload struct {
|
||||||
|
Success func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
Session struct {
|
Session struct {
|
||||||
ExpiresAt func(childComplexity int) int
|
ExpiresAt func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
@@ -778,6 +783,7 @@ type MutationResolver interface {
|
|||||||
CreateDraftPolicyVersion(ctx context.Context, input types.CreateDraftPolicyVersionInput) (*types.CreateDraftPolicyVersionPayload, error)
|
CreateDraftPolicyVersion(ctx context.Context, input types.CreateDraftPolicyVersionInput) (*types.CreateDraftPolicyVersionPayload, error)
|
||||||
UpdatePolicyVersion(ctx context.Context, input types.UpdatePolicyVersionInput) (*types.UpdatePolicyVersionPayload, error)
|
UpdatePolicyVersion(ctx context.Context, input types.UpdatePolicyVersionInput) (*types.UpdatePolicyVersionPayload, error)
|
||||||
RequestSignature(ctx context.Context, input types.RequestSignatureInput) (*types.RequestSignaturePayload, error)
|
RequestSignature(ctx context.Context, input types.RequestSignatureInput) (*types.RequestSignaturePayload, error)
|
||||||
|
SendSigningNotifications(ctx context.Context, input types.SendSigningNotificationsInput) (*types.SendSigningNotificationsPayload, error)
|
||||||
CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error)
|
CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error)
|
||||||
}
|
}
|
||||||
type OrganizationResolver interface {
|
type OrganizationResolver interface {
|
||||||
@@ -2035,6 +2041,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.Mutation.RequestSignature(childComplexity, args["input"].(types.RequestSignatureInput)), true
|
return e.complexity.Mutation.RequestSignature(childComplexity, args["input"].(types.RequestSignatureInput)), true
|
||||||
|
|
||||||
|
case "Mutation.sendSigningNotifications":
|
||||||
|
if e.complexity.Mutation.SendSigningNotifications == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Mutation_sendSigningNotifications_args(context.TODO(), rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Mutation.SendSigningNotifications(childComplexity, args["input"].(types.SendSigningNotificationsInput)), true
|
||||||
|
|
||||||
case "Mutation.unassignTask":
|
case "Mutation.unassignTask":
|
||||||
if e.complexity.Mutation.UnassignTask == nil {
|
if e.complexity.Mutation.UnassignTask == nil {
|
||||||
break
|
break
|
||||||
@@ -2944,6 +2962,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.RiskEdge.Node(childComplexity), true
|
return e.complexity.RiskEdge.Node(childComplexity), true
|
||||||
|
|
||||||
|
case "SendSigningNotificationsPayload.success":
|
||||||
|
if e.complexity.SendSigningNotificationsPayload.Success == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.SendSigningNotificationsPayload.Success(childComplexity), true
|
||||||
|
|
||||||
case "Session.expiresAt":
|
case "Session.expiresAt":
|
||||||
if e.complexity.Session.ExpiresAt == nil {
|
if e.complexity.Session.ExpiresAt == nil {
|
||||||
break
|
break
|
||||||
@@ -3672,6 +3697,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
|
|||||||
ec.unmarshalInputRequestEvidenceInput,
|
ec.unmarshalInputRequestEvidenceInput,
|
||||||
ec.unmarshalInputRequestSignatureInput,
|
ec.unmarshalInputRequestSignatureInput,
|
||||||
ec.unmarshalInputRiskOrder,
|
ec.unmarshalInputRiskOrder,
|
||||||
|
ec.unmarshalInputSendSigningNotificationsInput,
|
||||||
ec.unmarshalInputTaskOrder,
|
ec.unmarshalInputTaskOrder,
|
||||||
ec.unmarshalInputUnassignTaskInput,
|
ec.unmarshalInputUnassignTaskInput,
|
||||||
ec.unmarshalInputUpdateFrameworkInput,
|
ec.unmarshalInputUpdateFrameworkInput,
|
||||||
@@ -3799,6 +3825,7 @@ directive @goModel(
|
|||||||
|
|
||||||
directive @goEnum(value: String) on ENUM_VALUE
|
directive @goEnum(value: String) on ENUM_VALUE
|
||||||
|
|
||||||
|
|
||||||
# Scalars
|
# Scalars
|
||||||
scalar CursorKey
|
scalar CursorKey
|
||||||
scalar Void
|
scalar Void
|
||||||
@@ -4793,6 +4820,7 @@ type Mutation {
|
|||||||
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
|
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
|
||||||
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
|
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
|
||||||
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
|
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
|
||||||
|
sendSigningNotifications(input: SendSigningNotificationsInput!): SendSigningNotificationsPayload!
|
||||||
|
|
||||||
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
|
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
|
||||||
}
|
}
|
||||||
@@ -5435,7 +5463,14 @@ input UpdatePolicyVersionInput {
|
|||||||
type UpdatePolicyVersionPayload {
|
type UpdatePolicyVersionPayload {
|
||||||
policyVersion: PolicyVersion!
|
policyVersion: PolicyVersion!
|
||||||
}
|
}
|
||||||
`, BuiltIn: false},
|
|
||||||
|
input SendSigningNotificationsInput {
|
||||||
|
organizationId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendSigningNotificationsPayload {
|
||||||
|
success: Boolean!
|
||||||
|
}`, BuiltIn: false},
|
||||||
}
|
}
|
||||||
var parsedSchema = gqlparser.MustLoadSchema(sources...)
|
var parsedSchema = gqlparser.MustLoadSchema(sources...)
|
||||||
|
|
||||||
@@ -6910,6 +6945,29 @@ func (ec *executionContext) field_Mutation_requestSignature_argsInput(
|
|||||||
return zeroVal, nil
|
return zeroVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Mutation_sendSigningNotifications_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]any{}
|
||||||
|
arg0, err := ec.field_Mutation_sendSigningNotifications_argsInput(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["input"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
func (ec *executionContext) field_Mutation_sendSigningNotifications_argsInput(
|
||||||
|
ctx context.Context,
|
||||||
|
rawArgs map[string]any,
|
||||||
|
) (types.SendSigningNotificationsInput, error) {
|
||||||
|
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
|
||||||
|
if tmp, ok := rawArgs["input"]; ok {
|
||||||
|
return ec.unmarshalNSendSigningNotificationsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSendSigningNotificationsInput(ctx, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroVal types.SendSigningNotificationsInput
|
||||||
|
return zeroVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Mutation_unassignTask_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
func (ec *executionContext) field_Mutation_unassignTask_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
@@ -16473,6 +16531,65 @@ func (ec *executionContext) fieldContext_Mutation_requestSignature(ctx context.C
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Mutation_sendSigningNotifications(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Mutation_sendSigningNotifications(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().SendSigningNotifications(rctx, fc.Args["input"].(types.SendSigningNotificationsInput))
|
||||||
|
})
|
||||||
|
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.SendSigningNotificationsPayload)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNSendSigningNotificationsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSendSigningNotificationsPayload(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Mutation_sendSigningNotifications(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 "success":
|
||||||
|
return ec.fieldContext_SendSigningNotificationsPayload_success(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type SendSigningNotificationsPayload", 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_sendSigningNotifications_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return fc, err
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_createVendorRiskAssessment(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_createVendorRiskAssessment(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Mutation_createVendorRiskAssessment(ctx, field)
|
fc, err := ec.fieldContext_Mutation_createVendorRiskAssessment(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -21806,6 +21923,50 @@ func (ec *executionContext) fieldContext_RiskEdge_node(_ context.Context, field
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _SendSigningNotificationsPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.SendSigningNotificationsPayload) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_SendSigningNotificationsPayload_success(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.Success, 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.(bool)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_SendSigningNotificationsPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "SendSigningNotificationsPayload",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Boolean does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Session_id(ctx context.Context, field graphql.CollectedField, obj *types.Session) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Session_id(ctx context.Context, field graphql.CollectedField, obj *types.Session) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Session_id(ctx, field)
|
fc, err := ec.fieldContext_Session_id(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -30448,6 +30609,33 @@ func (ec *executionContext) unmarshalInputRiskOrder(ctx context.Context, obj any
|
|||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalInputSendSigningNotificationsInput(ctx context.Context, obj any) (types.SendSigningNotificationsInput, error) {
|
||||||
|
var it types.SendSigningNotificationsInput
|
||||||
|
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) unmarshalInputTaskOrder(ctx context.Context, obj any) (types.TaskOrderBy, error) {
|
func (ec *executionContext) unmarshalInputTaskOrder(ctx context.Context, obj any) (types.TaskOrderBy, error) {
|
||||||
var it types.TaskOrderBy
|
var it types.TaskOrderBy
|
||||||
asMap := map[string]any{}
|
asMap := map[string]any{}
|
||||||
@@ -34161,6 +34349,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
out.Invalids++
|
||||||
}
|
}
|
||||||
|
case "sendSigningNotifications":
|
||||||
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
|
return ec._Mutation_sendSigningNotifications(ctx, field)
|
||||||
|
})
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
case "createVendorRiskAssessment":
|
case "createVendorRiskAssessment":
|
||||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
return ec._Mutation_createVendorRiskAssessment(ctx, field)
|
return ec._Mutation_createVendorRiskAssessment(ctx, field)
|
||||||
@@ -36225,6 +36420,45 @@ func (ec *executionContext) _RiskEdge(ctx context.Context, sel ast.SelectionSet,
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sendSigningNotificationsPayloadImplementors = []string{"SendSigningNotificationsPayload"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _SendSigningNotificationsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SendSigningNotificationsPayload) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, sendSigningNotificationsPayloadImplementors)
|
||||||
|
|
||||||
|
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("SendSigningNotificationsPayload")
|
||||||
|
case "success":
|
||||||
|
out.Values[i] = ec._SendSigningNotificationsPayload_success(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 sessionImplementors = []string{"Session"}
|
var sessionImplementors = []string{"Session"}
|
||||||
|
|
||||||
func (ec *executionContext) _Session(ctx context.Context, sel ast.SelectionSet, obj *types.Session) graphql.Marshaler {
|
func (ec *executionContext) _Session(ctx context.Context, sel ast.SelectionSet, obj *types.Session) graphql.Marshaler {
|
||||||
@@ -40544,6 +40778,25 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNSendSigningNotificationsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSendSigningNotificationsInput(ctx context.Context, v any) (types.SendSigningNotificationsInput, error) {
|
||||||
|
res, err := ec.unmarshalInputSendSigningNotificationsInput(ctx, v)
|
||||||
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNSendSigningNotificationsPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSendSigningNotificationsPayload(ctx context.Context, sel ast.SelectionSet, v types.SendSigningNotificationsPayload) graphql.Marshaler {
|
||||||
|
return ec._SendSigningNotificationsPayload(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNSendSigningNotificationsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSendSigningNotificationsPayload(ctx context.Context, sel ast.SelectionSet, v *types.SendSigningNotificationsPayload) 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._SendSigningNotificationsPayload(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) {
|
func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) {
|
||||||
res, err := graphql.UnmarshalString(v)
|
res, err := graphql.UnmarshalString(v)
|
||||||
return res, graphql.ErrorOnPath(ctx, err)
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
|||||||
@@ -723,6 +723,14 @@ type RiskEdge struct {
|
|||||||
Node *Risk `json:"node"`
|
Node *Risk `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendSigningNotificationsInput struct {
|
||||||
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendSigningNotificationsPayload struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
|||||||
@@ -1134,6 +1134,20 @@ func (r *mutationResolver) RequestSignature(ctx context.Context, input types.Req
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendSigningNotifications is the resolver for the sendSigningNotifications field.
|
||||||
|
func (r *mutationResolver) SendSigningNotifications(ctx context.Context, input types.SendSigningNotificationsInput) (*types.SendSigningNotificationsPayload, error) {
|
||||||
|
svc := GetTenantService(ctx, r.proboSvc, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
err := svc.Policies.SendSigningNotifications(ctx, input.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot send signing notifications: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.SendSigningNotificationsPayload{
|
||||||
|
Success: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateVendorRiskAssessment is the resolver for the createVendorRiskAssessment field.
|
// CreateVendorRiskAssessment is the resolver for the createVendorRiskAssessment field.
|
||||||
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
|
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
|
||||||
svc := GetTenantService(ctx, r.proboSvc, input.VendorID.TenantID())
|
svc := GetTenantService(ctx, r.proboSvc, input.VendorID.TenantID())
|
||||||
|
|||||||
Reference in New Issue
Block a user