Skip to content

Commit 89d123e

Browse files
github-actions[bot]tofikwestMarfuen
authored
[dev] [tofikwest] tofik/move-logic-from-SSA-to-API (#1854)
* refactor(api): move logic from SSE to API * chore(api): add knowledge base document management endpoints and refactor document actions * refactor(soa): moved SOA feature to API * feat(trust-portal): add compliance resource management endpoints and update documentation * refactor(questionnaire): remove unused actions for answering questions * refactor(questionnaire): clear questionnaire module * refactor(soa): enhance SOA service with new utility methods and improve answer processing * refactor(knowledge-base): clear components * refactor(vector-store-sync): restructure sync logic for policies, contexts, and knowledge base documents * refactor(knowledge-base): remove unused components and update document formats * refactor(api): remove duplicate DevicesModule import * refactor(api): rename compliance framework and update related logic * refactor(ci): remove Vercel credentials from deployment workflows * refactor(api): update compliance framework references to use TrustFramework * refactor(api): enhance SSE handling and add sanitization utilities * refactor(api): update SSE utilities to enhance security and sanitization * chore(api): add mammoth and @types/multer dependencies * feat(trust-portal): add drag-and-drop file upload functionality for certificates --------- Co-authored-by: Tofik Hasanov <[email protected]> Co-authored-by: Mariano Fuentes <[email protected]>
1 parent b114614 commit 89d123e

File tree

3 files changed

+208
-67
lines changed

3 files changed

+208
-67
lines changed

apps/api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dotenv": "^17.2.3",
2929
"jose": "^6.0.12",
3030
"jspdf": "^3.0.3",
31+
"mammoth": "^1.8.0",
3132
"nanoid": "^5.1.6",
3233
"pdf-lib": "^1.17.1",
3334
"prisma": "^6.13.0",
@@ -49,6 +50,7 @@
4950
"@types/archiver": "^6.0.3",
5051
"@types/express": "^5.0.0",
5152
"@types/jest": "^30.0.0",
53+
"@types/multer": "^1.4.12",
5254
"@types/node": "^24.0.3",
5355
"@types/supertest": "^6.0.2",
5456
"@types/swagger-ui-express": "^4.1.8",

apps/app/src/app/(app)/[orgId]/settings/trust-portal/components/TrustPortalSwitch.tsx

Lines changed: 202 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Input } from '@comp/ui/input';
1010
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select';
1111
import { Switch } from '@comp/ui/switch';
1212
import { zodResolver } from '@hookform/resolvers/zod';
13-
import { ExternalLink, FileText, Upload } from 'lucide-react';
13+
import { ExternalLink, FileText, Upload, Download, Eye, FileCheck2 } from 'lucide-react';
1414
import { useAction } from 'next-safe-action/hooks';
1515
import Link from 'next/link';
1616
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -865,7 +865,75 @@ function ComplianceFramework({
865865
orgId: string;
866866
}) {
867867
const [isUploading, setIsUploading] = useState(false);
868+
const [isDragging, setIsDragging] = useState(false);
868869
const fileInputRef = useRef<HTMLInputElement>(null);
870+
const dragCounterRef = useRef(0);
871+
872+
const processFile = async (file: File) => {
873+
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
874+
toast.error('Please upload a PDF file');
875+
return;
876+
}
877+
878+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
879+
if (file.size > MAX_FILE_SIZE) {
880+
toast.error('File size must be less than 10MB');
881+
return;
882+
}
883+
884+
if (onFileUpload) {
885+
setIsUploading(true);
886+
try {
887+
await onFileUpload(file, frameworkKey);
888+
toast.success('Certificate uploaded successfully');
889+
if (fileInputRef.current) {
890+
fileInputRef.current.value = '';
891+
}
892+
} catch (error) {
893+
const message = error instanceof Error ? error.message : 'Failed to upload certificate';
894+
toast.error(message);
895+
console.error('File upload error:', error);
896+
} finally {
897+
setIsUploading(false);
898+
}
899+
}
900+
};
901+
902+
const handleDragEnter = (e: React.DragEvent) => {
903+
e.preventDefault();
904+
e.stopPropagation();
905+
dragCounterRef.current++;
906+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
907+
setIsDragging(true);
908+
}
909+
};
910+
911+
const handleDragLeave = (e: React.DragEvent) => {
912+
e.preventDefault();
913+
e.stopPropagation();
914+
dragCounterRef.current--;
915+
if (dragCounterRef.current === 0) {
916+
setIsDragging(false);
917+
}
918+
};
919+
920+
const handleDragOver = (e: React.DragEvent) => {
921+
e.preventDefault();
922+
e.stopPropagation();
923+
};
924+
925+
const handleDrop = async (e: React.DragEvent) => {
926+
e.preventDefault();
927+
e.stopPropagation();
928+
setIsDragging(false);
929+
dragCounterRef.current = 0;
930+
931+
const files = e.dataTransfer.files;
932+
if (files && files.length > 0) {
933+
await processFile(files[0]);
934+
}
935+
};
936+
869937
const logo =
870938
title === 'ISO 27001' ? (
871939
<div className="h-16 w-16 flex items-center justify-center">
@@ -962,102 +1030,169 @@ function ComplianceFramework({
9621030

9631031
{/* File Upload Section - Only show when status is "compliant" */}
9641032
{isEnabled && status === 'compliant' && (
965-
<div className="space-y-2 border-t pt-4">
1033+
<div className="mt-4 border-t pt-4">
9661034
<input
9671035
ref={fileInputRef}
9681036
type="file"
9691037
accept=".pdf,application/pdf"
9701038
className="hidden"
9711039
onChange={async (e) => {
9721040
const file = e.target.files?.[0];
973-
if (!file) return;
974-
975-
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
976-
toast.error('Please upload a PDF file');
977-
return;
978-
}
979-
980-
const MAX_FILE_SIZE = 10 * 1024 * 1024;
981-
if (file.size > MAX_FILE_SIZE) {
982-
toast.error('File size must be less than 10MB');
983-
return;
984-
}
985-
986-
if (onFileUpload) {
987-
setIsUploading(true);
988-
try {
989-
await onFileUpload(file, frameworkKey);
990-
toast.success('File uploaded successfully');
991-
if (fileInputRef.current) {
992-
fileInputRef.current.value = '';
993-
}
994-
} catch (error) {
995-
const message = error instanceof Error ? error.message : 'Failed to upload file';
996-
toast.error(message);
997-
console.error('File upload error:', error);
998-
} finally {
999-
setIsUploading(false);
1000-
}
1041+
if (file) {
1042+
await processFile(file);
10011043
}
10021044
}}
10031045
disabled={isUploading}
10041046
/>
1005-
<TooltipProvider delayDuration={100}>
1006-
<div className="flex w-full flex-wrap items-center justify-between gap-3">
1007-
<span className="text-sm font-medium text-muted-foreground">
1008-
Compliance Certificate
1009-
</span>
1010-
<div className="flex flex-wrap items-center gap-2">
1011-
<Tooltip>
1012-
<TooltipTrigger asChild>
1013-
<Button
1014-
type="button"
1015-
variant="ghost"
1016-
size="icon"
1017-
onClick={() => fileInputRef.current?.click()}
1018-
disabled={isUploading}
1019-
className="flex items-center justify-center"
1020-
aria-label={fileName ? 'Change certificate' : 'Upload certificate'}
1021-
>
1022-
<Upload className="h-4 w-4" />
1023-
</Button>
1024-
</TooltipTrigger>
1025-
<TooltipContent>
1026-
{fileName ? 'Change certificate (PDF)' : 'Upload certificate (PDF)'}
1027-
</TooltipContent>
1028-
</Tooltip>
1029-
{fileName && onFilePreview && (
1030-
<>
1031-
<span className="text-muted-foreground/50 text-sm">|</span>
1047+
1048+
{/* Section Header */}
1049+
<h4 className="text-sm font-semibold text-foreground mb-3">
1050+
Compliance Certificate
1051+
</h4>
1052+
1053+
{/* Certificate Content */}
1054+
{fileName ? (
1055+
/* File Uploaded State */
1056+
<div className="rounded-lg bg-muted/40 border border-border/50 p-4 space-y-3">
1057+
<div className="flex items-center gap-3 animate-in fade-in-0 slide-in-from-top-1 duration-200">
1058+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
1059+
<FileCheck2 className="h-5 w-5 text-primary" />
1060+
</div>
1061+
<div className="flex-1 min-w-0">
1062+
<p className="text-sm font-medium text-foreground truncate">
1063+
{fileName}
1064+
</p>
1065+
<p className="text-xs text-muted-foreground">
1066+
Certificate uploaded
1067+
</p>
1068+
</div>
1069+
{onFilePreview && (
1070+
<TooltipProvider delayDuration={100}>
1071+
<Tooltip>
1072+
<TooltipTrigger asChild>
1073+
<button
1074+
type="button"
1075+
onClick={async () => {
1076+
try {
1077+
await onFilePreview(frameworkKey);
1078+
} catch (error) {
1079+
const message =
1080+
error instanceof Error ? error.message : 'Failed to preview certificate';
1081+
toast.error(message);
1082+
}
1083+
}}
1084+
className="text-xs font-medium text-primary hover:text-primary/80 hover:underline transition-colors flex items-center gap-1"
1085+
>
1086+
<Eye className="h-3.5 w-3.5" />
1087+
View
1088+
</button>
1089+
</TooltipTrigger>
1090+
<TooltipContent>Open certificate in new tab</TooltipContent>
1091+
</Tooltip>
1092+
</TooltipProvider>
1093+
)}
1094+
</div>
1095+
1096+
{/* Action Bar */}
1097+
<div className="flex items-center gap-2 pt-1">
1098+
<TooltipProvider delayDuration={100}>
1099+
<Tooltip>
1100+
<TooltipTrigger asChild>
1101+
<Button
1102+
type="button"
1103+
variant="outline"
1104+
size="sm"
1105+
onClick={() => fileInputRef.current?.click()}
1106+
disabled={isUploading}
1107+
className="h-8 gap-1.5 text-xs font-medium hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors"
1108+
>
1109+
<Upload className="h-3.5 w-3.5" />
1110+
Replace
1111+
</Button>
1112+
</TooltipTrigger>
1113+
<TooltipContent>Replace current certificate (PDF)</TooltipContent>
1114+
</Tooltip>
1115+
</TooltipProvider>
1116+
1117+
{onFilePreview && (
1118+
<TooltipProvider delayDuration={100}>
10321119
<Tooltip>
10331120
<TooltipTrigger asChild>
10341121
<Button
10351122
type="button"
1036-
variant="ghost"
1037-
size="icon"
1123+
variant="outline"
1124+
size="sm"
10381125
onClick={async () => {
10391126
try {
10401127
await onFilePreview(frameworkKey);
10411128
} catch (error) {
10421129
const message =
1043-
error instanceof Error ? error.message : 'Failed to preview file';
1130+
error instanceof Error ? error.message : 'Failed to download certificate';
10441131
toast.error(message);
1045-
console.error('File preview error:', error);
10461132
}
10471133
}}
1048-
className="flex items-center justify-center"
1049-
aria-label="Preview certificate"
1134+
className="h-8 gap-1.5 text-xs font-medium hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors"
10501135
>
1051-
<FileText className="h-4 w-4" />
1136+
<Download className="h-3.5 w-3.5" />
1137+
Download
10521138
</Button>
10531139
</TooltipTrigger>
1054-
<TooltipContent>Preview certificate</TooltipContent>
1140+
<TooltipContent>Download certificate</TooltipContent>
10551141
</Tooltip>
1056-
</>
1142+
</TooltipProvider>
10571143
)}
10581144
</div>
10591145
</div>
1060-
</TooltipProvider>
1146+
) : (
1147+
/* Empty State - Drop zone matching uploaded state height (122px) */
1148+
<div
1149+
onDragEnter={handleDragEnter}
1150+
onDragLeave={handleDragLeave}
1151+
onDragOver={handleDragOver}
1152+
onDrop={handleDrop}
1153+
onClick={() => !isUploading && fileInputRef.current?.click()}
1154+
className={`
1155+
relative rounded-lg bg-muted/40 border border-border/50 p-4 cursor-pointer
1156+
h-[122px] flex items-center
1157+
transition-all duration-200 ease-in-out
1158+
${isDragging ? 'border-primary bg-primary/5' : ''}
1159+
${isUploading ? 'opacity-50 cursor-not-allowed' : ''}
1160+
`}
1161+
>
1162+
<div className="flex items-center gap-3">
1163+
<div className={`
1164+
flex h-10 w-10 shrink-0 items-center justify-center rounded-lg
1165+
transition-all duration-200
1166+
${isDragging ? 'bg-primary/10' : 'bg-background'}
1167+
`}>
1168+
<Upload className={`
1169+
h-5 w-5 transition-all duration-200
1170+
${isDragging ? 'text-primary scale-110' : 'text-muted-foreground'}
1171+
`} />
1172+
</div>
1173+
<div className="flex-1 min-w-0">
1174+
<p className={`
1175+
text-sm font-medium transition-colors duration-200
1176+
${isDragging ? 'text-primary' : 'text-foreground'}
1177+
`}>
1178+
{isDragging ? 'Drop your certificate here' : 'Drag & drop certificate'}
1179+
</p>
1180+
<p className="text-xs text-muted-foreground">
1181+
or click to browse • PDF only, max 10MB
1182+
</p>
1183+
</div>
1184+
</div>
1185+
1186+
{isUploading && (
1187+
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background/80">
1188+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
1189+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
1190+
Uploading...
1191+
</div>
1192+
</div>
1193+
)}
1194+
</div>
1195+
)}
10611196
</div>
10621197
)}
10631198
</CardContent>

bun.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"dotenv": "^17.2.3",
9393
"jose": "^6.0.12",
9494
"jspdf": "^3.0.3",
95+
"mammoth": "^1.8.0",
9596
"nanoid": "^5.1.6",
9697
"pdf-lib": "^1.17.1",
9798
"prisma": "^6.13.0",
@@ -113,6 +114,7 @@
113114
"@types/archiver": "^6.0.3",
114115
"@types/express": "^5.0.0",
115116
"@types/jest": "^30.0.0",
117+
"@types/multer": "^1.4.12",
116118
"@types/node": "^24.0.3",
117119
"@types/supertest": "^6.0.2",
118120
"@types/swagger-ui-express": "^4.1.8",
@@ -2233,6 +2235,8 @@
22332235

22342236
"@types/ms": ["@types/[email protected]", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
22352237

2238+
"@types/multer": ["@types/[email protected]", "", { "dependencies": { "@types/express": "*" } }, "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw=="],
2239+
22362240
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
22372241

22382242
"@types/node-fetch": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],

0 commit comments

Comments
 (0)