Add import mitigations

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-30 15:02:46 +02:00
parent 2baa2eb0d1
commit 4e4e2369fc
8 changed files with 2028 additions and 1335 deletions

View File

@@ -1,9 +1,10 @@
import { Suspense, useEffect, useState } from "react";
import { Suspense, useEffect, useState, useRef } from "react";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
useMutation,
} from "react-relay";
import { useParams, useNavigate, Link } from "react-router";
import { Badge } from "@/components/ui/badge";
@@ -20,16 +21,17 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { MitigationListViewQuery as MitigationListViewQueryType } from "./__generated__/MitigationListViewQuery.graphql";
import { MitigationListViewSkeleton } from "./MitigationListPage";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { MitigationListViewImportMitigationMutation as MitigationListViewImportMitigationMutationType } from "./__generated__/MitigationListViewImportMitigationMutation.graphql";
const mitigationListViewQuery = graphql`
query MitigationListViewQuery($organizationId: ID!, $first: Int) {
organization: node(id: $organizationId) {
id
... on Organization {
mitigations(
first: $first
orderBy: { direction: ASC, field: CREATED_AT }
) @connection(key: "MitigationListView_mitigations") {
mitigations(first: $first)
@connection(key: "MitigationListView_mitigations") {
__id
edges {
node {
id
@@ -48,6 +50,28 @@ const mitigationListViewQuery = graphql`
}
`;
const importMitigationMutation = graphql`
mutation MitigationListViewImportMitigationMutation(
$input: ImportMitigationInput!
$connections: [ID!]!
) {
importMitigation(input: $input) {
mitigationEdges @appendEdge(connections: $connections) {
node {
id
name
description
category
state
importance
createdAt
updatedAt
}
}
}
}
`;
interface Mitigation {
id?: string;
name?: string;
@@ -93,6 +117,14 @@ function MitigationListContent({
const navigate = useNavigate();
const { organizationId } = useParams();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isImporting, setIsImporting] = useState(false);
const { toast } = useToast();
const [importMitigation] =
useMutation<MitigationListViewImportMitigationMutationType>(
importMitigationMutation
);
// Monitor URL hash for changes and update state accordingly
const [hashValue, setHashValue] = useState(window.location.hash);
@@ -249,18 +281,75 @@ function MitigationListContent({
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsImporting(true);
importMitigation({
variables: {
connections: [data.organization.mitigations.__id],
input: {
organizationId: organizationId!,
file: null,
},
},
uploadables: {
"input.file": file,
},
onCompleted: () => {
setIsImporting(false);
toast({
title: "Mitigations imported",
description: "Mitigations have been imported successfully.",
variant: "default",
});
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
},
onError: (error) => {
setIsImporting(false);
toast({
title: "Error importing mitigations",
description: error.message,
variant: "destructive",
});
},
});
};
return (
<PageTemplate
title="Mitigations"
description="Mitigations are actions taken to reduce the risk. Add them to track their implementation status."
actions={
<Button asChild>
<Link to={`/organizations/${organizationId}/mitigations/new`}>
New Mitigation
</Link>
</Button>
<div className="flex gap-4">
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={isImporting}
>
{isImporting ? "Importing..." : "Import Mitigations"}
</Button>
<Button asChild>
<Link to={`/organizations/${organizationId}/mitigations/new`}>
New Mitigation
</Link>
</Button>
</div>
}
>
{/* Hidden file input for mitigation import */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: "none" }}
accept=".json"
/>
{/* Global Progress Summary */}
<div className="mb-8">
<div className="flex items-center justify-between mb-1">

View File

@@ -0,0 +1,217 @@
/**
* @generated SignedSource<<d4590a13a15cf248104340854bc46aa7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type ImportMitigationInput = {
file: any;
organizationId: string;
};
export type MitigationListViewImportMitigationMutation$variables = {
connections: ReadonlyArray<string>;
input: ImportMitigationInput;
};
export type MitigationListViewImportMitigationMutation$data = {
readonly importMitigation: {
readonly mitigationEdges: ReadonlyArray<{
readonly node: {
readonly category: string;
readonly createdAt: string;
readonly description: string;
readonly id: string;
readonly importance: MitigationImportance;
readonly name: string;
readonly state: MitigationState;
readonly updatedAt: string;
};
}>;
};
};
export type MitigationListViewImportMitigationMutation = {
response: MitigationListViewImportMitigationMutation$data;
variables: MitigationListViewImportMitigationMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "MitigationEdge",
"kind": "LinkedField",
"name": "mitigationEdges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Mitigation",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "importance",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MitigationListViewImportMitigationMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "ImportMitigationPayload",
"kind": "LinkedField",
"name": "importMitigation",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MitigationListViewImportMitigationMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "ImportMitigationPayload",
"kind": "LinkedField",
"name": "importMitigation",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "mitigationEdges",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "84c038c572378999bfaacb87e8172b1e",
"id": null,
"metadata": {},
"name": "MitigationListViewImportMitigationMutation",
"operationKind": "mutation",
"text": "mutation MitigationListViewImportMitigationMutation(\n $input: ImportMitigationInput!\n) {\n importMitigation(input: $input) {\n mitigationEdges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "59e3d4a8e71cee0dcee606b93d706dad";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<c919ae742bdab52e5e68fb5c7fe92812>>
* @generated SignedSource<<a98cd84786da5d6229cb66bc24ad92d0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,6 +19,7 @@ export type MitigationListViewQuery$data = {
readonly organization: {
readonly id: string;
readonly mitigations?: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly category: string;
@@ -65,21 +66,13 @@ v3 = {
"storageKey": null
},
v4 = {
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "CREATED_AT"
}
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v6 = [
v5 = [
{
"alias": null,
"args": null,
@@ -146,7 +139,7 @@ v6 = [
"name": "updatedAt",
"storageKey": null
},
(v5/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
@@ -184,15 +177,26 @@ v6 = [
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
v7 = [
v6 = [
{
"kind": "Variable",
"name": "first",
"variableName": "first"
},
(v4/*: any*/)
}
];
return {
"fragment": {
@@ -218,15 +222,13 @@ return {
"selections": [
{
"alias": "mitigations",
"args": [
(v4/*: any*/)
],
"args": null,
"concreteType": "MitigationConnection",
"kind": "LinkedField",
"name": "__MitigationListView_mitigations_connection",
"plural": false,
"selections": (v6/*: any*/),
"storageKey": "__MitigationListView_mitigations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
"selections": (v5/*: any*/),
"storageKey": null
}
],
"type": "Organization",
@@ -256,27 +258,25 @@ return {
"name": "node",
"plural": false,
"selections": [
(v5/*: any*/),
(v4/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v7/*: any*/),
"args": (v6/*: any*/),
"concreteType": "MitigationConnection",
"kind": "LinkedField",
"name": "mitigations",
"plural": false,
"selections": (v6/*: any*/),
"selections": (v5/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": (v7/*: any*/),
"filters": [
"orderBy"
],
"args": (v6/*: any*/),
"filters": null,
"handle": "connection",
"key": "MitigationListView_mitigations",
"kind": "LinkedHandle",
@@ -292,7 +292,7 @@ return {
]
},
"params": {
"cacheID": "cb49e5e7740ec71ce0b8e1abbdbcf24d",
"cacheID": "9951f5e4be50df7a883af63426fe5fde",
"id": null,
"metadata": {
"connection": [
@@ -309,11 +309,11 @@ return {
},
"name": "MitigationListViewQuery",
"operationKind": "query",
"text": "query MitigationListViewQuery(\n $organizationId: ID!\n $first: Int\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: $first, orderBy: {direction: ASC, field: CREATED_AT}) {\n edges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query MitigationListViewQuery(\n $organizationId: ID!\n $first: Int\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: $first) {\n edges {\n node {\n id\n name\n description\n category\n state\n importance\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ee7fdf984d5cb376068ba53d0a56c59b";
(node as any).hash = "c2da6b42a986dd18cd18603c5f5809ad";
export default node;

View File

@@ -46,6 +46,14 @@ type (
State *coredata.MitigationState
Importance *coredata.MitigationImportance
}
ImportMitigationRequest struct {
Mitigations []struct {
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
} `json:"mitigations"`
}
)
func (s MitigationService) Get(
@@ -68,6 +76,65 @@ func (s MitigationService) Get(
return mitigation, nil
}
func (s MitigationService) Import(
ctx context.Context,
organizationID gid.GID,
req ImportMitigationRequest,
) (*page.Page[*coredata.Mitigation, coredata.MitigationOrderField], error) {
importedMitigations := coredata.Mitigations{}
for _, mitigation := range req.Mitigations {
now := time.Now()
mitigationID, err := gid.NewGID(organizationID.TenantID(), coredata.MitigationEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create global id: %w", err)
}
importedMitigations = append(importedMitigations, &coredata.Mitigation{
ID: mitigationID,
OrganizationID: organizationID,
Name: mitigation.Name,
Description: mitigation.Description,
Category: mitigation.Category,
State: coredata.MitigationStateNotStarted,
Standards: []string{},
Importance: coredata.MitigationImportancePreferred,
CreatedAt: now,
UpdatedAt: now,
})
}
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
for _, mitigation := range importedMitigations {
if err := mitigation.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert mitigation: %w", err)
}
}
return nil
},
)
if err != nil {
return nil, fmt.Errorf("cannot import mitigations: %w", err)
}
cursor := page.NewCursor(
len(importedMitigations),
nil,
page.Head,
page.OrderBy[coredata.MitigationOrderField]{
Field: coredata.MitigationOrderFieldCreatedAt,
Direction: page.OrderDirectionAsc,
},
)
return page.NewPage(importedMitigations, cursor), nil
}
func (s MitigationService) Update(
ctx context.Context,
req UpdateMitigationRequest,

View File

@@ -916,7 +916,7 @@ type UpdateMitigationPayload {
}
type ImportMitigationPayload {
success: Boolean!
mitigationEdges: [MitigationEdge!]!
}
type CreateTaskPayload {

File diff suppressed because it is too large Load Diff

View File

@@ -256,6 +256,15 @@ type ImportFrameworkPayload struct {
FrameworkEdge *FrameworkEdge `json:"frameworkEdge"`
}
type ImportMitigationInput struct {
OrganizationID gid.GID `json:"organizationId"`
File graphql.Upload `json:"file"`
}
type ImportMitigationPayload struct {
MitigationEdges []*MitigationEdge `json:"mitigationEdges"`
}
type InviteUserInput struct {
OrganizationID gid.GID `json:"organizationId"`
Email string `json:"email"`

View File

@@ -86,6 +86,170 @@ func (r *mitigationResolver) Tasks(ctx context.Context, obj *types.Mitigation, f
return types.NewTaskConnection(page), nil
}
// CreateOrganization is the resolver for the createOrganization field.
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
svc := r.proboSvc.WithTenant(gid.NewTenantID())
organization, err := svc.Organizations.Create(ctx, probo.CreateOrganizationRequest{
Name: input.Name,
})
if err != nil {
return nil, fmt.Errorf("cannot create organization: %w", err)
}
err = r.usrmgrSvc.EnrollUserInOrganization(ctx, UserFromContext(ctx).ID, organization.ID)
if err != nil {
return nil, fmt.Errorf("cannot add user to organization: %w", err)
}
tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
*tenantIDs = append(*tenantIDs, organization.ID.TenantID())
return &types.CreateOrganizationPayload{
OrganizationEdge: types.NewOrganizationEdge(organization, coredata.OrganizationOrderFieldCreatedAt),
}, nil
}
// UpdateOrganization is the resolver for the updateOrganization field.
func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
req := probo.UpdateOrganizationRequest{
ID: input.OrganizationID,
Name: input.Name,
}
if input.Logo != nil {
req.File = input.Logo.File
}
organization, err := svc.Organizations.Update(ctx, req)
if err != nil {
return nil, fmt.Errorf("cannot update organization: %w", err)
}
return &types.UpdateOrganizationPayload{
Organization: types.NewOrganization(organization),
}, nil
}
// DeleteOrganization is the resolver for the deleteOrganization field.
func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) {
panic(fmt.Errorf("not implemented: DeleteOrganization - deleteOrganization"))
}
// ConfirmEmail is the resolver for the confirmEmail field.
func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.ConfirmEmailInput) (*types.ConfirmEmailPayload, error) {
err := r.usrmgrSvc.ConfirmEmail(ctx, input.Token)
if err != nil {
return nil, err
}
return &types.ConfirmEmailPayload{Success: true}, nil
}
// InviteUser is the resolver for the inviteUser field.
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
user := UserFromContext(ctx)
organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("failed to list organizations for user: %w", err))
}
for _, organization := range organizations {
if organization.ID == input.OrganizationID {
err := r.usrmgrSvc.InviteUser(ctx, input.OrganizationID, input.FullName, input.Email)
if err != nil {
return nil, err
}
return &types.InviteUserPayload{Success: true}, nil
}
}
return nil, fmt.Errorf("organization not found")
}
// RemoveUser is the resolver for the removeUser field.
func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) {
user := UserFromContext(ctx)
organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("failed to list organizations for user: %w", err))
}
for _, organization := range organizations {
if organization.ID == input.OrganizationID {
err := r.usrmgrSvc.RemoveUser(ctx, input.OrganizationID, input.UserID)
if err != nil {
return nil, err
}
return &types.RemoveUserPayload{Success: true}, nil
}
}
return nil, fmt.Errorf("organization not found")
}
// CreatePeople is the resolver for the createPeople field.
func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.CreatePeoplePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
people, err := svc.Peoples.Create(ctx, probo.CreatePeopleRequest{
OrganizationID: input.OrganizationID,
FullName: input.FullName,
PrimaryEmailAddress: input.PrimaryEmailAddress,
AdditionalEmailAddresses: []string{},
Kind: input.Kind,
})
if err != nil {
return nil, fmt.Errorf("cannot create people: %w", err)
}
return &types.CreatePeoplePayload{
PeopleEdge: types.NewPeopleEdge(people, coredata.PeopleOrderFieldFullName),
}, nil
}
// UpdatePeople is the resolver for the updatePeople field.
func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdatePeopleInput) (*types.UpdatePeoplePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID())
people, err := svc.Peoples.Update(ctx, probo.UpdatePeopleRequest{
ID: input.ID,
FullName: input.FullName,
PrimaryEmailAddress: input.PrimaryEmailAddress,
AdditionalEmailAddresses: &input.AdditionalEmailAddresses,
Kind: input.Kind,
})
if err != nil {
return nil, fmt.Errorf("cannot update people: %w", err)
}
return &types.UpdatePeoplePayload{
People: types.NewPeople(people),
}, nil
}
// DeletePeople is the resolver for the deletePeople field.
func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.PeopleID.TenantID())
err := svc.Peoples.Delete(ctx, input.PeopleID)
if err != nil {
return nil, fmt.Errorf("cannot delete people: %w", err)
}
return &types.DeletePeoplePayload{
DeletedPeopleID: input.PeopleID,
}, nil
}
// CreateVendor is the resolver for the createVendor field.
func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.CreateVendorPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
@@ -149,194 +313,6 @@ func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteV
}, nil
}
// CreatePeople is the resolver for the createPeople field.
func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.CreatePeoplePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
people, err := svc.Peoples.Create(ctx, probo.CreatePeopleRequest{
OrganizationID: input.OrganizationID,
FullName: input.FullName,
PrimaryEmailAddress: input.PrimaryEmailAddress,
AdditionalEmailAddresses: []string{},
Kind: input.Kind,
})
if err != nil {
return nil, fmt.Errorf("cannot create people: %w", err)
}
return &types.CreatePeoplePayload{
PeopleEdge: types.NewPeopleEdge(people, coredata.PeopleOrderFieldFullName),
}, nil
}
// UpdatePeople is the resolver for the updatePeople field.
func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdatePeopleInput) (*types.UpdatePeoplePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID())
people, err := svc.Peoples.Update(ctx, probo.UpdatePeopleRequest{
ID: input.ID,
FullName: input.FullName,
PrimaryEmailAddress: input.PrimaryEmailAddress,
AdditionalEmailAddresses: &input.AdditionalEmailAddresses,
Kind: input.Kind,
})
if err != nil {
return nil, fmt.Errorf("cannot update people: %w", err)
}
return &types.UpdatePeoplePayload{
People: types.NewPeople(people),
}, nil
}
// DeletePeople is the resolver for the deletePeople field.
func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.PeopleID.TenantID())
err := svc.Peoples.Delete(ctx, input.PeopleID)
if err != nil {
return nil, fmt.Errorf("cannot delete people: %w", err)
}
return &types.DeletePeoplePayload{
DeletedPeopleID: input.PeopleID,
}, nil
}
// CreateOrganization is the resolver for the createOrganization field.
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
svc := r.proboSvc.WithTenant(gid.NewTenantID())
organization, err := svc.Organizations.Create(ctx, probo.CreateOrganizationRequest{
Name: input.Name,
})
if err != nil {
return nil, fmt.Errorf("cannot create organization: %w", err)
}
err = r.usrmgrSvc.EnrollUserInOrganization(ctx, UserFromContext(ctx).ID, organization.ID)
if err != nil {
return nil, fmt.Errorf("cannot add user to organization: %w", err)
}
tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
*tenantIDs = append(*tenantIDs, organization.ID.TenantID())
return &types.CreateOrganizationPayload{
OrganizationEdge: types.NewOrganizationEdge(organization, coredata.OrganizationOrderFieldCreatedAt),
}, nil
}
// UpdateOrganization is the resolver for the updateOrganization field.
func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
req := probo.UpdateOrganizationRequest{
ID: input.OrganizationID,
Name: input.Name,
}
if input.Logo != nil {
req.File = input.Logo.File
}
organization, err := svc.Organizations.Update(ctx, req)
if err != nil {
return nil, fmt.Errorf("cannot update organization: %w", err)
}
return &types.UpdateOrganizationPayload{
Organization: types.NewOrganization(organization),
}, nil
}
// DeleteOrganization is the resolver for the deleteOrganization field.
func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) {
panic(fmt.Errorf("not implemented: DeleteOrganization - deleteOrganization"))
}
// CreateTask is the resolver for the createTask field.
func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
task, err := svc.Tasks.Create(ctx, probo.CreateTaskRequest{
MitigationID: input.MitigationID,
Name: input.Name,
Description: input.Description,
TimeEstimate: input.TimeEstimate,
})
if err != nil {
return nil, fmt.Errorf("cannot create task: %w", err)
}
return &types.CreateTaskPayload{
TaskEdge: types.NewTaskEdge(task, coredata.TaskOrderFieldCreatedAt),
}, nil
}
// UpdateTask is the resolver for the updateTask field.
func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTaskInput) (*types.UpdateTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Update(ctx, probo.UpdateTaskRequest{
TaskID: input.TaskID,
Name: input.Name,
Description: input.Description,
State: input.State,
TimeEstimate: input.TimeEstimate,
})
if err != nil {
return nil, fmt.Errorf("cannot update task: %w", err)
}
return &types.UpdateTaskPayload{
Task: types.NewTask(task),
}, nil
}
// DeleteTask is the resolver for the deleteTask field.
func (r *mutationResolver) DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
err := svc.Tasks.Delete(ctx, input.TaskID)
if err != nil {
return nil, fmt.Errorf("cannot delete task: %w", err)
}
return &types.DeleteTaskPayload{
DeletedTaskID: input.TaskID,
}, nil
}
// AssignTask is the resolver for the assignTask field.
func (r *mutationResolver) AssignTask(ctx context.Context, input types.AssignTaskInput) (*types.AssignTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Assign(ctx, input.TaskID, input.AssignedToID)
if err != nil {
return nil, fmt.Errorf("cannot assign task: %w", err)
}
return &types.AssignTaskPayload{
Task: types.NewTask(task),
}, nil
}
// UnassignTask is the resolver for the unassignTask field.
func (r *mutationResolver) UnassignTask(ctx context.Context, input types.UnassignTaskInput) (*types.UnassignTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Unassign(ctx, input.TaskID)
if err != nil {
return nil, fmt.Errorf("cannot unassign task: %w", err)
}
return &types.UnassignTaskPayload{
Task: types.NewTask(task),
}, nil
}
// CreateFramework is the resolver for the createFramework field.
func (r *mutationResolver) CreateFramework(ctx context.Context, input types.CreateFrameworkInput) (*types.CreateFrameworkPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
@@ -446,6 +422,111 @@ func (r *mutationResolver) UpdateMitigation(ctx context.Context, input types.Upd
}, nil
}
// ImportMitigation is the resolver for the importMitigation field.
func (r *mutationResolver) ImportMitigation(ctx context.Context, input types.ImportMitigationInput) (*types.ImportMitigationPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
var req probo.ImportMitigationRequest
if err := json.NewDecoder(input.File.File).Decode(&req.Mitigations); err != nil {
return nil, fmt.Errorf("cannot unmarshal mitigation: %w", err)
}
mitigations, err := svc.Mitigations.Import(ctx, input.OrganizationID, req)
if err != nil {
return nil, fmt.Errorf("cannot import mitigation: %w", err)
}
mitigationEdges := make([]*types.MitigationEdge, len(mitigations.Data))
for i, mitigation := range mitigations.Data {
mitigationEdges[i] = types.NewMitigationEdge(mitigation, coredata.MitigationOrderFieldCreatedAt)
}
return &types.ImportMitigationPayload{
MitigationEdges: mitigationEdges,
}, nil
}
// CreateTask is the resolver for the createTask field.
func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
task, err := svc.Tasks.Create(ctx, probo.CreateTaskRequest{
MitigationID: input.MitigationID,
Name: input.Name,
Description: input.Description,
TimeEstimate: input.TimeEstimate,
})
if err != nil {
return nil, fmt.Errorf("cannot create task: %w", err)
}
return &types.CreateTaskPayload{
TaskEdge: types.NewTaskEdge(task, coredata.TaskOrderFieldCreatedAt),
}, nil
}
// UpdateTask is the resolver for the updateTask field.
func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTaskInput) (*types.UpdateTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Update(ctx, probo.UpdateTaskRequest{
TaskID: input.TaskID,
Name: input.Name,
Description: input.Description,
State: input.State,
TimeEstimate: input.TimeEstimate,
})
if err != nil {
return nil, fmt.Errorf("cannot update task: %w", err)
}
return &types.UpdateTaskPayload{
Task: types.NewTask(task),
}, nil
}
// DeleteTask is the resolver for the deleteTask field.
func (r *mutationResolver) DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
err := svc.Tasks.Delete(ctx, input.TaskID)
if err != nil {
return nil, fmt.Errorf("cannot delete task: %w", err)
}
return &types.DeleteTaskPayload{
DeletedTaskID: input.TaskID,
}, nil
}
// AssignTask is the resolver for the assignTask field.
func (r *mutationResolver) AssignTask(ctx context.Context, input types.AssignTaskInput) (*types.AssignTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Assign(ctx, input.TaskID, input.AssignedToID)
if err != nil {
return nil, fmt.Errorf("cannot assign task: %w", err)
}
return &types.AssignTaskPayload{
Task: types.NewTask(task),
}, nil
}
// UnassignTask is the resolver for the unassignTask field.
func (r *mutationResolver) UnassignTask(ctx context.Context, input types.UnassignTaskInput) (*types.UnassignTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Unassign(ctx, input.TaskID)
if err != nil {
return nil, fmt.Errorf("cannot unassign task: %w", err)
}
return &types.UnassignTaskPayload{
Task: types.NewTask(task),
}, nil
}
// UploadEvidence is the resolver for the uploadEvidence field.
func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
@@ -554,63 +635,6 @@ func (r *mutationResolver) DeletePolicy(ctx context.Context, input types.DeleteP
}, nil
}
// ConfirmEmail is the resolver for the confirmEmail field.
func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.ConfirmEmailInput) (*types.ConfirmEmailPayload, error) {
err := r.usrmgrSvc.ConfirmEmail(ctx, input.Token)
if err != nil {
return nil, err
}
return &types.ConfirmEmailPayload{Success: true}, nil
}
// InviteUser is the resolver for the inviteUser field.
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
user := UserFromContext(ctx)
organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("failed to list organizations for user: %w", err))
}
for _, organization := range organizations {
if organization.ID == input.OrganizationID {
err := r.usrmgrSvc.InviteUser(ctx, input.OrganizationID, input.FullName, input.Email)
if err != nil {
return nil, err
}
return &types.InviteUserPayload{Success: true}, nil
}
}
return nil, fmt.Errorf("organization not found")
}
// RemoveUser is the resolver for the removeUser field.
func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) {
user := UserFromContext(ctx)
organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("failed to list organizations for user: %w", err))
}
for _, organization := range organizations {
if organization.ID == input.OrganizationID {
err := r.usrmgrSvc.RemoveUser(ctx, input.OrganizationID, input.UserID)
if err != nil {
return nil, err
}
return &types.RemoveUserPayload{Success: true}, nil
}
}
return nil, fmt.Errorf("organization not found")
}
// LogoURL is the resolver for the logoUrl field.
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())