Add policy sign email notif

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-04-29 22:08:21 -07:00
parent ae2ca41d35
commit a2bec08d16
13 changed files with 954 additions and 18 deletions

View File

@@ -13,6 +13,7 @@ import AuthLayout from "./layouts/AuthLayout";
import { RelayEnvironment } from "./RelayEnvironment";
import { AuthenticationRoutes } from "./pages/authentication/Routes";
import { OrganizationsRoutes } from "./pages/organizations/Routes";
import SigningRequestsPage from "./pages/SigningRequestsPage";
posthog.init(process.env.POSTHOG_KEY!, {
api_host: process.env.POSTHOG_HOST,
@@ -53,6 +54,11 @@ function App() {
element={<OrganizationsRoutes />}
/>
<Route
path="policies/signing-requests"
element={<SigningRequestsPage />}
/>
<Route
path="*"
element={

View 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>
);
}

View File

@@ -29,6 +29,7 @@ import { format } from "date-fns";
import type { PolicyListViewQuery, PolicyListViewQuery$data } from "./__generated__/PolicyListViewQuery.graphql";
import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql";
import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql";
import type { PolicyListViewSendSigningNotificationsMutation } from "./__generated__/PolicyListViewSendSigningNotificationsMutation.graphql";
import { PageTemplate } from "@/components/PageTemplate";
import { PolicyListViewSkeleton } from "./PolicyListPage";
import {
@@ -117,6 +118,13 @@ const createPolicyMutation = graphql`
}
`;
const sendSigningNotificationsMutation = graphql`
mutation PolicyListViewSendSigningNotificationsMutation($input: SendSigningNotificationsInput!) {
sendSigningNotifications(input: $input) {
success
}
}
`;
function PolicyTableRow({
policy,
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'}`}
contentEditable
suppressContentEditableWarning
onInput={(e) => setTitle(e.currentTarget.textContent || "")}
onInput={(e) => {
const newText = e.currentTarget.textContent || "";
setTitle(newText);
}}
style={{ WebkitTapHighlightColor: 'transparent' }}
onClick={(e) => {
if (!title) {
@@ -368,12 +379,22 @@ function CreatePolicyModal({
}
}}
onFocus={(e) => {
if (!title) {
if (e.currentTarget.textContent === "Enter policy title...") {
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>
</div>
<Textarea
@@ -452,6 +473,8 @@ function PolicyListViewContent({
const [createModalOpen, setCreateModalOpen] = useState(false);
const [sendSigningNotifications, _] = useMutation<PolicyListViewSendSigningNotificationsMutation>(sendSigningNotificationsMutation);
const handleOpenModal = () => {
setCreateModalOpen(true);
};
@@ -460,14 +483,29 @@ function PolicyListViewContent({
setCreateModalOpen(open);
};
const handleSendSigningNotifications = () => {
sendSigningNotifications({
variables: {
input: {
organizationId: organizationId!,
},
},
});
};
return (
<PageTemplate
title="Policies"
actions={
<Button onClick={handleOpenModal}>
<Plus className="mr-2 h-4 w-4" />
New policy
<div>
<Button onClick={handleOpenModal}>
<Plus className="mr-2 h-4 w-4" />
New policy
</Button>
<Button onClick={handleSendSigningNotifications}>
Send signing notifications
</Button>
</div>
}
>
{/* Policy table */}

View File

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

View File

@@ -293,3 +293,52 @@ WHERE %s
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
}

View File

@@ -53,6 +53,53 @@ func (pvs PolicyVersionSignature) CursorKey(orderBy PolicyVersionSignatureOrderF
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(
ctx context.Context,
conn pg.Conn,
@@ -195,3 +242,40 @@ WHERE
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
}

View File

@@ -3,19 +3,21 @@ package probo
import (
"context"
"fmt"
"net/url"
"time"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/statelesstoken"
"go.gearno.de/kit/pg"
)
type PolicyService struct {
svc *TenantService
}
type (
PolicyService struct {
svc *TenantService
}
CreatePolicyRequest struct {
OrganizationID gid.GID
Title string
@@ -34,6 +36,15 @@ type (
RequestedBy 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(
@@ -163,6 +174,125 @@ func (s *PolicyService) Create(
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(
ctx context.Context,
req UpdatePolicyVersionRequest,

View File

@@ -31,15 +31,18 @@ type (
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
hostname string
tokenSecret string
}
TenantService struct {
pg *pg.Client
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
scope coredata.Scoper
pg *pg.Client
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
scope coredata.Scoper
hostname string
tokenSecret string
Frameworks *FrameworkService
Mesures *MesureService
Tasks *TaskService
@@ -61,6 +64,8 @@ func NewService(
pgClient *pg.Client,
s3Client *s3.Client,
bucket string,
hostname string,
tokenSecret string,
) (*Service, error) {
if bucket == "" {
return nil, fmt.Errorf("bucket is required")
@@ -71,6 +76,8 @@ func NewService(
s3: s3Client,
bucket: bucket,
encryptionKey: encryptionKey,
hostname: hostname,
tokenSecret: tokenSecret,
}
return svc, nil
@@ -82,7 +89,9 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
s3: s.s3,
bucket: s.bucket,
encryptionKey: s.encryptionKey,
hostname: s.hostname,
scope: coredata.NewScope(tenantID),
tokenSecret: s.tokenSecret,
}
tenantService.Frameworks = &FrameworkService{svc: tenantService}

View File

@@ -197,7 +197,15 @@ func (impl *Implm) Run(
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 {
return fmt.Errorf("cannot create probo service: %w", err)
}

View File

@@ -12,6 +12,7 @@ directive @goModel(
directive @goEnum(value: String) on ENUM_VALUE
# Scalars
scalar CursorKey
scalar Void
@@ -1006,6 +1007,7 @@ type Mutation {
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
sendSigningNotifications(input: SendSigningNotificationsInput!): SendSigningNotificationsPayload!
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
}
@@ -1648,3 +1650,11 @@ input UpdatePolicyVersionInput {
type UpdatePolicyVersionPayload {
policyVersion: PolicyVersion!
}
input SendSigningNotificationsInput {
organizationId: ID!
}
type SendSigningNotificationsPayload {
success: Boolean!
}

View File

@@ -351,6 +351,7 @@ type ComplexityRoot struct {
RemoveUser func(childComplexity int, input types.RemoveUserInput) int
RequestEvidence func(childComplexity int, input types.RequestEvidenceInput) 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
UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int
UpdateMesure func(childComplexity int, input types.UpdateMesureInput) int
@@ -537,6 +538,10 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
SendSigningNotificationsPayload struct {
Success func(childComplexity int) int
}
Session struct {
ExpiresAt 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)
UpdatePolicyVersion(ctx context.Context, input types.UpdatePolicyVersionInput) (*types.UpdatePolicyVersionPayload, 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)
}
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
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":
if e.complexity.Mutation.UnassignTask == nil {
break
@@ -2944,6 +2962,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.Session.ExpiresAt == nil {
break
@@ -3672,6 +3697,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputRequestEvidenceInput,
ec.unmarshalInputRequestSignatureInput,
ec.unmarshalInputRiskOrder,
ec.unmarshalInputSendSigningNotificationsInput,
ec.unmarshalInputTaskOrder,
ec.unmarshalInputUnassignTaskInput,
ec.unmarshalInputUpdateFrameworkInput,
@@ -3799,6 +3825,7 @@ directive @goModel(
directive @goEnum(value: String) on ENUM_VALUE
# Scalars
scalar CursorKey
scalar Void
@@ -4793,6 +4820,7 @@ type Mutation {
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
sendSigningNotifications(input: SendSigningNotificationsInput!): SendSigningNotificationsPayload!
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
}
@@ -5435,7 +5463,14 @@ input UpdatePolicyVersionInput {
type UpdatePolicyVersionPayload {
policyVersion: PolicyVersion!
}
`, BuiltIn: false},
input SendSigningNotificationsInput {
organizationId: ID!
}
type SendSigningNotificationsPayload {
success: Boolean!
}`, BuiltIn: false},
}
var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -6910,6 +6945,29 @@ func (ec *executionContext) field_Mutation_requestSignature_argsInput(
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) {
var err error
args := map[string]any{}
@@ -16473,6 +16531,65 @@ func (ec *executionContext) fieldContext_Mutation_requestSignature(ctx context.C
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) {
fc, err := ec.fieldContext_Mutation_createVendorRiskAssessment(ctx, field)
if err != nil {
@@ -21806,6 +21923,50 @@ func (ec *executionContext) fieldContext_RiskEdge_node(_ context.Context, field
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) {
fc, err := ec.fieldContext_Session_id(ctx, field)
if err != nil {
@@ -30448,6 +30609,33 @@ func (ec *executionContext) unmarshalInputRiskOrder(ctx context.Context, obj any
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) {
var it types.TaskOrderBy
asMap := map[string]any{}
@@ -34161,6 +34349,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
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":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createVendorRiskAssessment(ctx, field)
@@ -36225,6 +36420,45 @@ func (ec *executionContext) _RiskEdge(ctx context.Context, sel ast.SelectionSet,
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"}
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) {
res, err := graphql.UnmarshalString(v)
return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -723,6 +723,14 @@ type RiskEdge struct {
Node *Risk `json:"node"`
}
type SendSigningNotificationsInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
type SendSigningNotificationsPayload struct {
Success bool `json:"success"`
}
type Session struct {
ID gid.GID `json:"id"`
ExpiresAt time.Time `json:"expiresAt"`

View File

@@ -1134,6 +1134,20 @@ func (r *mutationResolver) RequestSignature(ctx context.Context, input types.Req
}, 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.
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.VendorID.TenantID())