From 8341d9d8cede92adf40ec684721cb71b368ab2df Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Mon, 19 May 2025 18:05:40 +0100 Subject: [PATCH] Fixed warehouse api auth and extended artefact type --- package-lock.json | 8 +-- package.json | 4 +- src/app/api/get-user/route.ts | 5 +- src/app/api/warehouse/route.ts | 109 ++++++++------------------------- src/app/shop/page.tsx | 5 +- src/app/warehouse/page.tsx | 20 +++--- src/components/AuthModal.tsx | 1 + src/types/ApiTypes.ts | 8 +++ src/types/Artefact.ts | 14 ----- src/types/JWT.ts | 5 ++ src/utils/apiAuthMiddleware.ts | 33 ++++++++++ 11 files changed, 91 insertions(+), 121 deletions(-) create mode 100644 src/types/ApiTypes.ts delete mode 100644 src/types/Artefact.ts create mode 100644 src/types/JWT.ts create mode 100644 src/utils/apiAuthMiddleware.ts diff --git a/package-lock.json b/package-lock.json index 5ec761d..0d202f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "react-leaflet": "^5.0.0", "react-node": "^1.0.2", "swr": "^2.3.3", - "zod": "^3.24.4" + "zod": "^3.25.3" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -8095,9 +8095,9 @@ } }, "node_modules/zod": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.0.tgz", - "integrity": "sha512-ficnZKUW0mlNivqeJkosTEkGbJ6NKCtSaOHGx5aXbtfeWMdRyzXLbAIn19my4C/KB7WPY/p9vlGPt+qpOp6c4Q==", + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.3.tgz", + "integrity": "sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 8a047c4..b6020f9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "react-leaflet": "^5.0.0", "react-node": "^1.0.2", "swr": "^2.3.3", - "zod": "^3.24.4" + "zod": "^3.25.3" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -52,4 +52,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/src/app/api/get-user/route.ts b/src/app/api/get-user/route.ts index fcf1e36..c2f97e3 100644 --- a/src/app/api/get-user/route.ts +++ b/src/app/api/get-user/route.ts @@ -11,10 +11,7 @@ export async function POST(req: Request) { try { cookieStore = await cookies(); const token = cookieStore.get("jwt")?.value; - - if (!token) { - return NextResponse.json({ error: "No JWT found" }, { status: 401 }); - } + if (!token) return NextResponse.json({ error: "No JWT found" }, { status: 401 }); const payload = await verifyJwt({ token, secret: env.JWT_SECRET_KEY }); diff --git a/src/app/api/warehouse/route.ts b/src/app/api/warehouse/route.ts index 0eedb14..1315238 100644 --- a/src/app/api/warehouse/route.ts +++ b/src/app/api/warehouse/route.ts @@ -1,105 +1,46 @@ import { NextResponse } from "next/server"; +import { JWTPayload } from "@appTypes/JWT"; +import { cookies } from "next/headers"; import { PrismaClient } from "@prismaclient"; import { env } from "@utils/env"; import { verifyJwt } from "@utils/verifyJwt"; +import { ExtendedArtefact } from "@appTypes/ApiTypes"; +import { apiAuthMiddleware } from "@utils/apiAuthMiddleware"; -const usingPrisma = false; -let prisma: PrismaClient; -if (usingPrisma) prisma = new PrismaClient(); - -// Artefact type -interface Artefact { - id: number; - name: string; - description: string; - location: string; - earthquakeId: string; - isRequired: boolean; - isSold: boolean; - isCollected: boolean; - dateAdded: string; -} +const prisma = new PrismaClient(); export async function POST(req: Request) { try { - // todo fix, moron the token will be in the cookie header - const json = await req.json(); // Parse incoming JSON data - const { token } = json.body; - if (!token) return NextResponse.json({ message: "Unauthorised" }, { status: 401 }); - await verifyJwt({ token, secret: env.JWT_SECRET_KEY }); + const authResult = await apiAuthMiddleware(); + if ("user" in authResult === false) return authResult; // Handle error response - const warehouseArtefacts: Artefact[] = [ - { - id: 1, - name: "Solidified Lava Chunk", - description: "A chunk of solidified lava from the 2023 Iceland eruption.", - location: "Reykjanes, Iceland", - earthquakeId: "EQ2023ICL", - isRequired: true, - isSold: false, - isCollected: false, - dateAdded: "2025-05-04", - }, - { - id: 2, - name: "Tephra Sample", - description: "Foreign debris from the 2022 Tonga volcanic eruption.", - location: "Tonga", - earthquakeId: "EQ2022TGA", - isRequired: false, - isSold: true, - isCollected: true, - dateAdded: "2025-05-03", - }, - { - id: 3, - name: "Ash Sample", - description: "Volcanic ash from the 2021 La Palma eruption.", - location: "La Palma, Spain", - earthquakeId: "EQ2021LPA", - isRequired: false, - isSold: false, - isCollected: false, - dateAdded: "2025-05-04", - }, - { - id: 4, - name: "Ground Soil", - description: "Soil sample from the 2020 Croatia earthquake site.", - location: "Zagreb, Croatia", - earthquakeId: "EQ2020CRO", - isRequired: true, - isSold: false, - isCollected: false, - dateAdded: "2025-05-02", - }, - { - id: 5, - name: "Basalt Fragment", - description: "Basalt rock from the 2019 New Zealand eruption.", - location: "White Island, New Zealand", - earthquakeId: "EQ2019NZL", - isRequired: false, - isSold: true, - isCollected: false, - dateAdded: "2025-05-04", - }, - ]; + const { user } = authResult; - let artefacts; - if (usingPrisma) artefacts = await prisma.artefact.findMany(); + if (user.role !== "SCIENTIST" && user.role !== "ADMIN") { + return NextResponse.json({ message: "Not authorised" }, { status: 401 }); + } + + const artefacts = await prisma.artefact.findMany({ + include: { + earthquake: true, + }, + }); if (artefacts) { - return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 }); + const extendedArtefacts: ExtendedArtefact[] = artefacts.map((x) => ({ + ...x, + location: x.earthquake.location, + date: x.earthquake.date, + })); + return NextResponse.json({ message: "Got artefacts successfully", artefacts: extendedArtefacts }, { status: 200 }); } else { - return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: warehouseArtefacts }, { status: 200 }); - // return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 }); + return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 }); } } catch (error) { console.error("Error in artefacts endpoint:", error); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); } finally { - if (usingPrisma) await prisma.$disconnect(); + await prisma.$disconnect(); } } diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx index 3665e94..d325b74 100644 --- a/src/app/shop/page.tsx +++ b/src/app/shop/page.tsx @@ -2,12 +2,11 @@ import Image from "next/image"; import { Dispatch, SetStateAction, useCallback, useState } from "react"; -import Artefact from "@appTypes/Artefact"; +import { ExtendedArtefact } from "@appTypes/ApiTypes"; import { Currency } from "@appTypes/StoreModel"; import { useStoreState } from "@hooks/store"; -// Artefacts Data -const artefacts: Artefact[] = [ +const artefacts: ExtendedArtefact[] = [ { id: 1, name: "Golden Scarab", diff --git a/src/app/warehouse/page.tsx b/src/app/warehouse/page.tsx index bd5a3b2..5bb23d0 100644 --- a/src/app/warehouse/page.tsx +++ b/src/app/warehouse/page.tsx @@ -3,17 +3,14 @@ import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { FaTimes } from "react-icons/fa"; import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6"; import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5"; +import { ExtendedArtefact } from "@appTypes/ApiTypes"; // import { Artefact } from "@appTypes/Prisma"; import type { Artefact } from "@prismaclient"; -interface WarehouseArtefact extends Artefact { - location: string; -} - // Warehouse Artefacts Data -const warehouseArtefacts: WarehouseArtefact[] = [ +const extendedArtefacts: ExtendedArtefact[] = [ { id: 1, name: "Solidified Lava Chunk", @@ -699,7 +696,7 @@ export default function Warehouse() { // Apply filters with loading state const filteredArtefacts = useMemo(() => { setIsFiltering(true); - const result = applyFilters(warehouseArtefacts, filters); + const result = applyFilters(extendedArtefacts, filters); setIsFiltering(false); return result; }, [filters]); @@ -707,10 +704,10 @@ export default function Warehouse() { const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact); // Overview stats - const totalArtefacts = warehouseArtefacts.length; + const totalArtefacts = extendedArtefacts.length; const today = new Date(); - const artefactsAddedToday = warehouseArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length; - const artefactsSoldToday = warehouseArtefacts.filter( + const artefactsAddedToday = extendedArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length; + const artefactsSoldToday = extendedArtefacts.filter( (a) => a.isSold && a.createdAt.toDateString() === today.toDateString() ).length; @@ -803,8 +800,11 @@ export default function Warehouse() { - {/* Modals */} + {/* // todo only admins and senior scientists can log, add not allowed modal for juniors */} + {/* // todo add new artefact/pallet saving */} + {/* // todo add existing artefact modifying */} + {/* // todo add pallet display */} {showLogModal && setShowLogModal(false)} />} {showBulkLogModal && setShowBulkLogModal(false)} />} {editArtefact && setEditArtefact(null)} />} diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index b1d7f6d..5dcee33 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -10,6 +10,7 @@ interface AuthModalProps { } export default function AuthModal({ isOpen, onClose }: AuthModalProps) { + // todo add login successful message const [isLogin, setIsLogin] = useState(true); const modalRef = useRef(null); const [isFailed, setIsFailed] = useState(false); diff --git a/src/types/ApiTypes.ts b/src/types/ApiTypes.ts new file mode 100644 index 0000000..11dece6 --- /dev/null +++ b/src/types/ApiTypes.ts @@ -0,0 +1,8 @@ +import { Artefact } from "@prismaclient"; + +interface ExtendedArtefact extends Artefact { + location: string; + date: Date; +} + +export type { ExtendedArtefact }; diff --git a/src/types/Artefact.ts b/src/types/Artefact.ts deleted file mode 100644 index 672ff4a..0000000 --- a/src/types/Artefact.ts +++ /dev/null @@ -1,14 +0,0 @@ -interface Artefact { - // todo change to string - id: number; - name: string; - description: string; - location: string; - earthquakeID: string; - observatory: string; - dateReleased: string; - image: string; - price: number; -} - -export default Artefact; diff --git a/src/types/JWT.ts b/src/types/JWT.ts new file mode 100644 index 0000000..3ae8c1c --- /dev/null +++ b/src/types/JWT.ts @@ -0,0 +1,5 @@ +interface JWTPayload { + userId: number; +} + +export type { JWTPayload }; diff --git a/src/utils/apiAuthMiddleware.ts b/src/utils/apiAuthMiddleware.ts new file mode 100644 index 0000000..5215914 --- /dev/null +++ b/src/utils/apiAuthMiddleware.ts @@ -0,0 +1,33 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { verifyJwt } from "@utils/verifyJwt"; +import { PrismaClient } from "@prismaclient"; +import type { JWTPayload } from "@/types/JWT"; +import { env } from "@utils/env"; + +const prisma = new PrismaClient(); + +export async function apiAuthMiddleware() { + const cookieStore = await cookies(); + const token = cookieStore.get("jwt")?.value; + + if (!token) { + return NextResponse.json({ error: "No JWT found" }, { status: 401 }); + } + + const payload = (await verifyJwt({ + token, + secret: env.JWT_SECRET_KEY, + })) as unknown as JWTPayload; + + const user = await prisma.user.findUnique({ + where: { id: payload.userId }, + }); + + if (!user) { + cookieStore.delete("jwt"); + return NextResponse.json({ error: "Failed to get user" }, { status: 401 }); + } + + return { user, payload }; +}