diff --git a/backend/package-lock.json b/backend/package-lock.json index 04a3fd2..4453be8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,7 +12,9 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "ejs": "^3.1.10", - "express": "^5.1.0" + "express": "^5.1.0", + "jose": "^6.0.12", + "mysql2": "^3.14.3" } }, "node_modules/accepts": { @@ -34,6 +36,15 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -176,6 +187,15 @@ } } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -381,6 +401,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -512,6 +541,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -529,6 +564,45 @@ "node": ">=10" } }, + "node_modules/jose": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -598,6 +672,38 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz", + "integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -789,6 +895,11 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -882,6 +993,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 81405a9..3614208 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,8 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "ejs": "^3.1.10", - "express": "^5.1.0" + "express": "^5.1.0", + "jose": "^6.0.12", + "mysql2": "^3.14.3" } } diff --git a/backend/routes/api.js b/backend/routes/api.js new file mode 100644 index 0000000..4ec867d --- /dev/null +++ b/backend/routes/api.js @@ -0,0 +1,26 @@ +import express from "express"; +import { loginFunc, getItemsFromDatabase } from "../services/database.js"; +import { authenticate, generateToken } from "../services/tokenService.js"; +const router = express.Router(); + +// Example endpoint +router.post("/login", async (req, res) => { + const result = await loginFunc(req.body.username, req.body.password); + if (result.success) { + const token = await generateToken({ username: req.body.username }); + res.status(200).json({ message: "Login successful", token }); + } else { + res.status(401).json({ message: "Invalid credentials" }); + } +}); + +router.get("/items", authenticate, async (req, res) => { + const result = await getItemsFromDatabase(); + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500).json({ message: "Failed to fetch items" }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 9661099..063e776 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,6 +11,11 @@ app.use(express.urlencoded({ extended: true, limit: "10mb" })); app.set("view engine", "ejs"); app.use(express.json({ limit: "10mb" })); +// Import API router +import apiRouter from "./routes/api.js"; + +app.use("/api", apiRouter); + app.get("/", (req, res) => { res.render("index.ejs"); }); @@ -24,4 +29,4 @@ app.use((err, req, res, next) => { // Log the error stack and send a generic error response console.error(err.stack); res.status(500).send("Something broke!"); -}); \ No newline at end of file +}); diff --git a/backend/services/database.js b/backend/services/database.js index e69de29..5550468 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -0,0 +1,30 @@ +import mysql from "mysql2"; +import dotenv from "dotenv"; +dotenv.config(); + +// Ein einzelner Pool reicht; der zweite Pool benutzte fälschlich DB_TABLE als Datenbank +const pool = mysql + .createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }) + .promise(); + +export const loginFunc = async (username, password) => { + const [result] = await pool.query( + "SELECT * FROM users WHERE username = ? AND password = ?", + [username, password] + ); + if (result.length > 0) return { success: true }; + return { success: false }; +}; + +export const getItemsFromDatabase = async () => { + const [result] = await pool.query("SELECT * FROM items"); + if (result.length > 0) { + return { success: true, data: result }; + } + return { success: false }; +}; diff --git a/backend/services/tokenService.js b/backend/services/tokenService.js new file mode 100644 index 0000000..6c5c46f --- /dev/null +++ b/backend/services/tokenService.js @@ -0,0 +1,26 @@ +import { SignJWT, jwtVerify } from "jose"; +import env from "dotenv"; +env.config(); +const secret = new TextEncoder().encode(process.env.SECRET_KEY); + +export async function generateToken(payload) { + const newToken = await new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("2h") // Token valid for 2 hours + .sign(secret); + console.log("Generated token: ", newToken); + return newToken; +} + +export async function authenticate(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; // Bearer + + if (token == null) return res.sendStatus(401); // No token present + + const { payload } = await jwtVerify(token, secret); + req.user = payload; + + next(); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index feb840f..fdd45e2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,41 +1,28 @@ import "./App.css"; import Layout from "./layout/Layout"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import Form1 from "./components/Form1"; import Form2 from "./components/Form2"; import Form3 from "./components/Form3"; +import LoginForm from "./components/LoginForm"; +import Cookies from "js-cookie"; +import { ToastContainer } from "react-toastify"; function App() { - const Items = [ - { - id: 1, - title: "Mock Book 1", - author: "Author 1", - description: "Description for Mock Book 1", - }, - { - id: 2, - title: "Mock Book 2", - author: "Author 2", - description: "Description for Mock Book 2", - }, - { - id: 3, - title: "Mock Book 3", - author: "Author 3", - description: "Description for Mock Book 3", - }, - ]; + const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { - localStorage.setItem("allItems", JSON.stringify(Items)); - localStorage.setItem("borrowableItems", JSON.stringify(Items)); + if (Cookies.get("token")) { + setIsLoggedIn(true); + } + + localStorage.setItem("borrowableItems", JSON.stringify([])); localStorage.setItem("borrowCode", "123456"); }, []); - return ( + // Mock flow without real logic: show the three sections stacked for design preview + return isLoggedIn ? ( - {/* Mock flow without real logic: show the three sections stacked for design preview */}
@@ -44,6 +31,23 @@ function App() {
+ ) : ( + <> + setIsLoggedIn(true)} /> + + ); } diff --git a/frontend/src/components/Form3.tsx b/frontend/src/components/Form3.tsx index ded9b69..9350284 100644 --- a/frontend/src/components/Form3.tsx +++ b/frontend/src/components/Form3.tsx @@ -1,13 +1,6 @@ import React from "react"; -import { myToast } from "../utils/toastify"; const Form3: React.FC = () => { - if (localStorage.getItem("borrowCode")) { - myToast("Ausleihe erfolgreich!", "success"); - } else { - myToast("Ausleihe fehlgeschlagen!", "error"); - } - const code = localStorage.getItem("borrowCode") ?? "—"; return ( diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 319c3e6..2a8262d 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Cookies from "js-cookie"; const Header: React.FC = () => { return ( @@ -9,6 +10,15 @@ const Header: React.FC = () => {

Schnell und unkompliziert Equipment reservieren

+ ); }; diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/LoginForm.tsx new file mode 100644 index 0000000..37748e5 --- /dev/null +++ b/frontend/src/components/LoginForm.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { useState } from "react"; +import { loginUser } from "../utils/fetchData"; +import { myToast } from "../utils/toastify"; + +type LoginFormProps = { + onLogin: () => void; +}; + +const LoginForm: React.FC = ({ onLogin }) => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await loginUser(username, password); + if (result.success) { + myToast("Login successful!", "success"); + onLogin(); + } else { + myToast("Login failed. Please check your credentials.", "error"); + } + }; + + return ( +
+
+

+ Login +

+
+
+ + setUsername(e.target.value)} + id="username" + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 px-3 py-2" + required + /> +
+
+ + setPassword(e.target.value)} + type="password" + id="password" + className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 px-3 py-2" + required + /> +
+ +
+
+
+ ); +}; + +export default LoginForm; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index cafb223..d5e7a7c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,7 +1,10 @@ import React from "react"; import Object from "./Object"; +import { useEffect } from "react"; const Sidebar: React.FC = () => { + useEffect(() => {}); + const items = JSON.parse(localStorage.getItem("allItems") || "[]"); return ( @@ -20,16 +23,16 @@ const Sidebar: React.FC = () => { d="M16.5 7.5V4.75A2.25 2.25 0 0 0 14.25 2.5h-4.5A2.25 2.25 0 0 0 7.5 4.75V7.5m9 0h-9m9 0v11.75A2.25 2.25 0 0 1 14.25 21.5h-4.5A2.25 2.25 0 0 1 7.5 19.25V7.5m9 0h-9" /> - Ausleih-Übersicht + Geräte Übersicht
{items.map((item: any) => (
- + ))} diff --git a/frontend/src/utils/fetchData.ts b/frontend/src/utils/fetchData.ts index e69de29..5eec60f 100644 --- a/frontend/src/utils/fetchData.ts +++ b/frontend/src/utils/fetchData.ts @@ -0,0 +1,53 @@ +import Cookies from "js-cookie"; +import { myToast } from "./toastify"; + +export const fetchAllData = async (token: string | undefined) => { + if (!token) return; + try { + // First we fetch all items that are potentially available for borrowing + const response = await fetch("http://localhost:8002/api/items", { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + myToast("Failed to fetch items", "error"); + return; + } + + const data = await response.json(); + localStorage.setItem("allItems", JSON.stringify(data)); + } catch (error) { + myToast("An error occurred", "error"); + } +}; + +export const loginUser = async (username: string, password: string) => { + try { + const response = await fetch("http://localhost:8002/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password }), + }); + + if (!response.ok) { + return { success: false } as const; + } + + const data = await response.json(); + if (data?.token) { + Cookies.set("token", data.token); + myToast("Login successful!", "success"); + fetchAllData(Cookies.get("token")); + return { success: true, token: data.token } as const; + } + + return { success: false } as const; + } catch (e) { + return { success: false } as const; + } +};