From 3d59e865597f42c0eb320c06b3d5e3ac4f76bb9e Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Tue, 13 May 2025 22:53:17 +0100 Subject: [PATCH 1/2] Made some changes --- src/app/api/login/route.ts | 17 ++- src/app/api/logout/route.ts | 13 +- src/app/api/signup/route.ts | 40 +++++-- src/app/api/warehouse/route.ts | 2 +- src/app/layout.tsx | 25 ++-- src/app/profile/page.tsx | 210 +++++++++++++++++++++++++++++++-- src/app/warehouse/page.tsx | 49 ++------ src/components/AuthModal.tsx | 50 +++++--- src/types/Axios.ts | 10 ++ src/types/Prisma.ts | 39 ++++++ src/types/StoreModel.ts | 41 +------ 11 files changed, 356 insertions(+), 140 deletions(-) create mode 100644 src/types/Axios.ts create mode 100644 src/types/Prisma.ts diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index 2eab077..84b0686 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -1,11 +1,11 @@ -import bcryptjs from "bcryptjs"; -import { SignJWT } from "jose"; -import { NextResponse } from "next/server"; +import bcryptjs from 'bcryptjs'; +import { SignJWT } from 'jose'; +import { NextResponse } from 'next/server'; -import { PrismaClient } from "@prisma/client"; -import { env } from "@utils/env"; +import { PrismaClient } from '@prisma/client'; +import { env } from '@utils/env'; -import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite"; +import { findUserByEmail, readUserCsv, User } from '../functions/csvReadWrite'; const usingPrisma = false; let prisma: PrismaClient; @@ -13,8 +13,7 @@ if (usingPrisma) prisma = new PrismaClient(); export async function POST(req: Request) { try { - const json = await req.json(); // Parse incoming JSON data - const { email, password } = json.body; + const { email, password } = await req.json(); // Parse incoming JSON data const userData = await readUserCsv(); console.log(userData); @@ -33,7 +32,7 @@ export async function POST(req: Request) { user = findUserByEmail(userData, email); } - if (user && bcryptjs.compareSync(password, usingPrisma ? user.passwordHash : user.password)) { + if (user && bcryptjs.compareSync(password, usingPrisma ? user.hashedPassword : user.password)) { // todo remove password from returned user // get user and relations diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts index 45017d2..958b48b 100644 --- a/src/app/api/logout/route.ts +++ b/src/app/api/logout/route.ts @@ -1,8 +1,15 @@ -// app/api/logout/route.ts import { cookies } from "next/headers"; import { NextResponse } from "next/server"; export async function GET() { - (await cookies()).delete("jwt"); - return NextResponse.json({ message: "Logged out" }); + try { + const cookieStore = await cookies(); + if (!cookieStore.has("jwt")) { + return NextResponse.json({ message: "No active session found" }, { status: 400 }); + } + cookieStore.delete("jwt"); + return NextResponse.json({ message: "Logged out" }); + } catch (error) { + return NextResponse.json({ message: "Error logging out", error }, { status: 500 }); + } } diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index 1dd81ef..8ed7e92 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -1,9 +1,13 @@ -import bcryptjs from "bcryptjs"; -import { NextResponse } from "next/server"; +import bcryptjs from 'bcryptjs'; +import { SignJWT } from 'jose'; +import { NextResponse } from 'next/server'; -import { PrismaClient } from "@prisma/client"; +import { PrismaClient } from '@prisma/client'; +import { env } from '@utils/env'; -import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite"; +import { + findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv +} from '../functions/csvReadWrite'; const usingPrisma = false; let prisma: PrismaClient; @@ -11,8 +15,7 @@ if (usingPrisma) prisma = new PrismaClient(); export async function POST(req: Request) { try { - const json = await req.json(); // Parse incoming JSON data - let { email, password, name } = json.body; + const { email, password, name } = await req.json(); // Parse incoming JSON data const accessLevel = "basic"; const userData = await readUserCsv(); @@ -57,9 +60,10 @@ export async function POST(req: Request) { } else { try { const passwordHash = await bcryptjs.hash(password, 10); + let user; if (usingPrisma) { - // todo add sending back newUser - const newUser = await prisma.user.create({ + // todo add sending back user + user = await prisma.user.create({ data: { name, email, @@ -67,10 +71,26 @@ export async function POST(req: Request) { }, }); } else { - userData.push({ name, email, password: passwordHash, accessLevel }); + user = { name, email, password: passwordHash, accessLevel }; + userData.push(user); } await writeUserCsv(userData); - return NextResponse.json({ message: "Account Created" }, { status: 201 }); + + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const token = await new SignJWT({ userId: user.id }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("2w") + .sign(secret); + + const response = NextResponse.json({ message: "Account Created" }, { status: 201 }); + response.cookies.set("jwt", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 3600 * 168 * 2, // 2 weeks + path: "/", + }); + return response; } catch (error) { console.error("Error in writting :", error); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); diff --git a/src/app/api/warehouse/route.ts b/src/app/api/warehouse/route.ts index 4a2fcd0..027d92e 100644 --- a/src/app/api/warehouse/route.ts +++ b/src/app/api/warehouse/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; +import { env } from "@utils/env"; import { PrismaClient } from "@prisma/client"; -import { env } from "@utils/env"; import { verifyJwt } from "@utils/verifyJwt"; const usingPrisma = false; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2d539a3..6e1c55b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -23,17 +23,20 @@ const store = createStore({ conversionRates: { GBP: 0.85, USD: 1.14, EUR: 1 }, tickers: { GBP: "£", USD: "$", EUR: "€" }, }, - // user: null, - user: { - id: 123456, - createdAt: new Date(8.64e15), - email: "emily.neighbour@dyson.com", - passwordHash: "", - name: "Emily Neighbour", - role: "ADMIN", - scientist: undefined, - purchasedArtefacts: [], - }, + user: null, + // user: { + // id: 123456, + // createdAt: new Date(8.64e15), + // email: "tim.howitz@dyson.com", + // passwordHash: "", + // name: "Tim Howitz", + // role: "ADMIN", + // scientist: undefined, + // purchasedArtefacts: [], + // }, + setUser: action((state, payload) => { + state.user = payload; + }), }); export default function RootLayout({ diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index eeb3f06..a83b849 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,22 +1,208 @@ "use client"; -import axios from "axios"; +import { useState, useEffect } from "react"; +import { useStoreActions } from "@hooks/store"; +import axios, { AxiosError } from "axios"; import { useRouter } from "next/navigation"; +import { User } from "@appTypes/Prisma"; +import { FaSignOutAlt } from "react-icons/fa"; +import { FaUser } from "react-icons/fa6"; export default function Profile() { const router = useRouter(); + const setUser = useStoreActions((actions) => actions.setUser) as (user: User | null) => void; + const [user, setUserState] = useState(null); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [role, setRole] = useState(""); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + const fetchUser = async () => { + try { + const userData: User = { + id: 1, + createdAt: new Date(), + name: "John Doe", + email: "john.doe@example.com", + passwordHash: "hashed_password", + role: "SCIENTIST", + scientist: undefined, + purchasedArtefacts: [], + }; + setUserState(userData); + setName(userData.name); + setEmail(userData.email); + setRole(userData.role); + } catch { + setError("Failed to load user data."); + } + }; + fetchUser(); + }, []); + + const handleSave = async () => { + if (!name || !email) { + setError("Name and email are required."); + return; + } + setIsSubmitting(true); + try { + await new Promise((resolve) => setTimeout(resolve, 500)); + setUserState({ ...user!, name, email, role }); + alert("Profile updated successfully."); + setError(""); + } catch { + setError("Failed to update profile. Please try again."); + } finally { + setIsSubmitting(false); + } + }; + + const handleLogout = async () => { + try { + const res = await axios.get("/api/logout"); + if (res.status === 200) { + setUser(null); + router.push("/"); + } else { + console.error("Failed to logout", res.data); + } + } catch (error) { + const axiosError = error as AxiosError<{ message: string }>; + if (axiosError.response && axiosError.response.status === 400) { + setUser(null); + router.push("/"); + } else { + console.error("Error during logout", axiosError); + } + } + }; return ( -
-

User

- +
+
+
+ +
+

User Profile

+
+
+ {error &&

{error}

} +
+
+ +
+
+
+ + setName(e.target.value)} + className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 text-sm" + aria-label="User Name" + disabled={isSubmitting} + /> +
+
+ + setEmail(e.target.value)} + className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 text-sm" + aria-label="User Email" + disabled={isSubmitting} + /> +
+
+ + +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 text-sm" + disabled={isSubmitting} + /> +
+
+ + setConfirmPassword(e.target.value)} + className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 text-sm" + disabled={isSubmitting} + /> +
+
+
+
+ + +
+
+
+
+
+
+
); } diff --git a/src/app/warehouse/page.tsx b/src/app/warehouse/page.tsx index cbc6dc7..a2f7540 100644 --- a/src/app/warehouse/page.tsx +++ b/src/app/warehouse/page.tsx @@ -1,22 +1,11 @@ "use client"; -import { Dispatch, SetStateAction, useMemo, useState } from "react"; -import { FaTimes } from "react-icons/fa"; -import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6"; +import { useState, useMemo } from "react"; +import { FaCalendarPlus, FaWarehouse, FaCartShopping } from "react-icons/fa6"; import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5"; - +import { FaTimes } from "react-icons/fa"; +import { SetStateAction, Dispatch } from "react"; // import type { Artefact } from "@prisma/client"; - -interface Artefact { - id: number; - name: string; - description: string; - location: string; - earthquakeId: string; - isRequired: boolean; - isSold: boolean; - isCollected: boolean; - dateAdded: string; -} +import { Artefact } from "@appTypes/Prisma"; // Warehouse Artefacts Data const warehouseArtefacts: Artefact[] = [ @@ -149,19 +138,7 @@ function ArtefactTable({ }: { artefacts: Artefact[]; filters: Record; - setFilters: Dispatch< - SetStateAction<{ - id: string; - name: string; - earthquakeId: string; - location: string; - description: string; - isRequired: string; - isSold: string; - isCollected: string; - dateAdded: string; - }> - >; + setFilters: Dispatch>>; setEditArtefact: (artefact: Artefact) => void; clearSort: () => void; }) { @@ -224,17 +201,7 @@ function ArtefactTable({ { - setFilters({ ...filters, [key]: value } as { - id: string; - name: string; - earthquakeId: string; - location: string; - description: string; - isRequired: string; - isSold: string; - isCollected: string; - dateAdded: string; - }); + setFilters({ ...filters, [key]: value } as Record); if (value === "") clearSortConfig(); }} type={key === "dateAdded" ? "date" : "text"} @@ -707,7 +674,7 @@ export default function Warehouse() { const [showLogModal, setShowLogModal] = useState(false); const [showBulkLogModal, setShowBulkLogModal] = useState(false); const [editArtefact, setEditArtefact] = useState(null); - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState>({ id: "", name: "", earthquakeId: "", diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index a728451..b1d7f6d 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -1,6 +1,8 @@ "use client"; import axios from "axios"; +import { useStoreActions } from "@hooks/store"; import { FormEvent, MouseEvent, useEffect, useRef, useState } from "react"; +import { ErrorRes } from "@appTypes/Axios"; interface AuthModalProps { isOpen: boolean; // bool for if the modal should be visible @@ -11,7 +13,8 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { const [isLogin, setIsLogin] = useState(true); const modalRef = useRef(null); const [isFailed, setIsFailed] = useState(false); - const [failMessage, setFailMessage] = useState(false); + const [failMessage, setFailMessage] = useState(); + const setUser = useStoreActions((actions) => actions.setUser); useEffect(() => { if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown @@ -26,30 +29,49 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { }; const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); // stops page from refreshing + e.preventDefault(); setIsFailed(false); + const formData = new FormData(e.currentTarget); const email = formData.get("email") as string; const password = formData.get("password") as string; const name = isLogin ? undefined : (formData.get("name") as string); try { - const res = await axios.post(`/api/${isLogin ? "login" : "signup"}`, { - headers: { "Content-Type": "application/json" }, - body: isLogin ? { email, password } : { name: name!, email, password }, - }); - if (res.status) { - res.data.user; + const res = await axios.post( + `/api/${isLogin ? "login" : "signup"}`, + isLogin ? { email, password } : { name, email, password }, + { + headers: { "Content-Type": "application/json" }, + } + ); + + if (res.status === 200) { + setUser(res.data.user); onClose(); - } else if (res.status >= 400 && res.status < 500) { - console.log("4xx error:", res.data); - setFailMessage(res.data.message); - setIsFailed(true); } else { - console.error("Error:", await res.data.message); + console.error("Unexpected response status:", res.status, res.data); + setFailMessage("Unexpected error occurred"); + setIsFailed(true); } } catch (error) { - console.error("Request failed:", error instanceof Error ? error.message : String(error)); + const axiosError = error as ErrorRes; + if (axiosError.response) { + const { status, data } = axiosError.response; + if (status >= 400 && status < 500) { + console.log("4xx error:", data); + setFailMessage(data.message || "Invalid request"); + setIsFailed(true); + } else { + console.error("Server error:", data); + setFailMessage("Server error occurred"); + setIsFailed(true); + } + } else { + console.error("Request failed:", axiosError.message); + setFailMessage("Network error occurred"); + setIsFailed(true); + } } }; diff --git a/src/types/Axios.ts b/src/types/Axios.ts new file mode 100644 index 0000000..51559f7 --- /dev/null +++ b/src/types/Axios.ts @@ -0,0 +1,10 @@ +import { AxiosError } from "axios"; + +interface ErrorResponse { + message: string; + error?: Error; +} + +type ErrorRes = AxiosError; + +export type { ErrorRes }; diff --git a/src/types/Prisma.ts b/src/types/Prisma.ts new file mode 100644 index 0000000..6ffd0bc --- /dev/null +++ b/src/types/Prisma.ts @@ -0,0 +1,39 @@ +interface Scientist { + id: number; + createdAt: Date; + name: string; + level: string; + user: User; + userId: number; + superior: Scientist | undefined; + superiorId: number | undefined; + subordinates: Scientist[]; + // earthquakes: Earthquake[]; + // observatories: Observatory[]; + artefacts: Artefact[]; +} + +interface Artefact { + id: number; + name: string; + description: string; + location: string; + earthquakeId: string; + isRequired: boolean; + isSold: boolean; + isCollected: boolean; + dateAdded: string; +} + +interface User { + id: number; + createdAt: Date; + name: string; + email: string; + passwordHash: string; + role: string; + scientist: Scientist | undefined; + purchasedArtefacts: Artefact[]; +} + +export type { Scientist, Artefact, User }; diff --git a/src/types/StoreModel.ts b/src/types/StoreModel.ts index 0fef253..3f1041b 100644 --- a/src/types/StoreModel.ts +++ b/src/types/StoreModel.ts @@ -1,44 +1,6 @@ import { Action } from "easy-peasy"; - // import type { User } from "@prisma/client"; - -interface Scientist { - id: number; - createdAt: Date; - name: string; - level: string; - user: User; - userId: number; - superior: Scientist | undefined; - superiorId: number | undefined; - subordinates: Scientist[]; - // earthquakes: Earthquake[]; - // observatories: Observatory[]; - artefacts: Artefact[]; -} - -interface Artefact { - id: number; - name: string; - description: string; - location: string; - earthquakeId: string; - isRequired: boolean; - isSold: boolean; - isCollected: boolean; - dateAdded: string; -} - -interface User { - id: number; - createdAt: Date; - name: string; - email: string; - passwordHash: string; - role: string; - scientist: Scientist | undefined; - purchasedArtefacts: Artefact[]; -} +import { User } from "@appTypes/Prisma"; type Currency = "GBP" | "USD" | "EUR"; @@ -53,6 +15,7 @@ interface CurrencyModel { interface StoreModel { currency: CurrencyModel; user: User | null; + setUser: Action; } export type { StoreModel, Currency }; From 158dbbf166f2fc692bf259993eaa89a9205f1fa6 Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Tue, 13 May 2025 23:05:14 +0100 Subject: [PATCH 2/2] Removed comment --- src/app/api/earthquakes/route.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/api/earthquakes/route.ts b/src/app/api/earthquakes/route.ts index addbd60..4c2529e 100644 --- a/src/app/api/earthquakes/route.ts +++ b/src/app/api/earthquakes/route.ts @@ -8,21 +8,21 @@ if (usingPrisma) prisma = new PrismaClient(); export async function POST(req: Request) { try { - const json = await req.json(); // Parse incoming JSON data - const {rangeDaysPrev} = json.body + const json = await req.json(); + const { rangeDaysPrev } = json.body; - const now = new Date() - const rangeBeginning = new Date(); - rangeBeginning.setDate(rangeBeginning.getDate() - rangeDaysPrev) + const now = new Date(); + const rangeBeginning = new Date(); + rangeBeginning.setDate(rangeBeginning.getDate() - rangeDaysPrev); - const earthquakes = await prisma.earthquake.findMany( - {where: { - date: { - gte: rangeBeginning, - lte: now - } - }} - ); + const earthquakes = await prisma.earthquake.findMany({ + where: { + date: { + gte: rangeBeginning, + lte: now, + }, + }, + }); if (earthquakes) { return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });