Added prisma stuff to auth api routes

This commit is contained in:
Tim Howitz 2025-04-29 18:07:25 +01:00
parent 668b8729a8
commit 67dfee01b2
6 changed files with 277 additions and 195 deletions

103
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@types/mapbox-gl": "^3.4.1", "@types/mapbox-gl": "^3.4.1",
"axios": "^1.9.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@ -2291,6 +2292,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -2317,6 +2324,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -2671,6 +2689,18 @@
"color-support": "bin.js" "color-support": "bin.js"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -2961,6 +2991,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -3274,7 +3313,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -4030,6 +4068,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -4063,6 +4121,42 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -4508,7 +4602,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@ -6431,6 +6524,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@types/mapbox-gl": "^3.4.1", "@types/mapbox-gl": "^3.4.1",
"axios": "^1.9.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",

View File

@ -1,31 +1,45 @@
import { NextResponse } from "next/server"; import bcrypt from 'bcrypt';
import {User, readUserCsv, findUserByEmail} from "../functions/csvReadWrite" import { NextResponse } from 'next/server';
import bcrypt from "bcrypt"
import { PrismaClient } from '@prisma/client';
import { findUserByEmail, readUserCsv, User } from '../functions/csvReadWrite';
const prisma = new PrismaClient();
const usingPrisma = false;
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); // Parse incoming JSON data const body = await request.json(); // Parse incoming JSON data
const { email, password } = body; const { email, password } = body;
console.log("Login API received data");
const userData = await readUserCsv(); const userData = await readUserCsv();
console.log(userData);
console.log(userData)
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
const foundUser = findUserByEmail(userData,email) let foundUser;
if (foundUser && await bcrypt.compare(password, foundUser.password)) {
console.log("User Details Correct") if (usingPrisma) {
return NextResponse.json({ message: "Login successful!" }, { status: 200 }); foundUser = await prisma.user.findUnique({
where: {
email: email, // use the email to uniquely identify the user
},
});
} else { } else {
console.log("User email or password is invalid") foundUser = findUserByEmail(userData, email);
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
} }
if (foundUser && (await bcrypt.compare(password, usingPrisma ? foundUser.hashedPassword : foundUser.password))) {
// todo remove password from returned user
return NextResponse.json({ message: "Login successful!", user: foundUser }, { status: 200 });
} else {
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
}
} 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,29 +1,45 @@
import { NextResponse } from "next/server"; import bcrypt from 'bcrypt';
import {User, readUserCsv, writeUserCsv, findUserByEmail, passwordStrengthCheck} from "../functions/csvReadWrite"; import { NextResponse } from 'next/server';
import bcrypt from "bcrypt"
import { PrismaClient } from '@prisma/client';
import {
findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv
} from '../functions/csvReadWrite';
const prisma = new PrismaClient();
const usingPrisma = false;
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); // Parse incoming JSON data const body = await request.json(); // Parse incoming JSON data
let {name, email, password } = body; let { email, password, name } = body;
const accessLevel = "basic"; const accessLevel = "basic";
console.log("Signin API received data");
const userData = await readUserCsv(); const userData = await readUserCsv();
console.log(userData) console.log(userData);
console.log("Name:", name); // ! remove console.log("Name:", name); // ! remove
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
const foundUser = findUserByEmail(userData,email) 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);
}
if (foundUser) { if (foundUser) {
console.log("Email already in the system")
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 });
} }
const passwordCheckResult = await passwordStrengthCheck(password) const passwordCheckResult = await passwordStrengthCheck(password);
if (passwordCheckResult === "short") { if (passwordCheckResult === "short") {
return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 }); return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 });
@ -41,17 +57,26 @@ export async function POST(request: Request) {
return NextResponse.json({ message: "Password check script failure" }, { status: 500 }); return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
} else { } else {
try { try {
password = await bcrypt.hash(password, 10); const passwordHash = await bcrypt.hash(password, 10);
userData.push({name,email,password,accessLevel}) if (usingPrisma) {
await writeUserCsv(userData) // todo add sending back newUser
const newUser = await prisma.user.create({
data: {
name,
email,
passwordHash,
},
});
} else {
userData.push({ name, email, password: passwordHash, accessLevel });
}
await writeUserCsv(userData);
return NextResponse.json({ message: "Account Created" }, { status: 201 }); return NextResponse.json({ message: "Account Created" }, { status: 201 });
} catch (error) { } catch (error) {
console.error("Error in writting :", error); console.error("Error in writting :", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} }
} }
} 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 });

View File

@ -1,109 +1,53 @@
// tells React if you're using its "server-side rendering" features. this component runs only on the client-side (browser)
"use client"; "use client";
// importing React hooks or utilities that enhance functionality inside the component import axios from 'axios';
import { FormEvent, MouseEvent, useEffect, useRef, useState } from "react"; import { FormEvent, MouseEvent, useEffect, useRef, useState } from 'react';
/*
useState: Used to manage state (data that changes over time).
FormEvent: TypeScript definition for form-related events like submission.
useRef: Used to access the DOM (the web page elements) directly.
MouseEvent: TypeScript definition for mouse-related events like clicks.
useEffect: Hook for running side effects (e.g., code that runs when something changes)
*/
// defining a type for the props (inputs) that AuthModal expects
interface AuthModalProps { interface AuthModalProps {
isOpen: boolean; // bool for if the modal should be visible isOpen: boolean; // bool for if the modal should be visible
onClose: () => void; //A function that will be executed to close the modal onClose: () => void; //A function that will be executed to close the modal
} }
// creates a React functional component
export default function AuthModal({ isOpen, onClose }: AuthModalProps) { export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
//AuthModalProps ensures TypeScript validates the props to match the type definition above
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);
const [failMessage, setFailMessage] = useState<boolean>(false); const [failMessage, setFailMessage] = useState<boolean>(false);
/*
useState is a React Hook that declares state variables in a functional component. It returns a two-element array
The state variable (isLogin) : Represents the current state value (e.g., true initially). This is the value you can use in your component.
The state updater function (setIsLogin) : A function that allows you to update the state variable. React takes care of re-rendering the component when the state is updated
*/
/*
modalRef allows direct access to the modal DOM element (the container div for the modal). This is useful for detecting if the user clicks outside of the modal
*/
// useEffect runs code after the component renders or when a dependency changes (in this case, isOpen as seen in the end [])
useEffect(() => { useEffect(() => {
if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown
}, [isOpen]); }, [isOpen]);
if (!isOpen) return null; // if is open is false, the model isnt shown if (!isOpen) return null; // if is open is false, the model isnt shown
// this is an arrow function. e: is used to specify that an event object is expected
const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => { const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) { if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose(); onClose();
} }
}; };
/*
.current gives a reference to the actual DOM element (the inner modal container)
e.target refers to the specific element the mouse click event occurred on
.contains(e.target) checks if the clicked element (e.target) is inside the modal (modalRef.current)
as Node specifies e.target is a Node type to TypeScript
? what is a node
/!... means we get true if the click is outside the modal
Logic : if modalRef.current exists and the click target (e.target) is not inside the modal , then the condition is true
This means the user clicked outside the modal
*/
// LS - The following bit contains the more important code for what I'm focused on
/*
Note : handleSubmit is typically used as a event handler for submitting a form in react.
For example:
<form onSubmit={handleSubmit}>
<input type="text" name="example" />
<button type="submit">Submit</button>
</form>
*/
/*
e is the parameter passsed into the function
async indicates the function runs asyncronously, meaning it performs tasks which take time.
an example of this would be API calls
*/
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // stops page from refreshing e.preventDefault(); // stops page from refreshing
setIsFailed(false); setIsFailed(false);
const formData = new FormData(e.currentTarget); // new variable of class FormData is created. This is part of the standard Web API included in modern web browsers const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string; // gets email from form response const email = formData.get("email") as string;
const password = formData.get("password") as string; // gets password from form response const password = formData.get("password") as string;
const name = isLogin ? undefined : (formData.get("name") as string); // if the form is in login mode, the name is undefine, otherwise the name is a value obtained from the response const name = isLogin ? undefined : (formData.get("name") as string);
let endpoint = isLogin ? "/api/login" : "/api/signup"; // sets endpoint for backend code (either sign up or login)
const body = isLogin ? { email, password } : { name: name!, email, password }; // creates a json body for the backend
try { try {
console.log("Sending data to API"); const res = await axios.post(`/api/${isLogin ? "login" : "signup"}`, {
const res = await fetch(endpoint, { headers: { "Content-Type": "application/json" },
// sends a request to the server at the end point body: isLogin ? { email, password } : { name: name!, email, password },
method: "POST", // Post is used since the form submission modifies the server side state
headers: { "Content-Type": "application/json" }, //indicates it expects a json object returned
body: JSON.stringify(body), // converts the body to a JSON string to be sent
}); });
if (res.ok) { if (res.status) {
//res.ok checks if the response is between 200-299 onClose();
console.log("Success!");
onClose(); // closes UI
} else if (res.status >= 400 && res.status < 500) { } else if (res.status >= 400 && res.status < 500) {
const responseBody = await res.json(); console.log("4xx error:", res.data);
console.log("4xx error:", responseBody.message); setFailMessage(res.data.message);
setFailMessage(responseBody.message);
setIsFailed(true); setIsFailed(true);
} else { } else {
console.error("Error:", await res.text()); // logs error with error message sent to console console.error("Error:", await res.data.message);
} }
} catch (error) { } catch (error) {
// catches any errors (e.g. Not connected to network)
console.error("Request failed:", error instanceof Error ? error.message : String(error)); console.error("Request failed:", error instanceof Error ? error.message : String(error));
} }
}; };

View File

@ -10,8 +10,7 @@ model User {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
email String @unique email String @unique
passwordHash String passwordHash String
firstname String name String
surname String
role String @default("USER") @db.VarChar(10) role String @default("USER") @db.VarChar(10)
scientist Scientist? @relation // Optional relation to Scientist scientist Scientist? @relation // Optional relation to Scientist
} }