From 8341d9d8cede92adf40ec684721cb71b368ab2df Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Mon, 19 May 2025 18:05:40 +0100 Subject: [PATCH 1/9] 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 }; +} From 885e694ad29c864b0e376e823cdea10fa54b3247 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Tue, 20 May 2025 18:26:40 +0100 Subject: [PATCH 2/9] Converted to a prisma singleton --- src/app/api/earthquakes/route.ts | 8 +--- src/app/api/get-user/route.ts | 6 +-- src/app/api/import-earthquakes/route.ts | 6 +-- src/app/api/import-observatories/route.ts | 5 +-- src/app/api/import-requests/route.ts | 6 +-- src/app/api/import-scientists/route.ts | 5 +-- src/app/api/import-users/route.ts | 5 +-- src/app/api/login/route.ts | 8 +--- src/app/api/observatories/route.ts | 11 +----- src/app/api/signup/route.ts | 45 ++++++++--------------- src/app/api/warehouse/route.ts | 6 +-- src/utils/apiAuthMiddleware.ts | 3 +- src/utils/prisma.ts | 9 +++++ 13 files changed, 36 insertions(+), 87 deletions(-) create mode 100644 src/utils/prisma.ts diff --git a/src/app/api/earthquakes/route.ts b/src/app/api/earthquakes/route.ts index c44dbd5..c12f380 100644 --- a/src/app/api/earthquakes/route.ts +++ b/src/app/api/earthquakes/route.ts @@ -1,10 +1,6 @@ import { NextResponse } from "next/server"; -import { PrismaClient } from "@prismaclient"; - -const usingPrisma = false; -let prisma: PrismaClient; -if (usingPrisma) prisma = new PrismaClient(); +import { prisma } from "@utils/prisma"; export async function POST(req: Request) { try { @@ -33,7 +29,5 @@ export async function POST(req: Request) { } catch (error) { console.error("Error in earthquakes endpoint:", error); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } finally { - if (usingPrisma) await prisma.$disconnect(); } } diff --git a/src/app/api/get-user/route.ts b/src/app/api/get-user/route.ts index c2f97e3..0904559 100644 --- a/src/app/api/get-user/route.ts +++ b/src/app/api/get-user/route.ts @@ -1,10 +1,8 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; import { env } from "@utils/env"; -import { PrismaClient } from "@prisma/client"; import { verifyJwt } from "@utils/verifyJwt"; - -const prisma = new PrismaClient(); +import { prisma } from "@utils/prisma"; export async function POST(req: Request) { let cookieStore; @@ -41,7 +39,5 @@ export async function POST(req: Request) { console.error("Error in user endpoint:", error); cookieStore?.delete("jwt"); // Delete JWT cookie on error return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/app/api/import-earthquakes/route.ts b/src/app/api/import-earthquakes/route.ts index cc9b564..7712819 100644 --- a/src/app/api/import-earthquakes/route.ts +++ b/src/app/api/import-earthquakes/route.ts @@ -1,13 +1,11 @@ import { parse } from "csv-parse/sync"; import fs from "fs/promises"; import { NextResponse } from "next/server"; +import { prisma } from "@utils/prisma"; import path from "path"; -import { PrismaClient } from "@prismaclient"; - // Path to your earthquakes.csv const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv"); -const prisma = new PrismaClient(); type CsvRow = { Date: string; @@ -50,7 +48,5 @@ export async function POST() { } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/app/api/import-observatories/route.ts b/src/app/api/import-observatories/route.ts index ca21887..fccd04c 100644 --- a/src/app/api/import-observatories/route.ts +++ b/src/app/api/import-observatories/route.ts @@ -3,11 +3,10 @@ import fs from "fs/promises"; import { NextResponse } from "next/server"; import path from "path"; -import { PrismaClient } from "@prismaclient"; +import { prisma } from "@utils/prisma"; // CSV location (update filename as needed) const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv"); -const prisma = new PrismaClient(); type CsvRow = { Name: string; @@ -61,7 +60,5 @@ export async function POST() { } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/app/api/import-requests/route.ts b/src/app/api/import-requests/route.ts index 4b63309..021ed92 100644 --- a/src/app/api/import-requests/route.ts +++ b/src/app/api/import-requests/route.ts @@ -3,10 +3,8 @@ import fs from "fs/promises"; import { NextResponse } from "next/server"; import path from "path"; -import { PrismaClient } from "@prismaclient"; - +import { prisma } from "@utils/prisma"; const csvFilePath = path.resolve(process.cwd(), "public/requests.csv"); -const prisma = new PrismaClient(); type RequestType = "NEW_USER" | "CHANGE_LEVEL" | "DELETE"; type RequestOutcome = "FULFILLED" | "REJECTED" | "IN_PROGRESS" | "CANCELLED" | "OTHER"; @@ -56,7 +54,5 @@ export async function POST() { } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/app/api/import-scientists/route.ts b/src/app/api/import-scientists/route.ts index 83c229b..b91cf52 100644 --- a/src/app/api/import-scientists/route.ts +++ b/src/app/api/import-scientists/route.ts @@ -3,11 +3,10 @@ import fs from "fs/promises"; import { NextResponse } from "next/server"; import path from "path"; -import { PrismaClient } from "@prismaclient"; +import { prisma } from "@utils/prisma"; // Path to CSV file const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv"); -const prisma = new PrismaClient(); type CsvRow = { Name: string; @@ -53,7 +52,5 @@ export async function POST() { } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/app/api/import-users/route.ts b/src/app/api/import-users/route.ts index 0ffe5ed..1e4f166 100644 --- a/src/app/api/import-users/route.ts +++ b/src/app/api/import-users/route.ts @@ -3,11 +3,10 @@ import fs from "fs/promises"; import { NextResponse } from "next/server"; import path from "path"; -import { PrismaClient } from "@prismaclient"; +import { prisma } from "@utils/prisma"; // Path to users.csv - adjust as needed const csvFilePath = path.resolve(process.cwd(), "public/users.csv"); -const prisma = new PrismaClient(); type CsvRow = { Name: string; @@ -51,7 +50,5 @@ export async function POST() { } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index 06b8657..a374d38 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -2,15 +2,11 @@ import bcryptjs from "bcryptjs"; import { SignJWT } from "jose"; import { NextResponse } from "next/server"; -import { PrismaClient } from "@prismaclient"; import { env } from "@utils/env"; +import { prisma } from "@utils/prisma"; import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite"; -const usingPrisma = false; -let prisma: PrismaClient; -if (usingPrisma) prisma = new PrismaClient(); - export async function POST(req: Request) { try { const { email, password } = await req.json(); // Parse incoming JSON data @@ -67,7 +63,5 @@ export async function POST(req: Request) { } catch (error) { console.error("Error in signup endpoint:", error); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } finally { - if (usingPrisma) await prisma.$disconnect(); } } diff --git a/src/app/api/observatories/route.ts b/src/app/api/observatories/route.ts index 28258fe..809efe5 100644 --- a/src/app/api/observatories/route.ts +++ b/src/app/api/observatories/route.ts @@ -1,10 +1,6 @@ import { NextResponse } from "next/server"; -import { PrismaClient } from "@prismaclient"; - -const usingPrisma = false; -let prisma: PrismaClient; -if (usingPrisma) prisma = new PrismaClient(); +import { prisma } from "@utils/prisma"; export async function GET(request: Request) { try { @@ -36,8 +32,7 @@ export async function GET(request: Request) { ]; // todo get earthquakes associated with observatories - let observatories; - if (usingPrisma) observatories = await prisma.observatory.findMany(); + const observatories = await prisma.observatory.findMany(); if (observatories) { return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 }); @@ -48,7 +43,5 @@ export async function GET(request: Request) { } catch (error) { console.error("Error in observatories endpoint:", error); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } finally { - if (usingPrisma) await prisma.$disconnect(); } } diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index b7449c3..a62510b 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -2,15 +2,11 @@ import bcryptjs from "bcryptjs"; import { SignJWT } from "jose"; import { NextResponse } from "next/server"; -import { PrismaClient } from "@prismaclient"; +import { prisma } from "@utils/prisma"; import { env } from "@utils/env"; import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite"; -const usingPrisma = false; -let prisma: PrismaClient; -if (usingPrisma) prisma = new PrismaClient(); - export async function POST(req: Request) { try { const { email, password, name } = await req.json(); // Parse incoming JSON data @@ -23,17 +19,11 @@ export async function POST(req: Request) { console.log("Email:", email); // ! remove console.log("Password:", password); // ! remove - let foundUser; - - if (usingPrisma) { - foundUser = await prisma.user.findUnique({ - where: { - email: email, // use the email to uniquely identify the user - }, - }); - } else { - foundUser = findUserByEmail(userData, email); - } + const foundUser = await prisma.user.findUnique({ + where: { + email: email, // use the email to uniquely identify the user + }, + }); if (foundUser) { return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 }); @@ -58,20 +48,15 @@ export async function POST(req: Request) { } else { try { const passwordHash = await bcryptjs.hash(password, 10); - let user; - if (usingPrisma) { - // todo add sending back user - user = await prisma.user.create({ - data: { - name, - email, - passwordHash, - }, - }); - } else { - user = { name, email, password: passwordHash, accessLevel }; - userData.push(user); - } + // todo add sending back user + const user = await prisma.user.create({ + data: { + name, + email, + passwordHash, + }, + }); + await writeUserCsv(userData); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); diff --git a/src/app/api/warehouse/route.ts b/src/app/api/warehouse/route.ts index 1315238..4dd3ef6 100644 --- a/src/app/api/warehouse/route.ts +++ b/src/app/api/warehouse/route.ts @@ -2,14 +2,12 @@ import { NextResponse } from "next/server"; import { JWTPayload } from "@appTypes/JWT"; import { cookies } from "next/headers"; -import { PrismaClient } from "@prismaclient"; +import { prisma } from "@utils/prisma"; import { env } from "@utils/env"; import { verifyJwt } from "@utils/verifyJwt"; import { ExtendedArtefact } from "@appTypes/ApiTypes"; import { apiAuthMiddleware } from "@utils/apiAuthMiddleware"; -const prisma = new PrismaClient(); - export async function POST(req: Request) { try { const authResult = await apiAuthMiddleware(); @@ -40,7 +38,5 @@ export async function POST(req: Request) { } catch (error) { console.error("Error in artefacts endpoint:", error); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); - } finally { - await prisma.$disconnect(); } } diff --git a/src/utils/apiAuthMiddleware.ts b/src/utils/apiAuthMiddleware.ts index 5215914..9d07052 100644 --- a/src/utils/apiAuthMiddleware.ts +++ b/src/utils/apiAuthMiddleware.ts @@ -1,11 +1,10 @@ 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(); +import { prisma } from "@utils/prisma"; export async function apiAuthMiddleware() { const cookieStore = await cookies(); diff --git a/src/utils/prisma.ts b/src/utils/prisma.ts new file mode 100644 index 0000000..93754e1 --- /dev/null +++ b/src/utils/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from "@prisma/client"; + +declare global { + var prisma: PrismaClient | undefined; +} + +const prisma = process.env.NODE_ENV === "production" ? new PrismaClient() : global.prisma ?? (global.prisma = new PrismaClient()); + +export { prisma }; From cb6dd0507194cc9c5ed45ba03644f7d6d57fde0e Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Thu, 22 May 2025 17:59:20 +0100 Subject: [PATCH 3/9] Fixed import artefacts and added imageName to artefact --- importersFixed.md | 6 ++ prisma/schema.prisma | 1 + public/Artefacts.csv | 2 +- src/app/api/import-artefacts/route.ts | 87 +++++++++++++++++++++++++++ src/app/api/import-artifacts/route.ts | 70 --------------------- 5 files changed, 95 insertions(+), 71 deletions(-) create mode 100644 importersFixed.md create mode 100644 src/app/api/import-artefacts/route.ts delete mode 100644 src/app/api/import-artifacts/route.ts diff --git a/importersFixed.md b/importersFixed.md new file mode 100644 index 0000000..9c49ae5 --- /dev/null +++ b/importersFixed.md @@ -0,0 +1,6 @@ +- [ ] Import users +- [x] Import artefacts +- [ ] Import earthquakes +- [ ] Import observatoies +- [ ] Import requests +- [ ] Import scientists diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 23d3a4c..86e5b9c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -91,6 +91,7 @@ model Artefact { type String @db.VarChar(50) // Lava, Tephra, Ash, Soil warehouseArea String description String + imageName String earthquakeId Int earthquake Earthquake @relation(fields: [earthquakeId], references: [id]) creatorId Int? diff --git a/public/Artefacts.csv b/public/Artefacts.csv index 8532919..4a9c189 100644 --- a/public/Artefacts.csv +++ b/public/Artefacts.csv @@ -1,4 +1,4 @@ -Name,Type,WarehouseArea,Description,earthquakeID,Price,Required,PickedUp,Picture +Name,Type,WarehouseArea,Description,EarthquakeCode,Price,Required,PickedUp,Picture Echo Bomb,Lava,ShelvingAreaA,A dense glossy black volcanic bomb with minor vesicles.,EV-7.4-Mexico-00035,120,no,no,EchoBomb.PNG Silvershade Ash,Ash,ShelvingAreaD,Fine light-grey volcanic ash collected near a village.,EV-6.0-Iceland-00018,40,no,no,SilvershadeAsh.PNG Strata Core,Soil,LoggingArea,Soil core with visible stratification showing evidence of liquefaction.,ET-6.9-Brazil-00046,30,no,no,StrataCore.PNG diff --git a/src/app/api/import-artefacts/route.ts b/src/app/api/import-artefacts/route.ts new file mode 100644 index 0000000..01d2b08 --- /dev/null +++ b/src/app/api/import-artefacts/route.ts @@ -0,0 +1,87 @@ +import { parse } from "csv-parse/sync"; +import fs from "fs/promises"; +import { NextResponse } from "next/server"; +import path from "path"; +import { stringToBool } from "@utils/parsingUtils"; +import { prisma } from "@utils/prisma"; +import { getRandomNumber } from "@utils/maths"; + +const csvFilePath = path.resolve(process.cwd(), "public/artefacts.csv"); + +type CsvRow = { + Type: string; + Name: string; + Description: string; + WarehouseArea: string; + EarthquakeCode: string; + Required?: string; + Price: string; + PickedUp?: string; + Picture: string; +}; + +export async function POST() { + try { + const fileContent = await fs.readFile(csvFilePath, "utf8"); + const records: CsvRow[] = parse(fileContent, { + columns: true, + skip_empty_lines: true, + }); + + const failedImports: { row: CsvRow; reason: string }[] = []; + + const artefacts = await Promise.all( + records.map(async (row) => { + const earthquake = await prisma.earthquake.findUnique({ + where: { code: row.EarthquakeCode }, + }); + + const creators = await prisma.user.findMany({ + where: { + role: { in: ["SCIENTIST", "ADMIN"] }, + }, + }); + const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null; + + if (!earthquake || !randomCreator) { + failedImports.push({ row, reason: `Earthquake: ${earthquake}, RandomCreator: ${randomCreator}` }); + return undefined; + } + + return { + name: row.Name, + description: row.Description, + type: row.Type, + warehouseArea: row.WarehouseArea, + earthquakeId: earthquake.id, + required: stringToBool(row.Required, true), + shopPrice: row.Price && row.Price !== "" ? parseFloat(row.Price) : getRandomNumber(20, 500), + pickedUp: stringToBool(row.PickedUp, false), + creatorId: randomCreator.id, + purchasedById: null, + imageName: row.Picture, + }; + }) + ); + + const validArtefacts = artefacts.filter((artefact): artefact is NonNullable => artefact !== undefined); + + await prisma.artefact.createMany({ + data: validArtefacts, + }); + + if (failedImports.length > 0) { + console.warn("Failed imports:", failedImports); + await fs.writeFile(path.resolve(process.cwd(), "failed_imports_artefacts.json"), JSON.stringify(failedImports, null, 2)); + } + + return NextResponse.json({ + success: true, + count: validArtefacts.length, + failedCount: failedImports.length, + }); + } catch (error: any) { + console.error(error); + return NextResponse.json({ success: false, error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/import-artifacts/route.ts b/src/app/api/import-artifacts/route.ts deleted file mode 100644 index a5b2356..0000000 --- a/src/app/api/import-artifacts/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { parse } from "csv-parse/sync"; -import fs from "fs/promises"; -import { NextResponse } from "next/server"; -import path from "path"; - -import { PrismaClient } from "@prismaclient"; - -// CSV location -const csvFilePath = path.resolve(process.cwd(), "public/artefacts.csv"); -const prisma = new PrismaClient(); - -type CsvRow = { - Type: string; - Name: string; - Description: string; - WarehouseArea: string; - EarthquakeId: string; - Required?: string; - ShopPrice?: string; - PickedUp?: string; -}; - -function stringToBool(val: string | undefined, defaultValue: boolean = false): boolean { - if (!val) return defaultValue; - return /^true$/i.test(val.trim()); -} - -export async function POST() { - try { - // 1. Read file - const fileContent = await fs.readFile(csvFilePath, "utf8"); - - // 2. Parse CSV - const records: CsvRow[] = parse(fileContent, { - columns: true, - skip_empty_lines: true, - }); - - // 3. Map records to artefact input - const artefacts = records.map((row) => ({ - name: row.Name, - description: row.Description, - type: row.Type, - warehouseArea: row.WarehouseArea, - // todo get earthquakeId where code === row.EarthquakeCode - earthquakeId: parseInt(row.EarthquakeId, 10), - required: stringToBool(row.Required, true), // default TRUE - shopPrice: row.ShopPrice && row.ShopPrice !== "" ? parseFloat(row.ShopPrice) : null, - pickedUp: stringToBool(row.PickedUp, false), // default FALSE - // todo add random selection for creatorId - creatorId: null, - purchasedById: null, - })); - - // 4. Bulk insert - await prisma.artefact.createMany({ - data: artefacts, - }); - - return NextResponse.json({ - success: true, - count: artefacts.length, - }); - } catch (error: any) { - console.error(error); - return NextResponse.json({ success: false, error: error.message }, { status: 500 }); - } finally { - await prisma.$disconnect(); - } -} From b40d0aedb443a2c56db91c5ede48866bc076fe39 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Thu, 22 May 2025 17:59:48 +0100 Subject: [PATCH 4/9] Fixed a few small things --- src/app/our-mission/page.tsx | 8 ++++---- src/app/shop/page.tsx | 4 ++-- src/types/StoreModel.ts | 2 +- src/utils/maths.ts | 5 +++++ src/utils/parsingUtils.ts | 7 +++++++ src/utils/prisma.ts | 4 ++-- 6 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 src/utils/maths.ts create mode 100644 src/utils/parsingUtils.ts diff --git a/src/app/our-mission/page.tsx b/src/app/our-mission/page.tsx index 5fb10e1..3116340 100644 --- a/src/app/our-mission/page.tsx +++ b/src/app/our-mission/page.tsx @@ -1,11 +1,11 @@ "use client"; import Image from "next/image"; -const OurMission = () => { +function OurMission() { return (
{/* Overlay for Readability */}
@@ -55,5 +55,5 @@ const OurMission = () => {
); -}; +} export default OurMission; diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx index d325b74..363d1e1 100644 --- a/src/app/shop/page.tsx +++ b/src/app/shop/page.tsx @@ -214,7 +214,7 @@ export default function Shop() { if (currentPage > 1) setCurrentPage((prev) => prev - 1); }; - function ArtefactCard({ artefact }: { artefact: Artefact }) { + function ArtefactCard({ artefact }: { artefact: ExtendedArtefact }) { return (
); } - function Modal({ artefact }: { artefact: Artefact }) { + function Modal({ artefact }: { artefact: ExtendedArtefact }) { if (!artefact) return null; const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) setSelectedArtefact(null); diff --git a/src/types/StoreModel.ts b/src/types/StoreModel.ts index 3f1041b..ae98ddd 100644 --- a/src/types/StoreModel.ts +++ b/src/types/StoreModel.ts @@ -1,5 +1,5 @@ import { Action } from "easy-peasy"; -// import type { User } from "@prisma/client"; +// import type { User } from "@prismaclient"; import { User } from "@appTypes/Prisma"; type Currency = "GBP" | "USD" | "EUR"; diff --git a/src/utils/maths.ts b/src/utils/maths.ts new file mode 100644 index 0000000..3a9d0b2 --- /dev/null +++ b/src/utils/maths.ts @@ -0,0 +1,5 @@ +function getRandomNumber(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export { getRandomNumber }; diff --git a/src/utils/parsingUtils.ts b/src/utils/parsingUtils.ts new file mode 100644 index 0000000..0cb211d --- /dev/null +++ b/src/utils/parsingUtils.ts @@ -0,0 +1,7 @@ +function stringToBool(val: string | undefined, defaultValue: boolean = false): boolean { + if (!val) return defaultValue; + const normalized = val.trim().toLowerCase(); + return normalized === "true" || normalized === "yes"; +} + +export { stringToBool }; diff --git a/src/utils/prisma.ts b/src/utils/prisma.ts index 93754e1..1ca360b 100644 --- a/src/utils/prisma.ts +++ b/src/utils/prisma.ts @@ -1,9 +1,9 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient } from "@prismaclient"; declare global { var prisma: PrismaClient | undefined; } -const prisma = process.env.NODE_ENV === "production" ? new PrismaClient() : global.prisma ?? (global.prisma = new PrismaClient()); +const prisma = new PrismaClient(); export { prisma }; From 68d47a4fe3b26cd147ea129379e16fa73ad63536 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Thu, 22 May 2025 18:18:34 +0100 Subject: [PATCH 5/9] Fixed users importing script --- importersFixed.md | 2 +- src/app/api/import-artefacts/route.ts | 6 +-- src/app/api/import-users/route.ts | 54 ++++++++++++++++++--------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/importersFixed.md b/importersFixed.md index 9c49ae5..050c3d0 100644 --- a/importersFixed.md +++ b/importersFixed.md @@ -1,4 +1,4 @@ -- [ ] Import users +- [x] Import users - [x] Import artefacts - [ ] Import earthquakes - [ ] Import observatoies diff --git a/src/app/api/import-artefacts/route.ts b/src/app/api/import-artefacts/route.ts index 01d2b08..7c39988 100644 --- a/src/app/api/import-artefacts/route.ts +++ b/src/app/api/import-artefacts/route.ts @@ -6,7 +6,7 @@ import { stringToBool } from "@utils/parsingUtils"; import { prisma } from "@utils/prisma"; import { getRandomNumber } from "@utils/maths"; -const csvFilePath = path.resolve(process.cwd(), "public/artefacts.csv"); +const csvFilePath = path.resolve(process.cwd(), "public/Artefacts.csv"); type CsvRow = { Type: string; @@ -45,7 +45,7 @@ export async function POST() { if (!earthquake || !randomCreator) { failedImports.push({ row, reason: `Earthquake: ${earthquake}, RandomCreator: ${randomCreator}` }); - return undefined; + return null; } return { @@ -64,7 +64,7 @@ export async function POST() { }) ); - const validArtefacts = artefacts.filter((artefact): artefact is NonNullable => artefact !== undefined); + const validArtefacts = artefacts.filter((artefact): artefact is NonNullable => artefact !== null); await prisma.artefact.createMany({ data: validArtefacts, diff --git a/src/app/api/import-users/route.ts b/src/app/api/import-users/route.ts index 1e4f166..77ae9d9 100644 --- a/src/app/api/import-users/route.ts +++ b/src/app/api/import-users/route.ts @@ -5,18 +5,21 @@ import path from "path"; import { prisma } from "@utils/prisma"; -// Path to users.csv - adjust as needed -const csvFilePath = path.resolve(process.cwd(), "public/users.csv"); +const csvFilePath = path.resolve(process.cwd(), "public/Users.csv"); type CsvRow = { + id: string; + createdAt: string; Name: string; Email: string; PasswordHash: string; - Role?: string; + Role: string; + Scientist: string; + PurchasedArtefacts: string; + Requests: string; }; function normalizeRole(role: string | undefined): string { - // Only allow ADMIN, SCIENTIST, GUEST; default GUEST if (!role || !role.trim()) return "GUEST"; const r = role.trim().toUpperCase(); return ["ADMIN", "SCIENTIST", "GUEST"].includes(r) ? r : "GUEST"; @@ -24,29 +27,46 @@ function normalizeRole(role: string | undefined): string { export async function POST() { try { - // 1. Read the CSV file const fileContent = await fs.readFile(csvFilePath, "utf8"); - - // 2. Parse the CSV const records: CsvRow[] = parse(fileContent, { columns: true, skip_empty_lines: true, }); - // 3. Transform each CSV row to User model format - const users = records.map((row) => ({ - name: row.Name, - email: row.Email, - passwordHash: row.PasswordHash, - role: normalizeRole(row.Role), - })); + const failedImports: { row: CsvRow; reason: string }[] = []; + + const users = await Promise.all( + records.map(async (row) => { + try { + return { + name: row.Name, + email: row.Email, + passwordHash: row.PasswordHash, + role: normalizeRole(row.Role), + }; + } catch (error: any) { + failedImports.push({ row, reason: error.message }); + return null; + } + }) + ); + + const validUsers = users.filter((user): user is NonNullable => user !== null); - // 4. Bulk create users in database await prisma.user.createMany({ - data: users, + data: validUsers, }); - return NextResponse.json({ success: true, count: users.length }); + if (failedImports.length > 0) { + console.warn("Failed imports:", failedImports); + await fs.writeFile(path.resolve(process.cwd(), "failed_imports_users.json"), JSON.stringify(failedImports, null, 2)); + } + + return NextResponse.json({ + success: true, + count: validUsers.length, + failedCount: failedImports.length, + }); } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); From 87d2cfbc25ef0c8214ed2394cc67dd5f05f79d77 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Sun, 25 May 2025 11:14:56 +0100 Subject: [PATCH 6/9] Fixed importing earthquakes --- importersFixed.md | 2 +- src/app/api/import-earthquakes/route.ts | 69 ++++++++++++++++++------- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/importersFixed.md b/importersFixed.md index 050c3d0..acdbd89 100644 --- a/importersFixed.md +++ b/importersFixed.md @@ -1,6 +1,6 @@ - [x] Import users - [x] Import artefacts -- [ ] Import earthquakes +- [x] Import earthquakes - [ ] Import observatoies - [ ] Import requests - [ ] Import scientists diff --git a/src/app/api/import-earthquakes/route.ts b/src/app/api/import-earthquakes/route.ts index 7712819..a42c6fb 100644 --- a/src/app/api/import-earthquakes/route.ts +++ b/src/app/api/import-earthquakes/route.ts @@ -1,10 +1,10 @@ import { parse } from "csv-parse/sync"; import fs from "fs/promises"; import { NextResponse } from "next/server"; -import { prisma } from "@utils/prisma"; import path from "path"; +import { prisma } from "@utils/prisma"; +import { getRandomNumber } from "@utils/maths"; -// Path to your earthquakes.csv const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv"); type CsvRow = { @@ -20,31 +20,60 @@ type CsvRow = { export async function POST() { try { - // 1. Read the CSV file const fileContent = await fs.readFile(csvFilePath, "utf8"); - // 2. Parse the CSV const records: CsvRow[] = parse(fileContent, { columns: true, skip_empty_lines: true, }); - // 3. Transform to fit Earthquake model - const earthquakes = records.map((row) => ({ - date: new Date(row.Date), - code: row.Code, - magnitude: parseFloat(row.Magnitude), - type: row.Type, - latitude: parseFloat(row.Latitude), - longitude: parseFloat(row.Longitude), - location: row.Location, - depth: row.Depth, // store as received - // todo add random selection for creatorId - creatorId: null, - })); - // 4. Bulk create earthquakes in database: + + const failedImports: { row: CsvRow; reason: string }[] = []; + + const earthquakes = await Promise.all( + records.map(async (row) => { + const creators = await prisma.user.findMany({ + where: { + role: { in: ["SCIENTIST", "ADMIN"] }, + }, + }); + const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null; + + if (!randomCreator) { + failedImports.push({ row, reason: `RandomCreator: ${randomCreator}` }); + return null; + } + + return { + date: new Date(row.Date), + code: row.Code, + magnitude: parseFloat(row.Magnitude), + type: row.Type, + latitude: parseFloat(row.Latitude), + longitude: parseFloat(row.Longitude), + location: row.Location, + depth: row.Depth, + creatorId: randomCreator.id, + }; + }) + ); + + const validEarthquakes = earthquakes.filter( + (earthquake): earthquake is NonNullable => earthquake !== null + ); + await prisma.earthquake.createMany({ - data: earthquakes, + data: validEarthquakes, + }); + + if (failedImports.length > 0) { + console.warn("Failed imports:", failedImports); + await fs.writeFile(path.resolve(process.cwd(), "failed_imports_earthquakes.json"), JSON.stringify(failedImports, null, 2)); + } + + return NextResponse.json({ + success: true, + count: validEarthquakes.length, + failedCount: failedImports.length, }); - return NextResponse.json({ success: true, count: earthquakes.length }); } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); From a03d265410be547877bcb85140daf0d5227340b8 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Sun, 25 May 2025 11:37:05 +0100 Subject: [PATCH 7/9] Fixed import observatories and a few small bugs --- importersFixed.md | 2 +- prisma/schema.prisma | 6 +- public/Observatories.csv | 51 +++++++-------- public/Scientists.csv | 2 +- src/app/api/import-observatories/route.ts | 75 ++++++++++++++--------- 5 files changed, 78 insertions(+), 58 deletions(-) diff --git a/importersFixed.md b/importersFixed.md index acdbd89..ef8cbfd 100644 --- a/importersFixed.md +++ b/importersFixed.md @@ -1,6 +1,6 @@ - [x] Import users - [x] Import artefacts - [x] Import earthquakes -- [ ] Import observatoies +- [x] Import observatories - [ ] Import requests - [ ] Import scientists diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 86e5b9c..40f0c40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -73,9 +73,9 @@ model Observatory { updatedAt DateTime @updatedAt name String location String - longitude String - latitude String - dateEstablished Int? + longitude Float + latitude Float + dateEstablished DateTime isFunctional Boolean seismicSensorOnline Boolean @default(true) creatorId Int? diff --git a/public/Observatories.csv b/public/Observatories.csv index 5557499..08c1752 100644 --- a/public/Observatories.csv +++ b/public/Observatories.csv @@ -1,25 +1,26 @@ -Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes -Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes -Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes -Kermadec Deep Motion Lab,"Kermadec Trench, northeast of New Zealand",-29.5000,-177.0000,2001-05-14,Yes -Japan Trench Monitoring Station,"Off the coast of Sendai, Japan",38.5000,143.0000,1968-09-01,Yes -Himalayan Rift Observatory,"Nepal-Tibet border, near Mount Everest",28.5000,86.5000,1998-12-08,Yes -East African Rift Monitoring Center,"Rift Valley, near Nairobi, Kenya",-1.2921,36.8219,2010-03-30,Yes -Reykjavik Seismic Monitoring Hub,"Mid-Atlantic Ridge, Reykjavik, Iceland",64.1355,-21.8954,1957-11-17,Yes -Azores Tectonic Research Base,"Azores Archipelago, Portugal",37.7412,-25.6756,1982-07-04,Yes -Sumatra Subduction Observatory,"West Coast of Indonesia, Aceh Province",4.6951,95.5539,2005-02-16,Yes -Tonga Trench Seismographic Unit,"Off the coast of Tonga",-20.0000,-175.0000,1994-08-21,Yes -San Andreas Fault Research Center,"San Francisco Bay Area, California, USA",37.7749,-122.4194,1929-10-07,Yes -Kamchatka Seismic Laboratory,"Kamchatka Peninsula, Russia",56.0000,160.0000,1978-01-12,Yes -Hawaii Island Seismic Research Station,"Hawaii Big Island, USA",19.8968,-155.5828,1989-05-06,Yes -Cascadia Ridge Offshore Observatory,"Offshore of Vancouver Island, British Columbia, Canada",48.4284,-123.3656,2003-11-18,Yes -Zagros Fault Zone Research Center,"Western Iran, near Kermanshah",34.0000,46.0000,1990-06-28,Yes -Manila Trench Seismic Observatory,"South China Sea, west of Luzon, Philippines",15.0000,120.0000,1996-04-09,Yes -Caribbean Subduction Monitoring Station,"Lesser Antilles Subduction Zone, Martinique",14.6000,-61.0000,2008-09-23,Yes -Gorda Plate Analysis Hub,"Mendocino Triple Junction, California, USA",40.0000,-124.0000,1952-12-01,Yes -Red Sea Rift Research Base,"Southern Red Sea, Eritrea",15.0000,42.0000,2015-07-15,Yes -Sumatra Coastal Reserve Observatory,"West Coast of Sumatra, Indonesia",-2.0000,100.5000,1980-03-11,No -Antarctic Polar Seismology Station,"Rift vicinity near Ross Ice Shelf, Antarctica",-78.3000,-166.2500,1974-06-01,No -Yucatan Seismic Monitoring Site,"Cocos Plate near Yucatan Peninsula, Mexico",20.7000,-90.8000,1965-09-23,No -Makran Subduction Fault Observatory,"Coastal Pakistan and Iran junction",25.5000,62.0000,1990-08-29,No -Baltic Continental Drift Center,"Southeastern Sweden, Baltic Shield zone",56.0000,15.0000,1987-12-12,No \ No newline at end of file +Name,Location,Latitude,Longitude,DateEstablished,Functional,SeismicSensorOnline +Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes,Yes +Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes,Yes +Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes,Yes +Kermadec Deep Motion Lab,"Kermadec Trench, northeast of New Zealand",-29.5000,-177.0000,2001-05-14,Yes,Yes +Japan Trench Monitoring Station,"Off the coast of Sendai, Japan",38.5000,143.0000,1968-09-01,Yes,Yes +Himalayan Rift Observatory,"Nepal-Tibet border, near Mount Everest",28.5000,86.5000,1998-12-08,Yes,Yes +East African Rift Monitoring Center,"Rift Valley, near Nairobi, Kenya",-1.2921,36.8219,2010-03-30,Yes,Yes +Reykjavik Seismic Monitoring Hub,"Mid-Atlantic Ridge, Reykjavik, Iceland",64.1355,-21.8954,1957-11-17,Yes,Yes +Azores Tectonic Research Base,"Azores Archipelago, Portugal",37.7412,-25.6756,1982-07-04,Yes,Yes +Sumatra Subduction Observatory,"West Coast of Indonesia, Aceh Province",4.6951,95.5539,2005-02-16,Yes,Yes +Tonga Trench Seismographic Unit,"Off the coast of Tonga",-20.0000,-175.0000,1994-08-21,Yes,Yes +San Andreas Fault Research Center,"San Francisco Bay Area, California, USA",37.7749,-122.4194,1929-10-07,Yes,Yes +Kamchatka Seismic Laboratory,"Kamchatka Peninsula, Russia",56.0000,160.0000,1978-01-12,Yes,Yes +Hawaii Island Seismic Research Station,"Hawaii Big Island, USA",19.8968,-155.5828,1989-05-06,Yes,Yes +Cascadia Ridge Offshore Observatory,"Offshore of Vancouver Island, British Columbia, Canada",48.4284,-123.3656,2003-11-18,Yes,Yes +Zagros Fault Zone Research Center,"Western Iran, near Kermanshah",34.0000,46.0000,1990-06-28,Yes,Yes +Manila Trench Seismic Observatory,"South China Sea, west of Luzon, Philippines",15.0000,120.0000,1996-04-09,Yes,Yes +Caribbean Subduction Monitoring Station,"Lesser Antilles Subduction Zone, Martinique",14.6000,-61.0000,2008-09-23,Yes,Yes +Gorda Plate Analysis Hub,"Mendocino Triple Junction, California, USA",40.0000,-124.0000,1952-12-01,Yes,Yes +Red Sea Rift Research Base,"Southern Red Sea, Eritrea",15.0000,42.0000,2015-07-15,Yes,Yes +Sumatra Coastal Reserve Observatory,"West Coast of Sumatra, Indonesia",-2.0000,100.5000,1980-03-11,Yes,Yes +Antarctic Polar Seismology Station,"Rift vicinity near Ross Ice Shelf, Antarctica",-78.3000,-166.2500,1974-06-01,Yes,Yes +Yucatan Seismic Monitoring Site,"Cocos Plate near Yucatan Peninsula, Mexico",20.7000,-90.8000,1965-09-23,Yes,Yes +Makran Subduction Fault Observatory,"Coastal Pakistan and Iran junction",25.5000,62.0000,1990-08-29,Yes,Yes +Baltic Continental Drift Center,"Southeastern Sweden, Baltic Shield zone",56.0000,15.0000,1987-12-12,Yes,Yes \ No newline at end of file diff --git a/public/Scientists.csv b/public/Scientists.csv index 4f883a8..0f28d60 100644 --- a/public/Scientists.csv +++ b/public/Scientists.csv @@ -1,10 +1,10 @@ +Name,Level,SuperiorName Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon Dr. Rajiv Menon,Senior,None Dr. Izzy Patterson,Senior,None Dr. Hiroshi Takeda,Senior,None Dr. Miriam Hassan,Senior,None Dr. Alice Johnson,Senior,None -Tim Howitz,Admin,None Dr. Natalia Petrova,Junior,Dr. Izzy Patteron Dr. Li Cheng,Junior,Dr. Rajiv Menon Dr. Javier Ortega,Junior,Dr. Izzy Patterson diff --git a/src/app/api/import-observatories/route.ts b/src/app/api/import-observatories/route.ts index fccd04c..1cf4651 100644 --- a/src/app/api/import-observatories/route.ts +++ b/src/app/api/import-observatories/route.ts @@ -1,11 +1,11 @@ import { parse } from "csv-parse/sync"; +import { stringToBool } from "@utils/parsingUtils"; import fs from "fs/promises"; import { NextResponse } from "next/server"; import path from "path"; - import { prisma } from "@utils/prisma"; +import { getRandomNumber } from "@utils/maths"; -// CSV location (update filename as needed) const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv"); type CsvRow = { @@ -13,49 +13,68 @@ type CsvRow = { Location: string; Latitude: string; Longitude: string; - DateEstablished?: string; + DateEstablished: string; Functional: string; - SeismicSensorOnline?: string; + SeismicSensorOnline: string; }; -function stringToBool(val: string | undefined): boolean { - // Accepts "TRUE", "true", "True", etc. - if (!val) return false; - return /^true$/i.test(val.trim()); -} - export async function POST() { try { - // 1. Read file const fileContent = await fs.readFile(csvFilePath, "utf8"); - - // 2. Parse CSV const records: CsvRow[] = parse(fileContent, { columns: true, skip_empty_lines: true, }); - // 3. Map records to Prisma inputs - const observatories = records.map((row) => ({ - name: row.Name, - location: row.Location, - latitude: row.Latitude, - longitude: row.Longitude, - dateEstablished: row.DateEstablished ? parseInt(row.DateEstablished, 10) : null, - functional: stringToBool(row.Functional), - seismicSensorOnline: row.SeismicSensorOnline ? stringToBool(row.SeismicSensorOnline) : true, // default true per schema - // todo add random selection of creatorId - creatorId: null, - })); + const failedImports: { row: CsvRow; reason: string }[] = []; + + const observatories = await Promise.all( + records.map(async (row) => { + const creators = await prisma.user.findMany({ + where: { + role: { in: ["SCIENTIST", "ADMIN"] }, + }, + }); + const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null; + + if (!randomCreator) { + failedImports.push({ row, reason: `RandomCreator: ${randomCreator}` }); + return null; + } + + return { + name: row.Name, + location: row.Location, + latitude: parseFloat(row.Latitude), + longitude: parseFloat(row.Longitude), + dateEstablished: new Date(row.DateEstablished), + isFunctional: stringToBool(row.Functional), + seismicSensorOnline: stringToBool(row.SeismicSensorOnline), + creatorId: randomCreator.id, + }; + }) + ); + + const validObservatories = observatories.filter( + (observatory): observatory is NonNullable => observatory !== null + ); - // 4. Bulk insert await prisma.observatory.createMany({ - data: observatories, + data: validObservatories, }); + if (failedImports.length > 0) { + console.warn("Failed imports:", failedImports); + await fs.writeFile( + path.resolve(process.cwd(), "failed_imports_observatories.json"), + JSON.stringify(failedImports, null, 2) + ); + } + return NextResponse.json({ success: true, - count: observatories.length, + count: validObservatories.length, + failedCount: failedImports.length, }); } catch (error: any) { console.error(error); From db6ba00ded958ccff5f5c8aa2f81abe7051b2858 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Sun, 25 May 2025 11:46:42 +0100 Subject: [PATCH 8/9] Added linking of nearest observatories to earthquakes --- src/app/api/import-earthquakes/route.ts | 54 ++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/app/api/import-earthquakes/route.ts b/src/app/api/import-earthquakes/route.ts index a42c6fb..bafe541 100644 --- a/src/app/api/import-earthquakes/route.ts +++ b/src/app/api/import-earthquakes/route.ts @@ -6,6 +6,7 @@ import { prisma } from "@utils/prisma"; import { getRandomNumber } from "@utils/maths"; const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv"); +const DISTANCE_THRESHOLD_KM = 500; // Max distance for observatory matching type CsvRow = { Date: string; @@ -18,6 +19,18 @@ type CsvRow = { Depth: string; }; +// Haversine formula to calculate distance between two points (in km) +function getDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { + const R = 6371; // Earth's radius in km + const dLat = (lat2 - lat1) * (Math.PI / 180); + const dLon = (lon2 - lon1) * (Math.PI / 180); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + export async function POST() { try { const fileContent = await fs.readFile(csvFilePath, "utf8"); @@ -28,12 +41,15 @@ export async function POST() { const failedImports: { row: CsvRow; reason: string }[] = []; + // Fetch all observatories once to avoid repeated queries + const observatories = await prisma.observatory.findMany({ + select: { id: true, latitude: true, longitude: true }, + }); + const earthquakes = await Promise.all( records.map(async (row) => { const creators = await prisma.user.findMany({ - where: { - role: { in: ["SCIENTIST", "ADMIN"] }, - }, + where: { role: { in: ["SCIENTIST", "ADMIN"] } }, }); const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null; @@ -42,16 +58,43 @@ export async function POST() { return null; } + const eqLat = parseFloat(row.Latitude); + const eqLon = parseFloat(row.Longitude); + + // Find observatories within distance threshold + let nearbyObservatories = observatories + .map((obs) => ({ + id: obs.id, + distance: getDistance(eqLat, eqLon, obs.latitude, obs.longitude), + })) + .filter((obs) => obs.distance <= DISTANCE_THRESHOLD_KM) + .map((obs) => ({ id: obs.id })); + + // If no observatories within range, find the nearest one + if (nearbyObservatories.length === 0) { + const nearest = observatories.reduce( + (min, obs) => { + const distance = getDistance(eqLat, eqLon, obs.latitude, obs.longitude); + return distance < min.distance ? { id: obs.id, distance } : min; + }, + { id: -1, distance: Infinity } + ); + if (nearest.id !== -1) { + nearbyObservatories = [{ id: nearest.id }]; + } + } + return { date: new Date(row.Date), code: row.Code, magnitude: parseFloat(row.Magnitude), type: row.Type, - latitude: parseFloat(row.Latitude), - longitude: parseFloat(row.Longitude), + latitude: eqLat, + longitude: eqLon, location: row.Location, depth: row.Depth, creatorId: randomCreator.id, + observatories: { connect: nearbyObservatories }, }; }) ); @@ -60,6 +103,7 @@ export async function POST() { (earthquake): earthquake is NonNullable => earthquake !== null ); + // Bulk insert earthquakes with observatory connections await prisma.earthquake.createMany({ data: validEarthquakes, }); From 7311e379e8fdc62df372d8bf9e1d4f93b6ac7d76 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Sun, 25 May 2025 11:57:51 +0100 Subject: [PATCH 9/9] Fixed scientists importing, and added linking by name --- importersFixed.md | 2 +- importersOrder.md | 6 ++ src/app/api/import-scientists/route.ts | 110 ++++++++++++++++++++----- 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 importersOrder.md diff --git a/importersFixed.md b/importersFixed.md index ef8cbfd..07a09dd 100644 --- a/importersFixed.md +++ b/importersFixed.md @@ -3,4 +3,4 @@ - [x] Import earthquakes - [x] Import observatories - [ ] Import requests -- [ ] Import scientists +- [x] Import scientists diff --git a/importersOrder.md b/importersOrder.md new file mode 100644 index 0000000..331dc33 --- /dev/null +++ b/importersOrder.md @@ -0,0 +1,6 @@ +1. Import users +2. Import scientists +3. Import observatories +4. Import earthquakes +5. Import artefacts +6. Import requests diff --git a/src/app/api/import-scientists/route.ts b/src/app/api/import-scientists/route.ts index b91cf52..f4a3bcb 100644 --- a/src/app/api/import-scientists/route.ts +++ b/src/app/api/import-scientists/route.ts @@ -2,21 +2,18 @@ import { parse } from "csv-parse/sync"; import fs from "fs/promises"; import { NextResponse } from "next/server"; import path from "path"; - import { prisma } from "@utils/prisma"; +import { getRandomNumber } from "@utils/maths"; -// Path to CSV file const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv"); type CsvRow = { Name: string; - Level?: string; - UserId: string; - SuperiorId?: string; + Level: string; + SuperiorName: string; }; function normalizeLevel(level: string | undefined): string { - // Only allow JUNIOR, SENIOR; default JUNIOR if (!level || !level.trim()) return "JUNIOR"; const lv = level.trim().toUpperCase(); return ["JUNIOR", "SENIOR"].includes(lv) ? lv : "JUNIOR"; @@ -24,31 +21,100 @@ function normalizeLevel(level: string | undefined): string { export async function POST() { try { - // 1. Read the CSV file const fileContent = await fs.readFile(csvFilePath, "utf8"); - - // 2. Parse the CSV const records: CsvRow[] = parse(fileContent, { columns: true, skip_empty_lines: true, }); - // 3. Transform each record for Prisma - // todo add senior scientists first - const scientists = records.map((row) => ({ - name: row.Name, - level: normalizeLevel(row.Level), - userId: parseInt(row.UserId, 10), - // todo get superior id by name from db - superiorId: row.SuperiorId && row.SuperiorId.trim() !== "" ? parseInt(row.SuperiorId, 10) : null, - })); + const failedImports: { row: CsvRow; reason: string }[] = []; - // 4. Bulk create scientists in database - await prisma.scientist.createMany({ - data: scientists, + // Fetch all users to match names + const users = await prisma.user.findMany({ + select: { id: true, name: true }, }); - return NextResponse.json({ success: true, count: scientists.length }); + // Process seniors first to ensure superiors exist for juniors + const seniorScientists = records.filter((row) => normalizeLevel(row.Level) === "SENIOR"); + const juniorScientists = records.filter((row) => normalizeLevel(row.Level) === "JUNIOR"); + const seniorScientistNames = new Map(); // Map name to ID + + const seniors = await Promise.all( + seniorScientists.map(async (row) => { + const user = users.find((u) => u.name === row.Name); + if (!user) { + failedImports.push({ row, reason: `User not found: ${row.Name}` }); + return null; + } + + return { + name: row.Name, + level: normalizeLevel(row.Level), + userId: user.id, + superiorId: null, // Seniors have no superior + }; + }) + ); + + const validSeniors = seniors.filter((senior): senior is NonNullable => senior !== null); + + // Create senior scientists and store their IDs + for (const senior of validSeniors) { + try { + const scientist = await prisma.scientist.create({ data: senior }); + seniorScientistNames.set(senior.name, scientist.id); + } catch (error: any) { + failedImports.push({ + row: { Name: senior.name, Level: senior.level, SuperiorName: "None" }, + reason: `Failed to create senior scientist: ${error.message}`, + }); + } + } + + // Process junior scientists + const juniors = await Promise.all( + juniorScientists.map(async (row) => { + const user = users.find((u) => u.name === row.Name); + if (!user) { + failedImports.push({ row, reason: `User not found: ${row.Name}` }); + return null; + } + + let superiorId: number | null = null; + if (row.SuperiorName && row.SuperiorName.trim() !== "None") { + superiorId = seniorScientistNames.get(row.SuperiorName.trim()) || null; + if (!superiorId) { + failedImports.push({ row, reason: `Superior not found: ${row.SuperiorName}` }); + return null; + } + } + + return { + name: row.Name, + level: normalizeLevel(row.Level), + userId: user.id, + superiorId, + }; + }) + ); + + const validJuniors = juniors.filter((junior): junior is NonNullable => junior !== null); + + // Bulk create junior scientists + await prisma.scientist.createMany({ + data: validJuniors, + }); + + if (failedImports.length > 0) { + console.warn("Failed imports:", failedImports); + await fs.writeFile(path.resolve(process.cwd(), "failed_imports_scientists.json"), JSON.stringify(failedImports, null, 2)); + } + + return NextResponse.json({ + success: true, + count: validSeniors.length + validJuniors.length, + failedCount: failedImports.length, + }); } catch (error: any) { console.error(error); return NextResponse.json({ success: false, error: error.message }, { status: 500 });