new storage for files

This commit is contained in:
Marco Pedone 2025-10-24 16:10:59 +02:00
parent f7003fc6b9
commit 6acfc08169
31 changed files with 1620 additions and 1958 deletions

View file

@ -13,6 +13,7 @@ TODOS:
- Modale conferma mostrare scorciatoia a doc lavoro o simile per facilitare - Modale conferma mostrare scorciatoia a doc lavoro o simile per facilitare
- Post modale conferma, mostrare se transitorio , modale con informativa tipo:" il saldo del servizio e il periodo di permanenza veranno adeguati alle condizioni stabilite insieme al locatore nel momento della trattativa e alla documentazione presentata come giustificazione di transitorietà" (modificato) - Post modale conferma, mostrare se transitorio , modale con informativa tipo:" il saldo del servizio e il periodo di permanenza veranno adeguati alle condizioni stabilite insieme al locatore nel momento della trattativa e alla documentazione presentata come giustificazione di transitorietà" (modificato)
- border radius immagini carousel annuncio
NEW IDEA TODOS: NEW IDEA TODOS:

View file

@ -2,8 +2,7 @@ import dynamic from "next/dynamic";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import type { TempTokensToken } from "~/schemas/public/TempTokens"; import type { FileMetadata } from "~/server/services/storage.service";
import { api } from "~/utils/api";
const PDFViewer = dynamic( const PDFViewer = dynamic(
() => import("./pdf-viewer").then((mod) => mod.PDFViewer), () => import("./pdf-viewer").then((mod) => mod.PDFViewer),
@ -17,11 +16,10 @@ export const AllegatoIframe = ({
allegato, allegato,
}: { }: {
className?: string; className?: string;
allegato: string; allegato: FileMetadata;
}) => { }) => {
const if_ref = useRef<HTMLIFrameElement>(null); const if_ref = useRef<HTMLIFrameElement>(null);
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const [token, setToken] = useState<TempTokensToken | null>(null);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [isIOS, setIsIOS] = useState(false); const [isIOS, setIsIOS] = useState(false);
const [isPDF, setIsPDF] = useState(false); const [isPDF, setIsPDF] = useState(false);
@ -37,7 +35,7 @@ export const AllegatoIframe = ({
setIsIOS(/iphone|ipad|ipod/i.test(userAgent)); setIsIOS(/iphone|ipad|ipod/i.test(userAgent));
// Try to detect if file is PDF from extension // Try to detect if file is PDF from extension
setIsPDF(allegato.toLowerCase().endsWith(".pdf")); setIsPDF(allegato.mimeType === "application/pdf");
}, [allegato]); }, [allegato]);
const handleIframeLoad = () => { const handleIframeLoad = () => {
@ -52,17 +50,7 @@ export const AllegatoIframe = ({
} }
}; };
useEffect(() => { const requestUrl = `/storage-api/get/${allegato.id}?mode=inline`;
async function set() {
const token = await getToken();
setToken(token);
}
set().catch((e) => console.error(e));
}, []);
if (!token) return <LoadingPage />;
const requestUrl = `/go-api/storage/get/${allegato}?token=${String(token)}`;
// For PDF on iOS, provide download link instead // For PDF on iOS, provide download link instead
if (isPDF && (isIOS || isMobile)) { if (isPDF && (isIOS || isMobile)) {
return ( return (

View file

@ -36,14 +36,11 @@ import {
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import { Separator } from "~/components/ui/separator"; import { Separator } from "~/components/ui/separator";
import { UploadModal } from "~/components/upload_modal"; import { UploadModal } from "~/components/upload_modal";
import { handleDownload } from "~/hooks/filesHooks";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { FileMetadata } from "~/server/services/storage.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export const AllegatiComp = ({ export const AllegatiComp = ({
@ -55,27 +52,27 @@ export const AllegatiComp = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: allegati, isLoading: isLoadingAllegati } = const { data: allegati, isLoading: isLoadingAllegati } =
api.storage.getUserStorage.useQuery({ userId: userId }); api.storage.retrieveUserFileData.useQuery({ userId });
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation(); const { mutate: deleteFile } = api.storage.deleteUserStorageEntry.useMutation(
{
const { mutate: deleteFile } = api.storage.deleteUserStorage.useMutation({ onMutate: () => {
onMutate: () => { const toastId = toast.loading(t.caricamento, {
const toastId = toast.loading(t.caricamento, { icon: "🪛",
icon: "🪛", });
}); return { toastId };
return { toastId }; },
onSuccess: async (_error, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate({
userId: userId,
});
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
}, },
onSuccess: async (_error, _variables, context) => { );
await utils.storage.getUserStorage.invalidate({
userId: userId,
});
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
});
const { mutate: renameFile } = api.storage.renameFile.useMutation({ const { mutate: renameFile } = api.storage.renameFile.useMutation({
onMutate: () => { onMutate: () => {
@ -86,7 +83,7 @@ export const AllegatiComp = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId: userId, userId: userId,
}); });
toast.success(t.successo, { toast.success(t.successo, {
@ -96,7 +93,7 @@ export const AllegatiComp = ({
}, },
}); });
const [editData, setEditData] = useState<{ const [editData, setEditData] = useState<{
id: StorageindexId; id: string;
filename: string; filename: string;
} | null>(null); } | null>(null);
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
@ -109,7 +106,7 @@ export const AllegatiComp = ({
return; return;
} }
renameFile({ renameFile({
fileId: formData.get("id") as StorageindexId, fileId: formData.get("id") as string,
name: formData.get("filename") as string, name: formData.get("filename") as string,
}); });
}; };
@ -128,13 +125,7 @@ export const AllegatiComp = ({
<h1 className="font-semibold text-xl">{t.allegati.da_info}</h1> <h1 className="font-semibold text-xl">{t.allegati.da_info}</h1>
{from_admin && ( {from_admin && (
<FileList <FileList
delete_onClick={(id) => deleteFile({ fileId: id, userId })} delete_onClick={(id) => deleteFile({ storageId: id, userId })}
download_onClick={(file) =>
handleDownload({
file,
getToken: async () => await getToken(),
})
}
files={from_admin} files={from_admin}
isAdmin={isAdmin} isAdmin={isAdmin}
selectHandler={(id, filename) => { selectHandler={(id, filename) => {
@ -158,13 +149,7 @@ export const AllegatiComp = ({
{not_from_admin && ( {not_from_admin && (
<FileList <FileList
delete_onClick={(id) => deleteFile({ fileId: id, userId })} delete_onClick={(id) => deleteFile({ storageId: id, userId })}
download_onClick={(file) =>
handleDownload({
file,
getToken: async () => await getToken(),
})
}
files={not_from_admin} files={not_from_admin}
isAdmin={isAdmin} isAdmin={isAdmin}
selectHandler={(id, filename) => { selectHandler={(id, filename) => {
@ -184,45 +169,56 @@ export const AllegatiComp = ({
}; };
export const ExtIcon = ({ export const ExtIcon = ({
ext, mimeType,
className, className,
}: { }: {
ext: string | null | undefined; mimeType: string;
className?: string; className?: string;
}) => { }) => {
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />; const lower = mimeType?.toLowerCase() || "";
if (ext.includes("pdf")) {
if (!lower) return <File className={cn("size-5 text-gray-500", className)} />;
// PDF
if (lower.includes("pdf")) {
return <FileText className={cn("size-5 text-red-500", className)} />; return <FileText className={cn("size-5 text-red-500", className)} />;
} }
// Images
if ( if (
ext.includes("image") || lower.startsWith("image/") ||
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) => /\b(jpg|jpeg|png|gif|svg|webp)\b/.test(lower)
ext.includes(ext),
)
) { ) {
return <ImageIcon className={cn("size-5 text-blue-500", className)} />; return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
} }
if (
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) => // Code files
ext.includes(ext), if (/\b(html|css|js|jsx|ts|tsx|json|xml)\b/.test(lower)) {
)
) {
return <FileCode className={cn("size-5 text-emerald-500", className)} />; return <FileCode className={cn("size-5 text-emerald-500", className)} />;
} }
if (["xls", "xlsx", "csv"].some((ext) => ext.includes(ext))) {
// Spreadsheets
if (/\b(xls|xlsx|csv)\b/.test(lower)) {
return ( return (
<FileSpreadsheet className={cn("size-5 text-green-600", className)} /> <FileSpreadsheet className={cn("size-5 text-green-600", className)} />
); );
} }
if (["zip", "rar", "7z", "tar", "gz"].some((ext) => ext.includes(ext))) {
// Archives
if (/\b(zip|rar|7z|tar|gz)\b/.test(lower)) {
return <FileArchive className={cn("size-5 text-amber-600", className)} />; return <FileArchive className={cn("size-5 text-amber-600", className)} />;
} }
if (["mp3", "wav", "ogg", "flac"].some((ext) => ext.includes(ext))) {
// Audio
if (/\b(mp3|wav|ogg|flac)\b/.test(lower)) {
return <FileAudio className={cn("size-5 text-purple-500", className)} />; return <FileAudio className={cn("size-5 text-purple-500", className)} />;
} }
if (["mp4", "avi", "mov", "wmv", "webm"].some((ext) => ext.includes(ext))) {
// Video
if (/\b(mp4|avi|mov|wmv|webm)\b/.test(lower)) {
return <FileVideo className={cn("size-5 text-pink-500", className)} />; return <FileVideo className={cn("size-5 text-pink-500", className)} />;
} }
return <File className={cn("size-5 text-gray-500", className)} />; return <File className={cn("size-5 text-gray-500", className)} />;
}; };
@ -235,7 +231,7 @@ const EditFileDialog = ({
open: boolean; open: boolean;
onOpenChange: (status: boolean) => void; onOpenChange: (status: boolean) => void;
data: { data: {
id: StorageindexId; id: string;
filename: string; filename: string;
} | null; } | null;
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void; handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
@ -282,17 +278,15 @@ const EditFileDialog = ({
}; };
interface FileListProps { interface FileListProps {
files: Storageindex[]; files: FileMetadata[];
delete_onClick: (id: StorageindexId) => void; delete_onClick: (id: string) => void;
download_onClick: (file: Storageindex) => void;
isAdmin: boolean; isAdmin: boolean;
selectHandler: (id: StorageindexId, filename: string) => void; selectHandler: (id: string, filename: string) => void;
} }
function FileList({ function FileList({
files, files,
delete_onClick, delete_onClick,
download_onClick,
isAdmin, isAdmin,
selectHandler, selectHandler,
}: FileListProps) { }: FileListProps) {
@ -312,7 +306,7 @@ function FileList({
> >
<div className="col-span-6 flex items-center gap-2 md:col-span-8"> <div className="col-span-6 flex items-center gap-2 md:col-span-8">
<div className="shrink-0"> <div className="shrink-0">
<ExtIcon ext={file.ext} /> <ExtIcon mimeType={file.mimeType} />
</div> </div>
<Link <Link
aria-label="Allegato" aria-label="Allegato"
@ -320,30 +314,23 @@ function FileList({
href={`/area-riservata/allegato-view/${file.id}`} href={`/area-riservata/allegato-view/${file.id}`}
target="_blank" target="_blank"
> >
{file.filename} {file.originalName}
</Link> </Link>
</div> </div>
<div className="col-span-4 text-muted-foreground md:col-span-3"> <div className="col-span-4 text-muted-foreground md:col-span-3">
{format(file.created_at, "d MMM yyyy")} {format(new Date(file.uploadedAt), "d MMM yyyy")}
</div> </div>
<div className="col-span-2 flex justify-end gap-1 md:col-span-1"> <div className="col-span-2 flex justify-end gap-1 md:col-span-1">
<Button <Link href={`/storage-api/get/${file.id}?mode=download`}>
aria-label="Download"
asChild
className="size-8"
size="icon"
title="Download"
variant="ghost"
>
<Button <Button
className="flex gap-1 text-sm" className="flex size-8 gap-1 text-sm"
onClick={() => download_onClick(file)} size="icon"
variant="outline" variant="outline"
> >
<Download className="size-4" /> <Download className="size-4" />
</Button> </Button>
</Button> </Link>
{isAdmin && ( {isAdmin && (
<DropdownMenu> <DropdownMenu>
@ -357,7 +344,7 @@ function FileList({
<DropdownMenuItem <DropdownMenuItem
aria-label="Rinomina" aria-label="Rinomina"
onClick={() => onClick={() =>
selectHandler(file.id, file.filename || "") selectHandler(file.id, file.originalName || "")
} }
> >
<Pencil className="mr-2 size-4" /> <Pencil className="mr-2 size-4" />

View file

@ -36,7 +36,7 @@ export const ChatAttachments = ({
data: attachments, data: attachments,
isLoading: attachmentsLoading, isLoading: attachmentsLoading,
refetch, refetch,
} = api.storage.getUserStorage.useQuery( } = api.storage.retrieveUserFileData.useQuery(
{ userId: chatUserData.id }, { userId: chatUserData.id },
{ refetchOnMount: true }, { refetchOnMount: true },
); );
@ -58,20 +58,20 @@ export const ChatAttachments = ({
<Paperclip className="text-muted-foreground" size={20} /> <Paperclip className="text-muted-foreground" size={20} />
</Button> </Button>
</CredenzaTrigger> </CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]"> <CredenzaContent className="max-h-[90vh] md:max-w-xl">
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle className="sr-only">Allegati</CredenzaTitle> <CredenzaTitle className="sr-only">Allegati</CredenzaTitle>
<CredenzaDescription className="sr-only"> <CredenzaDescription className="sr-only">
Chat Attachments Chat Attachments
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody className="max-h-[80vh] space-y-3 overflow-auto pb-5"> <CredenzaBody className="max-h-[80vh] space-y-5 overflow-auto pb-5 md:px-1">
<UploadComponent <UploadComponent
isAdmin={userData.isAdmin} isAdmin={userData.isAdmin}
userId={chatUserData.id} userId={chatUserData.id}
/> />
<div className="h-auto max-h-[40rem] max-w-lg overflow-auto rounded-lg border-2 border-gray-300 py-4"> <div className="h-auto max-h-[40rem] max-w-lg overflow-auto rounded-lg">
{attachmentsLoading ? ( {attachmentsLoading ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
@ -84,7 +84,7 @@ export const ChatAttachments = ({
</div> </div>
) : ( ) : (
<ul className="px-2"> <ul className="px-2">
<h3 className="px-3 font-semibold text-gray-700 text-lg"> <h3 className="font-semibold text-gray-700 text-lg">
{t.allegati.allegati_recenti} {t.allegati.allegati_recenti}
</h3> </h3>
{attachments?.map((file, idx) => { {attachments?.map((file, idx) => {
@ -92,28 +92,30 @@ export const ChatAttachments = ({
return ( return (
<li <li
className="flex items-center justify-between border-b px-3 py-1" className="flex flex-row items-center justify-between border-b py-1"
key={`${file.filename}-${idx}`} key={`${file.originalName}-${idx}`}
> >
<Link <Link
aria-label="Visualizza Allegato" aria-label="Visualizza Allegato"
className={cn(
buttonVariants({
variant: "link",
}),
"flex h-8 items-center gap-2 truncate p-0 text-base",
)}
href={`/area-riservata/allegato-view/${file.id}`} href={`/area-riservata/allegato-view/${file.id}`}
onTouchStart={() => console.log("touching")}
target="_blank" target="_blank"
> >
{file.ext && <ExtIcon ext={file.ext} />} <Button
<span className="truncate">{file.filename}</span> className="flex h-8 items-center gap-2 truncate p-0 text-base has-[>svg]:px-0"
variant="link"
>
<ExtIcon mimeType={file.mimeType} />
<span className="max-w-[60vw] truncate">
{file.originalName}
</span>
</Button>
</Link> </Link>
<span <span
className="text-muted-foreground text-xs" className="text-muted-foreground text-xs"
key={new Date().toString()} key={new Date().toString()}
> >
{TimeSince(file.created_at)} {TimeSince(new Date(file.uploadedAt))}
</span> </span>
</li> </li>
); );
@ -123,8 +125,6 @@ export const ChatAttachments = ({
</> </>
)} )}
</div> </div>
</CredenzaBody>
<CredenzaFooter className="flex flex-row items-center justify-between gap-2">
<Link <Link
aria-label="Visualizza Allegati" aria-label="Visualizza Allegati"
className={cn( className={cn(
@ -142,6 +142,8 @@ export const ChatAttachments = ({
</span> </span>
)} )}
</Link> </Link>
</CredenzaBody>
<CredenzaFooter className="flex flex-col items-center justify-between gap-2">
<CredenzaClose asChild> <CredenzaClose asChild>
<Button className="w-full">{t.chiudi}</Button> <Button className="w-full">{t.chiudi}</Button>
</CredenzaClose> </CredenzaClose>

File diff suppressed because it is too large Load diff

View file

@ -63,11 +63,8 @@ import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export const AnnuncioActions = () => { export const AnnuncioActions = () => {
@ -495,7 +492,7 @@ const ContrattoSection = ({
}) => { }) => {
const { userId } = useServizio(); const { userId } = useServizio();
const { data } = useServizioAnnuncio(); const { data } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>( const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_contratto_ref, data.doc_contratto_ref,
); );
@ -505,7 +502,7 @@ const ContrattoSection = ({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId, userId,
}); });
await utils.storage.getAll.invalidate(); await utils.storage.getAll.invalidate();
@ -519,7 +516,7 @@ const ContrattoSection = ({
}); });
if (selectedDoc) { if (selectedDoc) {
setUserStorage({ setUserStorage({
fileId: selectedDoc, storageId: selectedDoc,
userId, userId,
}); });
} }
@ -587,7 +584,7 @@ const RicevutaSection = ({
}) => { }) => {
const { userId } = useServizio(); const { userId } = useServizio();
const { data } = useServizioAnnuncio(); const { data } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>( const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_registrazione_ref, data.doc_registrazione_ref,
); );
@ -597,7 +594,7 @@ const RicevutaSection = ({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId, userId,
}); });
await utils.storage.getAll.invalidate(); await utils.storage.getAll.invalidate();
@ -611,7 +608,7 @@ const RicevutaSection = ({
}); });
if (selectedDoc) { if (selectedDoc) {
setUserStorage({ setUserStorage({
fileId: selectedDoc, storageId: selectedDoc,
userId, userId,
}); });
} }
@ -762,7 +759,7 @@ const DocSection = ({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId, userId,
}); });
await utils.storage.getAll.invalidate(); await utils.storage.getAll.invalidate();
@ -812,7 +809,7 @@ const DocSection = ({
}); });
setUserStorage({ setUserStorage({
fileId: f.id, storageId: f.id,
userId, userId,
}); });
}} }}
@ -828,10 +825,10 @@ const DocInfo = ({
storageId, storageId,
children, children,
}: { }: {
storageId: StorageindexId; storageId: string;
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
const { data: file_info } = api.storage.getStorageFromId.useQuery({ const { data: file_info } = api.storage.getFileInfos.useQuery({
storageId, storageId,
}); });
if (!file_info) return null; if (!file_info) return null;
@ -852,9 +849,9 @@ const DocInfo = ({
id="selected-file" id="selected-file"
target="_blank" target="_blank"
> >
{file_info.ext && <ExtIcon ext={file_info.ext} />} <ExtIcon mimeType={file_info.mimeType} />
<span className="max-w-40 truncate sm:max-w-80"> <span className="max-w-40 truncate sm:max-w-80">
{file_info.filename} {file_info.originalName}
</span> </span>
<ExternalLink className="size-4" /> <ExternalLink className="size-4" />
</Link> </Link>
@ -870,9 +867,9 @@ const DocCombo = ({
onSelect, onSelect,
current, current,
}: { }: {
files: Storageindex[]; files: FileMetadataWithAdmin[];
onSelect: (file: Storageindex) => void; onSelect: (file: FileMetadataWithAdmin) => void;
current: StorageindexId | null; current: string | null;
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -909,8 +906,10 @@ const DocCombo = ({
value={file.id} value={file.id}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{file.ext && <ExtIcon ext={file.ext} />} <ExtIcon mimeType={file.mimeType} />
<span className="block font-medium">{file.filename}</span> <span className="block font-medium">
{file.originalName}
</span>
{current === file.id && ( {current === file.id && (
<Check className="size-4 text-accent-foreground" /> <Check className="size-4 text-accent-foreground" />
)} )}

View file

@ -1,38 +1,29 @@
import Link from "next/link";
import { AllegatoIframe } from "~/components/allegato-iframe"; import { AllegatoIframe } from "~/components/allegato-iframe";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { handleDownload } from "~/hooks/filesHooks";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export const FileSection = ({ storageId }: { storageId: StorageindexId }) => { export const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({ const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId, storageId,
}); });
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { t } = useTranslation(); const { t } = useTranslation();
if (isLoading) return <LoadingPage />; if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>; if (!fileInfos) return <p>{t.file_section.error}</p>;
return ( return (
<> <>
<AllegatoIframe <AllegatoIframe
allegato={fileInfos.id + fileInfos.ext} allegato={fileInfos}
className="h-full max-h-[70vh] min-h-[60vh]" className="h-full max-h-[70vh] min-h-[60vh]"
/> />
<Button <Link
className="underline underline-offset-1 after:content-['_↗']" className="underline underline-offset-1 after:content-['_↗']"
onClick={() => href={`/storage-api/get/${fileInfos.id}?mode=download`}
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
variant="link"
> >
{t.file_section.download} {t.file_section.download}
</Button> </Link>
</> </>
); );
}; };

View file

@ -38,28 +38,27 @@ import { Input } from "~/components/ui/input";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useStorageTable } from "~/providers/StorageTableProvider"; import { useStorageTable } from "~/providers/StorageTableProvider";
import type { import {
Storageindex, editFile,
StorageindexId, type FileMetadataWithAdmin,
} from "~/schemas/public/Storageindex"; } from "~/server/services/storage.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export const StorageTable = ({ export const StorageTable = ({
data, data,
handleDelete, handleDelete,
}: { }: {
data: Storageindex[]; data: FileMetadataWithAdmin[];
handleDelete: (id: StorageindexId) => void; handleDelete: (id: string) => void;
}) => { }) => {
const { setTable, cb_onStateChange } = useStorageTable(); const { setTable, cb_onStateChange } = useStorageTable();
const [deleteConfirmId, setDeleteConfirmId] = useState<StorageindexId | null>( const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
null,
);
const [renameTarget, setRenameTarget] = useState<{ const [renameTarget, setRenameTarget] = useState<{
allegatoId: StorageindexId; allegatoId: string;
filename: string | null; filename: string | null;
} | null>(null); } | null>(null);
@ -74,23 +73,13 @@ export const StorageTable = ({
} }
}, [tableInstance, setTable]); }, [tableInstance, setTable]);
const tabledata = data.map((row) => { const tabledata = data;
return {
actions: "Azioni",
created_at: row.created_at,
expires_at: row.expires_at,
ext: row.ext,
filename: row.filename,
from_admin: row.from_admin,
id: row.id,
};
});
const columns_titles = { const columns_titles = {
actions: "Azioni", actions: "Azioni",
created_at: "Data creazione", uploadedAt: "Data creazione",
ext: "Estensione", type: "Tipo file",
filename: "Nome file", originalName: "Nome file",
from_admin: "Caricato da", from_admin: "Caricato da",
id: "Codice", id: "Codice",
}; };
@ -120,7 +109,7 @@ export const StorageTable = ({
id: "select", id: "select",
}, },
{ {
accessorKey: "filename", accessorKey: "originalName",
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<Link <Link
@ -129,7 +118,7 @@ export const StorageTable = ({
href={`/area-riservata/allegato-view/${row.original.id}`} href={`/area-riservata/allegato-view/${row.original.id}`}
target="_blank" target="_blank"
> >
{row.original.filename} {row.original.originalName}
</Link> </Link>
); );
}, },
@ -137,14 +126,14 @@ export const StorageTable = ({
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.filename} title={columns_titles.originalName}
/> />
), ),
}, },
{ {
accessorKey: "created_at", accessorKey: "uploadedAt",
cell: ({ row }) => { cell: ({ row }) => {
const date = row.getValue("created_at"); const date = row.getValue("uploadedAt");
return new Date(date as Date).toLocaleDateString(); return new Date(date as Date).toLocaleDateString();
}, },
@ -152,7 +141,7 @@ export const StorageTable = ({
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.created_at} title={columns_titles.uploadedAt}
/> />
), ),
}, },
@ -203,7 +192,7 @@ export const StorageTable = ({
onClick={() => onClick={() =>
setRenameTarget({ setRenameTarget({
allegatoId: data.id, allegatoId: data.id,
filename: data.filename, filename: data.originalName,
}) })
} }
role="button" role="button"
@ -231,7 +220,7 @@ export const StorageTable = ({
]; ];
const searchFiltro: SearchFiltro = { const searchFiltro: SearchFiltro = {
columnName: "filename", columnName: "originalName",
placeholder: "Cerca file ...", placeholder: "Cerca file ...",
}; };
return ( return (
@ -240,7 +229,7 @@ export const StorageTable = ({
columns={columns} columns={columns}
columns_titles={columns_titles} columns_titles={columns_titles}
data={tabledata} data={tabledata}
defaultSort={[{ desc: true, id: "created_at" }]} defaultSort={[{ desc: true, id: "uploadedAt" }]}
onStateChange={cb_onStateChange} onStateChange={cb_onStateChange}
onTableInit={setTableInstance} onTableInit={setTableInstance}
pinnedFiltri={undefined} pinnedFiltri={undefined}
@ -289,42 +278,32 @@ const RinominaDialog = ({
renameTarget, renameTarget,
setRenameTarget, setRenameTarget,
}: { }: {
renameTarget: { allegatoId: StorageindexId; filename: string | null }; renameTarget: { allegatoId: string; filename: string | null };
setRenameTarget: ( setRenameTarget: (
target: { allegatoId: StorageindexId; filename: string | null } | null, target: { allegatoId: string; filename: string | null } | null,
) => void; ) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: renameFile } = api.storage.renameFile.useMutation({ const handleEditSubmit = async (e: FormEvent<HTMLFormElement>) => {
onMutate: () => {
setRenameTarget(null);
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getAll.invalidate();
toast.success(t.successo, {
icon: "👍",
id: context?.toastId,
});
},
});
const handleEditSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const form = e.currentTarget; const form = e.currentTarget;
const formData = new FormData(form); const formData = new FormData(form);
if (!formData.get("filename")) { if (!formData.get("filename")) {
return; return;
} }
renameFile({ const rename = await editFile(
fileId: renameTarget.allegatoId, renameTarget.allegatoId,
name: formData.get("filename") as string, formData.get("filename") as string,
}); );
if (rename.success) {
setRenameTarget(null);
toast.success("File rinominato con successo", { icon: "👍" });
await utils.storage.getAll.invalidate();
} else {
toast.error(rename.error);
}
}; };
return ( return (
<Dialog <Dialog

View file

@ -31,7 +31,7 @@ const buttonVariants = cva(
grey: "bg-accent/30 text-accent-foreground shadow-sm hover:bg-accent/80 dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white", grey: "bg-accent/30 text-accent-foreground shadow-sm hover:bg-accent/80 dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white",
info: "bg-sky-500 text-white hover:bg-sky-500/90 dark:bg-sky-900 dark:text-white dark:hover:bg-sky-900/90", info: "bg-sky-500 text-white hover:bg-sky-500/90 dark:bg-sky-900 dark:text-white dark:hover:bg-sky-900/90",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline active:underline",
orange: orange:
"bg-orange-500 text-white hover:bg-orange-500/90 dark:bg-orange-900 dark:text-white dark:hover:bg-orange-900/90", "bg-orange-500 text-white hover:bg-orange-500/90 dark:bg-orange-900 dark:text-white dark:hover:bg-orange-900/90",
outline: outline:

View file

@ -22,9 +22,8 @@ import {
FileUploadTrigger, FileUploadTrigger,
} from "~/components/custom_ui/fileUpload"; } from "~/components/custom_ui/fileUpload";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Upload } from "~/lib/storage_manager"; import { uploadHandler } from "~/hooks/storageClienSideHooks";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -88,19 +87,18 @@ export const UploadComponent = ({
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const { mutateAsync: getToken } = api.storage.getStorageTokenM.useMutation(); const { mutateAsync: setUserStorage } =
api.storage.addUserStorage.useMutation({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ onMutate: () => {
onMutate: () => { toast(t.allegati.allegato_caricato, { icon: "📤" });
toast(t.allegati.allegato_caricato, { icon: "📤" }); },
}, onSettled: async () => {
onSettled: async () => { await utils.storage.getAll.invalidate();
await utils.storage.getAll.invalidate(); await utils.storage.retrieveUserFileData.invalidate({
await utils.storage.getUserStorage.invalidate({ userId,
userId, });
}); },
}, });
});
const acceptedMimeTypes = [ const acceptedMimeTypes = [
"image/jpeg", // jpg, jpeg "image/jpeg", // jpg, jpeg
@ -159,19 +157,24 @@ export const UploadComponent = ({
onProgress: (file: File, progress: number) => void; onProgress: (file: File, progress: number) => void;
}, },
) => { ) => {
if (files.length === 0) {
return;
}
try { try {
const from_admin = isAdmin ?? true;
setIsUploading(true); setIsUploading(true);
const { status, attachments } = await Upload({
files: files, const { status, attachments } = await uploadHandler({
from_admin: isAdmin ?? true, files,
getToken, from_admin,
}); });
if (userId) { if (userId) {
for (const storageId of attachments) { for (const storageId of attachments) {
setUserStorage({ await setUserStorage({
fileId: storageId as StorageindexId, storageId,
userId, userId,
from_admin,
}); });
} }
} }

View file

@ -28,6 +28,7 @@ export const env = createEnv({
SKEBBY_USER: z.string(), SKEBBY_USER: z.string(),
SKEBBY_PASS: z.string(), SKEBBY_PASS: z.string(),
REVALIDATION_SECRET: z.string(), REVALIDATION_SECRET: z.string(),
STORAGE_URL: z.string(),
STORAGE_TOKEN: z.string(), STORAGE_TOKEN: z.string(),
}, },
client: { client: {

View file

@ -1,34 +0,0 @@
import type { Storageindex } from "~/schemas/public/Storageindex";
import type { TempTokensToken } from "~/schemas/public/TempTokens";
export const handleDownload = async ({
file,
getToken,
}: {
file: Storageindex;
getToken: () => Promise<TempTokensToken | TempTokensToken[]>;
}) => {
const auth_token = (await getToken()) as TempTokensToken;
try {
const response = await fetch(
`/go-api/storage/get/${file.id}${file.ext}?token=${auth_token}`,
);
if (!response.ok) {
throw new Error("File fetch failed");
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
// biome-ignore lint/style/noNonNullAssertion: <known to be non-null>
a.download = file.filename!;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch {
throw new Error("File download failed");
}
};

View file

@ -0,0 +1,83 @@
import { add } from "date-fns";
import z from "zod";
type UploadResponse =
| {
success: true;
id: string;
}
| {
success: false;
error: string;
};
// Helper to handle file uploads
export async function uploadFile(
file: File,
expiresAt: string | undefined,
): Promise<UploadResponse> {
const formData = new FormData();
formData.append("file", file);
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
try {
const response = await fetch(`/storage-api/upload`, {
method: "POST",
body: formData,
});
if (response.ok) {
const res = await response.json();
const parsed = z
.object({
id: z.string(),
})
.parse(res);
const fileID = parsed.id;
console.log("File uploaded successfully.");
return { success: true, id: fileID };
}
return {
success: false,
error: `Caricamento fallito con status: ${response.status}`,
};
} catch (e) {
return {
success: false,
error: `Errore caricamento file: ${(e as Error).message}`,
};
}
}
export const uploadHandler = async ({
files,
from_admin,
}: {
files: File[];
from_admin: boolean;
}) => {
const attachments = [];
const failed: string[] = [];
const expirationDate = add(new Date(), { days: 60 }).toISOString();
if (files.length === 0) {
console.error("No files provided");
return { attachments: [], error: "No files provided", status: false };
}
for (const file of files) {
const upload = await uploadFile(
file,
from_admin ? expirationDate : undefined,
);
if (upload.success) {
attachments.push(upload.id);
} else {
console.error(`Error uploading file ${file.name}: ${upload.error}`);
failed.push(file.name);
}
}
return { attachments, failed, status: true };
};

View file

@ -1,96 +0,0 @@
import type { TempTokensToken } from "~/schemas/public/TempTokens";
export const Upload = async ({
files,
getToken,
from_admin,
}: {
files: File[];
getToken: ({ num }: { num: number }) => Promise<TempTokensToken[]>;
from_admin: boolean;
}) => {
const attachments = [];
if (files.length === 0) {
console.error("No files provided");
return { attachments: [], error: "No files provided", status: false };
}
if (files.length > 1) {
const authTokens = await getToken({
num: files.length,
});
if (authTokens.length !== files.length) {
console.error(
"Error: number of tokens does not match number of files",
authTokens.length,
files.length,
);
return {
attachments: [],
error: "Error: number of tokens does not match number of files",
status: false,
};
}
for (const file of files) {
const attach = await uploadFile({
file,
from_admin,
// biome-ignore lint/style/noNonNullAssertion: <intended>
token: authTokens.pop()!,
});
attachments.push(attach);
}
} else {
const authTokens = (
await getToken({
num: 1,
})
)[0];
if (!authTokens) {
console.error("Error: no token received");
return {
attachments: [],
error: "Error: no token received",
status: false,
};
}
const attach = await uploadFile({
// biome-ignore lint/style/noNonNullAssertion: <intended>
file: files[0]!,
from_admin,
token: authTokens,
});
attachments.push(attach);
}
return { attachments, status: true };
};
const uploadFile = async ({
file,
token,
from_admin,
}: {
file: File;
token: TempTokensToken;
from_admin: boolean;
}) => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(
`/go-api/storage/upload?token=${token}&from_admin=${from_admin}`,
{
body: formData,
method: "POST",
},
);
if (!response.ok) {
throw new Error(
`Error uploading file: ${response.status} ${response.statusText}`,
);
}
return response.text();
};

View file

@ -38,10 +38,7 @@ import {
type StorageTable as StorageTableType, type StorageTable as StorageTableType,
useStorageTable, useStorageTable,
} from "~/providers/StorageTableProvider"; } from "~/providers/StorageTableProvider";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { Users } from "~/schemas/public/Users"; import type { Users } from "~/schemas/public/Users";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -50,7 +47,7 @@ const AdminStorage: NextPageWithLayout = () => {
const { data, isLoading, refetch } = api.storage.getAll.useQuery(); const { data, isLoading, refetch } = api.storage.getAll.useQuery();
const { t } = useTranslation(); const { t } = useTranslation();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: deleteFile } = api.storage.deleteStorage.useMutation({ const { mutate: deleteFiles } = api.storage.deleteFile.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading(t.caricamento, { const toastId = toast.loading(t.caricamento, {
icon: "🪛", icon: "🪛",
@ -67,8 +64,8 @@ const AdminStorage: NextPageWithLayout = () => {
}, },
}); });
const handleDelete = (id: StorageindexId) => { const handleDelete = (id: string) => {
deleteFile({ storageId: id }); deleteFiles({ storageId: id });
}; };
const handleRefetch = async () => { const handleRefetch = async () => {
@ -117,7 +114,7 @@ const StorageTableHeader = ({
handleDelete, handleDelete,
}: { }: {
handleRefetch: () => Promise<void>; handleRefetch: () => Promise<void>;
handleDelete: (id: StorageindexId) => void; handleDelete: (id: string) => void;
}) => { }) => {
const { table } = useStorageTable(); const { table } = useStorageTable();
if (!table) return null; if (!table) return null;
@ -158,7 +155,7 @@ const Actions = ({
handleDelete, handleDelete,
table, table,
}: { }: {
handleDelete: (id: StorageindexId) => void; handleDelete: (id: string) => void;
table: StorageTableType; table: StorageTableType;
}) => { }) => {
const { selectedRows } = useStorageTable(); const { selectedRows } = useStorageTable();
@ -190,9 +187,9 @@ const Actions = ({
</Button> </Button>
{visbOpen && ( {visbOpen && (
<VisibComponent <VisibComponent
allegati={selectedRows}
open={visbOpen} open={visbOpen}
setOpen={setVisbOpen} setOpen={setVisbOpen}
storageIds={selectedRows.map((r) => r.id)}
/> />
)} )}
</div> </div>
@ -200,21 +197,21 @@ const Actions = ({
}; };
const VisibComponent = ({ const VisibComponent = ({
allegati, storageIds,
open, open,
setOpen, setOpen,
}: { }: {
allegati: Storageindex[]; storageIds: string[];
open: boolean; open: boolean;
setOpen: (v: boolean) => void; setOpen: (v: boolean) => void;
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data, isLoading } = api.storage.getStorageVisibility.useQuery({ const { data, isLoading } = api.storage.getStorageVisibility.useQuery({
storageIds: allegati.map((a) => a.id), storageIds,
}); });
const { data: users } = api.users.getUsers.useQuery(); const { data: users } = api.users.getUsers.useQuery();
const { mutate: remove } = api.storage.removeStorageVisibility.useMutation({ const { mutate: remove } = api.storage.deleteUserStorageEntry.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading("Rimozione utente in corso", { const toastId = toast.loading("Rimozione utente in corso", {
icon: "🪛", icon: "🪛",
@ -229,7 +226,7 @@ const VisibComponent = ({
}); });
}, },
}); });
const { mutate: add } = api.storage.addStorageVisibility.useMutation({ const { mutate: add } = api.storage.addUserStorage.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading("Aggiunta utente in corso", { const toastId = toast.loading("Aggiunta utente in corso", {
icon: "🪛", icon: "🪛",
@ -245,7 +242,7 @@ const VisibComponent = ({
}, },
}); });
const { mutate: reset } = api.storage.resetStorageVisibility.useMutation({ const { mutate: reset } = api.storage.deleteFilesFromUserStorage.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading("Reset visibilità in corso", { const toastId = toast.loading("Reset visibilità in corso", {
icon: "🪛", icon: "🪛",
@ -280,17 +277,19 @@ const VisibComponent = ({
<p>Seleziona utente:</p> <p>Seleziona utente:</p>
<UtentiCombo <UtentiCombo
onSelect={(user) => { onSelect={(user) => {
for (const allegato of allegati) { for (const allegato of storageIds) {
add({ storageId: allegato.id, userId: user.id }); add({
storageId: allegato,
userId: user.id,
from_admin: true,
});
} }
}} }}
users={users || []} users={users || []}
/> />
<Button <Button
className="ml-auto" className="ml-auto"
onClick={() => onClick={() => reset({ storageIds })}
reset({ storageId: allegati.map((a) => a.id) })
}
variant="destructive" variant="destructive"
> >
Reset Reset
@ -305,22 +304,22 @@ const VisibComponent = ({
key={allegato.id} key={allegato.id}
> >
<p className="font-semibold text-lg"> <p className="font-semibold text-lg">
File: {allegato.filename} File: {allegato.originalName}
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<p>Visibile a:</p> <p>Visibile a:</p>
{allegato.users_storages.length === 0 ? ( {allegato.users.length === 0 ? (
<p>Nessuno</p> <p>Nessuno</p>
) : ( ) : (
<ul className="flex gap-2"> <ul className="flex gap-2">
{allegato.users_storages.map((user) => ( {allegato.users.map((user) => (
<li key={user.userid}> <li key={user.id}>
<Button <Button
className="after:content-['X']" className="after:content-['X']"
onClick={() => onClick={() =>
remove({ remove({
storageId: allegato.id, storageId: allegato.id,
userId: user.userid, userId: user.id,
}) })
} }
variant="outline" variant="outline"

View file

@ -40,7 +40,7 @@ export const getServerSideProps = (async (context) => {
const helpers = await TrpcAuthedFetchingIstance({ access_token }); const helpers = await TrpcAuthedFetchingIstance({ access_token });
if (helpers) { if (helpers) {
await helpers.trpc.storage.getUserStorage.prefetch({ await helpers.trpc.storage.retrieveUserFileData.prefetch({
userId: parsed.data, userId: parsed.data,
}); });
return { return {

View file

@ -1,9 +1,11 @@
import { Download, Pen, Trash } from "lucide-react"; import { Download, Pen, Trash } from "lucide-react";
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type FormEvent, useState } from "react"; import { type FormEvent, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import z from "zod";
import { AllegatoIframe } from "~/components/allegato-iframe"; import { AllegatoIframe } from "~/components/allegato-iframe";
import { Confirm } from "~/components/confirm"; import { Confirm } from "~/components/confirm";
import { DocNotFoundPage } from "~/components/doc_not_found"; import { DocNotFoundPage } from "~/components/doc_not_found";
@ -19,17 +21,14 @@ import {
DialogTrigger, DialogTrigger,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { handleDownload } from "~/hooks/filesHooks";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useEnforcedSession } from "~/providers/SessionProvider"; import { useEnforcedSession } from "~/providers/SessionProvider";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zStorageIndexId } from "~/server/utils/zod_types";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
type AllegatoViewProps = { type AllegatoViewProps = {
allegatoId: StorageindexId; allegatoId: string;
}; };
const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
@ -37,13 +36,12 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
}: AllegatoViewProps) => { }: AllegatoViewProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const session = useEnforcedSession(); const session = useEnforcedSession();
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation(); const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
storageId: allegatoId, storageId: allegatoId,
}); });
const utils = api.useUtils(); const utils = api.useUtils();
const router = useRouter(); const router = useRouter();
const { mutate: deleteFile } = api.storage.deleteStorage.useMutation({ const { mutate: deleteFile } = api.storage.deleteFile.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading(t.caricamento, { const toastId = toast.loading(t.caricamento, {
icon: "🪛", icon: "🪛",
@ -51,7 +49,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate(); await utils.storage.retrieveUserFileData.invalidate();
toast.success(t.allegati.allegato_rimosso, { toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️", icon: "🗑️",
@ -69,29 +67,25 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
return ( return (
<> <>
<Head> <Head>
<title>Infoalloggi.it | {fileInfos.filename}</title> <title>Infoalloggi.it | {fileInfos.originalName}</title>
</Head> </Head>
<div className="flex h-full w-full grow flex-col md:flex-row"> <div className="flex h-full w-full grow flex-col md:flex-row">
<div className="flex grow flex-col justify-center space-y-2 p-4"> <div className="flex grow flex-col justify-center space-y-2 p-4">
<div className="mx-auto flex flex-wrap items-center gap-4"> <div className="mx-auto flex flex-wrap items-center gap-4">
<span>{fileInfos.filename}</span> <span>{fileInfos.originalName}</span>
<span>caricato il {fileInfos.created_at.toLocaleString("it")}</span> <span>
<Button caricato il {new Date(fileInfos.uploadedAt).toLocaleString("it")}
className="flex gap-2" </span>
onClick={() => <Link href={`/storage-api/get/${fileInfos.id}?mode=download`}>
handleDownload({ <Button className="flex gap-2">
file: fileInfos, <Download size={16} />
getToken: async () => await getToken(), Download
}) </Button>
} </Link>
>
<Download size={16} />
Download
</Button>
<RinominaDialog <RinominaDialog
allegatoId={allegatoId} allegatoId={allegatoId}
filename={fileInfos.filename} filename={fileInfos.originalName}
/> />
{session.user.isAdmin && ( {session.user.isAdmin && (
<Confirm <Confirm
@ -106,7 +100,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
</Confirm> </Confirm>
)} )}
</div> </div>
<AllegatoIframe allegato={fileInfos.id + fileInfos.ext} /> <AllegatoIframe allegato={fileInfos} />
</div> </div>
</div> </div>
</> </>
@ -125,7 +119,7 @@ export const getServerSideProps = (async (context) => {
}; };
} }
const parsedStorageId = zStorageIndexId.safeParse(allegatoId); const parsedStorageId = z.string().safeParse(allegatoId);
if (!parsedStorageId.success) { if (!parsedStorageId.success) {
return { return {
@ -137,7 +131,7 @@ export const getServerSideProps = (async (context) => {
const helper = await TrpcAuthedFetchingIstance({ access_token }); const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) { if (helper) {
await helper.trpc.storage.getStorageFromId.prefetch({ await helper.trpc.storage.getFileInfos.prefetch({
storageId: parsedStorageId.data, storageId: parsedStorageId.data,
}); });
@ -162,7 +156,7 @@ const RinominaDialog = ({
allegatoId, allegatoId,
filename, filename,
}: { }: {
allegatoId: StorageindexId; allegatoId: string;
filename: string | null; filename: string | null;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -178,7 +172,7 @@ const RinominaDialog = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageFromId.invalidate({ await utils.storage.getFileInfos.invalidate({
storageId: allegatoId, storageId: allegatoId,
}); });
toast.success(t.successo, { toast.success(t.successo, {

View file

@ -91,7 +91,7 @@ const ConfirmSection = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate(); await utils.storage.retrieveUserFileData.invalidate();
toast.success("Confermato", { toast.success("Confermato", {
icon: "👍", icon: "👍",

View file

@ -1,16 +1,14 @@
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Link from "next/link";
import { AllegatoIframe } from "~/components/allegato-iframe"; import { AllegatoIframe } from "~/components/allegato-iframe";
import { DocNotFoundPage } from "~/components/doc_not_found"; import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import { handleDownload } from "~/hooks/filesHooks";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types"; import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -49,33 +47,23 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
); );
}; };
const FileSection = ({ storageId }: { storageId: StorageindexId }) => { const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({ const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId, storageId,
}); });
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { t } = useTranslation(); const { t } = useTranslation();
if (isLoading) return <LoadingPage />; if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>; if (!fileInfos) return <p>{t.file_section.error}</p>;
return ( return (
<> <>
<AllegatoIframe <AllegatoIframe allegato={fileInfos} className="h-full max-h-[70vh]" />
allegato={fileInfos.id + fileInfos.ext}
className="h-full max-h-[70vh]"
/>
<Button <Link
className="underline underline-offset-1 after:content-['_↗']" className="underline underline-offset-1 after:content-['_↗']"
onClick={() => href={`/storage-api/get/${fileInfos.id}?mode=download`}
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
variant="link"
> >
{t.file_section.download} {t.file_section.download}
</Button> </Link>
</> </>
); );
}; };

View file

@ -1,16 +1,14 @@
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Link from "next/link";
import { AllegatoIframe } from "~/components/allegato-iframe"; import { AllegatoIframe } from "~/components/allegato-iframe";
import { DocNotFoundPage } from "~/components/doc_not_found"; import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import { handleDownload } from "~/hooks/filesHooks";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types"; import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -45,33 +43,23 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
); );
}; };
const FileSection = ({ storageId }: { storageId: StorageindexId }) => { const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({ const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId, storageId,
}); });
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { t } = useTranslation(); const { t } = useTranslation();
if (isLoading) return <LoadingPage />; if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>; if (!fileInfos) return <p>{t.file_section.error}</p>;
return ( return (
<> <>
<AllegatoIframe <AllegatoIframe allegato={fileInfos} className="h-full max-h-[70vh]" />
allegato={fileInfos.id + fileInfos.ext}
className="h-full max-h-[70vh]"
/>
<Button <Link
className="underline underline-offset-1 after:content-['_↗']" className="underline underline-offset-1 after:content-['_↗']"
onClick={() => href={`/storage-api/get/${fileInfos.id}?mode=download`}
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
variant="link"
> >
{t.file_section.download} {t.file_section.download}
</Button> </Link>
</> </>
); );
}; };

View file

@ -147,135 +147,6 @@ export default Test;
*/ */
const Test: NextPage = () => { const Test: NextPage = () => {
return <FileManagementPage />; return <div>Test page</div>;
}; };
export default Test; export default Test;
import { useEffect, useState } from "react";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import {
deleteFile,
type FileMetadata,
fetchFiles,
uploadFile,
} from "~/server/storage";
function FileManagementPage() {
const [files, setFiles] = useState<FileMetadata[]>([]);
const [loading, setLoading] = useState(true);
const loadData = async () => {
setLoading(true);
const fileList = await fetchFiles();
setFiles(fileList);
setLoading(false);
};
useEffect(() => {
loadData();
}, []);
const [inputFile, setInputFile] = useState<File | null>(null);
const [inputExpiresAt, setInputExpiresAt] = useState<string | undefined>(
undefined,
);
const handleFileUpload = async () => {
if (inputFile) {
const success = await uploadFile(inputFile, inputExpiresAt);
if (success) {
// Reload list after successful upload
await loadData();
}
}
};
const handleDelete = async (fileID: string) => {
const confirmDelete = window.confirm("Are you sure?");
if (confirmDelete) {
const success = await deleteFile(fileID);
if (success) {
await loadData();
}
}
};
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center gap-4">
<h1 className="font-bold text-2xl">File Storage</h1>
<Button
onClick={async () => {
await loadData();
}}
>
Refresh
</Button>
</div>
<Input
className=""
onChange={(e) => {
setInputFile(e.currentTarget.files?.[0] || null);
}}
type="file"
/>
<Input
className=""
onChange={(e) => {
setInputExpiresAt(new Date(e.currentTarget.value).toISOString());
}}
type="datetime-local"
/>
<Button onClick={handleFileUpload}>Upload</Button>
{loading ? (
<LoadingPage />
) : (
<ul className="space-y-2">
{files.map((file) => (
<li
className="flex items-center justify-between gap-3 rounded-lg border p-3"
key={file.id}
>
{file.mimeType.includes("image") && (
<img
alt={file.originalName}
className="size-20"
src={`/storage-api/file/${file.id}`}
/>
)}
<div className="flex-1">
<p className="font-semibold">{file.originalName}</p>
<p className="text-gray-500 text-sm">
{file.mimeType} | Size: {(file.size / 1024 / 1024).toFixed(2)}{" "}
MB
</p>
<p>Uploaded At: {new Date(file.uploadedAt).toLocaleString()}</p>
{file.expiresAt && (
<p>Expires At: {new Date(file.expiresAt).toLocaleString()}</p>
)}
</div>
<div className="flex space-x-2">
<a
className="rounded bg-blue-500 px-3 py-1 text-sm text-white"
href={`/storage-api/file/${file.id}`}
>
Download
</a>
<button
className="rounded bg-red-500 px-3 py-1 text-sm text-white"
onClick={() => handleDelete(file.id)}
type="button"
>
Delete
</button>
</div>
</li>
))}
</ul>
)}
</div>
);
}

View file

@ -6,27 +6,16 @@ import {
useContext, useContext,
useState, useState,
} from "react"; } from "react";
import type { import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
type TData = { type TData = FileMetadataWithAdmin;
id: StorageindexId;
filename: string | null;
ext: string | null;
created_at: Date;
from_admin: boolean;
actions: string;
expires_at: Date | null;
};
export type StorageTable = Table<TData>; export type StorageTable = Table<TData>;
const StorageTableContext = createContext<{ const StorageTableContext = createContext<{
table: StorageTable | undefined; table: StorageTable | undefined;
setTable: (table: StorageTable | undefined) => void; setTable: (table: StorageTable | undefined) => void;
selectedRows: Storageindex[]; selectedRows: TData[];
cb_onStateChange: () => void; cb_onStateChange: () => void;
}>({ }>({
cb_onStateChange: () => {}, cb_onStateChange: () => {},
@ -39,7 +28,7 @@ const StorageTableContext = createContext<{
export const StorageTableProvider = ({ children }: { children: ReactNode }) => { export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
const [table, setTable] = useState<StorageTable | undefined>(undefined); const [table, setTable] = useState<StorageTable | undefined>(undefined);
const [selectedRows, setSelectedRows] = useState<Storageindex[]>([]); const [selectedRows, setSelectedRows] = useState<TData[]>([]);
const cb_onStateChange = useCallback(() => { const cb_onStateChange = useCallback(() => {
if (table) { if (table) {
@ -55,16 +44,7 @@ export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
}, },
[], [],
); );
setSelectedRows( setSelectedRows(selected);
selected.map((r) => ({
created_at: r.created_at,
expires_at: r.expires_at,
ext: r.ext,
filename: r.filename,
from_admin: r.from_admin,
id: r.id,
})),
);
} }
}, [table]); }, [table]);

View file

@ -19,7 +19,6 @@ import type { default as PrezziarioTable } from './Prezziario';
import type { default as RatelimiterTable } from './Ratelimiter'; import type { default as RatelimiterTable } from './Ratelimiter';
import type { default as ChatsTable } from './Chats'; import type { default as ChatsTable } from './Chats';
import type { default as EtichetteTable } from './Etichette'; import type { default as EtichetteTable } from './Etichette';
import type { default as StorageindexTable } from './Storageindex';
import type { default as MessagesTable } from './Messages'; import type { default as MessagesTable } from './Messages';
import type { default as UsersAnagraficaTable } from './UsersAnagrafica'; import type { default as UsersAnagraficaTable } from './UsersAnagrafica';
import type { default as BanlistTable } from './Banlist'; import type { default as BanlistTable } from './Banlist';
@ -63,8 +62,6 @@ export default interface PublicSchema {
etichette: EtichetteTable; etichette: EtichetteTable;
storageindex: StorageindexTable;
messages: MessagesTable; messages: MessagesTable;
users_anagrafica: UsersAnagraficaTable; users_anagrafica: UsersAnagraficaTable;

View file

@ -3,7 +3,6 @@
import type { ServizioServizioId } from './Servizio'; import type { ServizioServizioId } from './Servizio';
import type { AnnunciId } from './Annunci'; import type { AnnunciId } from './Annunci';
import type { StorageindexId } from './Storageindex';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Represents the table public.servizio_annunci */ /** Represents the table public.servizio_annunci */
@ -16,7 +15,7 @@ export default interface ServizioAnnunciTable {
open_contatti_at: ColumnType<Date | null, Date | string | null, Date | string | null>; open_contatti_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
doc_conferma_ref: ColumnType<StorageindexId | null, StorageindexId | null, StorageindexId | null>; doc_conferma_ref: ColumnType<string | null, string | null, string | null>;
user_confirmed_at: ColumnType<Date | null, Date | string | null, Date | string | null>; user_confirmed_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
@ -32,7 +31,7 @@ export default interface ServizioAnnunciTable {
hasConfermaAdmin: ColumnType<boolean, boolean | undefined, boolean>; hasConfermaAdmin: ColumnType<boolean, boolean | undefined, boolean>;
doc_contratto_ref: ColumnType<StorageindexId | null, StorageindexId | null, StorageindexId | null>; doc_contratto_ref: ColumnType<string | null, string | null, string | null>;
gestionale_id: ColumnType<number | null, number | null, number | null>; gestionale_id: ColumnType<number | null, number | null, number | null>;
@ -42,7 +41,7 @@ export default interface ServizioAnnunciTable {
contratto_tipo: ColumnType<string | null, string | null, string | null>; contratto_tipo: ColumnType<string | null, string | null, string | null>;
doc_registrazione_ref: ColumnType<StorageindexId | null, StorageindexId | null, StorageindexId | null>; doc_registrazione_ref: ColumnType<string | null, string | null, string | null>;
doc_conferma_added: ColumnType<boolean, boolean | undefined, boolean>; doc_conferma_added: ColumnType<boolean, boolean | undefined, boolean>;

View file

@ -1,28 +0,0 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.storageindex */
export type StorageindexId = string & { __brand: 'public.storageindex' };
/** Represents the table public.storageindex */
export default interface StorageindexTable {
id: ColumnType<StorageindexId, StorageindexId | undefined, StorageindexId>;
created_at: ColumnType<Date, Date | string | undefined, Date | string>;
filename: ColumnType<string | null, string | null, string | null>;
ext: ColumnType<string | null, string | null, string | null>;
from_admin: ColumnType<boolean, boolean | undefined, boolean>;
expires_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
}
export type Storageindex = Selectable<StorageindexTable>;
export type NewStorageindex = Insertable<StorageindexTable>;
export type StorageindexUpdate = Updateable<StorageindexTable>;

View file

@ -2,14 +2,15 @@
// This file is automatically generated by Kanel. Do not modify manually. // This file is automatically generated by Kanel. Do not modify manually.
import type { UsersId } from './Users'; import type { UsersId } from './Users';
import type { StorageindexId } from './Storageindex';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Represents the table public.users_storage */ /** Represents the table public.users_storage */
export default interface UsersStorageTable { export default interface UsersStorageTable {
userid: ColumnType<UsersId, UsersId, UsersId>; userId: ColumnType<UsersId, UsersId, UsersId>;
storageid: ColumnType<StorageindexId, StorageindexId, StorageindexId>; storageId: ColumnType<string, string, string>;
from_admin: ColumnType<boolean, boolean | undefined, boolean>;
} }
export type UsersStorage = Selectable<UsersStorageTable>; export type UsersStorage = Selectable<UsersStorageTable>;

View file

@ -1,151 +1,90 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod/v4"; import z from "zod";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
protectedProcedure, protectedProcedure,
} from "~/server/api/trpc"; } from "~/server/api/trpc";
import { import {
addUserStorageHandler, addUserStorage,
deleteUserStorageHandler, deleteUserStorageEntry,
getAllHandler, getAllWithFromAdmin,
getFromIdHandler,
getMultipleWithRelations, getMultipleWithRelations,
getUserStorageHandler, purgeFileFromUserStorages,
retrieveUserFiles,
} from "~/server/controllers/storage.controller"; } from "~/server/controllers/storage.controller";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { RateLimiterHandler } from "~/server/services/ratelimiter";
import { import {
addUserStorageJunc, deleteFile,
deleteHandler, editFile,
deleteUserStorageJunc, getFileInfo,
getNewTokenHandler,
updateHandler,
} from "~/server/services/storage.service"; } from "~/server/services/storage.service";
import { zStorageIndexId, zUserId } from "~/server/utils/zod_types"; import { zUserId } from "~/server/utils/zod_types";
const ratelimit = new RateLimiterHandler({
analytics: false,
maxRequests: 50,
windowSize: 60,
});
export const storageRouter = createTRPCRouter({ export const storageRouter = createTRPCRouter({
addStorageVisibility: adminProcedure getFileInfos: protectedProcedure
.input(z.object({ storageId: zStorageIndexId, userId: zUserId })) .input(z.object({ storageId: z.string() }))
.query(async ({ input }) => {
return (await getFileInfo(input.storageId)) || undefined;
}),
deleteFile: protectedProcedure
.input(z.object({ storageId: z.string() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await addUserStorageJunc({ const deletion = await deleteFile(input.storageId);
db, if (!deletion.success) {
storageId: input.storageId, throw new TRPCError({
userId: input.userId, code: "INTERNAL_SERVER_ERROR",
message: deletion.error,
});
}
await purgeFileFromUserStorages({
...input,
}); });
}), }),
deleteUserStorageEntry: protectedProcedure
.input(z.object({ storageId: z.string(), userId: zUserId }))
.mutation(async ({ input }) => {
return await deleteUserStorageEntry(input);
}),
deleteFilesFromUserStorage: adminProcedure
.input(z.object({ storageIds: z.string().array() }))
.mutation(async ({ input }) => {
for (const storageId of input.storageIds) {
await purgeFileFromUserStorages({ storageId });
}
return "ok";
}),
retrieveUserFileData: protectedProcedure
.input(z.object({ userId: zUserId }))
.query(async ({ input }) => {
return await retrieveUserFiles(input);
}),
//OLD
addUserStorage: protectedProcedure addUserStorage: protectedProcedure
.input( .input(z.custom<NewUsersStorage>())
z.object({
fileId: zStorageIndexId,
userId: zUserId,
}),
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await addUserStorageHandler({ return await addUserStorage(input);
db,
storageId: input.fileId,
userId: input.userId,
});
}),
deleteStorage: protectedProcedure
.input(z.object({ storageId: zStorageIndexId }))
.mutation(async ({ input }) => {
return await deleteHandler({
cb: async (fileId) => {
await db
.deleteFrom("storageindex")
.where("id", "=", fileId)
.execute();
},
db,
fileId: input.storageId,
});
}),
deleteUserStorage: protectedProcedure
.input(z.object({ fileId: zStorageIndexId, userId: zUserId }))
.mutation(async ({ input }) => {
return await deleteUserStorageHandler({
db,
storageId: input.fileId,
userId: input.userId,
});
}), }),
getAll: protectedProcedure.query(async () => { getAll: protectedProcedure.query(async () => {
return await getAllHandler({ db }); return await getAllWithFromAdmin();
}), }),
getStorageFromId: protectedProcedure
.input(z.object({ storageId: zStorageIndexId }))
.query(async ({ input }) => {
return await getFromIdHandler({
db,
storage_id: input.storageId,
});
}),
getStorageToken: protectedProcedure.mutation(async () => {
const { success } = await ratelimit.limit("getStorageToken");
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
const token = (await getNewTokenHandler({ db }))[0];
if (!token) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while creating token",
});
}
return token;
}),
getStorageTokenM: protectedProcedure
.input(z.object({ num: z.number() }))
.mutation(async ({ input }) => {
const { success } = await ratelimit.limit("getStorageToken");
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
return await getNewTokenHandler({ db, num: input.num });
}),
getStorageVisibility: adminProcedure getStorageVisibility: adminProcedure
.input(z.object({ storageIds: zStorageIndexId.array() })) .input(z.object({ storageIds: z.string().array() }))
.query(async ({ input }) => { .query(async ({ input }) => {
return await getMultipleWithRelations({ return await getMultipleWithRelations({
db, db,
ids: input.storageIds, ids: input.storageIds,
}); });
}), }),
getUserStorage: protectedProcedure
.input(z.object({ userId: zUserId }))
.query(async ({ input }) => {
return await getUserStorageHandler({ db, userId: input.userId });
}),
removeStorageVisibility: adminProcedure
.input(z.object({ storageId: zStorageIndexId, userId: zUserId }))
.mutation(async ({ input }) => {
await deleteUserStorageJunc({
db,
storageId: input.storageId,
userId: input.userId,
});
}),
renameFile: adminProcedure renameFile: adminProcedure
.input(z.object({ fileId: zStorageIndexId, name: z.string() })) .input(z.object({ fileId: z.string(), name: z.string() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await updateHandler({ return await editFile(input.fileId, input.name);
data: { filename: input.name },
db,
fileId: input.fileId,
});
}),
resetStorageVisibility: adminProcedure
.input(z.object({ storageId: zStorageIndexId.array() }))
.mutation(async ({ input }) => {
await db
.deleteFrom("users_storage")
.where("storageid", "in", input.storageId)
.execute();
return "ok";
}), }),
}); });

View file

@ -1,75 +1,121 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { jsonArrayFrom } from "kysely/helpers/postgres";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { Querier } from "~/server/db"; import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import { import { db, type Querier } from "~/server/db";
addUserStorageJunc, import { fetchFiles, retriveFilesSafely } from "../services/storage.service";
deleteHandler,
deleteUserStorageJunc,
} from "~/server/services/storage.service";
export const getAllHandler = async ({ db }: { db: Querier }) => { export const getMultipleWithRelations = async ({
return await db.selectFrom("storageindex").selectAll().execute();
};
export const getFromIdHandler = async ({
db, db,
storage_id, ids,
}: { }: {
db: Querier; db: Querier;
storage_id: StorageindexId; ids: string[];
}) => { }) => {
return await db const files = await fetchFiles();
.selectFrom("storageindex") const userStorage = await db
.selectAll() .selectFrom("users_storage")
.where("id", "=", storage_id) .selectAll("users_storage")
.executeTakeFirst(); .innerJoin("users", "users.id", "users_storage.userId")
.select("users.username")
.where(
"storageId",
"in",
files.map((f) => f.id),
)
.execute();
return ids
.map((id) => {
const file = files.find((f) => f.id === id);
if (!file) return null;
const users = userStorage
.filter((us) => us.storageId === id)
.map((us) => ({
id: us.userId,
username: us.username,
}));
return {
...file,
users,
};
})
.filter((f) => f !== null);
}; };
export const getUserStorageHandler = async ({ export const retrieveUserFiles = async ({ userId }: { userId: UsersId }) => {
db,
userId,
}: {
db: Querier;
userId: UsersId;
}): Promise<Storageindex[]> => {
try { try {
return await db //all files linked to the user
.selectFrom("storageindex") const files = await db
.selectAll("storageindex") .selectFrom("users_storage")
.rightJoin("users_storage", "users_storage.storageid", "storageindex.id") .select(["storageId", "from_admin"])
.where("users_storage.userid", "=", userId) .where("userId", "=", userId)
.orderBy("created_at", "desc")
.execute(); .execute();
} catch { const { available, unavailable } = await retriveFilesSafely(
files.map((f) => f.storageId),
);
//remove entries in users_storage for files that no longer exist in storage
await db
.deleteFrom("users_storage")
.where("userId", "=", userId)
.where("storageId", "in", unavailable)
.execute();
//merge file metadata with users_storage info
const userFiles = files
.map((uf) => {
const file = available.find((f) => f.id === uf.storageId);
if (!file) return null;
return {
...file,
from_admin: uf.from_admin,
};
})
.filter((f) => f !== null);
return userFiles;
} catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Error while getting user storage", message: `Error while getting user storage: ${(e as Error).message}`,
}); });
} }
}; };
export const addUserStorageHandler = async ({ export const getAllWithFromAdmin = async () => {
db,
userId,
storageId,
}: {
db: Querier;
userId: UsersId;
storageId: StorageindexId;
}) => {
try { try {
await addUserStorageJunc({ const files = await db
db, .selectFrom("users_storage")
storageId, .select(["storageId", "from_admin"])
userId, .execute();
const allFiles = await fetchFiles();
const allFilesWithFromAdmin = allFiles.map((file) => {
const userStorageEntry = files.find((f) => f.storageId === file.id);
return {
...file,
from_admin: userStorageEntry ? userStorageEntry.from_admin : true,
};
}); });
return "ok"; return allFilesWithFromAdmin;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while getting user storage: ${(e as Error).message}`,
});
}
};
export const addUserStorage = async (data: NewUsersStorage) => {
try {
return await db
.insertInto("users_storage")
.values(data)
.onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing())
.returningAll()
.execute();
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@ -78,114 +124,58 @@ export const addUserStorageHandler = async ({
} }
}; };
export const deleteUserStorageHandler = async ({ export const getUserStorage = async ({ userId }: { userId: UsersId }) => {
db,
userId,
storageId,
}: {
db: Querier;
userId: UsersId;
storageId: StorageindexId;
}) => {
try { try {
const file = await db return await db
.selectFrom("users_storage") .selectFrom("users_storage")
.where("userId", "=", userId)
.selectAll() .selectAll()
.where("userid", "=", userId) .execute();
.where("storageid", "=", storageId) } catch (e) {
.executeTakeFirst();
if (!file) {
return "no file";
}
await deleteHandler({
cb: async (fileId) => {
await db.transaction().execute(async (trx) => {
await deleteUserStorageJunc({
db: trx,
storageId: fileId,
userId,
});
await trx
.deleteFrom("storageindex")
.where("id", "=", fileId)
.execute();
});
},
db,
fileId: file.storageid,
});
return "ok";
} catch {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Error while deleting user storage", message: `Error while getting user storage: ${(e as Error).message}`,
}); });
} }
}; };
// export const deleteAllUserStorageHandler = async ({ export const deleteUserStorageEntry = async ({
// db, userId,
// userId, storageId,
// }: {
// db: Querier;
// userId: UsersId;
// }) => {
// try {
// const files = await db
// .selectFrom("users_storage")
// .select("storageid")
// .where("userid", "=", userId)
// .execute();
// if (!files || files.length === 0) {
// return "no files";
// }
// await deleteFiles({
// db,
// refs: files,
// cb: async (fileIds) => {
// const ids = fileIds.map((file) => file.storageid);
// await db.transaction().execute(async (trx) => {
// await trx
// .deleteFrom("users_storage")
// .where("userid", "=", userId)
// .where("storageid", "in", ids)
// .execute();
// await trx.deleteFrom("storageindex").where("id", "in", ids).execute();
// });
// },
// });
// return "ok";
// } catch {
// throw new TRPCError({
// code: "INTERNAL_SERVER_ERROR",
// message: "Error while deleting user storage",
// });
// }
// };
export const getMultipleWithRelations = async ({
db,
ids,
}: { }: {
db: Querier; userId: UsersId;
ids: StorageindexId[]; storageId: string;
}) => { }) => {
return await db try {
.selectFrom("storageindex") await db
.selectAll("storageindex") .deleteFrom("users_storage")
.select((eb) => [ .where("userId", "=", userId)
jsonArrayFrom( .where("storageId", "=", storageId)
eb .execute();
.selectFrom("users_storage") return "ok";
.leftJoin("users", "users.id", "users_storage.userid") } catch (e) {
.select(["users.username"]) throw new TRPCError({
.whereRef("users_storage.storageid", "=", "storageindex.id") code: "INTERNAL_SERVER_ERROR",
.selectAll("users_storage"), message: `Error while deleting user storage: ${(e as Error).message}`,
).as("users_storages"), });
]) }
.where("id", "in", ids) };
.execute();
export const purgeFileFromUserStorages = async ({
storageId,
}: {
storageId: string;
}) => {
try {
await db
.deleteFrom("users_storage")
.where("storageId", "=", storageId)
.execute();
return "ok";
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while deleting user storage: ${(e as Error).message}`,
});
}
}; };

View file

@ -1,146 +1,210 @@
import { TRPCError } from "@trpc/server"; import z from "zod";
import { env } from "~/env"; import { env } from "~/env";
import type {
StorageindexId,
StorageindexUpdate,
} from "~/schemas/public/Storageindex";
import type { TempTokensToken } from "~/schemas/public/TempTokens";
import type { UsersId } from "~/schemas/public/Users";
import type { Querier } from "~/server/db";
import { deleteFile } from "../storage";
export const getNewTokenHandler = async ({ const FileMetadataSchema = z.object({
db, id: z.string(),
num, originalName: z.string(),
}: { size: z.number(),
db: Querier; mimeType: z.string(),
num?: number; blockCount: z.number(),
}): Promise<TempTokensToken[]> => { uploadedAt: z.string(),
const tokens: TempTokensToken[] = []; expiresAt: z.string().nullable(),
if (num && num > 1) { });
for (let i = 0; i < num; i++) {
const tok = await newToken({ db });
tokens.push(tok.token); export type FileMetadata = z.infer<typeof FileMetadataSchema>;
}
return tokens; export type FileMetadataWithAdmin = FileMetadata & {
} from_admin: boolean;
const tok = await newToken({ db });
return [tok.token];
}; };
const newToken = async ({ db }: { db: Querier }) => { // Helper to fetch the list of all files
export async function fetchFiles(): Promise<FileMetadata[]> {
try { try {
return await db const response = await fetch(
.insertInto("temp_tokens") `${env.STORAGE_URL}/files?token=${env.STORAGE_TOKEN}`,
.defaultValues() );
.returning("token") if (!response.ok) {
.executeTakeFirstOrThrow(); console.error("Failed to fetch file list:", response.statusText);
} catch { return [];
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while creating token",
});
}
};
export const deleteHandler = async ({
db,
fileId,
cb,
}: {
db: Querier;
fileId: StorageindexId;
cb?: (fileId: StorageindexId) => Promise<void>;
}) => {
try {
const storage = await db
.selectFrom("storageindex")
.select(["id", "ext"])
.where("id", "=", fileId)
.executeTakeFirstOrThrow();
await deleteFile(storage.id);
if (cb) {
await cb(fileId);
} }
const parse = FileMetadataSchema.array().safeParse(await response.json());
if (parse.success) {
return parse.data;
}
console.error("Failed to parse file metadata:", parse.error);
return [];
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error fetching file list:", error);
throw new TRPCError({ return [];
code: "INTERNAL_SERVER_ERROR",
message: "Error while deleting file",
});
} }
}; }
export const updateHandler = async ({ export async function getFileInfo(id: string): Promise<FileMetadata | null> {
db,
fileId,
data,
}: {
db: Querier;
fileId: StorageindexId;
data: StorageindexUpdate;
}) => {
try { try {
await db const response = await fetch(
.updateTable("storageindex") `${env.STORAGE_URL}/info/${id}?token=${env.STORAGE_TOKEN}`,
.set(data) );
.where("id", "=", fileId) if (!response.ok) {
.execute(); console.error("Failed to fetch file list:", response.statusText);
return "ok"; return null;
} catch (e) { }
throw new TRPCError({ const parse = FileMetadataSchema.safeParse(await response.json());
code: "INTERNAL_SERVER_ERROR",
message: `Error while updating file: ${(e as Error).message}`,
});
}
};
export const addUserStorageJunc = async ({ if (parse.success) {
db, return parse.data;
userId, }
storageId, console.error("Failed to parse file metadata:", parse.error);
}: { return null;
db: Querier; } catch (error) {
userId: UsersId; console.error("Error fetching file list:", error);
storageId: StorageindexId; return null;
}) => {
try {
return await db
.insertInto("users_storage")
.values({ storageid: storageId, userid: userId })
.onConflict((oc) => oc.columns(["userid", "storageid"]).doNothing())
.returningAll()
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while adding user storage: ${(e as Error).message}`,
});
} }
}; }
export const deleteUserStorageJunc = async ({ type EditResponse =
db, | {
userId, success: true;
storageId, id: string;
}: { }
db: Querier; | {
userId: UsersId; success: false;
storageId: StorageindexId; error: string;
}) => { };
try { export async function editFile(
await db id: string,
.deleteFrom("users_storage") filename?: string,
.where("userid", "=", userId) expiresAt?: string,
.where("storageid", "=", storageId) ): Promise<EditResponse> {
.execute(); const formData = new FormData();
return "ok"; formData.append("id", id);
} catch (e) { if (expiresAt) {
throw new TRPCError({ formData.append("expires_at", expiresAt);
code: "INTERNAL_SERVER_ERROR",
message: `Error while deleting user storage: ${(e as Error).message}`,
});
} }
}; if (filename) {
formData.append("filename", filename);
}
try {
const response = await fetch(
`${env.STORAGE_URL}/edit?token=${env.STORAGE_TOKEN}`,
{
method: "POST",
body: formData,
},
);
if (response.ok) {
const res = await response.json();
const parsed = z
.object({
id: z.string(),
})
.parse(res);
const fileID = parsed.id;
console.log("File edited successfully.");
return { success: true, id: fileID };
}
return {
success: false,
error: `Modifica fallita con status: ${response.status}`,
};
} catch (e) {
return {
success: false,
error: `Errore Modifica file: ${(e as Error).message}`,
};
}
}
type DeleteResponse =
| {
success: true;
}
| {
success: false;
error: string;
};
// Helper to handle file deletion
export async function deleteFile(fileID: string): Promise<DeleteResponse> {
try {
// The Go server accepts POST with form data for delete
const response = await fetch(
`${env.STORAGE_URL}/delete/${fileID}?token=${env.STORAGE_TOKEN}`,
{
method: "DELETE",
},
);
// Treat redirect as success
if (response.ok || (response.status >= 300 && response.status < 400)) {
console.log(`File ${fileID} deleted successfully.`);
return { success: true };
}
return {
success: false,
error: `Eliminazione fallita con status: ${response.status}`,
};
} catch (error) {
console.error("Error during file deletion:", error);
return { success: false, error: (error as Error).message };
}
}
// export const handleDownload = async (file: FileMetadata) => {
// try {
// const response = await fetch(
// `${env.STORAGE_URL}/get/${file.id}?token=${env.STORAGE_TOKEN}&mode=download`,
// );
// if (!response.ok) {
// throw new Error("File fetch failed");
// }
// const blob = await response.blob();
// const url = window.URL.createObjectURL(blob);
// const a = document.createElement("a");
// a.href = url;
// a.download = file.originalName;
// document.body.appendChild(a);
// a.click();
// a.remove();
// window.URL.revokeObjectURL(url);
// } catch {
// throw new Error("File download failed");
// }
// };
const safeRetrive = z.object({
available: z.array(FileMetadataSchema),
unavailable: z.array(z.string()),
});
export async function retriveFilesSafely(ids: string[]) {
try {
const response = await fetch(
`${env.STORAGE_URL}/files-check?token=${env.STORAGE_TOKEN}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ids),
},
);
if (!response.ok) {
throw new Error("File fetch failed");
}
const parse = safeRetrive.safeParse(await response.json());
if (parse.success) {
return parse.data;
}
throw new Error("File fetch parse failed");
} catch (e) {
throw new Error(`File fetch failed: ${(e as Error).message}`);
}
}

View file

@ -1,87 +0,0 @@
import z from "zod";
const FileMetadataSchema = z.object({
id: z.string(),
originalName: z.string(),
size: z.number(),
mimeType: z.string(),
blockCount: z.number(),
uploadedAt: z.string(),
expiresAt: z.string().nullable(),
});
export type FileMetadata = z.infer<typeof FileMetadataSchema>;
// Helper to fetch the list of all files
export async function fetchFiles(): Promise<FileMetadata[]> {
try {
const response = await fetch(`/storage-api/api/files`);
if (!response.ok) {
console.error("Failed to fetch file list:", response.statusText);
return [];
}
const parse = FileMetadataSchema.array().safeParse(await response.json());
if (parse.success) {
return parse.data;
}
console.error("Failed to parse file metadata:", parse.error);
return [];
} catch (error) {
console.error("Error fetching file list:", error);
return [];
}
}
// Helper to handle file uploads
export async function uploadFile(
file: File,
expiresAt: string | undefined,
): Promise<boolean> {
const formData = new FormData();
formData.append("file", file);
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
try {
const response = await fetch(`/storage-api/upload`, {
method: "POST",
body: formData,
});
// The Go server redirects on success (StatusSeeOther), which may cause CORS issues
// or be difficult to read in a browser environment.
// We'll treat any successful status (2xx or 3xx) as a success for now.
if (response.ok || (response.status >= 300 && response.status < 400)) {
console.log("File uploaded successfully.");
return true;
}
throw new Error(`Upload failed with status: ${response.status}`);
} catch (error) {
console.error("Error during file upload:", error);
return false;
}
}
// Helper to delete a file
export async function deleteFile(fileID: string): Promise<boolean> {
try {
// The Go server accepts POST with form data for delete
const response = await fetch(`/storage-api/delete/${fileID}`, {
method: "DELETE",
});
// Treat redirect as success
if (response.ok || (response.status >= 300 && response.status < 400)) {
console.log(`File ${fileID} deleted successfully.`);
return true;
}
throw new Error(`Delete failed with status: ${response.status}`);
} catch (error) {
console.error("Error during file deletion:", error);
return false;
}
}

View file

@ -10,7 +10,6 @@ import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { PaymentsId } from "~/schemas/public/Payments"; import type { PaymentsId } from "~/schemas/public/Payments";
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario"; import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { EventDataTypes } from "~/server/controllers/event_queue.controller"; import type { EventDataTypes } from "~/server/controllers/event_queue.controller";
@ -45,11 +44,6 @@ export const zFlagsId = z.custom<FlagsId>(
"falied to validate FlagsId", "falied to validate FlagsId",
); );
export const zStorageIndexId = z.custom<StorageindexId>(
(val) => typeof val === "string",
"falied to validate StorageIndexId",
);
export const zEtichettaId = z.custom<EtichetteIdEtichetta>( export const zEtichettaId = z.custom<EtichetteIdEtichetta>(
(val) => typeof val === "number", (val) => typeof val === "number",
"falied to validate EtichettaId", "falied to validate EtichettaId",