From 2c3d53f3173b805e5d176e3bdc55f92b54f03168 Mon Sep 17 00:00:00 2001 From: Brandon Egger Date: Mon, 21 Aug 2023 09:54:08 -0500 Subject: [PATCH 1/6] add route authentication to file upload --- prisma/schema.prisma | 6 ++++ src/pages/api/resources/photo/[id].tsx | 39 ++++++++++++++++++-------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 60106fd..2ca430b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,11 +63,17 @@ type Manufacturer { notice String? } +type Photo { + name String + data Bytes +} + model AuditoryResource { id String @id @default(auto()) @map("_id") @db.ObjectId icon String name String description String + photo Photo? manufacturer Manufacturer? ages RangeInput skills Skill[] diff --git a/src/pages/api/resources/photo/[id].tsx b/src/pages/api/resources/photo/[id].tsx index 3257ea3..eb945c4 100644 --- a/src/pages/api/resources/photo/[id].tsx +++ b/src/pages/api/resources/photo/[id].tsx @@ -1,7 +1,9 @@ import { type NextApiHandler } from "next"; import formidable from "formidable"; -import * as path from "path"; +import * as fs from "fs"; import { prisma } from "~/server/db"; +import { getServerAuthSession } from "~/server/auth"; +import { Role } from "@prisma/client"; /** * Returns filename for a given filepath. @@ -17,6 +19,12 @@ const handler: NextApiHandler = async (req, res) => { return; } + const authSession = await getServerAuthSession({ req, res }); + if (!authSession?.user || authSession.user.role !== Role.ADMIN) { + res.writeHead(401, "Not authorized"); + return; + } + const { id } = req.query; if (Array.isArray(id) || !id) { @@ -29,17 +37,21 @@ const handler: NextApiHandler = async (req, res) => { keepExtensions: true, }); - const localUploadPath: Promise = new Promise((resolve, reject) => { - form.parse(req, (_err, _fields, files) => { - const photo = Array.isArray(files.photo) ? files.photo[0] : files.photo; - if (!photo) { - reject("Invalid file type sent (or none provided)"); - return; - } + const uploadPhoto: Promise = new Promise( + (resolve, reject) => { + form.parse(req, (_err, _fields, files) => { + const photo = Array.isArray(files.photo) ? files.photo[0] : files.photo; + if (!photo) { + reject("Invalid file type sent (or none provided)"); + return; + } - resolve(path.join("uploads", getFileName(photo.filepath))); - }); - }); + resolve(photo); + }); + } + ); + + const photoBuffer = fs.readFileSync((await uploadPhoto).filepath); try { await prisma.auditoryResource.update({ @@ -47,7 +59,10 @@ const handler: NextApiHandler = async (req, res) => { id, }, data: { - icon: await localUploadPath, + photo: { + name: getFileName((await uploadPhoto).filepath), + data: photoBuffer, + }, }, }); } catch (error: unknown) { From 4dbc46c1c07ec7df0e9b9162f5deea2995db9d4e Mon Sep 17 00:00:00 2001 From: Brandon Egger Date: Tue, 22 Aug 2023 12:55:01 -0500 Subject: [PATCH 2/6] fix file upload --- src/pages/api/resources/photo/[id].tsx | 4 +++- src/pages/resources/[id]/edit.tsx | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/api/resources/photo/[id].tsx b/src/pages/api/resources/photo/[id].tsx index eb945c4..73dfb12 100644 --- a/src/pages/api/resources/photo/[id].tsx +++ b/src/pages/api/resources/photo/[id].tsx @@ -60,7 +60,9 @@ const handler: NextApiHandler = async (req, res) => { }, data: { photo: { - name: getFileName((await uploadPhoto).filepath), + name: getFileName( + (await uploadPhoto).originalFilename ?? "resource ICON" + ), data: photoBuffer, }, }, diff --git a/src/pages/resources/[id]/edit.tsx b/src/pages/resources/[id]/edit.tsx index 3ee4ea7..a8d9d15 100644 --- a/src/pages/resources/[id]/edit.tsx +++ b/src/pages/resources/[id]/edit.tsx @@ -60,9 +60,6 @@ const EditResourcePage = ( }); const onSubmit: SubmitHandler = async (data) => { - // TODO: Fix file upload, currently it is not updating correctly on the server side - // May also need to look into re-rendering static pages when icon changes - // Also need to add authentication of route! if (updateIconFile) { const data = new FormData(); data.append("photo", updateIconFile); @@ -75,8 +72,6 @@ const EditResourcePage = ( } ); - console.log("uploading icon"); - if (uploadResponse.status !== 200) { setServerError( "Failed uploading resource icon file. Changes did not save!" From 0e311b0aa214a9744f547bc3fc652ea82834c253 Mon Sep 17 00:00:00 2001 From: Brandon Egger Date: Tue, 22 Aug 2023 13:02:12 -0500 Subject: [PATCH 3/6] add resource table separate icon component --- src/components/ResourceTable.tsx | 48 ++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/ResourceTable.tsx b/src/components/ResourceTable.tsx index a61f5b3..40e82e6 100644 --- a/src/components/ResourceTable.tsx +++ b/src/components/ResourceTable.tsx @@ -9,12 +9,50 @@ import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import Link from "next/link"; import { translateEnumPlatform, translateEnumSkill } from "~/utils/enumWordLut"; -import { type ChangeEvent } from "react"; +import { useEffect, type ChangeEvent, useState } from "react"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { type ParsedUrlQuery, type ParsedUrlQueryInput } from "querystring"; import { useRouter } from "next/router"; import { PriceIcon } from "~/prices/Icons"; +export const ResourcePhoto = ({ resource }: { resource: AuditoryResource }) => { + const [blobSrc, setBlobSrc] = useState(undefined); + + useEffect(() => { + if (!resource.photo?.data) { + return; + } + + const blob = new Blob([resource.photo.data], { type: "image/png" }); + setBlobSrc(URL.createObjectURL(blob)); + }, []); + + const commonProps = { + width: 512, + height: 512, + }; + + if (resource.photo?.data) { + return ( + {`${resource.name} + ); + } + + return ( + {`${resource.name} + ); +}; + export const ResourceInfo = ({ resource, showMoreInfo, @@ -46,13 +84,7 @@ export const ResourceInfo = ({ {showMoreInfo ? (
- {`${resource.name} + more info From 8c7682251ca18714a0094f1c3bcc562320a0e873 Mon Sep 17 00:00:00 2001 From: Brandon Egger Date: Tue, 22 Aug 2023 13:35:11 -0500 Subject: [PATCH 4/6] add comment to explain why issue is occuring --- src/server/api/routers/auditoryResources.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/api/routers/auditoryResources.ts b/src/server/api/routers/auditoryResources.ts index 4ab1e86..1252dae 100644 --- a/src/server/api/routers/auditoryResources.ts +++ b/src/server/api/routers/auditoryResources.ts @@ -137,6 +137,7 @@ export const auditoryResourceRouter = createTRPCRouter({ }), ]); + // TODO: The issue here is the photo binary data can't be sent over tRPC which will cause the request to be unparsable by the client return { count, resources, From 712e263bfa0280f6e698eaaf072decfe9f5aec10 Mon Sep 17 00:00:00 2001 From: Brandon Egger Date: Tue, 22 Aug 2023 14:15:46 -0500 Subject: [PATCH 5/6] add buffer support extension --- src/utils/api.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/utils/api.ts b/src/utils/api.ts index f4f4ad5..7bb159f 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -17,6 +17,15 @@ const getBaseUrl = () => { return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost }; +superjson.registerCustom( + { + isApplicable: (v): v is Buffer => v instanceof Buffer, + serialize: v => [...v], + deserialize: v => Buffer.from(v) + }, + "buffer" +); + /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ config() { From e61f9e10ce87958161dd50ec6e5ec959074b4acb Mon Sep 17 00:00:00 2001 From: Brandon Egger Date: Tue, 22 Aug 2023 14:18:48 -0500 Subject: [PATCH 6/6] fix lint errors --- src/components/ResourceTable.tsx | 15 ++------------- src/utils/api.ts | 4 ++-- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/components/ResourceTable.tsx b/src/components/ResourceTable.tsx index 40e82e6..a4c40b3 100644 --- a/src/components/ResourceTable.tsx +++ b/src/components/ResourceTable.tsx @@ -25,28 +25,17 @@ export const ResourcePhoto = ({ resource }: { resource: AuditoryResource }) => { const blob = new Blob([resource.photo.data], { type: "image/png" }); setBlobSrc(URL.createObjectURL(blob)); - }, []); + }, [resource.photo]); const commonProps = { width: 512, height: 512, }; - if (resource.photo?.data) { - return ( - {`${resource.name} - ); - } - return ( {`${resource.name} diff --git a/src/utils/api.ts b/src/utils/api.ts index 7bb159f..984777a 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -20,8 +20,8 @@ const getBaseUrl = () => { superjson.registerCustom( { isApplicable: (v): v is Buffer => v instanceof Buffer, - serialize: v => [...v], - deserialize: v => Buffer.from(v) + serialize: (v) => [...v], + deserialize: (v) => Buffer.from(v), }, "buffer" );