diff --git a/apps/infoalloggi/next.config.ts b/apps/infoalloggi/next.config.ts index 8f3e88b..560d1e6 100644 --- a/apps/infoalloggi/next.config.ts +++ b/apps/infoalloggi/next.config.ts @@ -59,6 +59,10 @@ async function createNextConfig(): Promise { reactStrictMode: true, rewrites: async () => { return [ + { + source: "/storage/:slug*", + destination: "http://localhost:8080/:slug*", + }, { destination: env.NODE_ENV === "production" ? "/404" : "/api/panel", source: "/api/panel", diff --git a/apps/infoalloggi/src/pages/test.tsx b/apps/infoalloggi/src/pages/test.tsx index a8149f5..47edd42 100644 --- a/apps/infoalloggi/src/pages/test.tsx +++ b/apps/infoalloggi/src/pages/test.tsx @@ -147,6 +147,135 @@ export default Test; */ const Test: NextPage = () => { - return
Test
; + return ; }; 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([]); + const [loading, setLoading] = useState(true); + + const loadData = async () => { + setLoading(true); + const fileList = await fetchFiles(); + setFiles(fileList); + setLoading(false); + }; + + useEffect(() => { + loadData(); + }, []); + + const [inputFile, setInputFile] = useState(null); + const [inputExpiresAt, setInputExpiresAt] = useState( + 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 ( +
+
+

File Storage

+ +
+ + { + setInputFile(e.currentTarget.files?.[0] || null); + }} + type="file" + /> + { + setInputExpiresAt(new Date(e.currentTarget.value).toISOString()); + }} + type="datetime-local" + /> + + {loading ? ( + + ) : ( +
    + {files.map((file) => ( +
  • + {file.mimeType.includes("image") && ( + {file.originalName} + )} +
    +

    {file.originalName}

    +

    + {file.mimeType} | Size: {(file.size / 1024 / 1024).toFixed(2)}{" "} + MB +

    +

    Uploaded At: {new Date(file.uploadedAt).toLocaleString()}

    + {file.expiresAt && ( +

    Expires At: {new Date(file.expiresAt).toLocaleString()}

    + )} +
    +
    + + Download + + +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/apps/infoalloggi/src/server/storage.ts b/apps/infoalloggi/src/server/storage.ts new file mode 100644 index 0000000..c79c1bb --- /dev/null +++ b/apps/infoalloggi/src/server/storage.ts @@ -0,0 +1,93 @@ +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; + +// Helper to fetch the list of all files +export async function fetchFiles(): Promise { + try { + const response = await fetch(`/storage/api/files`); + if (!response.ok) { + throw new Error(`Failed to fetch files: ${response.statusText}`); + } + 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 { + const formData = new FormData(); + formData.append("file", file); + if (expiresAt) { + formData.append("expires_at", expiresAt); + } + + try { + const response = await fetch(`/storage/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 { + const formData = new URLSearchParams(); + formData.append("id", fileID); + + try { + // The Go server accepts POST with form data for delete + const response = await fetch(`/storage/delete`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData.toString(), + }); + + // 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; + } +}