@@ -13,6 +13,8 @@ import {
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
fetchQuery,
|
||||
useRelayEnvironment,
|
||||
} from "react-relay";
|
||||
import {
|
||||
CheckCircle2,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
File as FileGeneric,
|
||||
FileText,
|
||||
Image,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -74,7 +77,6 @@ const controlOverviewPageQuery = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fileUrl
|
||||
mimeType
|
||||
filename
|
||||
size
|
||||
@@ -153,6 +155,18 @@ const uploadEvidenceMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
// Add a GraphQL query to fetch the fileUrl for an evidence item
|
||||
const getEvidenceFileUrlQuery = graphql`
|
||||
query ControlOverviewPageGetEvidenceFileUrlQuery($evidenceId: ID!) {
|
||||
node(id: $evidenceId) {
|
||||
... on Evidence {
|
||||
id
|
||||
fileUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function ControlOverviewPageContent({
|
||||
queryRef,
|
||||
}: {
|
||||
@@ -165,6 +179,7 @@ function ControlOverviewPageContent({
|
||||
const { toast } = useToast();
|
||||
const { organizationId, frameworkId, controlId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const environment = useRelayEnvironment();
|
||||
const [updateTaskState] =
|
||||
useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
|
||||
updateTaskStateMutation
|
||||
@@ -207,6 +222,16 @@ function ControlOverviewPageContent({
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Add state for the preview modal
|
||||
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
||||
const [previewEvidence, setPreviewEvidence] = useState<{
|
||||
id: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
fileUrl?: string;
|
||||
} | null>(null);
|
||||
const [isLoadingFileUrl, setIsLoadingFileUrl] = useState(false);
|
||||
|
||||
const tasks = data.control.tasks?.edges.map((edge) => edge.node) || [];
|
||||
|
||||
const getEvidenceConnectionId = useCallback(
|
||||
@@ -537,6 +562,54 @@ function ControlOverviewPageContent({
|
||||
}
|
||||
};
|
||||
|
||||
// Simplified preview handler that opens the modal and sets the evidence
|
||||
const handlePreviewEvidence = (evidence: {
|
||||
id: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
}) => {
|
||||
// Just open the modal with the evidence info
|
||||
setPreviewEvidence({
|
||||
id: evidence.id,
|
||||
filename: evidence.filename,
|
||||
mimeType: evidence.mimeType,
|
||||
});
|
||||
setIsPreviewModalOpen(true);
|
||||
|
||||
// We'll fetch the fileUrl when the modal opens
|
||||
setIsLoadingFileUrl(true);
|
||||
|
||||
// Use GraphQL query to fetch the fileUrl
|
||||
fetchQuery(environment, getEvidenceFileUrlQuery, {
|
||||
evidenceId: evidence.id,
|
||||
})
|
||||
.toPromise()
|
||||
.then((response) => {
|
||||
// Type assertion for the response
|
||||
const data = response as { node?: { id: string; fileUrl?: string } };
|
||||
|
||||
if (data?.node?.fileUrl) {
|
||||
setPreviewEvidence((prev) => {
|
||||
if (!prev) return null;
|
||||
return { ...prev, fileUrl: data.node!.fileUrl };
|
||||
});
|
||||
} else {
|
||||
throw new Error("File URL not available in response");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching file URL:", error);
|
||||
toast({
|
||||
title: "Error fetching file URL",
|
||||
description: error.message || "Could not load the file preview",
|
||||
variant: "destructive",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingFileUrl(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -829,27 +902,38 @@ function ControlOverviewPageContent({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{evidence.mimeType.startsWith("image/") && (
|
||||
<a
|
||||
href={evidence.fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{evidence.mimeType.startsWith("image/") ? (
|
||||
<button
|
||||
onClick={() =>
|
||||
handlePreviewEvidence(evidence)
|
||||
}
|
||||
className="p-1 rounded-full hover:bg-gray-100"
|
||||
title="Preview"
|
||||
title="Preview Image"
|
||||
>
|
||||
<Eye className="w-4 h-4 text-gray-600" />
|
||||
</a>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handlePreviewEvidence(evidence);
|
||||
}}
|
||||
className="p-1 rounded-full hover:bg-gray-100"
|
||||
title="View File"
|
||||
>
|
||||
<Eye className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href={evidence.fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handlePreviewEvidence(evidence);
|
||||
}}
|
||||
className="p-1 rounded-full hover:bg-gray-100"
|
||||
title="Download"
|
||||
download
|
||||
>
|
||||
<Download className="w-4 h-4 text-gray-600" />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -942,6 +1026,85 @@ function ControlOverviewPageContent({
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add the Preview Modal */}
|
||||
<Dialog open={isPreviewModalOpen} onOpenChange={setIsPreviewModalOpen}>
|
||||
<DialogContent className="sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<span>{previewEvidence?.filename}</span>
|
||||
<button
|
||||
onClick={() => setIsPreviewModalOpen(false)}
|
||||
className="rounded-full p-1 hover:bg-gray-100"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center justify-center min-h-[300px] bg-gray-50 rounded-md p-4">
|
||||
{isLoadingFileUrl ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||
<p className="text-gray-500">Loading preview...</p>
|
||||
</div>
|
||||
) : previewEvidence?.fileUrl ? (
|
||||
previewEvidence.mimeType.startsWith("image/") ? (
|
||||
<img
|
||||
src={previewEvidence.fileUrl}
|
||||
alt={previewEvidence.filename}
|
||||
className="max-h-[70vh] object-contain"
|
||||
/>
|
||||
) : previewEvidence.mimeType.includes("pdf") ? (
|
||||
<iframe
|
||||
src={previewEvidence.fileUrl}
|
||||
className="w-full h-[70vh]"
|
||||
title={previewEvidence.filename}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<FileGeneric className="w-16 h-16 text-gray-400" />
|
||||
<p className="text-gray-600">
|
||||
Preview not available for this file type
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (previewEvidence?.fileUrl) {
|
||||
window.open(previewEvidence.fileUrl, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<FileGeneric className="w-12 h-12 text-gray-400" />
|
||||
<p className="text-gray-500">Failed to load preview</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsPreviewModalOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{previewEvidence?.fileUrl && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (previewEvidence?.fileUrl) {
|
||||
window.open(previewEvidence.fileUrl, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
134
apps/console/src/pages/__generated__/ControlOverviewPageGetEvidenceFileUrlQuery.graphql.ts
generated
Normal file
134
apps/console/src/pages/__generated__/ControlOverviewPageGetEvidenceFileUrlQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @generated SignedSource<<4cf4f895bdcd936c74cb70f562210df7>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type ControlOverviewPageGetEvidenceFileUrlQuery$variables = {
|
||||
evidenceId: string;
|
||||
};
|
||||
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
|
||||
readonly node: {
|
||||
readonly fileUrl?: string;
|
||||
readonly id?: string;
|
||||
};
|
||||
};
|
||||
export type ControlOverviewPageGetEvidenceFileUrlQuery = {
|
||||
response: ControlOverviewPageGetEvidenceFileUrlQuery$data;
|
||||
variables: ControlOverviewPageGetEvidenceFileUrlQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "evidenceId"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "evidenceId"
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"type": "Evidence",
|
||||
"abstractKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"type": "Evidence",
|
||||
"abstractKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "debb5fda7cec88257a50998c78ee1e33",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query ControlOverviewPageGetEvidenceFileUrlQuery(\n $evidenceId: ID!\n) {\n node(id: $evidenceId) {\n __typename\n ... on Evidence {\n id\n fileUrl\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "a97d9390a7415ff87916e268458f5ead";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<005ee5181eeb4c0865e7e76dcb6b3aac>>
|
||||
* @generated SignedSource<<3c761f476fc4dcbc2f175eefa625bb28>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -31,8 +31,7 @@ export type ControlOverviewPageQuery$data = {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly createdAt: string;
|
||||
readonly fileUrl: string;
|
||||
readonly createdAt: any;
|
||||
readonly filename: string;
|
||||
readonly id: string;
|
||||
readonly mimeType: string;
|
||||
@@ -173,13 +172,6 @@ v11 = [
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -421,7 +413,7 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "0747630c87fd59b419e5264fcede33bf",
|
||||
"cacheID": "4364cf9b985fb746dde62c1e94063390",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
@@ -444,11 +436,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 category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n evidences(first: 50) {\n edges {\n node {\n id\n fileUrl\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 category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\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"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "0fc29eb31e33b025168ac8972109f853";
|
||||
(node as any).hash = "b91e90b278ccedc6b44d3a3c53593e73";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<f1cdd20d14ca9fefe868019e5aa43741>>
|
||||
* @generated SignedSource<<09af19b5a7657e04606a42962db9db2c>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -23,7 +23,7 @@ export type ControlOverviewPageUploadEvidenceMutation$data = {
|
||||
readonly uploadEvidence: {
|
||||
readonly evidenceEdge: {
|
||||
readonly node: {
|
||||
readonly createdAt: string;
|
||||
readonly createdAt: any;
|
||||
readonly fileUrl: string;
|
||||
readonly id: string;
|
||||
readonly mimeType: string;
|
||||
|
||||
@@ -320,7 +320,7 @@ type EvidenceEdge {
|
||||
|
||||
type Evidence implements Node {
|
||||
id: ID!
|
||||
fileUrl: String!
|
||||
fileUrl: String! @goField(forceResolver: true)
|
||||
mimeType: String!
|
||||
size: Int!
|
||||
state: EvidenceState!
|
||||
|
||||
@@ -382,6 +382,8 @@ type ControlResolver interface {
|
||||
Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error)
|
||||
}
|
||||
type EvidenceResolver interface {
|
||||
FileURL(ctx context.Context, obj *types.Evidence) (string, error)
|
||||
|
||||
StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error)
|
||||
}
|
||||
type FrameworkResolver interface {
|
||||
@@ -2188,7 +2190,7 @@ type EvidenceEdge {
|
||||
|
||||
type Evidence implements Node {
|
||||
id: ID!
|
||||
fileUrl: String!
|
||||
fileUrl: String! @goField(forceResolver: true)
|
||||
mimeType: String!
|
||||
size: Int!
|
||||
state: EvidenceState!
|
||||
@@ -5184,7 +5186,7 @@ func (ec *executionContext) _Evidence_fileUrl(ctx context.Context, field graphql
|
||||
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.FileURL, nil
|
||||
return ec.resolvers.Evidence().FileURL(rctx, obj)
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
@@ -5205,8 +5207,8 @@ func (ec *executionContext) fieldContext_Evidence_fileUrl(_ context.Context, fie
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Evidence",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
@@ -14061,10 +14063,36 @@ func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet,
|
||||
atomic.AddUint32(&out.Invalids, 1)
|
||||
}
|
||||
case "fileUrl":
|
||||
out.Values[i] = ec._Evidence_fileUrl(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
atomic.AddUint32(&out.Invalids, 1)
|
||||
field := field
|
||||
|
||||
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
||||
res = ec._Evidence_fileUrl(ctx, field, obj)
|
||||
if res == graphql.Null {
|
||||
atomic.AddUint32(&fs.Invalids, 1)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
if field.Deferrable != nil {
|
||||
dfs, ok := deferred[field.Deferrable.Label]
|
||||
di := 0
|
||||
if ok {
|
||||
dfs.AddField(field)
|
||||
di = len(dfs.Values) - 1
|
||||
} else {
|
||||
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
|
||||
deferred[field.Deferrable.Label] = dfs
|
||||
}
|
||||
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
|
||||
return innerFunc(ctx, dfs)
|
||||
})
|
||||
|
||||
// don't run the out.Concurrently() call below
|
||||
out.Values[i] = graphql.Null
|
||||
continue
|
||||
}
|
||||
|
||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||
case "mimeType":
|
||||
out.Values[i] = ec._Evidence_mimeType(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
|
||||
@@ -7,6 +7,7 @@ package console_v1
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/api/console/v1/schema"
|
||||
"github.com/getprobo/probo/pkg/api/console/v1/types"
|
||||
@@ -41,6 +42,16 @@ func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *
|
||||
return types.NewTaskConnection(page), nil
|
||||
}
|
||||
|
||||
// FileURL is the resolver for the fileUrl field.
|
||||
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (string, error) {
|
||||
fileURL, err := r.proboSvc.GetEvidenceFileURL(ctx, obj.ID, 15*time.Minute)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot generate file URL: %w", err)
|
||||
}
|
||||
|
||||
return *fileURL, nil
|
||||
}
|
||||
|
||||
// StateTransisions is the resolver for the stateTransisions field.
|
||||
func (r *evidenceResolver) StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error) {
|
||||
cursor := types.NewCursor(first, after, last, before)
|
||||
|
||||
52
pkg/probo/get_evidence_file_url.go
Normal file
52
pkg/probo/get_evidence_file_url.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 probo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
)
|
||||
|
||||
func (s Service) GetEvidenceFileURL(
|
||||
ctx context.Context,
|
||||
evidenceID gid.GID,
|
||||
expiresIn time.Duration,
|
||||
) (*string, error) {
|
||||
evidence, err := s.GetEvidence(ctx, evidenceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get evidence: %w", err)
|
||||
}
|
||||
|
||||
presignClient := s3.NewPresignClient(s.s3)
|
||||
|
||||
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(evidence.ObjectKey),
|
||||
ResponseContentType: aws.String(evidence.MimeType),
|
||||
ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", evidence.Filename)),
|
||||
}, func(opts *s3.PresignOptions) {
|
||||
opts.Expires = expiresIn
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot presign GetObject request: %w", err)
|
||||
}
|
||||
|
||||
return &presignedReq.URL, nil
|
||||
}
|
||||
Reference in New Issue
Block a user