Merge branch 'master' of 192.168.11.4:thowitz-work/tremor-tracker

This commit is contained in:
Tim Howitz 2025-05-25 18:54:00 +01:00
commit 0a34470e65
33 changed files with 550 additions and 426 deletions

6
importersFixed.md Normal file
View File

@ -0,0 +1,6 @@
- [x] Import users
- [x] Import artefacts
- [x] Import earthquakes
- [x] Import observatories
- [ ] Import requests
- [x] Import scientists

6
importersOrder.md Normal file
View File

@ -0,0 +1,6 @@
1. Import users
2. Import scientists
3. Import observatories
4. Import earthquakes
5. Import artefacts
6. Import requests

8
package-lock.json generated
View File

@ -35,7 +35,7 @@
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-node": "^1.0.2", "react-node": "^1.0.2",
"swr": "^2.3.3", "swr": "^2.3.3",
"zod": "^3.24.4" "zod": "^3.25.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@ -8095,9 +8095,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.0", "version": "3.25.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.0.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.3.tgz",
"integrity": "sha512-ficnZKUW0mlNivqeJkosTEkGbJ6NKCtSaOHGx5aXbtfeWMdRyzXLbAIn19my4C/KB7WPY/p9vlGPt+qpOp6c4Q==", "integrity": "sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@ -38,7 +38,7 @@
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-node": "^1.0.2", "react-node": "^1.0.2",
"swr": "^2.3.3", "swr": "^2.3.3",
"zod": "^3.24.4" "zod": "^3.25.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@ -52,4 +52,4 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5"
} }
} }

View File

@ -70,9 +70,9 @@ model Observatory {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
name String name String
location String location String
longitude String longitude Float
latitude String latitude Float
dateEstablished Int? dateEstablished DateTime
isFunctional Boolean isFunctional Boolean
seismicSensorOnline Boolean @default(true) seismicSensorOnline Boolean @default(true)
creatorId Int? creatorId Int?
@ -88,7 +88,7 @@ model Artefact {
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
warehouseArea String warehouseArea String
description String description String
imagePath String imageName String
earthquakeId Int earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id]) earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int? creatorId Int?

View File

@ -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 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 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 Strata Core,Soil,LoggingArea,Soil core with visible stratification showing evidence of liquefaction.,ET-6.9-Brazil-00046,30,no,no,StrataCore.PNG

1 Name Type WarehouseArea Description earthquakeID EarthquakeCode Price Required PickedUp Picture
2 Echo Bomb Lava ShelvingAreaA A dense glossy black volcanic bomb with minor vesicles. EV-7.4-Mexico-00035 120 no no EchoBomb.PNG
3 Silvershade Ash Ash ShelvingAreaD Fine light-grey volcanic ash collected near a village. EV-6.0-Iceland-00018 40 no no SilvershadeAsh.PNG
4 Strata Core Soil LoggingArea Soil core with visible stratification showing evidence of liquefaction. ET-6.9-Brazil-00046 30 no no StrataCore.PNG

View File

@ -1,25 +1,26 @@
Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes Name,Location,Latitude,Longitude,DateEstablished,Functional,SeismicSensorOnline
Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes,Yes
Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes,Yes
Kermadec Deep Motion Lab,"Kermadec Trench, northeast of New Zealand",-29.5000,-177.0000,2001-05-14,Yes Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes,Yes
Japan Trench Monitoring Station,"Off the coast of Sendai, Japan",38.5000,143.0000,1968-09-01,Yes Kermadec Deep Motion Lab,"Kermadec Trench, northeast of New Zealand",-29.5000,-177.0000,2001-05-14,Yes,Yes
Himalayan Rift Observatory,"Nepal-Tibet border, near Mount Everest",28.5000,86.5000,1998-12-08,Yes Japan Trench Monitoring Station,"Off the coast of Sendai, Japan",38.5000,143.0000,1968-09-01,Yes,Yes
East African Rift Monitoring Center,"Rift Valley, near Nairobi, Kenya",-1.2921,36.8219,2010-03-30,Yes Himalayan Rift Observatory,"Nepal-Tibet border, near Mount Everest",28.5000,86.5000,1998-12-08,Yes,Yes
Reykjavik Seismic Monitoring Hub,"Mid-Atlantic Ridge, Reykjavik, Iceland",64.1355,-21.8954,1957-11-17,Yes East African Rift Monitoring Center,"Rift Valley, near Nairobi, Kenya",-1.2921,36.8219,2010-03-30,Yes,Yes
Azores Tectonic Research Base,"Azores Archipelago, Portugal",37.7412,-25.6756,1982-07-04,Yes Reykjavik Seismic Monitoring Hub,"Mid-Atlantic Ridge, Reykjavik, Iceland",64.1355,-21.8954,1957-11-17,Yes,Yes
Sumatra Subduction Observatory,"West Coast of Indonesia, Aceh Province",4.6951,95.5539,2005-02-16,Yes Azores Tectonic Research Base,"Azores Archipelago, Portugal",37.7412,-25.6756,1982-07-04,Yes,Yes
Tonga Trench Seismographic Unit,"Off the coast of Tonga",-20.0000,-175.0000,1994-08-21,Yes Sumatra Subduction Observatory,"West Coast of Indonesia, Aceh Province",4.6951,95.5539,2005-02-16,Yes,Yes
San Andreas Fault Research Center,"San Francisco Bay Area, California, USA",37.7749,-122.4194,1929-10-07,Yes Tonga Trench Seismographic Unit,"Off the coast of Tonga",-20.0000,-175.0000,1994-08-21,Yes,Yes
Kamchatka Seismic Laboratory,"Kamchatka Peninsula, Russia",56.0000,160.0000,1978-01-12,Yes San Andreas Fault Research Center,"San Francisco Bay Area, California, USA",37.7749,-122.4194,1929-10-07,Yes,Yes
Hawaii Island Seismic Research Station,"Hawaii Big Island, USA",19.8968,-155.5828,1989-05-06,Yes Kamchatka Seismic Laboratory,"Kamchatka Peninsula, Russia",56.0000,160.0000,1978-01-12,Yes,Yes
Cascadia Ridge Offshore Observatory,"Offshore of Vancouver Island, British Columbia, Canada",48.4284,-123.3656,2003-11-18,Yes Hawaii Island Seismic Research Station,"Hawaii Big Island, USA",19.8968,-155.5828,1989-05-06,Yes,Yes
Zagros Fault Zone Research Center,"Western Iran, near Kermanshah",34.0000,46.0000,1990-06-28,Yes Cascadia Ridge Offshore Observatory,"Offshore of Vancouver Island, British Columbia, Canada",48.4284,-123.3656,2003-11-18,Yes,Yes
Manila Trench Seismic Observatory,"South China Sea, west of Luzon, Philippines",15.0000,120.0000,1996-04-09,Yes Zagros Fault Zone Research Center,"Western Iran, near Kermanshah",34.0000,46.0000,1990-06-28,Yes,Yes
Caribbean Subduction Monitoring Station,"Lesser Antilles Subduction Zone, Martinique",14.6000,-61.0000,2008-09-23,Yes Manila Trench Seismic Observatory,"South China Sea, west of Luzon, Philippines",15.0000,120.0000,1996-04-09,Yes,Yes
Gorda Plate Analysis Hub,"Mendocino Triple Junction, California, USA",40.0000,-124.0000,1952-12-01,Yes Caribbean Subduction Monitoring Station,"Lesser Antilles Subduction Zone, Martinique",14.6000,-61.0000,2008-09-23,Yes,Yes
Red Sea Rift Research Base,"Southern Red Sea, Eritrea",15.0000,42.0000,2015-07-15,Yes Gorda Plate Analysis Hub,"Mendocino Triple Junction, California, USA",40.0000,-124.0000,1952-12-01,Yes,Yes
Sumatra Coastal Reserve Observatory,"West Coast of Sumatra, Indonesia",-2.0000,100.5000,1980-03-11,No Red Sea Rift Research Base,"Southern Red Sea, Eritrea",15.0000,42.0000,2015-07-15,Yes,Yes
Antarctic Polar Seismology Station,"Rift vicinity near Ross Ice Shelf, Antarctica",-78.3000,-166.2500,1974-06-01,No Sumatra Coastal Reserve Observatory,"West Coast of Sumatra, Indonesia",-2.0000,100.5000,1980-03-11,Yes,Yes
Yucatan Seismic Monitoring Site,"Cocos Plate near Yucatan Peninsula, Mexico",20.7000,-90.8000,1965-09-23,No Antarctic Polar Seismology Station,"Rift vicinity near Ross Ice Shelf, Antarctica",-78.3000,-166.2500,1974-06-01,Yes,Yes
Makran Subduction Fault Observatory,"Coastal Pakistan and Iran junction",25.5000,62.0000,1990-08-29,No Yucatan Seismic Monitoring Site,"Cocos Plate near Yucatan Peninsula, Mexico",20.7000,-90.8000,1965-09-23,Yes,Yes
Baltic Continental Drift Center,"Southeastern Sweden, Baltic Shield zone",56.0000,15.0000,1987-12-12,No 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
1 Pacific Apex Seismic Center Name Aleutian Trench, Alaska, USA Location 53.0000 Latitude -168.0000 Longitude 1973-06-15 DateEstablished Yes Functional SeismicSensorOnline
2 Cascadia Quake Research Institute Pacific Apex Seismic Center Oregon Coast, USA Aleutian Trench, Alaska, USA 44.5000 53.0000 -124.0000 -168.0000 1985-03-22 1973-06-15 Yes Yes
3 Andes Fault Survey Observatory Cascadia Quake Research Institute Nazca-South American Plate, Santiago, Chile Oregon Coast, USA -33.4500 44.5000 -70.6667 -124.0000 1992-10-10 1985-03-22 Yes Yes
4 Kermadec Deep Motion Lab Andes Fault Survey Observatory Kermadec Trench, northeast of New Zealand Nazca-South American Plate, Santiago, Chile -29.5000 -33.4500 -177.0000 -70.6667 2001-05-14 1992-10-10 Yes Yes
5 Japan Trench Monitoring Station Kermadec Deep Motion Lab Off the coast of Sendai, Japan Kermadec Trench, northeast of New Zealand 38.5000 -29.5000 143.0000 -177.0000 1968-09-01 2001-05-14 Yes Yes
6 Himalayan Rift Observatory Japan Trench Monitoring Station Nepal-Tibet border, near Mount Everest Off the coast of Sendai, Japan 28.5000 38.5000 86.5000 143.0000 1998-12-08 1968-09-01 Yes Yes
7 East African Rift Monitoring Center Himalayan Rift Observatory Rift Valley, near Nairobi, Kenya Nepal-Tibet border, near Mount Everest -1.2921 28.5000 36.8219 86.5000 2010-03-30 1998-12-08 Yes Yes
8 Reykjavik Seismic Monitoring Hub East African Rift Monitoring Center Mid-Atlantic Ridge, Reykjavik, Iceland Rift Valley, near Nairobi, Kenya 64.1355 -1.2921 -21.8954 36.8219 1957-11-17 2010-03-30 Yes Yes
9 Azores Tectonic Research Base Reykjavik Seismic Monitoring Hub Azores Archipelago, Portugal Mid-Atlantic Ridge, Reykjavik, Iceland 37.7412 64.1355 -25.6756 -21.8954 1982-07-04 1957-11-17 Yes Yes
10 Sumatra Subduction Observatory Azores Tectonic Research Base West Coast of Indonesia, Aceh Province Azores Archipelago, Portugal 4.6951 37.7412 95.5539 -25.6756 2005-02-16 1982-07-04 Yes Yes
11 Tonga Trench Seismographic Unit Sumatra Subduction Observatory Off the coast of Tonga West Coast of Indonesia, Aceh Province -20.0000 4.6951 -175.0000 95.5539 1994-08-21 2005-02-16 Yes Yes
12 San Andreas Fault Research Center Tonga Trench Seismographic Unit San Francisco Bay Area, California, USA Off the coast of Tonga 37.7749 -20.0000 -122.4194 -175.0000 1929-10-07 1994-08-21 Yes Yes
13 Kamchatka Seismic Laboratory San Andreas Fault Research Center Kamchatka Peninsula, Russia San Francisco Bay Area, California, USA 56.0000 37.7749 160.0000 -122.4194 1978-01-12 1929-10-07 Yes Yes
14 Hawaii Island Seismic Research Station Kamchatka Seismic Laboratory Hawaii Big Island, USA Kamchatka Peninsula, Russia 19.8968 56.0000 -155.5828 160.0000 1989-05-06 1978-01-12 Yes Yes
15 Cascadia Ridge Offshore Observatory Hawaii Island Seismic Research Station Offshore of Vancouver Island, British Columbia, Canada Hawaii Big Island, USA 48.4284 19.8968 -123.3656 -155.5828 2003-11-18 1989-05-06 Yes Yes
16 Zagros Fault Zone Research Center Cascadia Ridge Offshore Observatory Western Iran, near Kermanshah Offshore of Vancouver Island, British Columbia, Canada 34.0000 48.4284 46.0000 -123.3656 1990-06-28 2003-11-18 Yes Yes
17 Manila Trench Seismic Observatory Zagros Fault Zone Research Center South China Sea, west of Luzon, Philippines Western Iran, near Kermanshah 15.0000 34.0000 120.0000 46.0000 1996-04-09 1990-06-28 Yes Yes
18 Caribbean Subduction Monitoring Station Manila Trench Seismic Observatory Lesser Antilles Subduction Zone, Martinique South China Sea, west of Luzon, Philippines 14.6000 15.0000 -61.0000 120.0000 2008-09-23 1996-04-09 Yes Yes
19 Gorda Plate Analysis Hub Caribbean Subduction Monitoring Station Mendocino Triple Junction, California, USA Lesser Antilles Subduction Zone, Martinique 40.0000 14.6000 -124.0000 -61.0000 1952-12-01 2008-09-23 Yes Yes
20 Red Sea Rift Research Base Gorda Plate Analysis Hub Southern Red Sea, Eritrea Mendocino Triple Junction, California, USA 15.0000 40.0000 42.0000 -124.0000 2015-07-15 1952-12-01 Yes Yes
21 Sumatra Coastal Reserve Observatory Red Sea Rift Research Base West Coast of Sumatra, Indonesia Southern Red Sea, Eritrea -2.0000 15.0000 100.5000 42.0000 1980-03-11 2015-07-15 No Yes Yes
22 Antarctic Polar Seismology Station Sumatra Coastal Reserve Observatory Rift vicinity near Ross Ice Shelf, Antarctica West Coast of Sumatra, Indonesia -78.3000 -2.0000 -166.2500 100.5000 1974-06-01 1980-03-11 No Yes Yes
23 Yucatan Seismic Monitoring Site Antarctic Polar Seismology Station Cocos Plate near Yucatan Peninsula, Mexico Rift vicinity near Ross Ice Shelf, Antarctica 20.7000 -78.3000 -90.8000 -166.2500 1965-09-23 1974-06-01 No Yes Yes
24 Makran Subduction Fault Observatory Yucatan Seismic Monitoring Site Coastal Pakistan and Iran junction Cocos Plate near Yucatan Peninsula, Mexico 25.5000 20.7000 62.0000 -90.8000 1990-08-29 1965-09-23 No Yes Yes
25 Baltic Continental Drift Center Makran Subduction Fault Observatory Southeastern Sweden, Baltic Shield zone Coastal Pakistan and Iran junction 56.0000 25.5000 15.0000 62.0000 1987-12-12 1990-08-29 No Yes Yes
26 Baltic Continental Drift Center Southeastern Sweden, Baltic Shield zone 56.0000 15.0000 1987-12-12 Yes Yes

View File

@ -1,10 +1,10 @@
Name,Level,SuperiorName
Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon
Dr. Rajiv Menon,Senior,None Dr. Rajiv Menon,Senior,None
Dr. Izzy Patterson,Senior,None Dr. Izzy Patterson,Senior,None
Dr. Hiroshi Takeda,Senior,None Dr. Hiroshi Takeda,Senior,None
Dr. Miriam Hassan,Senior,None Dr. Miriam Hassan,Senior,None
Dr. Alice Johnson,Senior,None Dr. Alice Johnson,Senior,None
Tim Howitz,Admin,None
Dr. Natalia Petrova,Junior,Dr. Izzy Patteron Dr. Natalia Petrova,Junior,Dr. Izzy Patteron
Dr. Li Cheng,Junior,Dr. Rajiv Menon Dr. Li Cheng,Junior,Dr. Rajiv Menon
Dr. Javier Ortega,Junior,Dr. Izzy Patterson Dr. Javier Ortega,Junior,Dr. Izzy Patterson

1 Dr. Emily Neighbour Carter Name Junior Level Dr. Rajiv Menon SuperiorName
1 Name Level SuperiorName
2 Dr. Emily Neighbour Carter Dr. Emily Neighbour Carter Junior Junior Dr. Rajiv Menon
3 Dr. Rajiv Menon Dr. Rajiv Menon Senior Senior None
4 Dr. Izzy Patterson Dr. Izzy Patterson Senior Senior None
5 Dr. Hiroshi Takeda Dr. Hiroshi Takeda Senior Senior None
6 Dr. Miriam Hassan Dr. Miriam Hassan Senior Senior None
7 Dr. Alice Johnson Dr. Alice Johnson Senior Senior None
Tim Howitz Admin None
8 Dr. Natalia Petrova Dr. Natalia Petrova Junior Junior Dr. Izzy Patteron
9 Dr. Li Cheng Dr. Li Cheng Junior Junior Dr. Rajiv Menon
10 Dr. Javier Ortega Dr. Javier Ortega Junior Junior Dr. Izzy Patterson

View File

@ -1,10 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { PrismaClient } from '@prismaclient'; import { prisma } from '@utils/prisma';
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
// todo add specification of date range in request // todo add specification of date range in request
@ -35,7 +31,5 @@ export async function POST(req: Request) {
} catch (error) { } catch (error) {
console.error("Error in earthquakes endpoint:", error); console.error("Error in earthquakes endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
} }
} }

View File

@ -1,20 +1,15 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { env } from "@utils/env"; import { env } from "@utils/env";
import { PrismaClient } from "@prisma/client";
import { verifyJwt } from "@utils/verifyJwt"; import { verifyJwt } from "@utils/verifyJwt";
import { prisma } from "@utils/prisma";
const prisma = new PrismaClient();
export async function POST(req: Request) { export async function POST(req: Request) {
let cookieStore; let cookieStore;
try { try {
cookieStore = await cookies(); cookieStore = await cookies();
const token = cookieStore.get("jwt")?.value; 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 }); const payload = await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
@ -44,7 +39,5 @@ export async function POST(req: Request) {
console.error("Error in user endpoint:", error); console.error("Error in user endpoint:", error);
cookieStore?.delete("jwt"); // Delete JWT cookie on error cookieStore?.delete("jwt"); // Delete JWT cookie on error
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
await prisma.$disconnect();
} }
} }

View File

@ -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 null;
}
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<typeof artefact> => artefact !== null);
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 });
}
}

View File

@ -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();
}
}

View File

@ -3,11 +3,11 @@ import fs from "fs/promises";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; import path from "path";
import { PrismaClient } from "@prismaclient"; import { getRandomNumber } from "@utils/maths";
import { prisma } from "@utils/prisma";
// Path to your earthquakes.csv
const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv"); const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv");
const prisma = new PrismaClient(); const DISTANCE_THRESHOLD_KM = 500; // Max distance for observatory matching
type CsvRow = { type CsvRow = {
Date: string; Date: string;
@ -20,49 +20,107 @@ type CsvRow = {
Depth: string; 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() { export async function POST() {
try { try {
// Get a random user ID
const randomUserArr: Array<{ id: number }> = await prisma.$queryRaw`
SELECT TOP 1 id FROM [user] ORDER BY NEWID()
`;
if (!randomUserArr.length) {
return NextResponse.json({ error: 'No users found.' }, { status: 404 });
}
const userId = randomUserArr[0].id;
// 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8"); const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, { const records: CsvRow[] = parse(fileContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
}); });
// 3. Transform to fit Earthquake model
const earthquakes = records.map((row) => ({ const failedImports: { row: CsvRow; reason: string }[] = [];
date: new Date(row.Date),
code: row.Code, // Fetch all observatories once to avoid repeated queries
magnitude: parseFloat(row.Magnitude), const observatories = await prisma.observatory.findMany({
type: row.Type, select: { id: true, latitude: true, longitude: true },
latitude: parseFloat(row.Latitude), });
longitude: parseFloat(row.Longitude),
location: row.Location, const earthquakes = await Promise.all(
depth: row.Depth, // store as received records.map(async (row) => {
// todo add random selection for creatorId const creators = await prisma.user.findMany({
creatorId: userId, where: { role: { in: ["SCIENTIST", "ADMIN"] } },
})); });
// 4. Bulk create earthquakes in database: const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null;
await prisma.earthquake.createMany({
data: earthquakes, if (!randomCreator) {
failedImports.push({ row, reason: `RandomCreator: ${randomCreator}` });
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: eqLat,
longitude: eqLon,
location: row.Location,
depth: row.Depth,
creatorId: randomCreator.id,
observatories: { connect: nearbyObservatories },
};
})
);
const validEarthquakes = earthquakes.filter(
(earthquake): earthquake is NonNullable<typeof earthquake> => earthquake !== null
);
// Bulk insert earthquakes with observatory connections
await prisma.earthquake.createMany({
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) { } catch (error: any) {
console.error(error); console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 }); return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
} }
} }

View File

@ -1,67 +1,83 @@
import { parse } from "csv-parse/sync"; import { parse } from "csv-parse/sync";
import { stringToBool } from "@utils/parsingUtils";
import fs from "fs/promises"; import fs from "fs/promises";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; import path from "path";
import { prisma } from "@utils/prisma";
import { getRandomNumber } from "@utils/maths";
import { PrismaClient } from "@prismaclient";
// CSV location (update filename as needed)
const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv"); const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv");
const prisma = new PrismaClient();
type CsvRow = { type CsvRow = {
Name: string; Name: string;
Location: string; Location: string;
Latitude: string; Latitude: string;
Longitude: string; Longitude: string;
DateEstablished?: string; DateEstablished: string;
Functional: 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() { export async function POST() {
try { try {
// 1. Read file
const fileContent = await fs.readFile(csvFilePath, "utf8"); const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse CSV
const records: CsvRow[] = parse(fileContent, { const records: CsvRow[] = parse(fileContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
}); });
// 3. Map records to Prisma inputs const failedImports: { row: CsvRow; reason: string }[] = [];
const observatories = records.map((row) => ({
name: row.Name, const observatories = await Promise.all(
location: row.Location, records.map(async (row) => {
latitude: row.Latitude, const creators = await prisma.user.findMany({
longitude: row.Longitude, where: {
dateEstablished: row.DateEstablished ? parseInt(row.DateEstablished, 10) : null, role: { in: ["SCIENTIST", "ADMIN"] },
functional: stringToBool(row.Functional), },
seismicSensorOnline: row.SeismicSensorOnline ? stringToBool(row.SeismicSensorOnline) : true, // default true per schema });
// todo add random selection of creatorId const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null;
creatorId: 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<typeof observatory> => observatory !== null
);
// 4. Bulk insert
await prisma.observatory.createMany({ 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({ return NextResponse.json({
success: true, success: true,
count: observatories.length, count: validObservatories.length,
failedCount: failedImports.length,
}); });
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 }); return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
} }
} }

View File

@ -3,10 +3,8 @@ import fs from "fs/promises";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; import path from "path";
import { PrismaClient } from "@prismaclient"; import { prisma } from "@utils/prisma";
const csvFilePath = path.resolve(process.cwd(), "public/requests.csv"); const csvFilePath = path.resolve(process.cwd(), "public/requests.csv");
const prisma = new PrismaClient();
type RequestType = "NEW_USER" | "CHANGE_LEVEL" | "DELETE"; type RequestType = "NEW_USER" | "CHANGE_LEVEL" | "DELETE";
type RequestOutcome = "FULFILLED" | "REJECTED" | "IN_PROGRESS" | "CANCELLED" | "OTHER"; type RequestOutcome = "FULFILLED" | "REJECTED" | "IN_PROGRESS" | "CANCELLED" | "OTHER";
@ -56,7 +54,5 @@ export async function POST() {
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 }); return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
} }
} }

View File

@ -2,22 +2,18 @@ import { parse } from "csv-parse/sync";
import fs from "fs/promises"; import fs from "fs/promises";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; import path from "path";
import { prisma } from "@utils/prisma";
import { getRandomNumber } from "@utils/maths";
import { PrismaClient } from "@prismaclient";
// Path to CSV file
const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv"); const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv");
const prisma = new PrismaClient();
type CsvRow = { type CsvRow = {
Name: string; Name: string;
Level?: string; Level: string;
UserId: string; SuperiorName: string;
SuperiorId?: string;
}; };
function normalizeLevel(level: string | undefined): string { function normalizeLevel(level: string | undefined): string {
// Only allow JUNIOR, SENIOR; default JUNIOR
if (!level || !level.trim()) return "JUNIOR"; if (!level || !level.trim()) return "JUNIOR";
const lv = level.trim().toUpperCase(); const lv = level.trim().toUpperCase();
return ["JUNIOR", "SENIOR"].includes(lv) ? lv : "JUNIOR"; return ["JUNIOR", "SENIOR"].includes(lv) ? lv : "JUNIOR";
@ -25,35 +21,102 @@ function normalizeLevel(level: string | undefined): string {
export async function POST() { export async function POST() {
try { try {
// 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8"); const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, { const records: CsvRow[] = parse(fileContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
}); });
// 3. Transform each record for Prisma const failedImports: { row: CsvRow; reason: string }[] = [];
// 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,
}));
// 4. Bulk create scientists in database // Fetch all users to match names
await prisma.scientist.createMany({ const users = await prisma.user.findMany({
data: scientists, 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<string, number>(); // 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<typeof senior> => 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<typeof junior> => 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) { } catch (error: any) {
console.error(error); console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 }); return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
} }
} }

View File

@ -3,21 +3,23 @@ import fs from "fs/promises";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; 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 csvFilePath = path.resolve(process.cwd(), "public/users.csv");
const prisma = new PrismaClient();
type CsvRow = { type CsvRow = {
id: string;
createdAt: string;
Name: string; Name: string;
Email: string; Email: string;
PasswordHash: string; PasswordHash: string;
Role?: string; Role: string;
Scientist: string;
PurchasedArtefacts: string;
Requests: string;
}; };
function normalizeRole(role: string | undefined): string { function normalizeRole(role: string | undefined): string {
// Only allow ADMIN, SCIENTIST, GUEST; default GUEST
if (!role || !role.trim()) return "GUEST"; if (!role || !role.trim()) return "GUEST";
const r = role.trim().toUpperCase(); const r = role.trim().toUpperCase();
return ["ADMIN", "SCIENTIST", "GUEST"].includes(r) ? r : "GUEST"; return ["ADMIN", "SCIENTIST", "GUEST"].includes(r) ? r : "GUEST";
@ -25,33 +27,48 @@ function normalizeRole(role: string | undefined): string {
export async function POST() { export async function POST() {
try { try {
// 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8"); const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, { const records: CsvRow[] = parse(fileContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
}); });
// 3. Transform each CSV row to User model format const failedImports: { row: CsvRow; reason: string }[] = [];
const users = records.map((row) => ({
name: row.Name, const users = await Promise.all(
email: row.Email, records.map(async (row) => {
passwordHash: row.PasswordHash, try {
role: normalizeRole(row.Role), 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<typeof user> => user !== null);
// 4. Bulk create users in database
await prisma.user.createMany({ 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) { } catch (error: any) {
console.error(error); console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 }); return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
} }
} }

View File

@ -2,15 +2,11 @@ import bcryptjs from "bcryptjs";
import { SignJWT } from "jose"; import { SignJWT } from "jose";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prismaclient";
import { env } from "@utils/env"; import { env } from "@utils/env";
import { prisma } from "@utils/prisma";
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite"; import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { email, password } = await req.json(); // Parse incoming JSON data const { email, password } = await req.json(); // Parse incoming JSON data
@ -67,7 +63,5 @@ export async function POST(req: Request) {
} catch (error) { } catch (error) {
console.error("Error in signup endpoint:", error); console.error("Error in signup endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
} }
} }

View File

@ -1,10 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from "next/server";
import { PrismaClient } from '@prismaclient'; import { prisma } from "@utils/prisma";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
// todo remove if (usingPrisma) code // todo remove if (usingPrisma) code
// todo add specification of date range in request // todo add specification of date range in request
@ -39,8 +35,7 @@ export async function GET(request: Request) {
]; ];
// todo get earthquakes associated with observatories // todo get earthquakes associated with observatories
let observatories; const observatories = await prisma.observatory.findMany();
if (usingPrisma) observatories = await prisma.observatory.findMany();
if (observatories) { if (observatories) {
return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 }); return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 });
@ -51,7 +46,5 @@ export async function GET(request: Request) {
} catch (error) { } catch (error) {
console.error("Error in observatories endpoint:", error); console.error("Error in observatories endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
} }
} }

View File

@ -2,17 +2,11 @@ import bcryptjs from "bcryptjs";
import { SignJWT } from "jose"; import { SignJWT } from "jose";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prismaclient";
import { env } from "@utils/env"; import { env } from "@utils/env";
import { prisma } from "@utils/prisma";
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite"; import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
// todo remove if (usingPrisma) code
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { email, password, name } = await req.json(); // Parse incoming JSON data const { email, password, name } = await req.json(); // Parse incoming JSON data
@ -26,17 +20,11 @@ export async function POST(req: Request) {
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
let foundUser; const foundUser = await prisma.user.findUnique({
where: {
if (usingPrisma) { email: email, // use the email to uniquely identify the user
foundUser = await prisma.user.findUnique({ },
where: { });
email: email, // use the email to uniquely identify the user
},
});
} else {
foundUser = findUserByEmail(userData, email);
}
if (foundUser) { if (foundUser) {
return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 }); return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 });
@ -61,20 +49,15 @@ export async function POST(req: Request) {
} else { } else {
try { try {
const passwordHash = await bcryptjs.hash(password, 10); const passwordHash = await bcryptjs.hash(password, 10);
let user; // todo add sending back user
if (usingPrisma) { const user = await prisma.user.create({
// todo add sending back user data: {
user = await prisma.user.create({ name,
data: { email,
name, passwordHash,
email, },
passwordHash, });
},
});
} else {
user = { name, email, password: passwordHash, accessLevel };
userData.push(user);
}
await writeUserCsv(userData); await writeUserCsv(userData);
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);

View File

@ -1,108 +1,42 @@
import { NextResponse } from 'next/server'; import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { PrismaClient } from '@prismaclient'; import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { env } from '@utils/env'; import { JWTPayload } from "@appTypes/JWT";
import { verifyJwt } from '@utils/verifyJwt'; import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
import { env } from "@utils/env";
const usingPrisma = false; import { prisma } from "@utils/prisma";
let prisma: PrismaClient; import { verifyJwt } from "@utils/verifyJwt";
if (usingPrisma) prisma = new PrismaClient();
// todo remove if (usingPrisma) code
// todo add specification of date range in request
// Artefact type
interface Artefact {
id: number;
name: string;
description: string;
location: string;
earthquakeId: string;
isRequired: boolean;
isSold: boolean;
isCollected: boolean;
dateAdded: string;
}
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
// todo fix, moron the token will be in the cookie header const authResult = await apiAuthMiddleware();
const json = await req.json(); // Parse incoming JSON data if ("user" in authResult === false) return authResult; // Handle error response
const { token } = json.body;
if (!token) return NextResponse.json({ message: "Unauthorised" }, { status: 401 });
await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
const warehouseArtefacts: Artefact[] = [ const { user } = authResult;
{
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",
},
];
let artefacts; if (user.role !== "SCIENTIST" && user.role !== "ADMIN") {
if (usingPrisma) artefacts = await prisma.artefact.findMany(); return NextResponse.json({ message: "Not authorised" }, { status: 401 });
}
const artefacts = await prisma.artefact.findMany({
include: {
earthquake: true,
},
});
if (artefacts) { 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 { } 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) { } catch (error) {
console.error("Error in artefacts endpoint:", error); console.error("Error in artefacts endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
} }
} }

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
const OurMission = () => { function OurMission() {
return ( return (
<div <div
className="relative bg-fixed bg-cover bg-center text-white " className="relative h-full bg-fixed bg-cover bg-center text-white "
style={{ backgroundImage: "url('destruction.jpg')", height: 845, overflow: "hidden" }} style={{ backgroundImage: "url('destruction.jpg')", overflow: "hidden" }}
> >
{/* Overlay for Readability */} {/* Overlay for Readability */}
<div className="absolute inset-0 bg-black bg-opacity-50"></div> <div className="absolute inset-0 bg-black bg-opacity-50"></div>
@ -55,5 +55,5 @@ const OurMission = () => {
</div> </div>
</div> </div>
); );
}; }
export default OurMission; export default OurMission;

View File

@ -1,13 +1,12 @@
"use client"; "use client";
import Image from 'next/image'; import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { Dispatch, SetStateAction, useCallback, useState } from "react";
import Artefact from '@appTypes/Artefact'; import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { Currency } from '@appTypes/StoreModel'; import { Currency } from "@appTypes/StoreModel";
import { useStoreState } from '@hooks/store'; import { useStoreState } from "@hooks/store";
// Artefacts Data const artefacts: ExtendedArtefact[] = [
const artefacts: Artefact[] = [
{ {
id: 1, id: 1,
name: "Golden Scarab", name: "Golden Scarab",
@ -215,7 +214,7 @@ export default function Shop() {
if (currentPage > 1) setCurrentPage((prev) => prev - 1); if (currentPage > 1) setCurrentPage((prev) => prev - 1);
}; };
function ArtefactCard({ artefact }: { artefact: Artefact }) { function ArtefactCard({ artefact }: { artefact: ExtendedArtefact }) {
return ( return (
<div <div
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform" className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
@ -234,7 +233,7 @@ export default function Shop() {
</div> </div>
); );
} }
function Modal({ artefact }: { artefact: Artefact }) { function Modal({ artefact }: { artefact: ExtendedArtefact }) {
if (!artefact) return null; if (!artefact) return null;
const handleOverlayClick = (e: React.MouseEvent) => { const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) setSelectedArtefact(null); if (e.target === e.currentTarget) setSelectedArtefact(null);

View File

@ -1,19 +1,17 @@
"use client"; "use client";
import { Dispatch, SetStateAction, useMemo, useState } from 'react'; import { Dispatch, SetStateAction, useMemo, useState } from "react";
import { FaTimes } from 'react-icons/fa'; import { FaTimes } from "react-icons/fa";
import { FaCalendarPlus, FaCartShopping, FaWarehouse } from 'react-icons/fa6'; import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6";
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from 'react-icons/io5'; import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5";
import { ExtendedArtefact } from "@appTypes/ApiTypes";
// import { Artefact } from "@appTypes/Prisma"; // import { Artefact } from "@appTypes/Prisma";
import type { Artefact } from "@prismaclient"; import type { Artefact } from "@prismaclient";
interface WarehouseArtefact extends Artefact {
location: string;
}
// Warehouse Artefacts Data // Warehouse Artefacts Data
const warehouseArtefacts: WarehouseArtefact[] = [ const extendedArtefacts: ExtendedArtefact[] = [
{ {
id: 1, id: 1,
name: "Solidified Lava Chunk", name: "Solidified Lava Chunk",
@ -705,7 +703,7 @@ export default function Warehouse() {
// Apply filters with loading state // Apply filters with loading state
const filteredArtefacts = useMemo(() => { const filteredArtefacts = useMemo(() => {
setIsFiltering(true); setIsFiltering(true);
const result = applyFilters(warehouseArtefacts, filters); const result = applyFilters(extendedArtefacts, filters);
setIsFiltering(false); setIsFiltering(false);
return result; return result;
}, [filters]); }, [filters]);
@ -713,10 +711,10 @@ export default function Warehouse() {
const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact); const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
// Overview stats // Overview stats
const totalArtefacts = warehouseArtefacts.length; const totalArtefacts = extendedArtefacts.length;
const today = new Date(); const today = new Date();
const artefactsAddedToday = warehouseArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length; const artefactsAddedToday = extendedArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length;
const artefactsSoldToday = warehouseArtefacts.filter( const artefactsSoldToday = extendedArtefacts.filter(
(a) => a.isSold && a.createdAt.toDateString() === today.toDateString() (a) => a.isSold && a.createdAt.toDateString() === today.toDateString()
).length; ).length;
@ -809,8 +807,11 @@ export default function Warehouse() {
</div> </div>
</div> </div>
</div> </div>
{/* Modals */} {/* 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 && <LogModal onClose={() => setShowLogModal(false)} />} {showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />} {showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />} {editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}

View File

@ -10,6 +10,7 @@ interface AuthModalProps {
} }
export default function AuthModal({ isOpen, onClose }: AuthModalProps) { export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
// todo add login successful message
const [isLogin, setIsLogin] = useState<boolean>(true); const [isLogin, setIsLogin] = useState<boolean>(true);
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const [isFailed, setIsFailed] = useState<boolean>(false); const [isFailed, setIsFailed] = useState<boolean>(false);

8
src/types/ApiTypes.ts Normal file
View File

@ -0,0 +1,8 @@
import { Artefact } from "@prismaclient";
interface ExtendedArtefact extends Artefact {
location: string;
date: Date;
}
export type { ExtendedArtefact };

View File

@ -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;

5
src/types/JWT.ts Normal file
View File

@ -0,0 +1,5 @@
interface JWTPayload {
userId: number;
}
export type { JWTPayload };

View File

@ -1,5 +1,5 @@
import { Action } from "easy-peasy"; import { Action } from "easy-peasy";
// import type { User } from "@prisma/client"; // import type { User } from "@prismaclient";
import { User } from "@appTypes/Prisma"; import { User } from "@appTypes/Prisma";
type Currency = "GBP" | "USD" | "EUR"; type Currency = "GBP" | "USD" | "EUR";

View File

@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { verifyJwt } from "@utils/verifyJwt";
import type { JWTPayload } from "@/types/JWT";
import { env } from "@utils/env";
import { prisma } from "@utils/prisma";
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 };
}

5
src/utils/maths.ts Normal file
View File

@ -0,0 +1,5 @@
function getRandomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export { getRandomNumber };

View File

@ -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 };

9
src/utils/prisma.ts Normal file
View File

@ -0,0 +1,9 @@
import { PrismaClient } from "@prismaclient";
declare global {
var prisma: PrismaClient | undefined;
}
const prisma = new PrismaClient();
export { prisma };