diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 9e735d9a8..0225537d1 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -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={} /> + } + /> + (null); + const [signingData, setSigningData] = useState(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 ( + + + + Loading Signing Requests + Please wait while we fetch your documents... + + + + ); + } + + // Error state + if (error) { + return ( + + + + Error + {error} + + + window.location.reload()}>Try Again + + + + ); + } + + // No documents or missing data + if (!signingData || signingData.documents.length === 0) { + return ( + + + + No Documents to Sign + There are no documents requiring your signature at this time. + + + + ); + } + + // Current document + const currentDoc = signingData.documents[currentDocIndex]; + const isLastDocument = currentDocIndex === signingData.documents.length - 1; + const allSigned = getSignedCount() === signingData.documents.length; + + return ( + + + Document Signing - Probo + + + + Document Signing Request + + From {signingData.requesterName} at {signingData.requesterOrganization} + + + + + + {getSignedCount()} of {signingData.documents.length} documents signed + + {Math.round(getProgressPercentage())}% + + + + + + + + {currentDoc.title} + + Document {currentDocIndex + 1} of {signingData.documents.length} + + + + + + + + + + + {currentDoc.signed ? ( + + ✓ Signed + {!isLastDocument && ( + + Next Document + + )} + + ) : ( + + Sign Document + + )} + + {allSigned && ( + + All documents have been signed + + )} + + + + ); +} \ No newline at end of file diff --git a/apps/console/src/pages/organizations/policies/PolicyListView.tsx b/apps/console/src/pages/organizations/policies/PolicyListView.tsx index 20a06ef35..670d69b71 100644 --- a/apps/console/src/pages/organizations/policies/PolicyListView.tsx +++ b/apps/console/src/pages/organizations/policies/PolicyListView.tsx @@ -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..."} (sendSigningNotificationsMutation); + const handleOpenModal = () => { setCreateModalOpen(true); }; @@ -460,14 +483,29 @@ function PolicyListViewContent({ setCreateModalOpen(open); }; + const handleSendSigningNotifications = () => { + sendSigningNotifications({ + variables: { + input: { + organizationId: organizationId!, + }, + }, + }); + }; + return ( - - New policy + + + + New policy + + Send signing notifications + + } > {/* Policy table */} diff --git a/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewSendSigningNotificationsMutation.graphql.ts b/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewSendSigningNotificationsMutation.graphql.ts new file mode 100644 index 000000000..917eeaaba --- /dev/null +++ b/apps/console/src/pages/organizations/policies/__generated__/PolicyListViewSendSigningNotificationsMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<> + * @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; diff --git a/pkg/coredata/people.go b/pkg/coredata/people.go index a5ded78da..077c6b62e 100644 --- a/pkg/coredata/people.go +++ b/pkg/coredata/people.go @@ -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 +} diff --git a/pkg/coredata/policy_version_signature.go b/pkg/coredata/policy_version_signature.go index 43f896989..b4b61dd77 100644 --- a/pkg/coredata/policy_version_signature.go +++ b/pkg/coredata/policy_version_signature.go @@ -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 +} diff --git a/pkg/probo/policy_service.go b/pkg/probo/policy_service.go index beb2f67f2..558dcb048 100644 --- a/pkg/probo/policy_service.go +++ b/pkg/probo/policy_service.go @@ -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, diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 3e2db3ade..3d772ccc4 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -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} diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 0ebb53551..ba26a8022 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -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) } diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index fca9f3081..929136fda 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -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! +} \ No newline at end of file diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index f67fd77ae..052a074c3 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -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) diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 22208e723..47b1aa235 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -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"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index f6143e872..e33ca0a55 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -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())
+ From {signingData.requesterName} at {signingData.requesterOrganization} +