Add evidence type link

close #44

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-20 17:25:29 +01:00
parent 7117c6a210
commit b8bbeaba11
13 changed files with 798 additions and 167 deletions

View File

@@ -20,7 +20,6 @@ import {
CheckCircle2,
Plus,
Trash2,
Upload,
FileIcon,
Loader2,
ChevronDown,
@@ -34,6 +33,7 @@ import {
UserPlus,
UserMinus,
User,
Link2,
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
@@ -60,6 +60,9 @@ import {
SelectTrigger,
} from "@/components/ui/select";
import ReactMarkdown from "react-markdown";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label";
import { Helmet } from "react-helmet-async";
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
@@ -72,7 +75,6 @@ import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssign
import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql";
import { Textarea } from "@/components/ui/textarea";
// Function to format ISO8601 duration to human-readable format
const formatDuration = (isoDuration: string): string => {
@@ -147,6 +149,8 @@ const controlOverviewPageQuery = graphql`
filename
size
state
type
url
createdAt
}
}
@@ -221,6 +225,8 @@ const uploadEvidenceMutation = graphql`
filename
fileUrl
mimeType
type
url
size
state
createdAt
@@ -496,6 +502,13 @@ function ControlOverviewPageContent({
[key: string]: string;
}>({});
// Add state variables for the evidence dialog and link evidence
const [evidenceDialogOpen, setEvidenceDialogOpen] = useState(false);
const [linkEvidenceName, setLinkEvidenceName] = useState("");
const [linkEvidenceUrl, setLinkEvidenceUrl] = useState("");
const [linkEvidenceDescription, setLinkEvidenceDescription] = useState("");
const [activeTab, setActiveTab] = useState<"file" | "link">("file");
const tasks = data.control.tasks?.edges.map((edge) => edge.node) || [];
const getEvidenceConnectionId = useCallback(
@@ -701,10 +714,12 @@ function ControlOverviewPageContent({
const handleUploadEvidence = (taskId: string, taskName: string) => {
setTaskForEvidence({ id: taskId, name: taskName });
// Instead of opening a modal, directly trigger the file input click
if (hiddenFileInputRef.current) {
hiddenFileInputRef.current.click();
}
setEvidenceDialogOpen(true);
// Reset form fields
setLinkEvidenceName("");
setLinkEvidenceUrl("");
setLinkEvidenceDescription("");
setActiveTab("file");
};
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -713,10 +728,10 @@ function ControlOverviewPageContent({
const file = e.target.files[0];
// Show toast for upload started
// Show toast for add started
toast({
title: "Upload started",
description: `Uploading ${file.name}...`,
title: "Adding document",
description: `Adding ${file.name}...`,
variant: "default",
});
@@ -728,7 +743,9 @@ function ControlOverviewPageContent({
input: {
taskId: taskForEvidence.id,
name: file.name,
type: "FILE",
file: null,
description: "Document evidence",
},
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
},
@@ -737,8 +754,8 @@ function ControlOverviewPageContent({
},
onCompleted: () => {
toast({
title: "Evidence uploaded",
description: "Evidence has been uploaded successfully.",
title: "Document added",
description: "Document evidence has been added successfully.",
variant: "default",
});
setTaskForEvidence(null);
@@ -749,7 +766,81 @@ function ControlOverviewPageContent({
},
onError: (error) => {
toast({
title: "Error uploading evidence",
title: "Error adding document",
description: error.message,
variant: "destructive",
});
},
});
};
const handleLinkEvidenceSubmit = () => {
if (!taskForEvidence) return;
// Validate form
if (!linkEvidenceName.trim()) {
toast({
title: "Error",
description: "Please provide a name for the evidence",
variant: "destructive",
});
return;
}
if (!linkEvidenceUrl.trim()) {
toast({
title: "Error",
description: "Please provide a URL for the evidence",
variant: "destructive",
});
return;
}
// Description is now optional for link evidence
// Remove the validation check for empty description
// Show toast for add started
toast({
title: "Adding link evidence",
description: `Adding ${linkEvidenceName}...`,
variant: "default",
});
// Get the evidence connection ID for this task
const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id);
// Use a default description if none is provided
const description =
linkEvidenceDescription.trim() || `Link to ${linkEvidenceUrl}`;
uploadEvidence({
variables: {
input: {
taskId: taskForEvidence.id,
name: linkEvidenceName,
type: "LINK",
url: linkEvidenceUrl,
description: description,
file: null,
},
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
},
onCompleted: () => {
toast({
title: "Link evidence added",
description: "Link evidence has been added successfully.",
variant: "default",
});
setTaskForEvidence(null);
setEvidenceDialogOpen(false);
// Reset form fields
setLinkEvidenceName("");
setLinkEvidenceUrl("");
setLinkEvidenceDescription("");
},
onError: (error) => {
toast({
title: "Error adding link evidence",
description: error.message,
variant: "destructive",
});
@@ -783,10 +874,10 @@ function ControlOverviewPageContent({
const file = files[0];
setUploadingTaskId(taskId);
// Show toast for upload started
// Show toast for add started
toast({
title: "Upload started",
description: `Uploading ${file.name}...`,
title: "Adding document",
description: `Adding ${file.name}...`,
variant: "default",
});
@@ -798,7 +889,9 @@ function ControlOverviewPageContent({
input: {
taskId: taskId,
name: file.name,
type: "FILE",
file: null,
description: "Document evidence",
},
connections: evidenceConnectionId ? [evidenceConnectionId] : [],
},
@@ -808,15 +901,15 @@ function ControlOverviewPageContent({
onCompleted: () => {
setUploadingTaskId(null);
toast({
title: "Evidence uploaded",
description: "Evidence has been uploaded successfully.",
title: "Document added",
description: "Document evidence has been added successfully.",
variant: "default",
});
},
onError: (error) => {
setUploadingTaskId(null);
toast({
title: "Error uploading evidence",
title: "Error adding document",
description: error.message,
variant: "destructive",
});
@@ -850,9 +943,11 @@ function ControlOverviewPageContent({
}
};
// Function to get file icon based on mime type
const getFileIcon = (mimeType: string) => {
if (mimeType.startsWith("image/")) {
// Function to get file icon based on mime type and evidence type
const getFileIcon = (mimeType: string, evidenceType: string) => {
if (evidenceType === "LINK") {
return <Link2 className="w-4 h-4 text-blue-600" />;
} else if (mimeType.startsWith("image/")) {
return <Image className="w-4 h-4 text-blue-500" />;
} else if (mimeType.includes("pdf")) {
return <FileText className="w-4 h-4 text-red-500" />;
@@ -1088,7 +1183,7 @@ function ControlOverviewPageContent({
<div className="flex items-center gap-3">
<div className="text-sm text-gray-500 flex items-center bg-gray-50 px-3 py-1.5 rounded-md border border-gray-200">
<FileIcon className="w-4 h-4 mr-2 text-blue-500" />
<span>Drag & drop files onto tasks to upload evidence</span>
<span>Drag & drop files onto tasks to add evidence</span>
</div>
<Dialog
open={isCreateTaskOpen}
@@ -1246,9 +1341,9 @@ function ControlOverviewPageContent({
{draggedOverTaskId === task?.id && (
<div className="absolute inset-0 flex items-center justify-center rounded-md z-10 bg-blue-50 bg-opacity-90 backdrop-blur-sm border-2 border-dashed border-blue-400">
<div className="flex items-center gap-2 text-blue-600 bg-white p-5 rounded-lg shadow-md">
<Upload className="w-4 h-4 text-blue-500" />
<FileText className="w-4 h-4 text-blue-500" />
<p className="text-sm font-medium text-center">
Drop file to upload evidence
Drop file to add as evidence
</p>
</div>
</div>
@@ -1258,7 +1353,7 @@ function ControlOverviewPageContent({
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-95 rounded-md z-10 backdrop-blur-sm">
<div className="flex flex-col items-center gap-3 text-blue-600">
<Loader2 className="w-10 h-10 animate-spin text-blue-500" />
<p className="font-medium">Uploading evidence...</p>
<p className="font-medium">Adding document...</p>
<p className="text-sm text-gray-500">Please wait</p>
</div>
</div>
@@ -1491,8 +1586,9 @@ function ControlOverviewPageContent({
handleUploadEvidence(task.id, task.name);
}
}}
title="Add Evidence"
>
<Upload className="w-4 h-4" />
<FileText className="w-4 h-4" />
</button>
<button
type="button"
@@ -1547,17 +1643,31 @@ function ControlOverviewPageContent({
>
<div className="flex items-center gap-3">
<div className="bg-white p-2 rounded-md border border-gray-200">
{getFileIcon(evidence.mimeType)}
{getFileIcon(
evidence.mimeType,
evidence.type
)}
</div>
<div>
<div className="text-sm font-medium text-gray-800">
{evidence.filename}
</div>
<div className="text-xs text-gray-500 flex items-center gap-2 mt-0.5">
<span className="font-medium text-gray-600">
{formatFileSize(evidence.size)}
</span>
<span></span>
{evidence.type === "FILE" ? (
<>
<span className="font-medium text-gray-600">
{formatFileSize(evidence.size)}
</span>
<span></span>
</>
) : evidence.url ? (
<>
<span className="font-medium text-blue-600 truncate max-w-[200px]">
{evidence.url}
</span>
<span></span>
</>
) : null}
<span>
{formatDate(evidence.createdAt)}
</span>
@@ -1565,38 +1675,45 @@ function ControlOverviewPageContent({
</div>
</div>
<div className="flex items-center gap-1">
{evidence.mimeType.startsWith("image/") ? (
<button
onClick={() =>
handlePreviewEvidence(evidence)
}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Preview Image"
>
<Eye className="w-4 h-4 text-blue-600" />
</button>
) : (
{evidence.type === "FILE" ? (
<>
{evidence.mimeType.startsWith("image/") ? (
<button
onClick={() =>
handlePreviewEvidence(evidence)
}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Preview Image"
>
<Eye className="w-4 h-4 text-blue-600" />
</button>
) : (
<button
onClick={(e) => {
e.preventDefault();
handlePreviewEvidence(evidence);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Download"
>
<Download className="w-4 h-4 text-blue-600" />
</button>
)}
</>
) : evidence.url ? (
<button
onClick={(e) => {
e.preventDefault();
handlePreviewEvidence(evidence);
if (evidence.url) {
window.open(evidence.url, "_blank");
}
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Download"
title="Open Link"
>
<Download className="w-4 h-4 text-blue-600" />
<Link2 className="w-4 h-4 text-blue-600" />
</button>
)}
<button
onClick={(e) => {
e.preventDefault();
handlePreviewEvidence(evidence);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Download"
>
<Download className="w-4 h-4 text-blue-600" />
</button>
) : null}
<button
onClick={(e) => {
e.preventDefault();
@@ -1632,6 +1749,99 @@ function ControlOverviewPageContent({
</div>
</div>
{/* Evidence Add Dialog */}
<Dialog open={evidenceDialogOpen} onOpenChange={setEvidenceDialogOpen}>
<DialogContent className="sm:max-w-[525px]">
<DialogHeader>
<DialogTitle>Add Evidence</DialogTitle>
<DialogDescription>
Choose the type of evidence you want to add to this task.
</DialogDescription>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={(value: string) =>
setActiveTab(value as "file" | "link")
}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="file">Document</TabsTrigger>
<TabsTrigger value="link">Link</TabsTrigger>
</TabsList>
<TabsContent value="file" className="space-y-4">
<div className="space-y-4 pt-4">
<p>Select a document to add as evidence.</p>
<Button
onClick={() => {
if (hiddenFileInputRef.current) {
hiddenFileInputRef.current.click();
setEvidenceDialogOpen(false);
}
}}
className="w-full"
>
Select Document
</Button>
</div>
</TabsContent>
<TabsContent value="link" className="space-y-4 pt-4">
<div className="space-y-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="evidence-name">Name</Label>
<Input
id="evidence-name"
value={linkEvidenceName}
onChange={(e) => setLinkEvidenceName(e.target.value)}
placeholder="Name for this evidence"
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="evidence-url">URL</Label>
<Input
id="evidence-url"
value={linkEvidenceUrl}
onChange={(e) => setLinkEvidenceUrl(e.target.value)}
placeholder="https://example.com"
type="url"
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="evidence-description">
Description (optional)
</Label>
<Textarea
id="evidence-description"
value={linkEvidenceDescription}
onChange={(e) =>
setLinkEvidenceDescription(e.target.value)
}
placeholder="Describe this evidence (optional)"
className="min-h-[100px]"
/>
</div>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button
variant="outline"
onClick={() => setEvidenceDialogOpen(false)}
>
Cancel
</Button>
{activeTab === "link" && (
<Button onClick={handleLinkEvidenceSubmit}>Add Link</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
{/* Hidden file input for direct uploads */}
<input
type="file"

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<4cf4f895bdcd936c74cb70f562210df7>>
* @generated SignedSource<<5fff8009247229346c3cda94359f2129>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -14,7 +14,7 @@ export type ControlOverviewPageGetEvidenceFileUrlQuery$variables = {
};
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
readonly node: {
readonly fileUrl?: string;
readonly fileUrl?: string | null | undefined;
readonly id?: string;
};
};

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<084485ae03f7089bfdee310d631b01eb>>
* @generated SignedSource<<e273ee54aacd3db274341d21b33809b2>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime';
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
export type EvidenceType = "FILE" | "LINK";
export type TaskState = "DONE" | "TODO";
export type ControlOverviewPageQuery$variables = {
controlId: string;
@@ -44,6 +45,8 @@ export type ControlOverviewPageQuery$data = {
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
}>;
};
@@ -251,6 +254,20 @@ v15 = [
"storageKey": null
},
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -480,7 +497,7 @@ return {
]
},
"params": {
"cacheID": "4bcea497959829f998cdab71797c8675",
"cacheID": "6f419912ad357b36c51113ef7672ad51",
"id": null,
"metadata": {
"connection": [
@@ -503,11 +520,11 @@ return {
},
"name": "ControlOverviewPageQuery",
"operationKind": "query",
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n version\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n version\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "4710a9799b3daf286afdd430f026e6c2";
(node as any).hash = "59a0c6e5a414410be244c7a62bba7696";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<313084c3c2422d51df42fe4d6b848d49>>
* @generated SignedSource<<6726ce2446381674b1992ece913fd680>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,10 +10,14 @@
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
export type EvidenceType = "FILE" | "LINK";
export type UploadEvidenceInput = {
file: any;
description: string;
file?: any | null | undefined;
name: string;
taskId: string;
type: EvidenceType;
url?: string | null | undefined;
};
export type ControlOverviewPageUploadEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
@@ -24,12 +28,14 @@ export type ControlOverviewPageUploadEvidenceMutation$data = {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: string;
readonly fileUrl: string;
readonly fileUrl: string | null | undefined;
readonly filename: string;
readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
};
};
@@ -101,6 +107,20 @@ v3 = {
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -194,16 +214,16 @@ return {
]
},
"params": {
"cacheID": "5edaf64bde176530d9952e3e8aeb15af",
"cacheID": "de3681706266cd453ddccb5fe2900ea8",
"id": null,
"metadata": {},
"name": "ControlOverviewPageUploadEvidenceMutation",
"operationKind": "mutation",
"text": "mutation ControlOverviewPageUploadEvidenceMutation(\n $input: UploadEvidenceInput!\n) {\n uploadEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n size\n state\n createdAt\n }\n }\n }\n}\n"
"text": "mutation ControlOverviewPageUploadEvidenceMutation(\n $input: UploadEvidenceInput!\n) {\n uploadEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "51f35fe735a747ddaeb0f0cfa298f7e2";
(node as any).hash = "396146017c07e06dc709199a0b5ad012";
export default node;

View File

@@ -28,15 +28,18 @@ import (
type (
Evidence struct {
ID gid.GID `db:"id"`
TaskID gid.GID `db:"task_id"`
State EvidenceState `db:"state"`
ObjectKey string `db:"object_key"`
MimeType string `db:"mime_type"`
Size uint64 `db:"size"`
Filename string `db:"filename"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
TaskID gid.GID `db:"task_id"`
State EvidenceState `db:"state"`
Type EvidenceType `db:"type"`
ObjectKey string `db:"object_key"`
MimeType string `db:"mime_type"`
Size uint64 `db:"size"`
Filename string `db:"filename"`
URL string `db:"url"`
Description string `db:"description"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Evidences []*Evidence
@@ -66,7 +69,10 @@ INSERT INTO
mime_type,
size,
state,
type,
filename,
url,
description,
created_at,
updated_at
)
@@ -78,7 +84,10 @@ VALUES (
@mime_type,
@size,
@state,
@type,
@filename,
@url,
@description,
@created_at,
@updated_at
)
@@ -95,6 +104,9 @@ VALUES (
"created_at": e.CreatedAt,
"updated_at": e.UpdatedAt,
"state": e.State,
"type": e.Type,
"url": e.URL,
"description": e.Description,
}
_, err := conn.Exec(ctx, q, args)
return err
@@ -111,10 +123,13 @@ SELECT
id,
task_id,
state,
type,
object_key,
mime_type,
size,
filename,
url,
description,
created_at,
updated_at
FROM
@@ -157,10 +172,13 @@ SELECT
id,
task_id,
state,
type,
object_key,
mime_type,
size,
filename,
url,
description,
created_at,
updated_at
FROM

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
EvidenceType uint8
)
const (
EvidenceTypeFile EvidenceType = iota
EvidenceTypeLink
)
func (et EvidenceType) MarshalText() ([]byte, error) {
return []byte(et.String()), nil
}
func (et *EvidenceType) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case EvidenceTypeFile.String():
*et = EvidenceTypeFile
case EvidenceTypeLink.String():
*et = EvidenceTypeLink
default:
return fmt.Errorf("invalid EvidenceType value: %q", val)
}
return nil
}
func (et EvidenceType) String() string {
var val string
switch et {
case EvidenceTypeFile:
val = "FILE"
case EvidenceTypeLink:
val = "LINK"
}
return val
}
func (et *EvidenceType) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for EvidenceType, expected string got %T", value)
}
return et.UnmarshalText([]byte(val))
}
func (et EvidenceType) Value() (driver.Value, error) {
return et.String(), nil
}

View File

@@ -0,0 +1,15 @@
-- Add evidence type enum
CREATE TYPE evidence_type AS ENUM (
'FILE',
'LINK'
);
ALTER TABLE evidences
ADD COLUMN type evidence_type NOT NULL DEFAULT 'FILE',
ADD COLUMN url TEXT NOT NULL DEFAULT '',
ADD COLUMN description TEXT NOT NULL DEFAULT '';
ALTER TABLE evidences
ALTER COLUMN type DROP DEFAULT,
ALTER COLUMN url DROP DEFAULT,
ALTER COLUMN description DROP DEFAULT;

View File

@@ -37,9 +37,12 @@ type (
}
CreateEvidenceRequest struct {
TaskID gid.GID
Name string
File io.Reader
TaskID gid.GID
Name string
Type coredata.EvidenceType
File io.Reader
URL string
Description string
}
)
@@ -73,50 +76,61 @@ func (s EvidenceService) Create(
return nil, fmt.Errorf("cannot create evidence global id: %w", err)
}
contentType := "application/octet-stream"
if req.Name != "" {
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
contentType = detectedType
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
State: coredata.EvidenceStateValid,
Type: req.Type,
Filename: req.Name,
URL: req.URL,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
if req.Type == coredata.EvidenceTypeFile {
contentType := "application/octet-stream"
if req.Name != "" {
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
contentType = detectedType
}
}
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, fmt.Errorf("cannot generate object key: %w", err)
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, fmt.Errorf("cannot generate object key: %w", err)
}
putObjectOutput, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
Body: req.File,
ContentType: aws.String(contentType),
})
if err != nil {
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
}
putObjectOutput, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
Body: req.File,
ContentType: aws.String(contentType),
})
if err != nil {
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
}
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
})
if err != nil {
return nil, fmt.Errorf("cannot get object metadata: %w", err)
}
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
})
if err != nil {
return nil, fmt.Errorf("cannot get object metadata: %w", err)
}
fmt.Println("putObjectOutput", putObjectOutput)
fmt.Println("putObjectOutput", putObjectOutput)
evidence.ObjectKey = objectKey.String()
evidence.MimeType = contentType
evidence.Size = uint64(*headOutput.ContentLength)
} else if req.Type == coredata.EvidenceTypeLink {
evidence.MimeType = "text/uri-list"
evidence.Size = uint64(len(req.URL))
evidence.ObjectKey = ""
}
task := &coredata.Task{}
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
State: coredata.EvidenceStateValid,
ObjectKey: objectKey.String(),
MimeType: contentType,
Size: uint64(*headOutput.ContentLength),
Filename: req.Name,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithTx(
ctx,
@@ -134,7 +148,7 @@ func (s EvidenceService) Create(
)
if err != nil {
// TODO try do delete file from s3
// TODO try do delete file from s3 if it's a file type
return nil, err
}
@@ -151,6 +165,10 @@ func (s EvidenceService) GenerateFileURL(
return nil, fmt.Errorf("cannot get evidence: %w", err)
}
if evidence.Type == coredata.EvidenceTypeLink {
return nil, fmt.Errorf("cannot generate file URL for link type evidence")
}
presignClient := s3.NewPresignClient(s.svc.s3)
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{

View File

@@ -411,13 +411,22 @@ type EvidenceEdge {
node: Evidence!
}
enum EvidenceType
@goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceType") {
FILE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeFile")
LINK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeLink")
}
type Evidence implements Node {
id: ID!
fileUrl: String! @goField(forceResolver: true)
fileUrl: String @goField(forceResolver: true)
mimeType: String!
size: Int!
state: EvidenceState!
type: EvidenceType!
filename: String!
url: String
description: String!
createdAt: Datetime!
updatedAt: Datetime!
@@ -707,7 +716,10 @@ type UpdateControlPayload {
input UploadEvidenceInput {
taskId: ID!
name: String!
file: Upload!
file: Upload
type: EvidenceType!
url: String
description: String!
}
type UploadEvidencePayload {

View File

@@ -141,14 +141,17 @@ type ComplexityRoot struct {
}
Evidence struct {
CreatedAt func(childComplexity int) int
FileURL func(childComplexity int) int
Filename func(childComplexity int) int
ID func(childComplexity int) int
MimeType func(childComplexity int) int
Size func(childComplexity int) int
State func(childComplexity int) int
UpdatedAt func(childComplexity int) int
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
FileURL func(childComplexity int) int
Filename func(childComplexity int) int
ID func(childComplexity int) int
MimeType func(childComplexity int) int
Size func(childComplexity int) int
State func(childComplexity int) int
Type func(childComplexity int) int
URL func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
EvidenceConnection struct {
@@ -420,7 +423,7 @@ type ControlResolver interface {
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error)
}
type EvidenceResolver interface {
FileURL(ctx context.Context, obj *types.Evidence) (string, error)
FileURL(ctx context.Context, obj *types.Evidence) (*string, error)
}
type FrameworkResolver interface {
Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error)
@@ -711,6 +714,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Evidence.CreatedAt(childComplexity), true
case "Evidence.description":
if e.complexity.Evidence.Description == nil {
break
}
return e.complexity.Evidence.Description(childComplexity), true
case "Evidence.fileUrl":
if e.complexity.Evidence.FileURL == nil {
break
@@ -753,6 +763,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Evidence.State(childComplexity), true
case "Evidence.type":
if e.complexity.Evidence.Type == nil {
break
}
return e.complexity.Evidence.Type(childComplexity), true
case "Evidence.url":
if e.complexity.Evidence.URL == nil {
break
}
return e.complexity.Evidence.URL(childComplexity), true
case "Evidence.updatedAt":
if e.complexity.Evidence.UpdatedAt == nil {
break
@@ -2502,13 +2526,22 @@ type EvidenceEdge {
node: Evidence!
}
enum EvidenceType
@goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceType") {
FILE @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeFile")
LINK @goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceTypeLink")
}
type Evidence implements Node {
id: ID!
fileUrl: String! @goField(forceResolver: true)
fileUrl: String @goField(forceResolver: true)
mimeType: String!
size: Int!
state: EvidenceState!
type: EvidenceType!
filename: String!
url: String
description: String!
createdAt: Datetime!
updatedAt: Datetime!
@@ -2798,7 +2831,10 @@ type UpdateControlPayload {
input UploadEvidenceInput {
taskId: ID!
name: String!
file: Upload!
file: Upload
type: EvidenceType!
url: String
description: String!
}
type UploadEvidencePayload {
@@ -5868,14 +5904,11 @@ func (ec *executionContext) _Evidence_fileUrl(ctx context.Context, field graphql
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
res := resTmp.(*string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
return ec.marshalOString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Evidence_fileUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -6005,6 +6038,44 @@ func (ec *executionContext) fieldContext_Evidence_state(_ context.Context, field
return fc, nil
}
func (ec *executionContext) _Evidence_type(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_type(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Type, 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.(coredata.EvidenceType)
fc.Result = res
return ec.marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Evidence_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Evidence",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type EvidenceType does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Evidence_filename(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_filename(ctx, field)
if err != nil {
@@ -6043,6 +6114,79 @@ func (ec *executionContext) fieldContext_Evidence_filename(_ context.Context, fi
return fc, nil
}
func (ec *executionContext) _Evidence_url(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_url(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.URL, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Evidence_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Evidence",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Evidence_description(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_description(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Description, 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.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Evidence_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Evidence",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Evidence_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_createdAt(ctx, field)
if err != nil {
@@ -6292,8 +6436,14 @@ func (ec *executionContext) fieldContext_EvidenceEdge_node(_ context.Context, fi
return ec.fieldContext_Evidence_size(ctx, field)
case "state":
return ec.fieldContext_Evidence_state(ctx, field)
case "type":
return ec.fieldContext_Evidence_type(ctx, field)
case "filename":
return ec.fieldContext_Evidence_filename(ctx, field)
case "url":
return ec.fieldContext_Evidence_url(ctx, field)
case "description":
return ec.fieldContext_Evidence_description(ctx, field)
case "createdAt":
return ec.fieldContext_Evidence_createdAt(ctx, field)
case "updatedAt":
@@ -15668,7 +15818,7 @@ func (ec *executionContext) unmarshalInputUploadEvidenceInput(ctx context.Contex
asMap[k] = v
}
fieldsInOrder := [...]string{"taskId", "name", "file"}
fieldsInOrder := [...]string{"taskId", "name", "file", "type", "url", "description"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -15691,11 +15841,32 @@ func (ec *executionContext) unmarshalInputUploadEvidenceInput(ctx context.Contex
it.Name = data
case "file":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file"))
data, err := ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v)
data, err := ec.unmarshalOUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v)
if err != nil {
return it, err
}
it.File = data
case "type":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type"))
data, err := ec.unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx, v)
if err != nil {
return it, err
}
it.Type = data
case "url":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("url"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.URL = data
case "description":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Description = data
}
}
@@ -16652,11 +16823,8 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
case "fileUrl":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
res = ec._Evidence_fileUrl(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
@@ -16695,11 +16863,23 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "type":
out.Values[i] = ec._Evidence_type(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "filename":
out.Values[i] = ec._Evidence_filename(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "url":
out.Values[i] = ec._Evidence_url(ctx, field, obj)
case "description":
out.Values[i] = ec._Evidence_description(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "createdAt":
out.Values[i] = ec._Evidence_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -20101,6 +20281,33 @@ var (
}
)
func (ec *executionContext) unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx context.Context, v any) (coredata.EvidenceType, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType[tmp]
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType(ctx context.Context, sel ast.SelectionSet, v coredata.EvidenceType) graphql.Marshaler {
res := graphql.MarshalString(marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType[v])
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
var (
unmarshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType = map[string]coredata.EvidenceType{
"FILE": coredata.EvidenceTypeFile,
"LINK": coredata.EvidenceTypeLink,
}
marshalNEvidenceType2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐEvidenceType = map[coredata.EvidenceType]string{
coredata.EvidenceTypeFile: "FILE",
coredata.EvidenceTypeLink: "LINK",
}
)
func (ec *executionContext) marshalNFramework2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v *types.Framework) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@@ -44,14 +44,25 @@ func NewEvidenceEdge(e *coredata.Evidence, orderBy coredata.EvidenceOrderField)
}
func NewEvidence(e *coredata.Evidence) *Evidence {
var fileURL *string = nil
var urlPtr *string = nil
if e.URL != "" {
urlCopy := e.URL
urlPtr = &urlCopy
}
return &Evidence{
ID: e.ID,
State: e.State,
FileURL: "",
Filename: e.Filename,
MimeType: e.MimeType,
Size: int(e.Size),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
ID: e.ID,
State: e.State,
Type: e.Type,
FileURL: fileURL,
Filename: e.Filename,
MimeType: e.MimeType,
Size: int(e.Size),
URL: urlPtr,
Description: e.Description,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}

View File

@@ -195,14 +195,17 @@ type DeleteVendorPayload struct {
}
type Evidence struct {
ID gid.GID `json:"id"`
FileURL string `json:"fileUrl"`
MimeType string `json:"mimeType"`
Size int `json:"size"`
State coredata.EvidenceState `json:"state"`
Filename string `json:"filename"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
FileURL *string `json:"fileUrl,omitempty"`
MimeType string `json:"mimeType"`
Size int `json:"size"`
State coredata.EvidenceState `json:"state"`
Type coredata.EvidenceType `json:"type"`
Filename string `json:"filename"`
URL *string `json:"url,omitempty"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Evidence) IsNode() {}
@@ -495,9 +498,12 @@ type UpdateVendorPayload struct {
}
type UploadEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
File graphql.Upload `json:"file"`
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
File *graphql.Upload `json:"file,omitempty"`
Type coredata.EvidenceType `json:"type"`
URL *string `json:"url,omitempty"`
Description string `json:"description"`
}
type UploadEvidencePayload struct {

View File

@@ -45,15 +45,20 @@ func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *
}
// FileURL is the resolver for the fileUrl field.
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (string, error) {
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*string, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
if obj.Type == coredata.EvidenceTypeLink {
return obj.URL, nil
}
fileURL, err := svc.Evidences.GenerateFileURL(ctx, obj.ID, 15*time.Minute)
if err != nil {
return "", fmt.Errorf("cannot generate file URL: %w", err)
return nil, fmt.Errorf("cannot generate file URL: %w", err)
}
return *fileURL, nil
result := *fileURL
return &result, nil
}
// Controls is the resolver for the controls field.
@@ -436,10 +441,28 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
var url string
if input.URL != nil {
url = *input.URL
}
req := probo.CreateEvidenceRequest{
TaskID: input.TaskID,
Name: input.Name,
File: input.File.File,
TaskID: input.TaskID,
Name: input.Name,
Type: input.Type,
URL: url,
Description: input.Description,
}
if input.Type == coredata.EvidenceTypeFile {
if input.File == nil {
return nil, fmt.Errorf("file is required for FILE type evidence")
}
req.File = input.File.File
} else if input.Type == coredata.EvidenceTypeLink {
if input.URL == nil || *input.URL == "" {
return nil, fmt.Errorf("URL is required for LINK type evidence")
}
}
evidence, err := svc.Evidences.Create(ctx, req)