From 16025f57991616baf3c4af4ee696e7b65e123bc6 Mon Sep 17 00:00:00 2001 From: VariaSlu Date: Thu, 21 Aug 2025 11:22:08 +0200 Subject: [PATCH 01/12] backend express db conneced --- backend/package.json | 15 +++++++++++---- backend/server.js | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/package.json b/backend/package.json index 08f29f2448..60f1bec2f9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,16 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", "cors": "^2.8.5", - "express": "^4.17.3", - "mongoose": "^8.4.0", - "nodemon": "^3.0.1" + "dotenv": "^17.2.1", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.18.0", + "mongoose": "^8.17.2", + "morgan": "^1.10.1" + }, + "devDependencies": { + "nodemon": "^3.1.10" } -} \ No newline at end of file +} diff --git a/backend/server.js b/backend/server.js index 070c875189..326d89f270 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,22 +1,42 @@ +import dotenv from "dotenv"; import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import morgan from "morgan"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; -mongoose.connect(mongoUrl); -mongoose.Promise = Promise; +dotenv.config(); // 1) Load backend/.env -const port = process.env.PORT || 8080; const app = express(); +const PORT = process.env.PORT || 8080; +const MONGO_URL = process.env.MONGO_URL || "mongodb://localhost/final-project"; +// 2) Middleware app.use(cors()); app.use(express.json()); +app.use(morgan("dev")); // handy request logs during dev -app.get("/", (req, res) => { +// 3) Mongo connect (with clear logs) +mongoose + .connect(MONGO_URL) + .then(() => console.log("✅ MongoDB connected")) + .catch((err) => console.error("❌ MongoDB connection error:", err.message)); + +// Helper: human-readable DB status +const dbStatus = () => { + const states = ["disconnected", "connected", "connecting", "disconnecting"]; + return states[mongoose.connection.readyState] || "unknown"; +}; + +// 4) Routes +app.get("/", (_req, res) => { res.send("Hello Technigo!"); }); -// Start the server -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); +app.get("/health", (_req, res) => { + res.json({ ok: true, db: dbStatus() }); +}); + +// 5) Start the server +app.listen(PORT, () => { + console.log(`🚀 Server running on http://localhost:${PORT}`); }); From d0e909b5b0765baf366b16e60cbb154dd5038570 Mon Sep 17 00:00:00 2001 From: VariaSlu Date: Thu, 21 Aug 2025 15:37:21 +0200 Subject: [PATCH 02/12] added User model, JWT middleware, and register/login routes --- backend/middlewares/auth.js | 13 ++++++++++++ backend/models/User.js | 11 ++++++++++ backend/routes/auth.js | 41 +++++++++++++++++++++++++++++++++++++ backend/server.js | 4 ++++ 4 files changed, 69 insertions(+) create mode 100644 backend/middlewares/auth.js create mode 100644 backend/models/User.js create mode 100644 backend/routes/auth.js diff --git a/backend/middlewares/auth.js b/backend/middlewares/auth.js new file mode 100644 index 0000000000..9617a9aa4b --- /dev/null +++ b/backend/middlewares/auth.js @@ -0,0 +1,13 @@ +import jwt from "jsonwebtoken"; + +export default function auth(req, res, next) { + const h = req.headers.authorization || ""; + const token = h.startsWith("Bearer ") ? h.slice(7) : null; + if (!token) return res.status(401).json({ error: "No token" }); + try { + req.user = jwt.verify(token, process.env.JWT_SECRET); // { id, iat, exp } + next(); + } catch { + res.status(401).json({ error: "Invalid token" }); + } +} diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000000..9a9d02ea1e --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema( + { + email: { type: String, required: true, unique: true, trim: true }, + passwordHash: { type: String, required: true } + }, + { timestamps: true } +); + +export default mongoose.model("User", userSchema); diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000000..7684665261 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,41 @@ +import express from "express"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import User from "../models/User.js"; + +const router = express.Router(); +const sign = (id) => jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "7d" }); + +router.post("/register", async (req, res) => { + try { + const { email, password } = req.body; + if (!email || !password) return res.status(400).json({ error: "Email and password required" }); + + const exists = await User.findOne({ email }); + if (exists) return res.status(409).json({ error: "Email already registered" }); + + const passwordHash = await bcrypt.hash(password, 10); + const user = await User.create({ email, passwordHash }); + + return res.status(201).json({ token: sign(user._id) }); + } catch (e) { + return res.status(500).json({ error: "Register failed" }); + } +}); + +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + const user = await User.findOne({ email }); + if (!user) return res.status(401).json({ error: "Invalid credentials" }); + + const ok = await bcrypt.compare(password, user.passwordHash); + if (!ok) return res.status(401).json({ error: "Invalid credentials" }); + + return res.json({ token: sign(user._id) }); + } catch (e) { + return res.status(500).json({ error: "Login failed" }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 326d89f270..66c10fafd4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,6 +3,8 @@ import express from "express"; import cors from "cors"; import mongoose from "mongoose"; import morgan from "morgan"; +import authRouter from "./routes/auth.js"; + dotenv.config(); // 1) Load backend/.env @@ -15,6 +17,8 @@ app.use(cors()); app.use(express.json()); app.use(morgan("dev")); // handy request logs during dev +app.use("/auth", authRouter); + // 3) Mongo connect (with clear logs) mongoose .connect(MONGO_URL) From 0168cbc0588102022bc6426a5a63c06de3d49b39 Mon Sep 17 00:00:00 2001 From: VariaSlu Date: Fri, 22 Aug 2025 14:46:59 +0200 Subject: [PATCH 03/12] set front and connected with back --- .gitignore | 1 + backend/models/Kid.js | 9 +++++++ backend/package.json | 5 +++- backend/routes/auth.js | 22 ++++++++++++++--- backend/routes/kids.js | 21 ++++++++++++++++ backend/server.js | 6 +++++ frontend/package.json | 6 ++++- frontend/src/App.jsx | 27 +++++++++++++++++---- frontend/src/lib/api.js | 12 ++++++++++ frontend/src/main.jsx | 2 +- frontend/src/pages/Dashboard.jsx | 15 ++++++++++++ frontend/src/pages/Kids.jsx | 41 ++++++++++++++++++++++++++++++++ frontend/src/pages/Login.jsx | 32 +++++++++++++++++++++++++ frontend/src/pages/Register.jsx | 32 +++++++++++++++++++++++++ frontend/src/store/auth.js | 14 +++++++++++ 15 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 backend/models/Kid.js create mode 100644 backend/routes/kids.js create mode 100644 frontend/src/lib/api.js create mode 100644 frontend/src/pages/Dashboard.jsx create mode 100644 frontend/src/pages/Kids.jsx create mode 100644 frontend/src/pages/Login.jsx create mode 100644 frontend/src/pages/Register.jsx create mode 100644 frontend/src/store/auth.js diff --git a/.gitignore b/.gitignore index 3d70248ba2..fac0d22bad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules .env.development.local .env.test.local .env.production.local +backend/.env build diff --git a/backend/models/Kid.js b/backend/models/Kid.js new file mode 100644 index 0000000000..7ae6fe7d4a --- /dev/null +++ b/backend/models/Kid.js @@ -0,0 +1,9 @@ +import mongoose from "mongoose"; + +const kidSchema = new mongoose.Schema({ + name: { type: String, required: true }, + birthdate: { type: Date, required: true }, + owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true } +}); + +export const Kid = mongoose.model("Kid", kidSchema); diff --git a/backend/package.json b/backend/package.json index 60f1bec2f9..bc3b46497e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,7 +19,10 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.18.0", "mongoose": "^8.17.2", - "morgan": "^1.10.1" + "morgan": "^1.10.1", + "react-hook-form": "^7.62.0", + "react-router-dom": "^7.8.1", + "zustand": "^5.0.8" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7684665261..ce92892b4d 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,32 +1,46 @@ import express from "express"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; +import mongoose from "mongoose"; import User from "../models/User.js"; const router = express.Router(); const sign = (id) => jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "7d" }); +// POST /auth/register router.post("/register", async (req, res) => { try { const { email, password } = req.body; + + // basic validation if (!email || !password) return res.status(400).json({ error: "Email and password required" }); + if (password.length < 6) return res.status(400).json({ error: "Password must be ≥ 6 chars" }); - const exists = await User.findOne({ email }); + // unique email + const exists = await User.findOne({ email: email.toLowerCase().trim() }); if (exists) return res.status(409).json({ error: "Email already registered" }); + // hash & save const passwordHash = await bcrypt.hash(password, 10); - const user = await User.create({ email, passwordHash }); + const user = await User.create({ email: email.toLowerCase().trim(), passwordHash }); + // return a token immediately (so the FE can log in) return res.status(201).json({ token: sign(user._id) }); } catch (e) { + // handle duplicate key race condition + if (e.code === 11000) return res.status(409).json({ error: "Email already registered" }); + console.error(e); return res.status(500).json({ error: "Register failed" }); } }); +// POST /auth/login router.post("/login", async (req, res) => { try { const { email, password } = req.body; - const user = await User.findOne({ email }); + if (!email || !password) return res.status(400).json({ error: "Email and password required" }); + + const user = await User.findOne({ email: email.toLowerCase().trim() }); if (!user) return res.status(401).json({ error: "Invalid credentials" }); const ok = await bcrypt.compare(password, user.passwordHash); @@ -34,8 +48,10 @@ router.post("/login", async (req, res) => { return res.json({ token: sign(user._id) }); } catch (e) { + console.error(e); return res.status(500).json({ error: "Login failed" }); } }); + export default router; diff --git a/backend/routes/kids.js b/backend/routes/kids.js new file mode 100644 index 0000000000..92d223b6a7 --- /dev/null +++ b/backend/routes/kids.js @@ -0,0 +1,21 @@ +import express from "express"; +import Kid from "../models/Kid.js"; +import auth from "../middlewares/auth.js"; // your JWT middleware + +const router = express.Router(); + +// GET all kids for logged in user +router.get("/", auth, async (req, res) => { + const kids = await Kid.find({ owner: req.user._id }); + res.json(kids); +}); + +// POST new kid +router.post("/", auth, async (req, res) => { + const { name, birthdate } = req.body; + const kid = new Kid({ name, birthdate, owner: req.user._id }); + await kid.save(); + res.status(201).json(kid); +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 66c10fafd4..7bfdb22c80 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,9 @@ import cors from "cors"; import mongoose from "mongoose"; import morgan from "morgan"; import authRouter from "./routes/auth.js"; +import "./models/User.js"; +import kidsRouter from "./routes/kids.js"; + dotenv.config(); // 1) Load backend/.env @@ -19,6 +22,9 @@ app.use(morgan("dev")); // handy request logs during dev app.use("/auth", authRouter); +app.use("/kids", kidsRouter); + + // 3) Mongo connect (with clear logs) mongoose .connect(MONGO_URL) diff --git a/frontend/package.json b/frontend/package.json index 7b2747e949..332716fb06 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,11 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.62.0", + "react-router-dom": "^7.8.1", + "recharts": "^3.1.2", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a24275e6e..d70463b93f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,27 @@ -export const App = () => { +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { useAuth } from "./store/auth"; +import Login from "./pages/Login"; +import Register from "./pages/Register"; +import Dashboard from "./pages/Dashboard"; +import Kids from "./pages/Kids"; +const Protected = ({ children }) => { + const token = useAuth((s) => s.token); + return token ? children : ; +}; + +const App = () => { return ( - <> -

Welcome to Final Project!

- + + + } /> + } /> + } /> + } /> + } /> + + ); }; + +export default App; diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 0000000000..f7cc9910f4 --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,12 @@ +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; + +export async function api(path, options = {}) { + const token = localStorage.getItem("token"); + const headers = new Headers(options.headers || {}); + headers.set("Content-Type", "application/json"); + if (token) headers.set("Authorization", `Bearer ${token}`); + + const res = await fetch(`${API_URL}${path}`, { ...options, headers }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 51294f3998..f18b0f0d14 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { App } from "./App.jsx"; +import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000000..38f0cec2ea --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,15 @@ +import { useAuth } from "../store/auth"; + +const Dashboard = () => { + const logout = useAuth((s) => s.logout); + + return ( +
+

Dashboard

+

🎉 You are logged in.

+ +
+ ); +}; + +export default Dashboard; diff --git a/frontend/src/pages/Kids.jsx b/frontend/src/pages/Kids.jsx new file mode 100644 index 0000000000..58a9172de6 --- /dev/null +++ b/frontend/src/pages/Kids.jsx @@ -0,0 +1,41 @@ +import { useState, useEffect } from "react"; +import { api } from "../lib/api"; + +const Kids = () => { + const [kids, setKids] = useState([]); + const [name, setName] = useState(""); + const [birthdate, setBirthdate] = useState(""); + + useEffect(() => { + api("/kids").then(setKids).catch(console.error); + }, []); + + const addKid = async (e) => { + e.preventDefault(); + const kid = await api("/kids", { + method: "POST", + body: JSON.stringify({ name, birthdate }) + }); + setKids([...kids, kid]); + setName(""); setBirthdate(""); + }; + + return ( +
+

Kids

+
+ setName(e.target.value)} placeholder="Name" required /> + setBirthdate(e.target.value)} type="date" required /> + +
+ +
    + {kids.map(k => ( +
  • {k.name} (🎂 {new Date(k.birthdate).toLocaleDateString()})
  • + ))} +
+
+ ); +}; + +export default Kids; diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000000..519d9d330a --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,32 @@ +import { useForm } from "react-hook-form"; +import { useAuth } from "../store/auth"; +import { api } from "../lib/api"; +import { Link, useNavigate } from "react-router-dom"; + +const Login = () => { + const { register, handleSubmit } = useForm(); + const setToken = useAuth((s) => s.setToken); + const nav = useNavigate(); + + const onSubmit = async (data) => { + const res = await api("/auth/login", { method: "POST", body: JSON.stringify(data) }); + setToken(res.token); + nav("/"); + }; + + return ( +
+

Login

+
+ + + + + +
+

No account? Register

+
+ ); +}; + +export default Login; diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000000..457bf78b4a --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,32 @@ +import { useForm } from "react-hook-form"; +import { useAuth } from "../store/auth"; +import { api } from "../lib/api"; +import { Link, useNavigate } from "react-router-dom"; + +const Register = () => { + const { register, handleSubmit } = useForm(); + const setToken = useAuth((s) => s.setToken); + const nav = useNavigate(); + + const onSubmit = async (data) => { + const res = await api("/auth/register", { method: "POST", body: JSON.stringify(data) }); + setToken(res.token); + nav("/"); + }; + + return ( +
+

Create account

+
+ + + + + +
+

Already have an account? Login

+
+ ); +}; + +export default Register; diff --git a/frontend/src/store/auth.js b/frontend/src/store/auth.js new file mode 100644 index 0000000000..bacd51c2a2 --- /dev/null +++ b/frontend/src/store/auth.js @@ -0,0 +1,14 @@ +import { create } from "zustand"; + +export const useAuth = create((set) => ({ + token: localStorage.getItem("token"), + setToken: (t) => { + if (t) localStorage.setItem("token", t); + else localStorage.removeItem("token"); + set({ token: t }); + }, + logout: () => { + localStorage.removeItem("token"); + set({ token: null }); + }, +})); From caca83c48c8c8b0470e31215cedaa0661fbc8a5b Mon Sep 17 00:00:00 2001 From: VariaSlu Date: Sat, 23 Aug 2025 17:57:35 +0200 Subject: [PATCH 04/12] added iteems --- backend/models/Item.js | 16 +++++++ backend/routes/items.js | 38 +++++++++++++++ backend/server.js | 6 ++- frontend/src/App.jsx | 9 +++- frontend/src/components/Nav.jsx | 32 +++++++++++++ frontend/src/lib/api.js | 2 +- frontend/src/pages/Items.jsx | 84 +++++++++++++++++++++++++++++++++ 7 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 backend/models/Item.js create mode 100644 backend/routes/items.js create mode 100644 frontend/src/components/Nav.jsx create mode 100644 frontend/src/pages/Items.jsx diff --git a/backend/models/Item.js b/backend/models/Item.js new file mode 100644 index 0000000000..ff8ac58a1e --- /dev/null +++ b/backend/models/Item.js @@ -0,0 +1,16 @@ +import mongoose from "mongoose"; + +const itemSchema = new mongoose.Schema( + { + userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true }, + childId: { type: mongoose.Schema.Types.ObjectId, ref: "Kid", required: true }, + type: { type: String, enum: ["jacket", "pants", "boots", "hat", "top", "gloves", "other"], default: "other" }, + size: { type: String, required: true }, + season: { type: String, enum: ["winter", "spring", "summer", "autumn", "all"], default: "all" }, + status: { type: String, enum: ["current", "needed", "stored", "to-sell"], default: "current" }, + notes: String + }, + { timestamps: true } +); + +export default mongoose.model("Item", itemSchema); diff --git a/backend/routes/items.js b/backend/routes/items.js new file mode 100644 index 0000000000..19b566f4e0 --- /dev/null +++ b/backend/routes/items.js @@ -0,0 +1,38 @@ +import express from "express"; +import Item from "../models/Item.js"; +import auth from "../middlewares/auth.js"; + +const router = express.Router(); + +// list items for user +router.get("/", auth, async (req, res) => { + const items = await Item.find({ userId: req.user.id }).lean(); + res.json(items); +}); + +// create +router.post("/", auth, async (req, res) => { + const { childId, type, size, season, status, notes } = req.body; + const item = await Item.create({ userId: req.user.id, childId, type, size, season, status, notes }); + res.status(201).json(item); +}); + +// update +router.patch("/:id", auth, async (req, res) => { + const item = await Item.findOneAndUpdate( + { _id: req.params.id, userId: req.user.id }, + req.body, + { new: true } + ); + if (!item) return res.status(404).json({ error: "Not found" }); + res.json(item); +}); + +// delete +router.delete("/:id", auth, async (req, res) => { + const ok = await Item.deleteOne({ _id: req.params.id, userId: req.user.id }); + if (!ok.deletedCount) return res.status(404).json({ error: "Not found" }); + res.json({ ok: true }); +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 7bfdb22c80..d84fa9f7c5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -6,13 +6,14 @@ import morgan from "morgan"; import authRouter from "./routes/auth.js"; import "./models/User.js"; import kidsRouter from "./routes/kids.js"; +import itemsRouter from "./routes/items.js"; dotenv.config(); // 1) Load backend/.env const app = express(); -const PORT = process.env.PORT || 8080; +const PORT = process.env.PORT || 8081; const MONGO_URL = process.env.MONGO_URL || "mongodb://localhost/final-project"; // 2) Middleware @@ -24,6 +25,9 @@ app.use("/auth", authRouter); app.use("/kids", kidsRouter); +app.use("/items", itemsRouter); + + // 3) Mongo connect (with clear logs) mongoose diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d70463b93f..b9195b94ed 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,6 +4,9 @@ import Login from "./pages/Login"; import Register from "./pages/Register"; import Dashboard from "./pages/Dashboard"; import Kids from "./pages/Kids"; +import Items from "./pages/Items"; +import Nav from "./components/Nav"; + const Protected = ({ children }) => { const token = useAuth((s) => s.token); @@ -13,12 +16,16 @@ const Protected = ({ children }) => { const App = () => { return ( +