refactor: update storage-related API and components for improved functionality

This commit is contained in:
Marco Pedone 2025-10-24 18:05:51 +02:00
parent 7b619e54db
commit 7f93bdeb98
10 changed files with 165 additions and 102 deletions

View file

@ -65,9 +65,10 @@ ENV TILES_URL="localhost:6379"
ENV SKEBBY_USER="mock" ENV SKEBBY_USER="mock"
ENV SKEBBY_PASS="mock" ENV SKEBBY_PASS="mock"
ENV REVALIDATION_SECRET="mock" ENV REVALIDATION_SECRET="mock"
ENV STORAGE_URL="mock"
ENV STORAGE_TOKEN="mock" ENV STORAGE_TOKEN="mock"
#TODO remove variabili non necessarie al build tanto la validazione viene saltata
RUN SKIP_ENV_VALIDATION=1 npm run build RUN SKIP_ENV_VALIDATION=1 npm run build
@ -132,6 +133,8 @@ ENV SKEBBY_USER=$SKEBBY_USER
ARG SKEBBY_PASS ARG SKEBBY_PASS
ENV SKEBBY_PASS=$SKEBBY_PASS ENV SKEBBY_PASS=$SKEBBY_PASS
ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c" ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c"
ARG STORAGE_URL
ENV STORAGE_URL=$STORAGE_URL
ARG STORAGE_TOKEN ARG STORAGE_TOKEN
ENV STORAGE_TOKEN=$STORAGE_TOKEN ENV STORAGE_TOKEN=$STORAGE_TOKEN

View file

@ -59,10 +59,6 @@ async function createNextConfig(): Promise<NextConfig> {
reactStrictMode: true, reactStrictMode: true,
rewrites: async () => { rewrites: async () => {
return [ return [
{
source: "/storage-api/:slug*",
destination: `http://localhost:8080/:slug*?token=${env.STORAGE_TOKEN}`,
},
{ {
destination: env.NODE_ENV === "production" ? "/404" : "/api/panel", destination: env.NODE_ENV === "production" ? "/404" : "/api/panel",
source: "/api/panel", source: "/api/panel",
@ -71,32 +67,6 @@ async function createNextConfig(): Promise<NextConfig> {
destination: `${env.BACKENDSERVER_URL}/images/get/:slug*`, destination: `${env.BACKENDSERVER_URL}/images/get/:slug*`,
source: "/go-api/images/get/:slug*", source: "/go-api/images/get/:slug*",
}, },
{
destination: `${env.BACKENDSERVER_URL}/storage/get/:slug*`,
has: [
{ key: "access_token", type: "cookie" },
{
key: "token",
type: "query",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
source: "/go-api/storage/get/:slug*",
},
{
destination: `${env.BACKENDSERVER_URL}/storage/upload:slug*`,
has: [
{ key: "access_token", type: "cookie" },
{
key: "token",
type: "query",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
source: "/go-api/storage/upload:slug*",
},
]; ];
}, },

View file

@ -500,7 +500,8 @@ const ContrattoSection = ({
const { data: files, isLoading } = api.storage.getAll.useQuery(); const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId, userId,
@ -592,7 +593,8 @@ const RicevutaSection = ({
const { data: files, isLoading } = api.storage.getAll.useQuery(); const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId, userId,
@ -757,7 +759,8 @@ const DocSection = ({
const { data: files, isLoading } = api.storage.getAll.useQuery(); const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({ const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({ await utils.storage.retrieveUserFileData.invalidate({
userId, userId,

View file

@ -36,13 +36,8 @@ import {
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useStorageTable } from "~/providers/StorageTableProvider"; import { useStorageTable } from "~/providers/StorageTableProvider";
import { import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
editFile,
type FileMetadataWithAdmin,
} from "~/server/services/storage.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export const StorageTable = ({ export const StorageTable = ({
@ -283,9 +278,24 @@ const RinominaDialog = ({
target: { allegatoId: string; filename: string | null } | null, target: { allegatoId: string; filename: string | null } | null,
) => void; ) => void;
}) => { }) => {
const { t } = useTranslation();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync: renameFile } = api.storage.renameFile.useMutation({
onMutate: () => {
const toastId = toast.loading("Modifica in corso", {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getAll.invalidate();
toast.success("File rinominato con successo", {
icon: "👍",
id: context?.toastId,
});
},
});
const handleEditSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleEditSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const form = e.currentTarget; const form = e.currentTarget;
@ -293,17 +303,10 @@ const RinominaDialog = ({
if (!formData.get("filename")) { if (!formData.get("filename")) {
return; return;
} }
const rename = await editFile( await renameFile({
renameTarget.allegatoId, fileId: renameTarget.allegatoId,
formData.get("filename") as string, name: formData.get("filename") as string,
); });
if (rename.success) {
setRenameTarget(null);
toast.success("File rinominato con successo", { icon: "👍" });
await utils.storage.getAll.invalidate();
} else {
toast.error(rename.error);
}
}; };
return ( return (
<Dialog <Dialog
@ -312,8 +315,8 @@ const RinominaDialog = ({
> >
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t.modifica}</DialogTitle> <DialogTitle>Rinomina</DialogTitle>
<DialogDescription className="sr-only">modifica</DialogDescription> <DialogDescription className="sr-only">Rinomina</DialogDescription>
</DialogHeader> </DialogHeader>
<form method="post" onSubmit={handleEditSubmit}> <form method="post" onSubmit={handleEditSubmit}>
@ -323,10 +326,10 @@ const RinominaDialog = ({
defaultValue={renameTarget.filename || undefined} defaultValue={renameTarget.filename || undefined}
minLength={5} minLength={5}
name="filename" name="filename"
placeholder="Modifica messaggio" placeholder="Rinomina File"
type="text" type="text"
/> />
<Button type="submit">{t.salva}</Button> <Button type="submit">Salva</Button>
</div> </div>
</form> </form>
</DialogContent> </DialogContent>

View file

@ -88,7 +88,7 @@ export const UploadComponent = ({
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const { mutateAsync: setUserStorage } = const { mutateAsync: setUserStorage } =
api.storage.addUserStorage.useMutation({ api.storage.addUserStorageEntry.useMutation({
onMutate: () => { onMutate: () => {
toast(t.allegati.allegato_caricato, { icon: "📤" }); toast(t.allegati.allegato_caricato, { icon: "📤" });
}, },

View file

@ -15,13 +15,13 @@ export async function uploadFile(
file: File, file: File,
expiresAt: string | undefined, expiresAt: string | undefined,
): Promise<UploadResponse> { ): Promise<UploadResponse> {
try {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
if (expiresAt) { if (expiresAt) {
formData.append("expires_at", expiresAt); formData.append("expires_at", expiresAt);
} }
try {
const response = await fetch(`/storage-api/upload`, { const response = await fetch(`/storage-api/upload`, {
method: "POST", method: "POST",
body: formData, body: formData,

View file

@ -1,14 +1,20 @@
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
import { authMiddleware } from "~/middlewares/auth_middleware"; import { authMiddleware } from "~/middlewares/auth_middleware";
import { apisMiddleware } from "./middlewares/api_middleware";
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
//console.log("Middleware triggered for:", request.nextUrl.pathname); //console.log("Middleware triggered for:", request.nextUrl.pathname);
const { pathname } = request.nextUrl;
if (pathname.startsWith("/storage-api/")) {
return await apisMiddleware(request);
}
return (await authMiddleware(request)) ?? NextResponse.next(); return (await authMiddleware(request)) ?? NextResponse.next();
} }
export const config = { export const config = {
matcher: [ matcher: [
"/storage-api/:path*",
{ {
source: "/", source: "/",
}, },

View file

@ -0,0 +1,82 @@
import { type NextRequest, NextResponse } from "next/server";
import { env } from "~/env";
import { TOKEN_CONFIG } from "~/server/auth/configs";
import { verifyToken } from "~/server/auth/jwt";
export const apisMiddleware = async (req: NextRequest) => {
const { pathname, searchParams } = req.nextUrl;
console.log("APIs Middleware triggered for:", pathname);
const accessToken = req.cookies.get(
TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME,
)?.value;
if (!accessToken) {
return new NextResponse("Unauthorized", { status: 401 });
}
const payload = await verifyToken(accessToken);
if (!payload) {
return new NextResponse("Unauthorized", { status: 401 });
}
// Build the storage API URL correctly
const slug = pathname.replace("/storage-api/", "");
// Preserve existing query params and add token
const params = new URLSearchParams(searchParams);
params.set("token", env.STORAGE_TOKEN);
const storageUrl = `${env.STORAGE_URL}/${slug}?${params.toString()}`;
try {
// Forward the request to storage API
const headers = new Headers(req.headers);
headers.delete("host");
headers.delete("cookie");
const response = await fetch(storageUrl, {
method: req.method,
headers: headers,
body:
req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
// @ts-expect-error - duplex needed for request streaming
duplex: "half",
});
if (!response.ok) {
console.error("Storage API error:", response.status);
return new NextResponse("Storage API error", { status: response.status });
}
// Get response data
const data = await response.arrayBuffer();
// Create response with proper headers
const proxyResponse = new NextResponse(data, {
status: response.status,
statusText: response.statusText,
});
// Copy relevant headers
const contentType = response.headers.get("Content-Type");
const contentDisposition = response.headers.get("Content-Disposition");
const contentLength = response.headers.get("Content-Length");
if (contentType) proxyResponse.headers.set("Content-Type", contentType);
if (contentDisposition)
proxyResponse.headers.set("Content-Disposition", contentDisposition);
if (contentLength)
proxyResponse.headers.set("Content-Length", contentLength);
// Set CORS headers
proxyResponse.headers.set("Access-Control-Allow-Origin", env.BASE_URL);
proxyResponse.headers.set("Access-Control-Allow-Credentials", "true");
proxyResponse.headers.delete("X-Frame-Options"); // Remove if set by storage API
proxyResponse.headers.set(
"Content-Security-Policy",
"frame-ancestors 'self'",
);
return proxyResponse;
} catch (error) {
console.error("Storage proxy error:", error);
return new NextResponse("Failed to proxy request", { status: 500 });
}
};

View file

@ -206,7 +206,7 @@ const VisibComponent = ({
setOpen: (v: boolean) => void; setOpen: (v: boolean) => void;
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data, isLoading } = api.storage.getStorageVisibility.useQuery({ const { data, isLoading } = api.storage.getStorageAccessDetails.useQuery({
storageIds, storageIds,
}); });
const { data: users } = api.users.getUsers.useQuery(); const { data: users } = api.users.getUsers.useQuery();
@ -219,14 +219,14 @@ const VisibComponent = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageVisibility.invalidate(); await utils.storage.getStorageAccessDetails.invalidate();
toast.success("Utente rimosso", { toast.success("Utente rimosso", {
icon: "🗑️", icon: "🗑️",
id: context?.toastId, id: context?.toastId,
}); });
}, },
}); });
const { mutate: add } = api.storage.addUserStorage.useMutation({ const { mutate: add } = api.storage.addUserStorageEntry.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading("Aggiunta utente in corso", { const toastId = toast.loading("Aggiunta utente in corso", {
icon: "🪛", icon: "🪛",
@ -234,7 +234,7 @@ const VisibComponent = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageVisibility.invalidate(); await utils.storage.getStorageAccessDetails.invalidate();
toast.success("Utente aggiunto", { toast.success("Utente aggiunto", {
icon: "👍", icon: "👍",
id: context?.toastId, id: context?.toastId,
@ -250,7 +250,7 @@ const VisibComponent = ({
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageVisibility.invalidate(); await utils.storage.getStorageAccessDetails.invalidate();
toast.success("Reset effettuato", { toast.success("Reset effettuato", {
icon: "👍", icon: "👍",
id: context?.toastId, id: context?.toastId,

View file

@ -60,10 +60,15 @@ export const storageRouter = createTRPCRouter({
.query(async ({ input }) => { .query(async ({ input }) => {
return await retrieveUserFiles(input); return await retrieveUserFiles(input);
}), }),
getStorageAccessDetails: adminProcedure
//OLD .input(z.object({ storageIds: z.string().array() }))
.query(async ({ input }) => {
addUserStorage: protectedProcedure return await getMultipleWithRelations({
db,
ids: input.storageIds,
});
}),
addUserStorageEntry: protectedProcedure
.input(z.custom<NewUsersStorage>()) .input(z.custom<NewUsersStorage>())
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await addUserStorage(input); return await addUserStorage(input);
@ -73,15 +78,6 @@ export const storageRouter = createTRPCRouter({
return await getAllWithFromAdmin(); return await getAllWithFromAdmin();
}), }),
getStorageVisibility: adminProcedure
.input(z.object({ storageIds: z.string().array() }))
.query(async ({ input }) => {
return await getMultipleWithRelations({
db,
ids: input.storageIds,
});
}),
renameFile: adminProcedure renameFile: adminProcedure
.input(z.object({ fileId: z.string(), name: z.string() })) .input(z.object({ fileId: z.string(), name: z.string() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {