new storage for files
This commit is contained in:
parent
f7003fc6b9
commit
6acfc08169
31 changed files with 1620 additions and 1958 deletions
|
|
@ -13,6 +13,7 @@ TODOS:
|
|||
- 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)
|
||||
|
||||
- border radius immagini carousel annuncio
|
||||
|
||||
NEW IDEA TODOS:
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import dynamic from "next/dynamic";
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { cn } from "~/lib/utils";
|
||||
import type { TempTokensToken } from "~/schemas/public/TempTokens";
|
||||
import { api } from "~/utils/api";
|
||||
import type { FileMetadata } from "~/server/services/storage.service";
|
||||
|
||||
const PDFViewer = dynamic(
|
||||
() => import("./pdf-viewer").then((mod) => mod.PDFViewer),
|
||||
|
|
@ -17,11 +16,10 @@ export const AllegatoIframe = ({
|
|||
allegato,
|
||||
}: {
|
||||
className?: string;
|
||||
allegato: string;
|
||||
allegato: FileMetadata;
|
||||
}) => {
|
||||
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 [isIOS, setIsIOS] = useState(false);
|
||||
const [isPDF, setIsPDF] = useState(false);
|
||||
|
|
@ -37,7 +35,7 @@ export const AllegatoIframe = ({
|
|||
setIsIOS(/iphone|ipad|ipod/i.test(userAgent));
|
||||
|
||||
// Try to detect if file is PDF from extension
|
||||
setIsPDF(allegato.toLowerCase().endsWith(".pdf"));
|
||||
setIsPDF(allegato.mimeType === "application/pdf");
|
||||
}, [allegato]);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
|
|
@ -52,17 +50,7 @@ export const AllegatoIframe = ({
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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)}`;
|
||||
const requestUrl = `/storage-api/get/${allegato.id}?mode=inline`;
|
||||
// For PDF on iOS, provide download link instead
|
||||
if (isPDF && (isIOS || isMobile)) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -36,14 +36,11 @@ import {
|
|||
} from "~/components/ui/dropdown-menu";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { UploadModal } from "~/components/upload_modal";
|
||||
import { handleDownload } from "~/hooks/filesHooks";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type {
|
||||
Storageindex,
|
||||
StorageindexId,
|
||||
} from "~/schemas/public/Storageindex";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { FileMetadata } from "~/server/services/storage.service";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const AllegatiComp = ({
|
||||
|
|
@ -55,11 +52,10 @@ export const AllegatiComp = ({
|
|||
}) => {
|
||||
const { t } = useTranslation();
|
||||
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.deleteUserStorage.useMutation({
|
||||
const { mutate: deleteFile } = api.storage.deleteUserStorageEntry.useMutation(
|
||||
{
|
||||
onMutate: () => {
|
||||
const toastId = toast.loading(t.caricamento, {
|
||||
icon: "🪛",
|
||||
|
|
@ -67,7 +63,7 @@ export const AllegatiComp = ({
|
|||
return { toastId };
|
||||
},
|
||||
onSuccess: async (_error, _variables, context) => {
|
||||
await utils.storage.getUserStorage.invalidate({
|
||||
await utils.storage.retrieveUserFileData.invalidate({
|
||||
userId: userId,
|
||||
});
|
||||
toast.success(t.allegati.allegato_rimosso, {
|
||||
|
|
@ -75,7 +71,8 @@ export const AllegatiComp = ({
|
|||
id: context?.toastId,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const { mutate: renameFile } = api.storage.renameFile.useMutation({
|
||||
onMutate: () => {
|
||||
|
|
@ -86,7 +83,7 @@ export const AllegatiComp = ({
|
|||
return { toastId };
|
||||
},
|
||||
onSuccess: async (_error, _variables, context) => {
|
||||
await utils.storage.getUserStorage.invalidate({
|
||||
await utils.storage.retrieveUserFileData.invalidate({
|
||||
userId: userId,
|
||||
});
|
||||
toast.success(t.successo, {
|
||||
|
|
@ -96,7 +93,7 @@ export const AllegatiComp = ({
|
|||
},
|
||||
});
|
||||
const [editData, setEditData] = useState<{
|
||||
id: StorageindexId;
|
||||
id: string;
|
||||
filename: string;
|
||||
} | null>(null);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
|
|
@ -109,7 +106,7 @@ export const AllegatiComp = ({
|
|||
return;
|
||||
}
|
||||
renameFile({
|
||||
fileId: formData.get("id") as StorageindexId,
|
||||
fileId: formData.get("id") 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>
|
||||
{from_admin && (
|
||||
<FileList
|
||||
delete_onClick={(id) => deleteFile({ fileId: id, userId })}
|
||||
download_onClick={(file) =>
|
||||
handleDownload({
|
||||
file,
|
||||
getToken: async () => await getToken(),
|
||||
})
|
||||
}
|
||||
delete_onClick={(id) => deleteFile({ storageId: id, userId })}
|
||||
files={from_admin}
|
||||
isAdmin={isAdmin}
|
||||
selectHandler={(id, filename) => {
|
||||
|
|
@ -158,13 +149,7 @@ export const AllegatiComp = ({
|
|||
|
||||
{not_from_admin && (
|
||||
<FileList
|
||||
delete_onClick={(id) => deleteFile({ fileId: id, userId })}
|
||||
download_onClick={(file) =>
|
||||
handleDownload({
|
||||
file,
|
||||
getToken: async () => await getToken(),
|
||||
})
|
||||
}
|
||||
delete_onClick={(id) => deleteFile({ storageId: id, userId })}
|
||||
files={not_from_admin}
|
||||
isAdmin={isAdmin}
|
||||
selectHandler={(id, filename) => {
|
||||
|
|
@ -184,45 +169,56 @@ export const AllegatiComp = ({
|
|||
};
|
||||
|
||||
export const ExtIcon = ({
|
||||
ext,
|
||||
mimeType,
|
||||
className,
|
||||
}: {
|
||||
ext: string | null | undefined;
|
||||
mimeType: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />;
|
||||
if (ext.includes("pdf")) {
|
||||
const lower = mimeType?.toLowerCase() || "";
|
||||
|
||||
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)} />;
|
||||
}
|
||||
|
||||
// Images
|
||||
if (
|
||||
ext.includes("image") ||
|
||||
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) =>
|
||||
ext.includes(ext),
|
||||
)
|
||||
lower.startsWith("image/") ||
|
||||
/\b(jpg|jpeg|png|gif|svg|webp)\b/.test(lower)
|
||||
) {
|
||||
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
|
||||
}
|
||||
if (
|
||||
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) =>
|
||||
ext.includes(ext),
|
||||
)
|
||||
) {
|
||||
|
||||
// Code files
|
||||
if (/\b(html|css|js|jsx|ts|tsx|json|xml)\b/.test(lower)) {
|
||||
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 (
|
||||
<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)} />;
|
||||
}
|
||||
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)} />;
|
||||
}
|
||||
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 <File className={cn("size-5 text-gray-500", className)} />;
|
||||
};
|
||||
|
||||
|
|
@ -235,7 +231,7 @@ const EditFileDialog = ({
|
|||
open: boolean;
|
||||
onOpenChange: (status: boolean) => void;
|
||||
data: {
|
||||
id: StorageindexId;
|
||||
id: string;
|
||||
filename: string;
|
||||
} | null;
|
||||
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
||||
|
|
@ -282,17 +278,15 @@ const EditFileDialog = ({
|
|||
};
|
||||
|
||||
interface FileListProps {
|
||||
files: Storageindex[];
|
||||
delete_onClick: (id: StorageindexId) => void;
|
||||
download_onClick: (file: Storageindex) => void;
|
||||
files: FileMetadata[];
|
||||
delete_onClick: (id: string) => void;
|
||||
isAdmin: boolean;
|
||||
selectHandler: (id: StorageindexId, filename: string) => void;
|
||||
selectHandler: (id: string, filename: string) => void;
|
||||
}
|
||||
|
||||
function FileList({
|
||||
files,
|
||||
delete_onClick,
|
||||
download_onClick,
|
||||
isAdmin,
|
||||
selectHandler,
|
||||
}: FileListProps) {
|
||||
|
|
@ -312,7 +306,7 @@ function FileList({
|
|||
>
|
||||
<div className="col-span-6 flex items-center gap-2 md:col-span-8">
|
||||
<div className="shrink-0">
|
||||
<ExtIcon ext={file.ext} />
|
||||
<ExtIcon mimeType={file.mimeType} />
|
||||
</div>
|
||||
<Link
|
||||
aria-label="Allegato"
|
||||
|
|
@ -320,30 +314,23 @@ function FileList({
|
|||
href={`/area-riservata/allegato-view/${file.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
{file.filename}
|
||||
{file.originalName}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<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 className="col-span-2 flex justify-end gap-1 md:col-span-1">
|
||||
<Link href={`/storage-api/get/${file.id}?mode=download`}>
|
||||
<Button
|
||||
aria-label="Download"
|
||||
asChild
|
||||
className="size-8"
|
||||
className="flex size-8 gap-1 text-sm"
|
||||
size="icon"
|
||||
title="Download"
|
||||
variant="ghost"
|
||||
>
|
||||
<Button
|
||||
className="flex gap-1 text-sm"
|
||||
onClick={() => download_onClick(file)}
|
||||
variant="outline"
|
||||
>
|
||||
<Download className="size-4" />
|
||||
</Button>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isAdmin && (
|
||||
<DropdownMenu>
|
||||
|
|
@ -357,7 +344,7 @@ function FileList({
|
|||
<DropdownMenuItem
|
||||
aria-label="Rinomina"
|
||||
onClick={() =>
|
||||
selectHandler(file.id, file.filename || "")
|
||||
selectHandler(file.id, file.originalName || "")
|
||||
}
|
||||
>
|
||||
<Pencil className="mr-2 size-4" />
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const ChatAttachments = ({
|
|||
data: attachments,
|
||||
isLoading: attachmentsLoading,
|
||||
refetch,
|
||||
} = api.storage.getUserStorage.useQuery(
|
||||
} = api.storage.retrieveUserFileData.useQuery(
|
||||
{ userId: chatUserData.id },
|
||||
{ refetchOnMount: true },
|
||||
);
|
||||
|
|
@ -58,20 +58,20 @@ export const ChatAttachments = ({
|
|||
<Paperclip className="text-muted-foreground" size={20} />
|
||||
</Button>
|
||||
</CredenzaTrigger>
|
||||
<CredenzaContent className="max-h-[90vh]">
|
||||
<CredenzaContent className="max-h-[90vh] md:max-w-xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle className="sr-only">Allegati</CredenzaTitle>
|
||||
<CredenzaDescription className="sr-only">
|
||||
Chat Attachments
|
||||
</CredenzaDescription>
|
||||
</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
|
||||
isAdmin={userData.isAdmin}
|
||||
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 ? (
|
||||
<LoadingPage />
|
||||
) : (
|
||||
|
|
@ -84,7 +84,7 @@ export const ChatAttachments = ({
|
|||
</div>
|
||||
) : (
|
||||
<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}
|
||||
</h3>
|
||||
{attachments?.map((file, idx) => {
|
||||
|
|
@ -92,28 +92,30 @@ export const ChatAttachments = ({
|
|||
|
||||
return (
|
||||
<li
|
||||
className="flex items-center justify-between border-b px-3 py-1"
|
||||
key={`${file.filename}-${idx}`}
|
||||
className="flex flex-row items-center justify-between border-b py-1"
|
||||
key={`${file.originalName}-${idx}`}
|
||||
>
|
||||
<Link
|
||||
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}`}
|
||||
onTouchStart={() => console.log("touching")}
|
||||
target="_blank"
|
||||
>
|
||||
{file.ext && <ExtIcon ext={file.ext} />}
|
||||
<span className="truncate">{file.filename}</span>
|
||||
<Button
|
||||
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>
|
||||
<span
|
||||
className="text-muted-foreground text-xs"
|
||||
key={new Date().toString()}
|
||||
>
|
||||
{TimeSince(file.created_at)}
|
||||
{TimeSince(new Date(file.uploadedAt))}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
|
|
@ -123,8 +125,6 @@ export const ChatAttachments = ({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter className="flex flex-row items-center justify-between gap-2">
|
||||
<Link
|
||||
aria-label="Visualizza Allegati"
|
||||
className={cn(
|
||||
|
|
@ -142,6 +142,8 @@ export const ChatAttachments = ({
|
|||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter className="flex flex-col items-center justify-between gap-2">
|
||||
<CredenzaClose asChild>
|
||||
<Button className="w-full">{t.chiudi}</Button>
|
||||
</CredenzaClose>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -63,11 +63,8 @@ import { useTranslation } from "~/providers/I18nProvider";
|
|||
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
|
||||
import type {
|
||||
Storageindex,
|
||||
StorageindexId,
|
||||
} from "~/schemas/public/Storageindex";
|
||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const AnnuncioActions = () => {
|
||||
|
|
@ -495,7 +492,7 @@ const ContrattoSection = ({
|
|||
}) => {
|
||||
const { userId } = useServizio();
|
||||
const { data } = useServizioAnnuncio();
|
||||
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>(
|
||||
const [selectedDoc, setSelectedDoc] = useState<string | null>(
|
||||
data.doc_contratto_ref,
|
||||
);
|
||||
|
||||
|
|
@ -505,7 +502,7 @@ const ContrattoSection = ({
|
|||
|
||||
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.storage.getUserStorage.invalidate({
|
||||
await utils.storage.retrieveUserFileData.invalidate({
|
||||
userId,
|
||||
});
|
||||
await utils.storage.getAll.invalidate();
|
||||
|
|
@ -519,7 +516,7 @@ const ContrattoSection = ({
|
|||
});
|
||||
if (selectedDoc) {
|
||||
setUserStorage({
|
||||
fileId: selectedDoc,
|
||||
storageId: selectedDoc,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
|
@ -587,7 +584,7 @@ const RicevutaSection = ({
|
|||
}) => {
|
||||
const { userId } = useServizio();
|
||||
const { data } = useServizioAnnuncio();
|
||||
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>(
|
||||
const [selectedDoc, setSelectedDoc] = useState<string | null>(
|
||||
data.doc_registrazione_ref,
|
||||
);
|
||||
|
||||
|
|
@ -597,7 +594,7 @@ const RicevutaSection = ({
|
|||
|
||||
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.storage.getUserStorage.invalidate({
|
||||
await utils.storage.retrieveUserFileData.invalidate({
|
||||
userId,
|
||||
});
|
||||
await utils.storage.getAll.invalidate();
|
||||
|
|
@ -611,7 +608,7 @@ const RicevutaSection = ({
|
|||
});
|
||||
if (selectedDoc) {
|
||||
setUserStorage({
|
||||
fileId: selectedDoc,
|
||||
storageId: selectedDoc,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
|
@ -762,7 +759,7 @@ const DocSection = ({
|
|||
|
||||
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.storage.getUserStorage.invalidate({
|
||||
await utils.storage.retrieveUserFileData.invalidate({
|
||||
userId,
|
||||
});
|
||||
await utils.storage.getAll.invalidate();
|
||||
|
|
@ -812,7 +809,7 @@ const DocSection = ({
|
|||
});
|
||||
|
||||
setUserStorage({
|
||||
fileId: f.id,
|
||||
storageId: f.id,
|
||||
userId,
|
||||
});
|
||||
}}
|
||||
|
|
@ -828,10 +825,10 @@ const DocInfo = ({
|
|||
storageId,
|
||||
children,
|
||||
}: {
|
||||
storageId: StorageindexId;
|
||||
storageId: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { data: file_info } = api.storage.getStorageFromId.useQuery({
|
||||
const { data: file_info } = api.storage.getFileInfos.useQuery({
|
||||
storageId,
|
||||
});
|
||||
if (!file_info) return null;
|
||||
|
|
@ -852,9 +849,9 @@ const DocInfo = ({
|
|||
id="selected-file"
|
||||
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">
|
||||
{file_info.filename}
|
||||
{file_info.originalName}
|
||||
</span>
|
||||
<ExternalLink className="size-4" />
|
||||
</Link>
|
||||
|
|
@ -870,9 +867,9 @@ const DocCombo = ({
|
|||
onSelect,
|
||||
current,
|
||||
}: {
|
||||
files: Storageindex[];
|
||||
onSelect: (file: Storageindex) => void;
|
||||
current: StorageindexId | null;
|
||||
files: FileMetadataWithAdmin[];
|
||||
onSelect: (file: FileMetadataWithAdmin) => void;
|
||||
current: string | null;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
|
@ -909,8 +906,10 @@ const DocCombo = ({
|
|||
value={file.id}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{file.ext && <ExtIcon ext={file.ext} />}
|
||||
<span className="block font-medium">{file.filename}</span>
|
||||
<ExtIcon mimeType={file.mimeType} />
|
||||
<span className="block font-medium">
|
||||
{file.originalName}
|
||||
</span>
|
||||
{current === file.id && (
|
||||
<Check className="size-4 text-accent-foreground" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,29 @@
|
|||
import Link from "next/link";
|
||||
import { AllegatoIframe } from "~/components/allegato-iframe";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { handleDownload } from "~/hooks/filesHooks";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
|
||||
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
|
||||
export const FileSection = ({ storageId }: { storageId: string }) => {
|
||||
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
|
||||
storageId,
|
||||
});
|
||||
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) return <LoadingPage />;
|
||||
if (!fileInfos) return <p>{t.file_section.error}</p>;
|
||||
return (
|
||||
<>
|
||||
<AllegatoIframe
|
||||
allegato={fileInfos.id + fileInfos.ext}
|
||||
allegato={fileInfos}
|
||||
className="h-full max-h-[70vh] min-h-[60vh]"
|
||||
/>
|
||||
|
||||
<Button
|
||||
<Link
|
||||
className="underline underline-offset-1 after:content-['_↗']"
|
||||
onClick={() =>
|
||||
handleDownload({
|
||||
file: fileInfos,
|
||||
getToken: async () => await getToken(),
|
||||
})
|
||||
}
|
||||
variant="link"
|
||||
href={`/storage-api/get/${fileInfos.id}?mode=download`}
|
||||
>
|
||||
{t.file_section.download}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,28 +38,27 @@ import { Input } from "~/components/ui/input";
|
|||
import { cn } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { useStorageTable } from "~/providers/StorageTableProvider";
|
||||
import type {
|
||||
Storageindex,
|
||||
StorageindexId,
|
||||
} from "~/schemas/public/Storageindex";
|
||||
import {
|
||||
editFile,
|
||||
type FileMetadataWithAdmin,
|
||||
} from "~/server/services/storage.service";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const StorageTable = ({
|
||||
data,
|
||||
handleDelete,
|
||||
}: {
|
||||
data: Storageindex[];
|
||||
data: FileMetadataWithAdmin[];
|
||||
|
||||
handleDelete: (id: StorageindexId) => void;
|
||||
handleDelete: (id: string) => void;
|
||||
}) => {
|
||||
const { setTable, cb_onStateChange } = useStorageTable();
|
||||
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<StorageindexId | null>(
|
||||
null,
|
||||
);
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||
|
||||
const [renameTarget, setRenameTarget] = useState<{
|
||||
allegatoId: StorageindexId;
|
||||
allegatoId: string;
|
||||
filename: string | null;
|
||||
} | null>(null);
|
||||
|
||||
|
|
@ -74,23 +73,13 @@ export const StorageTable = ({
|
|||
}
|
||||
}, [tableInstance, setTable]);
|
||||
|
||||
const tabledata = data.map((row) => {
|
||||
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 tabledata = data;
|
||||
|
||||
const columns_titles = {
|
||||
actions: "Azioni",
|
||||
created_at: "Data creazione",
|
||||
ext: "Estensione",
|
||||
filename: "Nome file",
|
||||
uploadedAt: "Data creazione",
|
||||
type: "Tipo file",
|
||||
originalName: "Nome file",
|
||||
from_admin: "Caricato da",
|
||||
id: "Codice",
|
||||
};
|
||||
|
|
@ -120,7 +109,7 @@ export const StorageTable = ({
|
|||
id: "select",
|
||||
},
|
||||
{
|
||||
accessorKey: "filename",
|
||||
accessorKey: "originalName",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
|
|
@ -129,7 +118,7 @@ export const StorageTable = ({
|
|||
href={`/area-riservata/allegato-view/${row.original.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
{row.original.filename}
|
||||
{row.original.originalName}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
|
|
@ -137,14 +126,14 @@ export const StorageTable = ({
|
|||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={columns_titles.filename}
|
||||
title={columns_titles.originalName}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
accessorKey: "uploadedAt",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("created_at");
|
||||
const date = row.getValue("uploadedAt");
|
||||
return new Date(date as Date).toLocaleDateString();
|
||||
},
|
||||
|
||||
|
|
@ -152,7 +141,7 @@ export const StorageTable = ({
|
|||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={columns_titles.created_at}
|
||||
title={columns_titles.uploadedAt}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
@ -203,7 +192,7 @@ export const StorageTable = ({
|
|||
onClick={() =>
|
||||
setRenameTarget({
|
||||
allegatoId: data.id,
|
||||
filename: data.filename,
|
||||
filename: data.originalName,
|
||||
})
|
||||
}
|
||||
role="button"
|
||||
|
|
@ -231,7 +220,7 @@ export const StorageTable = ({
|
|||
];
|
||||
|
||||
const searchFiltro: SearchFiltro = {
|
||||
columnName: "filename",
|
||||
columnName: "originalName",
|
||||
placeholder: "Cerca file ...",
|
||||
};
|
||||
return (
|
||||
|
|
@ -240,7 +229,7 @@ export const StorageTable = ({
|
|||
columns={columns}
|
||||
columns_titles={columns_titles}
|
||||
data={tabledata}
|
||||
defaultSort={[{ desc: true, id: "created_at" }]}
|
||||
defaultSort={[{ desc: true, id: "uploadedAt" }]}
|
||||
onStateChange={cb_onStateChange}
|
||||
onTableInit={setTableInstance}
|
||||
pinnedFiltri={undefined}
|
||||
|
|
@ -289,42 +278,32 @@ const RinominaDialog = ({
|
|||
renameTarget,
|
||||
setRenameTarget,
|
||||
}: {
|
||||
renameTarget: { allegatoId: StorageindexId; filename: string | null };
|
||||
renameTarget: { allegatoId: string; filename: string | null };
|
||||
setRenameTarget: (
|
||||
target: { allegatoId: StorageindexId; filename: string | null } | null,
|
||||
target: { allegatoId: string; filename: string | null } | null,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutate: renameFile } = api.storage.renameFile.useMutation({
|
||||
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>) => {
|
||||
const handleEditSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
if (!formData.get("filename")) {
|
||||
return;
|
||||
}
|
||||
renameFile({
|
||||
fileId: renameTarget.allegatoId,
|
||||
name: formData.get("filename") as string,
|
||||
});
|
||||
const rename = await editFile(
|
||||
renameTarget.allegatoId,
|
||||
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 (
|
||||
<Dialog
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
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:
|
||||
"bg-orange-500 text-white hover:bg-orange-500/90 dark:bg-orange-900 dark:text-white dark:hover:bg-orange-900/90",
|
||||
outline:
|
||||
|
|
|
|||
|
|
@ -22,9 +22,8 @@ import {
|
|||
FileUploadTrigger,
|
||||
} from "~/components/custom_ui/fileUpload";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Upload } from "~/lib/storage_manager";
|
||||
import { uploadHandler } from "~/hooks/storageClienSideHooks";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
|
|
@ -88,15 +87,14 @@ export const UploadComponent = ({
|
|||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const { mutateAsync: getToken } = api.storage.getStorageTokenM.useMutation();
|
||||
|
||||
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
|
||||
const { mutateAsync: setUserStorage } =
|
||||
api.storage.addUserStorage.useMutation({
|
||||
onMutate: () => {
|
||||
toast(t.allegati.allegato_caricato, { icon: "📤" });
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.storage.getAll.invalidate();
|
||||
await utils.storage.getUserStorage.invalidate({
|
||||
await utils.storage.retrieveUserFileData.invalidate({
|
||||
userId,
|
||||
});
|
||||
},
|
||||
|
|
@ -159,19 +157,24 @@ export const UploadComponent = ({
|
|||
onProgress: (file: File, progress: number) => void;
|
||||
},
|
||||
) => {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const from_admin = isAdmin ?? true;
|
||||
setIsUploading(true);
|
||||
const { status, attachments } = await Upload({
|
||||
files: files,
|
||||
from_admin: isAdmin ?? true,
|
||||
getToken,
|
||||
|
||||
const { status, attachments } = await uploadHandler({
|
||||
files,
|
||||
from_admin,
|
||||
});
|
||||
|
||||
if (userId) {
|
||||
for (const storageId of attachments) {
|
||||
setUserStorage({
|
||||
fileId: storageId as StorageindexId,
|
||||
await setUserStorage({
|
||||
storageId,
|
||||
userId,
|
||||
from_admin,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const env = createEnv({
|
|||
SKEBBY_USER: z.string(),
|
||||
SKEBBY_PASS: z.string(),
|
||||
REVALIDATION_SECRET: z.string(),
|
||||
STORAGE_URL: z.string(),
|
||||
STORAGE_TOKEN: z.string(),
|
||||
},
|
||||
client: {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
83
apps/infoalloggi/src/hooks/storageClienSideHooks.ts
Normal file
83
apps/infoalloggi/src/hooks/storageClienSideHooks.ts
Normal 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 };
|
||||
};
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -38,10 +38,7 @@ import {
|
|||
type StorageTable as StorageTableType,
|
||||
useStorageTable,
|
||||
} from "~/providers/StorageTableProvider";
|
||||
import type {
|
||||
Storageindex,
|
||||
StorageindexId,
|
||||
} from "~/schemas/public/Storageindex";
|
||||
|
||||
import type { Users } from "~/schemas/public/Users";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { api } from "~/utils/api";
|
||||
|
|
@ -50,7 +47,7 @@ const AdminStorage: NextPageWithLayout = () => {
|
|||
const { data, isLoading, refetch } = api.storage.getAll.useQuery();
|
||||
const { t } = useTranslation();
|
||||
const utils = api.useUtils();
|
||||
const { mutate: deleteFile } = api.storage.deleteStorage.useMutation({
|
||||
const { mutate: deleteFiles } = api.storage.deleteFile.useMutation({
|
||||
onMutate: () => {
|
||||
const toastId = toast.loading(t.caricamento, {
|
||||
icon: "🪛",
|
||||
|
|
@ -67,8 +64,8 @@ const AdminStorage: NextPageWithLayout = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const handleDelete = (id: StorageindexId) => {
|
||||
deleteFile({ storageId: id });
|
||||
const handleDelete = (id: string) => {
|
||||
deleteFiles({ storageId: id });
|
||||
};
|
||||
|
||||
const handleRefetch = async () => {
|
||||
|
|
@ -117,7 +114,7 @@ const StorageTableHeader = ({
|
|||
handleDelete,
|
||||
}: {
|
||||
handleRefetch: () => Promise<void>;
|
||||
handleDelete: (id: StorageindexId) => void;
|
||||
handleDelete: (id: string) => void;
|
||||
}) => {
|
||||
const { table } = useStorageTable();
|
||||
if (!table) return null;
|
||||
|
|
@ -158,7 +155,7 @@ const Actions = ({
|
|||
handleDelete,
|
||||
table,
|
||||
}: {
|
||||
handleDelete: (id: StorageindexId) => void;
|
||||
handleDelete: (id: string) => void;
|
||||
table: StorageTableType;
|
||||
}) => {
|
||||
const { selectedRows } = useStorageTable();
|
||||
|
|
@ -190,9 +187,9 @@ const Actions = ({
|
|||
</Button>
|
||||
{visbOpen && (
|
||||
<VisibComponent
|
||||
allegati={selectedRows}
|
||||
open={visbOpen}
|
||||
setOpen={setVisbOpen}
|
||||
storageIds={selectedRows.map((r) => r.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -200,21 +197,21 @@ const Actions = ({
|
|||
};
|
||||
|
||||
const VisibComponent = ({
|
||||
allegati,
|
||||
storageIds,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
allegati: Storageindex[];
|
||||
storageIds: string[];
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
}) => {
|
||||
const utils = api.useUtils();
|
||||
const { data, isLoading } = api.storage.getStorageVisibility.useQuery({
|
||||
storageIds: allegati.map((a) => a.id),
|
||||
storageIds,
|
||||
});
|
||||
const { data: users } = api.users.getUsers.useQuery();
|
||||
|
||||
const { mutate: remove } = api.storage.removeStorageVisibility.useMutation({
|
||||
const { mutate: remove } = api.storage.deleteUserStorageEntry.useMutation({
|
||||
onMutate: () => {
|
||||
const toastId = toast.loading("Rimozione utente in corso", {
|
||||
icon: "🪛",
|
||||
|
|
@ -229,7 +226,7 @@ const VisibComponent = ({
|
|||
});
|
||||
},
|
||||
});
|
||||
const { mutate: add } = api.storage.addStorageVisibility.useMutation({
|
||||
const { mutate: add } = api.storage.addUserStorage.useMutation({
|
||||
onMutate: () => {
|
||||
const toastId = toast.loading("Aggiunta utente in corso", {
|
||||
icon: "🪛",
|
||||
|
|
@ -245,7 +242,7 @@ const VisibComponent = ({
|
|||
},
|
||||
});
|
||||
|
||||
const { mutate: reset } = api.storage.resetStorageVisibility.useMutation({
|
||||
const { mutate: reset } = api.storage.deleteFilesFromUserStorage.useMutation({
|
||||
onMutate: () => {
|
||||
const toastId = toast.loading("Reset visibilità in corso", {
|
||||
icon: "🪛",
|
||||
|
|
@ -280,17 +277,19 @@ const VisibComponent = ({
|
|||
<p>Seleziona utente:</p>
|
||||
<UtentiCombo
|
||||
onSelect={(user) => {
|
||||
for (const allegato of allegati) {
|
||||
add({ storageId: allegato.id, userId: user.id });
|
||||
for (const allegato of storageIds) {
|
||||
add({
|
||||
storageId: allegato,
|
||||
userId: user.id,
|
||||
from_admin: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
users={users || []}
|
||||
/>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
onClick={() =>
|
||||
reset({ storageId: allegati.map((a) => a.id) })
|
||||
}
|
||||
onClick={() => reset({ storageIds })}
|
||||
variant="destructive"
|
||||
>
|
||||
Reset
|
||||
|
|
@ -305,22 +304,22 @@ const VisibComponent = ({
|
|||
key={allegato.id}
|
||||
>
|
||||
<p className="font-semibold text-lg">
|
||||
File: {allegato.filename}
|
||||
File: {allegato.originalName}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<p>Visibile a:</p>
|
||||
{allegato.users_storages.length === 0 ? (
|
||||
{allegato.users.length === 0 ? (
|
||||
<p>Nessuno</p>
|
||||
) : (
|
||||
<ul className="flex gap-2">
|
||||
{allegato.users_storages.map((user) => (
|
||||
<li key={user.userid}>
|
||||
{allegato.users.map((user) => (
|
||||
<li key={user.id}>
|
||||
<Button
|
||||
className="after:content-['X']"
|
||||
onClick={() =>
|
||||
remove({
|
||||
storageId: allegato.id,
|
||||
userId: user.userid,
|
||||
userId: user.id,
|
||||
})
|
||||
}
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const getServerSideProps = (async (context) => {
|
|||
|
||||
const helpers = await TrpcAuthedFetchingIstance({ access_token });
|
||||
if (helpers) {
|
||||
await helpers.trpc.storage.getUserStorage.prefetch({
|
||||
await helpers.trpc.storage.retrieveUserFileData.prefetch({
|
||||
userId: parsed.data,
|
||||
});
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Download, Pen, Trash } from "lucide-react";
|
||||
import type { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type FormEvent, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import z from "zod";
|
||||
import { AllegatoIframe } from "~/components/allegato-iframe";
|
||||
import { Confirm } from "~/components/confirm";
|
||||
import { DocNotFoundPage } from "~/components/doc_not_found";
|
||||
|
|
@ -19,17 +21,14 @@ import {
|
|||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { handleDownload } from "~/hooks/filesHooks";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { useEnforcedSession } from "~/providers/SessionProvider";
|
||||
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { zStorageIndexId } from "~/server/utils/zod_types";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type AllegatoViewProps = {
|
||||
allegatoId: StorageindexId;
|
||||
allegatoId: string;
|
||||
};
|
||||
|
||||
const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
|
||||
|
|
@ -37,13 +36,12 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
|
|||
}: AllegatoViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const session = useEnforcedSession();
|
||||
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
||||
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
|
||||
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
|
||||
storageId: allegatoId,
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
const { mutate: deleteFile } = api.storage.deleteStorage.useMutation({
|
||||
const { mutate: deleteFile } = api.storage.deleteFile.useMutation({
|
||||
onMutate: () => {
|
||||
const toastId = toast.loading(t.caricamento, {
|
||||
icon: "🪛",
|
||||
|
|
@ -51,7 +49,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
|
|||
return { toastId };
|
||||
},
|
||||
onSuccess: async (_error, _variables, context) => {
|
||||
await utils.storage.getUserStorage.invalidate();
|
||||
await utils.storage.retrieveUserFileData.invalidate();
|
||||
|
||||
toast.success(t.allegati.allegato_rimosso, {
|
||||
icon: "🗑️",
|
||||
|
|
@ -69,29 +67,25 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Infoalloggi.it | {fileInfos.filename}</title>
|
||||
<title>Infoalloggi.it | {fileInfos.originalName}</title>
|
||||
</Head>
|
||||
|
||||
<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="mx-auto flex flex-wrap items-center gap-4">
|
||||
<span>{fileInfos.filename}</span>
|
||||
<span>caricato il {fileInfos.created_at.toLocaleString("it")}</span>
|
||||
<Button
|
||||
className="flex gap-2"
|
||||
onClick={() =>
|
||||
handleDownload({
|
||||
file: fileInfos,
|
||||
getToken: async () => await getToken(),
|
||||
})
|
||||
}
|
||||
>
|
||||
<span>{fileInfos.originalName}</span>
|
||||
<span>
|
||||
caricato il {new Date(fileInfos.uploadedAt).toLocaleString("it")}
|
||||
</span>
|
||||
<Link href={`/storage-api/get/${fileInfos.id}?mode=download`}>
|
||||
<Button className="flex gap-2">
|
||||
<Download size={16} />
|
||||
Download
|
||||
</Button>
|
||||
</Link>
|
||||
<RinominaDialog
|
||||
allegatoId={allegatoId}
|
||||
filename={fileInfos.filename}
|
||||
filename={fileInfos.originalName}
|
||||
/>
|
||||
{session.user.isAdmin && (
|
||||
<Confirm
|
||||
|
|
@ -106,7 +100,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
|
|||
</Confirm>
|
||||
)}
|
||||
</div>
|
||||
<AllegatoIframe allegato={fileInfos.id + fileInfos.ext} />
|
||||
<AllegatoIframe allegato={fileInfos} />
|
||||
</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) {
|
||||
return {
|
||||
|
|
@ -137,7 +131,7 @@ export const getServerSideProps = (async (context) => {
|
|||
|
||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||
if (helper) {
|
||||
await helper.trpc.storage.getStorageFromId.prefetch({
|
||||
await helper.trpc.storage.getFileInfos.prefetch({
|
||||
storageId: parsedStorageId.data,
|
||||
});
|
||||
|
||||
|
|
@ -162,7 +156,7 @@ const RinominaDialog = ({
|
|||
allegatoId,
|
||||
filename,
|
||||
}: {
|
||||
allegatoId: StorageindexId;
|
||||
allegatoId: string;
|
||||
filename: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -178,7 +172,7 @@ const RinominaDialog = ({
|
|||
return { toastId };
|
||||
},
|
||||
onSuccess: async (_error, _variables, context) => {
|
||||
await utils.storage.getStorageFromId.invalidate({
|
||||
await utils.storage.getFileInfos.invalidate({
|
||||
storageId: allegatoId,
|
||||
});
|
||||
toast.success(t.successo, {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const ConfirmSection = ({
|
|||
return { toastId };
|
||||
},
|
||||
onSuccess: async (_error, _variables, context) => {
|
||||
await utils.storage.getUserStorage.invalidate();
|
||||
await utils.storage.retrieveUserFileData.invalidate();
|
||||
|
||||
toast.success("Confermato", {
|
||||
icon: "👍",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import type { GetServerSideProps } from "next";
|
||||
import Link from "next/link";
|
||||
import { AllegatoIframe } from "~/components/allegato-iframe";
|
||||
import { DocNotFoundPage } from "~/components/doc_not_found";
|
||||
import { AreaRiservataLayout } from "~/components/Layout";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { Status500 } from "~/components/status-page";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { handleDownload } from "~/hooks/filesHooks";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
||||
import { api } from "~/utils/api";
|
||||
|
|
@ -49,33 +47,23 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
|
||||
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
|
||||
const FileSection = ({ storageId }: { storageId: string }) => {
|
||||
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
|
||||
storageId,
|
||||
});
|
||||
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) return <LoadingPage />;
|
||||
if (!fileInfos) return <p>{t.file_section.error}</p>;
|
||||
return (
|
||||
<>
|
||||
<AllegatoIframe
|
||||
allegato={fileInfos.id + fileInfos.ext}
|
||||
className="h-full max-h-[70vh]"
|
||||
/>
|
||||
<AllegatoIframe allegato={fileInfos} className="h-full max-h-[70vh]" />
|
||||
|
||||
<Button
|
||||
<Link
|
||||
className="underline underline-offset-1 after:content-['_↗']"
|
||||
onClick={() =>
|
||||
handleDownload({
|
||||
file: fileInfos,
|
||||
getToken: async () => await getToken(),
|
||||
})
|
||||
}
|
||||
variant="link"
|
||||
href={`/storage-api/get/${fileInfos.id}?mode=download`}
|
||||
>
|
||||
{t.file_section.download}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import type { GetServerSideProps } from "next";
|
||||
import Link from "next/link";
|
||||
import { AllegatoIframe } from "~/components/allegato-iframe";
|
||||
import { DocNotFoundPage } from "~/components/doc_not_found";
|
||||
import { AreaRiservataLayout } from "~/components/Layout";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { Status500 } from "~/components/status-page";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { handleDownload } from "~/hooks/filesHooks";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
||||
import { api } from "~/utils/api";
|
||||
|
|
@ -45,33 +43,23 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
|
||||
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
|
||||
const FileSection = ({ storageId }: { storageId: string }) => {
|
||||
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
|
||||
storageId,
|
||||
});
|
||||
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) return <LoadingPage />;
|
||||
if (!fileInfos) return <p>{t.file_section.error}</p>;
|
||||
return (
|
||||
<>
|
||||
<AllegatoIframe
|
||||
allegato={fileInfos.id + fileInfos.ext}
|
||||
className="h-full max-h-[70vh]"
|
||||
/>
|
||||
<AllegatoIframe allegato={fileInfos} className="h-full max-h-[70vh]" />
|
||||
|
||||
<Button
|
||||
<Link
|
||||
className="underline underline-offset-1 after:content-['_↗']"
|
||||
onClick={() =>
|
||||
handleDownload({
|
||||
file: fileInfos,
|
||||
getToken: async () => await getToken(),
|
||||
})
|
||||
}
|
||||
variant="link"
|
||||
href={`/storage-api/get/${fileInfos.id}?mode=download`}
|
||||
>
|
||||
{t.file_section.download}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -147,135 +147,6 @@ export default Test;
|
|||
*/
|
||||
|
||||
const Test: NextPage = () => {
|
||||
return <FileManagementPage />;
|
||||
return <div>Test page</div>;
|
||||
};
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,27 +6,16 @@ import {
|
|||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import type {
|
||||
Storageindex,
|
||||
StorageindexId,
|
||||
} from "~/schemas/public/Storageindex";
|
||||
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
|
||||
|
||||
type TData = {
|
||||
id: StorageindexId;
|
||||
filename: string | null;
|
||||
ext: string | null;
|
||||
created_at: Date;
|
||||
from_admin: boolean;
|
||||
actions: string;
|
||||
expires_at: Date | null;
|
||||
};
|
||||
type TData = FileMetadataWithAdmin;
|
||||
|
||||
export type StorageTable = Table<TData>;
|
||||
|
||||
const StorageTableContext = createContext<{
|
||||
table: StorageTable | undefined;
|
||||
setTable: (table: StorageTable | undefined) => void;
|
||||
selectedRows: Storageindex[];
|
||||
selectedRows: TData[];
|
||||
cb_onStateChange: () => void;
|
||||
}>({
|
||||
cb_onStateChange: () => {},
|
||||
|
|
@ -39,7 +28,7 @@ const StorageTableContext = createContext<{
|
|||
export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [table, setTable] = useState<StorageTable | undefined>(undefined);
|
||||
|
||||
const [selectedRows, setSelectedRows] = useState<Storageindex[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<TData[]>([]);
|
||||
|
||||
const cb_onStateChange = useCallback(() => {
|
||||
if (table) {
|
||||
|
|
@ -55,16 +44,7 @@ export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
|
|||
},
|
||||
[],
|
||||
);
|
||||
setSelectedRows(
|
||||
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,
|
||||
})),
|
||||
);
|
||||
setSelectedRows(selected);
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import type { default as PrezziarioTable } from './Prezziario';
|
|||
import type { default as RatelimiterTable } from './Ratelimiter';
|
||||
import type { default as ChatsTable } from './Chats';
|
||||
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 UsersAnagraficaTable } from './UsersAnagrafica';
|
||||
import type { default as BanlistTable } from './Banlist';
|
||||
|
|
@ -63,8 +62,6 @@ export default interface PublicSchema {
|
|||
|
||||
etichette: EtichetteTable;
|
||||
|
||||
storageindex: StorageindexTable;
|
||||
|
||||
messages: MessagesTable;
|
||||
|
||||
users_anagrafica: UsersAnagraficaTable;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import type { ServizioServizioId } from './Servizio';
|
||||
import type { AnnunciId } from './Annunci';
|
||||
import type { StorageindexId } from './Storageindex';
|
||||
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
|
||||
|
||||
/** 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>;
|
||||
|
||||
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>;
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ export default interface ServizioAnnunciTable {
|
|||
|
||||
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>;
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ export default interface ServizioAnnunciTable {
|
|||
|
||||
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>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -2,14 +2,15 @@
|
|||
// This file is automatically generated by Kanel. Do not modify manually.
|
||||
|
||||
import type { UsersId } from './Users';
|
||||
import type { StorageindexId } from './Storageindex';
|
||||
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
|
||||
|
||||
/** Represents the table public.users_storage */
|
||||
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>;
|
||||
|
|
|
|||
|
|
@ -1,151 +1,90 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
import z from "zod";
|
||||
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import {
|
||||
addUserStorageHandler,
|
||||
deleteUserStorageHandler,
|
||||
getAllHandler,
|
||||
getFromIdHandler,
|
||||
addUserStorage,
|
||||
deleteUserStorageEntry,
|
||||
getAllWithFromAdmin,
|
||||
getMultipleWithRelations,
|
||||
getUserStorageHandler,
|
||||
purgeFileFromUserStorages,
|
||||
retrieveUserFiles,
|
||||
} from "~/server/controllers/storage.controller";
|
||||
import { db } from "~/server/db";
|
||||
import { RateLimiterHandler } from "~/server/services/ratelimiter";
|
||||
import {
|
||||
addUserStorageJunc,
|
||||
deleteHandler,
|
||||
deleteUserStorageJunc,
|
||||
getNewTokenHandler,
|
||||
updateHandler,
|
||||
deleteFile,
|
||||
editFile,
|
||||
getFileInfo,
|
||||
} from "~/server/services/storage.service";
|
||||
import { zStorageIndexId, zUserId } from "~/server/utils/zod_types";
|
||||
|
||||
const ratelimit = new RateLimiterHandler({
|
||||
analytics: false,
|
||||
maxRequests: 50,
|
||||
windowSize: 60,
|
||||
});
|
||||
import { zUserId } from "~/server/utils/zod_types";
|
||||
|
||||
export const storageRouter = createTRPCRouter({
|
||||
addStorageVisibility: adminProcedure
|
||||
.input(z.object({ storageId: zStorageIndexId, userId: zUserId }))
|
||||
.mutation(async ({ input }) => {
|
||||
await addUserStorageJunc({
|
||||
db,
|
||||
storageId: input.storageId,
|
||||
userId: input.userId,
|
||||
});
|
||||
}),
|
||||
|
||||
addUserStorage: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
fileId: zStorageIndexId,
|
||||
userId: zUserId,
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await addUserStorageHandler({
|
||||
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 () => {
|
||||
return await getAllHandler({ db });
|
||||
}),
|
||||
getStorageFromId: protectedProcedure
|
||||
.input(z.object({ storageId: zStorageIndexId }))
|
||||
getFileInfos: protectedProcedure
|
||||
.input(z.object({ storageId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getFromIdHandler({
|
||||
db,
|
||||
storage_id: input.storageId,
|
||||
});
|
||||
return (await getFileInfo(input.storageId)) || undefined;
|
||||
}),
|
||||
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) {
|
||||
deleteFile: protectedProcedure
|
||||
.input(z.object({ storageId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const deletion = await deleteFile(input.storageId);
|
||||
if (!deletion.success) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error while creating token",
|
||||
message: deletion.error,
|
||||
});
|
||||
}
|
||||
return token;
|
||||
await purgeFileFromUserStorages({
|
||||
...input,
|
||||
});
|
||||
}),
|
||||
getStorageTokenM: protectedProcedure
|
||||
.input(z.object({ num: z.number() }))
|
||||
deleteUserStorageEntry: protectedProcedure
|
||||
.input(z.object({ storageId: z.string(), userId: zUserId }))
|
||||
.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 });
|
||||
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
|
||||
.input(z.custom<NewUsersStorage>())
|
||||
.mutation(async ({ input }) => {
|
||||
return await addUserStorage(input);
|
||||
}),
|
||||
|
||||
getAll: protectedProcedure.query(async () => {
|
||||
return await getAllWithFromAdmin();
|
||||
}),
|
||||
|
||||
getStorageVisibility: adminProcedure
|
||||
.input(z.object({ storageIds: zStorageIndexId.array() }))
|
||||
.input(z.object({ storageIds: z.string().array() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getMultipleWithRelations({
|
||||
db,
|
||||
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
|
||||
.input(z.object({ fileId: zStorageIndexId, name: z.string() }))
|
||||
.input(z.object({ fileId: z.string(), name: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await updateHandler({
|
||||
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";
|
||||
return await editFile(input.fileId, input.name);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,75 +1,121 @@
|
|||
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 { Querier } from "~/server/db";
|
||||
import {
|
||||
addUserStorageJunc,
|
||||
deleteHandler,
|
||||
deleteUserStorageJunc,
|
||||
} from "~/server/services/storage.service";
|
||||
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
|
||||
import { db, type Querier } from "~/server/db";
|
||||
import { fetchFiles, retriveFilesSafely } from "../services/storage.service";
|
||||
|
||||
export const getAllHandler = async ({ db }: { db: Querier }) => {
|
||||
return await db.selectFrom("storageindex").selectAll().execute();
|
||||
};
|
||||
|
||||
export const getFromIdHandler = async ({
|
||||
export const getMultipleWithRelations = async ({
|
||||
db,
|
||||
storage_id,
|
||||
ids,
|
||||
}: {
|
||||
db: Querier;
|
||||
storage_id: StorageindexId;
|
||||
ids: string[];
|
||||
}) => {
|
||||
return await db
|
||||
.selectFrom("storageindex")
|
||||
.selectAll()
|
||||
.where("id", "=", storage_id)
|
||||
.executeTakeFirst();
|
||||
const files = await fetchFiles();
|
||||
const userStorage = await db
|
||||
.selectFrom("users_storage")
|
||||
.selectAll("users_storage")
|
||||
.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 ({
|
||||
db,
|
||||
userId,
|
||||
}: {
|
||||
db: Querier;
|
||||
userId: UsersId;
|
||||
}): Promise<Storageindex[]> => {
|
||||
export const retrieveUserFiles = async ({ userId }: { userId: UsersId }) => {
|
||||
try {
|
||||
return await db
|
||||
.selectFrom("storageindex")
|
||||
.selectAll("storageindex")
|
||||
.rightJoin("users_storage", "users_storage.storageid", "storageindex.id")
|
||||
.where("users_storage.userid", "=", userId)
|
||||
.orderBy("created_at", "desc")
|
||||
//all files linked to the user
|
||||
const files = await db
|
||||
.selectFrom("users_storage")
|
||||
.select(["storageId", "from_admin"])
|
||||
.where("userId", "=", userId)
|
||||
.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({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error while getting user storage",
|
||||
message: `Error while getting user storage: ${(e as Error).message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const addUserStorageHandler = async ({
|
||||
db,
|
||||
userId,
|
||||
storageId,
|
||||
}: {
|
||||
db: Querier;
|
||||
userId: UsersId;
|
||||
storageId: StorageindexId;
|
||||
}) => {
|
||||
export const getAllWithFromAdmin = async () => {
|
||||
try {
|
||||
await addUserStorageJunc({
|
||||
db,
|
||||
storageId,
|
||||
userId,
|
||||
const files = await db
|
||||
.selectFrom("users_storage")
|
||||
.select(["storageId", "from_admin"])
|
||||
.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) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
|
|
@ -78,114 +124,58 @@ export const addUserStorageHandler = async ({
|
|||
}
|
||||
};
|
||||
|
||||
export const deleteUserStorageHandler = async ({
|
||||
db,
|
||||
export const getUserStorage = async ({ userId }: { userId: UsersId }) => {
|
||||
try {
|
||||
return await db
|
||||
.selectFrom("users_storage")
|
||||
.where("userId", "=", userId)
|
||||
.selectAll()
|
||||
.execute();
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error while getting user storage: ${(e as Error).message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserStorageEntry = async ({
|
||||
userId,
|
||||
storageId,
|
||||
}: {
|
||||
db: Querier;
|
||||
userId: UsersId;
|
||||
storageId: StorageindexId;
|
||||
storageId: string;
|
||||
}) => {
|
||||
try {
|
||||
const file = await db
|
||||
.selectFrom("users_storage")
|
||||
.selectAll()
|
||||
.where("userid", "=", userId)
|
||||
.where("storageid", "=", storageId)
|
||||
.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)
|
||||
await db
|
||||
.deleteFrom("users_storage")
|
||||
.where("userId", "=", userId)
|
||||
.where("storageId", "=", storageId)
|
||||
.execute();
|
||||
});
|
||||
},
|
||||
db,
|
||||
fileId: file.storageid,
|
||||
});
|
||||
return "ok";
|
||||
} catch {
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error while deleting user storage",
|
||||
message: `Error while deleting user storage: ${(e as Error).message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// export const deleteAllUserStorageHandler = async ({
|
||||
// db,
|
||||
// userId,
|
||||
// }: {
|
||||
// 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,
|
||||
export const purgeFileFromUserStorages = async ({
|
||||
storageId,
|
||||
}: {
|
||||
db: Querier;
|
||||
ids: StorageindexId[];
|
||||
storageId: string;
|
||||
}) => {
|
||||
return await db
|
||||
.selectFrom("storageindex")
|
||||
.selectAll("storageindex")
|
||||
.select((eb) => [
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("users_storage")
|
||||
.leftJoin("users", "users.id", "users_storage.userid")
|
||||
.select(["users.username"])
|
||||
.whereRef("users_storage.storageid", "=", "storageindex.id")
|
||||
.selectAll("users_storage"),
|
||||
).as("users_storages"),
|
||||
])
|
||||
.where("id", "in", ids)
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,146 +1,210 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import z from "zod";
|
||||
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 ({
|
||||
db,
|
||||
num,
|
||||
}: {
|
||||
db: Querier;
|
||||
num?: number;
|
||||
}): Promise<TempTokensToken[]> => {
|
||||
const tokens: TempTokensToken[] = [];
|
||||
if (num && num > 1) {
|
||||
for (let i = 0; i < num; i++) {
|
||||
const tok = await newToken({ db });
|
||||
|
||||
tokens.push(tok.token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
const tok = await newToken({ db });
|
||||
return [tok.token];
|
||||
};
|
||||
|
||||
const newToken = async ({ db }: { db: Querier }) => {
|
||||
try {
|
||||
return await db
|
||||
.insertInto("temp_tokens")
|
||||
.defaultValues()
|
||||
.returning("token")
|
||||
.executeTakeFirstOrThrow();
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error while creating token",
|
||||
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>;
|
||||
|
||||
export type FileMetadataWithAdmin = FileMetadata & {
|
||||
from_admin: boolean;
|
||||
};
|
||||
|
||||
export const deleteHandler = async ({
|
||||
db,
|
||||
fileId,
|
||||
cb,
|
||||
}: {
|
||||
db: Querier;
|
||||
fileId: StorageindexId;
|
||||
cb?: (fileId: StorageindexId) => Promise<void>;
|
||||
}) => {
|
||||
// Helper to fetch the list of all files
|
||||
export async function fetchFiles(): Promise<FileMetadata[]> {
|
||||
try {
|
||||
const storage = await db
|
||||
.selectFrom("storageindex")
|
||||
.select(["id", "ext"])
|
||||
.where("id", "=", fileId)
|
||||
.executeTakeFirstOrThrow();
|
||||
await deleteFile(storage.id);
|
||||
|
||||
if (cb) {
|
||||
await cb(fileId);
|
||||
const response = await fetch(
|
||||
`${env.STORAGE_URL}/files?token=${env.STORAGE_TOKEN}`,
|
||||
);
|
||||
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:", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error while deleting file",
|
||||
});
|
||||
console.error("Error fetching file list:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const updateHandler = async ({
|
||||
db,
|
||||
fileId,
|
||||
data,
|
||||
}: {
|
||||
db: Querier;
|
||||
fileId: StorageindexId;
|
||||
data: StorageindexUpdate;
|
||||
}) => {
|
||||
export async function getFileInfo(id: string): Promise<FileMetadata | null> {
|
||||
try {
|
||||
await db
|
||||
.updateTable("storageindex")
|
||||
.set(data)
|
||||
.where("id", "=", fileId)
|
||||
.execute();
|
||||
return "ok";
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error while updating file: ${(e as Error).message}`,
|
||||
});
|
||||
const response = await fetch(
|
||||
`${env.STORAGE_URL}/info/${id}?token=${env.STORAGE_TOKEN}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch file list:", response.statusText);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const parse = FileMetadataSchema.safeParse(await response.json());
|
||||
|
||||
export const addUserStorageJunc = async ({
|
||||
db,
|
||||
userId,
|
||||
storageId,
|
||||
}: {
|
||||
db: Querier;
|
||||
userId: UsersId;
|
||||
storageId: StorageindexId;
|
||||
}) => {
|
||||
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}`,
|
||||
});
|
||||
if (parse.success) {
|
||||
return parse.data;
|
||||
}
|
||||
console.error("Failed to parse file metadata:", parse.error);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching file list:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserStorageJunc = async ({
|
||||
db,
|
||||
userId,
|
||||
storageId,
|
||||
}: {
|
||||
db: Querier;
|
||||
userId: UsersId;
|
||||
storageId: StorageindexId;
|
||||
}) => {
|
||||
try {
|
||||
await db
|
||||
.deleteFrom("users_storage")
|
||||
.where("userid", "=", userId)
|
||||
.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}`,
|
||||
});
|
||||
type EditResponse =
|
||||
| {
|
||||
success: true;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
export async function editFile(
|
||||
id: string,
|
||||
filename?: string,
|
||||
expiresAt?: string,
|
||||
): Promise<EditResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append("id", id);
|
||||
if (expiresAt) {
|
||||
formData.append("expires_at", expiresAt);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
|||
import type { PaymentsId } from "~/schemas/public/Payments";
|
||||
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||
import type { StorageindexId } from "~/schemas/public/Storageindex";
|
||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { EventDataTypes } from "~/server/controllers/event_queue.controller";
|
||||
|
|
@ -45,11 +44,6 @@ export const zFlagsId = z.custom<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>(
|
||||
(val) => typeof val === "number",
|
||||
"falied to validate EtichettaId",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue