From 0d42d97d70ea78b5730f8ed4b220af215d7e160b Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Wed, 22 Apr 2026 08:16:51 +1000 Subject: [PATCH] Add JWT auth with protected routes and per-user data isolation Frontend: - Login and Register pages wired up to API - PrivateRoute redirects unauthenticated users to /login - Token persisted in localStorage, restored on page load - Axios instance automatically attaches Bearer token, redirects on 401 Backend: - userId field added to all models (Product, Order, Customer, Expense) - All queries scoped to authenticated user's userId - Register/login return JWT token Co-Authored-By: Claude Sonnet 4.6 --- client/src/App.tsx | 25 +++++--- client/src/components/PrivateRoute.tsx | 10 +++ client/src/pages/Login.tsx | 63 +++++++++++++++---- client/src/pages/Register.tsx | 86 ++++++++++++++++++++++++++ client/src/store/slices/authSlice.ts | 25 ++++++-- client/src/utils/api.ts | 27 ++++++++ server/src/models/Customer.ts | 2 + server/src/models/Expense.ts | 2 + server/src/models/Order.ts | 2 + server/src/models/Product.ts | 2 + server/src/routes/analytics.ts | 44 ++++++------- server/src/routes/customers.ts | 25 +++++--- server/src/routes/expenses.ts | 27 ++++---- server/src/routes/orders.ts | 31 ++++++---- server/src/routes/products.ts | 27 ++++---- 15 files changed, 304 insertions(+), 94 deletions(-) create mode 100644 client/src/components/PrivateRoute.tsx create mode 100644 client/src/pages/Register.tsx create mode 100644 client/src/utils/api.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 51aff2d..fbc1846 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,6 +3,7 @@ import { Provider } from 'react-redux'; import { Toaster } from 'react-hot-toast'; import { store } from './store'; import Layout from './components/Layout'; +import PrivateRoute from './components/PrivateRoute'; import Products from './pages/Products'; import Orders from './pages/Orders'; import Analytics from './pages/Analytics'; @@ -11,6 +12,7 @@ import Expenses from './pages/Expenses'; import Settings from './pages/Settings'; import DataImport from './pages/DataImport'; import Login from './pages/Login'; +import Register from './pages/Register'; import './App.css'; function App() { @@ -21,15 +23,18 @@ function App() { } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + @@ -38,4 +43,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/client/src/components/PrivateRoute.tsx b/client/src/components/PrivateRoute.tsx new file mode 100644 index 0000000..b1afdad --- /dev/null +++ b/client/src/components/PrivateRoute.tsx @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux'; +import { Navigate, Outlet } from 'react-router-dom'; +import { RootState } from '../store'; + +const PrivateRoute = () => { + const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated); + return isAuthenticated ? : ; +}; + +export default PrivateRoute; diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 1918fd5..76e7b90 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -1,4 +1,29 @@ +import { useState, FormEvent } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { loginStart, loginSuccess, loginFailure } from '../store/slices/authSlice'; +import { RootState } from '../store'; +import api from '../utils/api'; + const Login = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { loading, error } = useSelector((state: RootState) => state.auth); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + dispatch(loginStart()); + try { + const { data } = await api.post('/auth/login', { email, password }); + dispatch(loginSuccess(data)); + navigate('/'); + } catch (err: any) { + dispatch(loginFailure(err.response?.data?.message || 'Login failed')); + } + }; + return (
@@ -6,34 +31,46 @@ const Login = () => {

Sign in to your account

+

+ Or{' '} + + create a new account + +

-
-
+ + {error && ( +
{error}
+ )} +
setEmail(e.target.value)} placeholder="Email address" className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> -
-
setPassword(e.target.value)} placeholder="Password" className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
-
- -
+
); }; -export default Login; \ No newline at end of file +export default Login; diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx new file mode 100644 index 0000000..0e7a799 --- /dev/null +++ b/client/src/pages/Register.tsx @@ -0,0 +1,86 @@ +import { useState, FormEvent } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { loginStart, loginSuccess, loginFailure } from '../store/slices/authSlice'; +import { RootState } from '../store'; +import api from '../utils/api'; + +const Register = () => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { loading, error } = useSelector((state: RootState) => state.auth); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + dispatch(loginStart()); + try { + const { data } = await api.post('/auth/register', { name, email, password }); + dispatch(loginSuccess(data)); + navigate('/'); + } catch (err: any) { + dispatch(loginFailure(err.response?.data?.message || 'Registration failed')); + } + }; + + return ( +
+
+
+

+ Create your account +

+

+ Already have an account?{' '} + + Sign in + +

+
+
+ {error && ( +
{error}
+ )} +
+ setName(e.target.value)} + placeholder="Full name" + className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> + setEmail(e.target.value)} + placeholder="Email address" + className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> + setPassword(e.target.value)} + placeholder="Password (min 8 characters)" + className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> +
+ +
+
+
+ ); +}; + +export default Register; diff --git a/client/src/store/slices/authSlice.ts b/client/src/store/slices/authSlice.ts index a1dfafc..7f704b5 100644 --- a/client/src/store/slices/authSlice.ts +++ b/client/src/store/slices/authSlice.ts @@ -1,17 +1,26 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +interface User { + id: string; + name: string; + email: string; +} + interface AuthState { isAuthenticated: boolean; - user: any | null; + user: User | null; token: string | null; loading: boolean; error: string | null; } +const storedToken = localStorage.getItem('token'); +const storedUser = localStorage.getItem('user'); + const initialState: AuthState = { - isAuthenticated: false, - user: null, - token: null, + isAuthenticated: !!storedToken, + user: storedUser ? JSON.parse(storedUser) : null, + token: storedToken, loading: false, error: null, }; @@ -24,12 +33,14 @@ const authSlice = createSlice({ state.loading = true; state.error = null; }, - loginSuccess: (state, action: PayloadAction<{ user: any; token: string }>) => { + loginSuccess: (state, action: PayloadAction<{ user: User; token: string }>) => { state.isAuthenticated = true; state.user = action.payload.user; state.token = action.payload.token; state.loading = false; state.error = null; + localStorage.setItem('token', action.payload.token); + localStorage.setItem('user', JSON.stringify(action.payload.user)); }, loginFailure: (state, action: PayloadAction) => { state.loading = false; @@ -40,9 +51,11 @@ const authSlice = createSlice({ state.user = null; state.token = null; state.error = null; + localStorage.removeItem('token'); + localStorage.removeItem('user'); }, }, }); export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions; -export default authSlice.reducer; \ No newline at end of file +export default authSlice.reducer; diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts new file mode 100644 index 0000000..ea435db --- /dev/null +++ b/client/src/utils/api.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api', +}); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default api; diff --git a/server/src/models/Customer.ts b/server/src/models/Customer.ts index 0ed8eff..27e25d2 100644 --- a/server/src/models/Customer.ts +++ b/server/src/models/Customer.ts @@ -14,6 +14,7 @@ export interface ICustomer extends Document { }; totalOrders: number; totalSpent: number; + userId: mongoose.Types.ObjectId; dateCreated: Date; dateUpdated: Date; } @@ -32,6 +33,7 @@ const CustomerSchema: Schema = new Schema({ }, totalOrders: { type: Number, default: 0 }, totalSpent: { type: Number, default: 0 }, + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, dateCreated: { type: Date, default: Date.now }, dateUpdated: { type: Date, default: Date.now }, }); diff --git a/server/src/models/Expense.ts b/server/src/models/Expense.ts index 8990fcf..4a95142 100644 --- a/server/src/models/Expense.ts +++ b/server/src/models/Expense.ts @@ -7,6 +7,7 @@ export interface IExpense extends Document { date: Date; receiptUrl?: string; notes?: string; + userId: mongoose.Types.ObjectId; dateCreated: Date; dateUpdated: Date; } @@ -22,6 +23,7 @@ const ExpenseSchema: Schema = new Schema({ date: { type: Date, required: true }, receiptUrl: { type: String }, notes: { type: String }, + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, dateCreated: { type: Date, default: Date.now }, dateUpdated: { type: Date, default: Date.now }, }); diff --git a/server/src/models/Order.ts b/server/src/models/Order.ts index 340c0b9..66ec14f 100644 --- a/server/src/models/Order.ts +++ b/server/src/models/Order.ts @@ -33,6 +33,7 @@ export interface IOrder extends Document { dateOrdered: Date; dateShipped?: Date; dateDelivered?: Date; + userId: mongoose.Types.ObjectId; dateCreated: Date; dateUpdated: Date; } @@ -78,6 +79,7 @@ const OrderSchema: Schema = new Schema({ dateOrdered: { type: Date, required: true }, dateShipped: { type: Date }, dateDelivered: { type: Date }, + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, dateCreated: { type: Date, default: Date.now }, dateUpdated: { type: Date, default: Date.now } }); diff --git a/server/src/models/Product.ts b/server/src/models/Product.ts index aeab547..02b2997 100644 --- a/server/src/models/Product.ts +++ b/server/src/models/Product.ts @@ -29,6 +29,7 @@ export interface IProduct extends Document { materials: string[]; isActive: boolean; etsyListingId?: string; + userId: mongoose.Types.ObjectId; dateCreated: Date; dateUpdated: Date; } @@ -62,6 +63,7 @@ const ProductSchema: Schema = new Schema({ materials: [{ type: String }], isActive: { type: Boolean, default: true }, etsyListingId: { type: String }, + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, dateCreated: { type: Date, default: Date.now }, dateUpdated: { type: Date, default: Date.now } }); diff --git a/server/src/routes/analytics.ts b/server/src/routes/analytics.ts index 9d0ffa4..2a7d11e 100644 --- a/server/src/routes/analytics.ts +++ b/server/src/routes/analytics.ts @@ -1,40 +1,42 @@ -import { Router, Request, Response } from 'express'; +import { Router, Response } from 'express'; +import mongoose from 'mongoose'; import Order from '../models/Order'; import Product from '../models/Product'; import Customer from '../models/Customer'; import Expense from '../models/Expense'; +import { AuthRequest } from '../middleware/authenticate'; const router = Router(); -router.get('/dashboard', async (req: Request, res: Response) => { +router.get('/dashboard', async (req: AuthRequest, res: Response) => { try { + const userId = new mongoose.Types.ObjectId(req.userId); + const [totalOrders, totalProducts, totalCustomers, revenueResult, expenseResult, recentOrders] = await Promise.all([ - Order.countDocuments({ paymentStatus: 'paid' }), - Product.countDocuments({ isActive: true }), - Customer.countDocuments(), + Order.countDocuments({ userId, paymentStatus: 'paid' }), + Product.countDocuments({ userId, isActive: true }), + Customer.countDocuments({ userId }), Order.aggregate([ - { $match: { paymentStatus: 'paid' } }, + { $match: { userId, paymentStatus: 'paid' } }, { $group: { _id: null, total: { $sum: '$total' } } }, ]), Expense.aggregate([ + { $match: { userId } }, { $group: { _id: null, total: { $sum: '$amount' } } }, ]), - Order.find({ paymentStatus: 'paid' }) + Order.find({ userId, paymentStatus: 'paid' }) .populate('customerId', 'name email') .sort({ dateOrdered: -1 }) .limit(5), ]); - const totalRevenue = revenueResult[0]?.total ?? 0; - const totalExpenses = expenseResult[0]?.total ?? 0; - const twelveMonthsAgo = new Date(); twelveMonthsAgo.setMonth(twelveMonthsAgo.getMonth() - 11); twelveMonthsAgo.setDate(1); twelveMonthsAgo.setHours(0, 0, 0, 0); const salesChart = await Order.aggregate([ - { $match: { paymentStatus: 'paid', dateOrdered: { $gte: twelveMonthsAgo } } }, + { $match: { userId, paymentStatus: 'paid', dateOrdered: { $gte: twelveMonthsAgo } } }, { $group: { _id: { year: { $year: '$dateOrdered' }, month: { $month: '$dateOrdered' } }, @@ -46,8 +48,8 @@ router.get('/dashboard', async (req: Request, res: Response) => { ]); res.json({ - totalRevenue, - totalExpenses, + totalRevenue: revenueResult[0]?.total ?? 0, + totalExpenses: expenseResult[0]?.total ?? 0, totalOrders, totalProducts, totalCustomers, @@ -59,10 +61,11 @@ router.get('/dashboard', async (req: Request, res: Response) => { } }); -router.get('/sales', async (req: Request, res: Response) => { +router.get('/sales', async (req: AuthRequest, res: Response) => { try { + const userId = new mongoose.Types.ObjectId(req.userId); const { from, to } = req.query; - const match: any = { paymentStatus: 'paid' }; + const match: any = { userId, paymentStatus: 'paid' }; if (from || to) { match.dateOrdered = {}; if (from) match.dateOrdered.$gte = new Date(from as string); @@ -87,10 +90,11 @@ router.get('/sales', async (req: Request, res: Response) => { } }); -router.get('/products', async (req: Request, res: Response) => { +router.get('/products', async (req: AuthRequest, res: Response) => { try { + const userId = new mongoose.Types.ObjectId(req.userId); const topProducts = await Order.aggregate([ - { $match: { paymentStatus: 'paid' } }, + { $match: { userId, paymentStatus: 'paid' } }, { $unwind: '$items' }, { $group: { @@ -103,20 +107,18 @@ router.get('/products', async (req: Request, res: Response) => { { $sort: { totalRevenue: -1 } }, { $limit: 10 }, ]); - res.json(topProducts); } catch (err) { res.status(500).json({ message: 'Failed to fetch product analytics', error: err }); } }); -router.get('/customers', async (req: Request, res: Response) => { +router.get('/customers', async (req: AuthRequest, res: Response) => { try { - const topCustomers = await Customer.find() + const topCustomers = await Customer.find({ userId: req.userId }) .sort({ totalSpent: -1 }) .limit(10) .select('name email totalOrders totalSpent'); - res.json(topCustomers); } catch (err) { res.status(500).json({ message: 'Failed to fetch customer analytics', error: err }); diff --git a/server/src/routes/customers.ts b/server/src/routes/customers.ts index 664de29..7b6f7c7 100644 --- a/server/src/routes/customers.ts +++ b/server/src/routes/customers.ts @@ -1,25 +1,26 @@ -import { Router, Request, Response } from 'express'; +import { Router, Response } from 'express'; import Customer from '../models/Customer'; +import { AuthRequest } from '../middleware/authenticate'; const router = Router(); -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (req: AuthRequest, res: Response) => { try { const { page = 1, limit = 20 } = req.query; - const customers = await Customer.find() + const customers = await Customer.find({ userId: req.userId }) .sort({ dateCreated: -1 }) .limit(Number(limit)) .skip((Number(page) - 1) * Number(limit)); - const total = await Customer.countDocuments(); + const total = await Customer.countDocuments({ userId: req.userId }); res.json({ customers, total, page: Number(page), limit: Number(limit) }); } catch (err) { res.status(500).json({ message: 'Failed to fetch customers', error: err }); } }); -router.post('/', async (req: Request, res: Response) => { +router.post('/', async (req: AuthRequest, res: Response) => { try { - const customer = new Customer(req.body); + const customer = new Customer({ ...req.body, userId: req.userId }); await customer.save(); res.status(201).json(customer); } catch (err) { @@ -27,9 +28,9 @@ router.post('/', async (req: Request, res: Response) => { } }); -router.get('/:id', async (req: Request, res: Response) => { +router.get('/:id', async (req: AuthRequest, res: Response) => { try { - const customer = await Customer.findById(req.params.id); + const customer = await Customer.findOne({ _id: req.params.id, userId: req.userId }); if (!customer) return res.status(404).json({ message: 'Customer not found' }); res.json(customer); } catch (err) { @@ -37,9 +38,13 @@ router.get('/:id', async (req: Request, res: Response) => { } }); -router.put('/:id', async (req: Request, res: Response) => { +router.put('/:id', async (req: AuthRequest, res: Response) => { try { - const customer = await Customer.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + const customer = await Customer.findOneAndUpdate( + { _id: req.params.id, userId: req.userId }, + req.body, + { new: true, runValidators: true } + ); if (!customer) return res.status(404).json({ message: 'Customer not found' }); res.json(customer); } catch (err) { diff --git a/server/src/routes/expenses.ts b/server/src/routes/expenses.ts index 252f4ca..bcab618 100644 --- a/server/src/routes/expenses.ts +++ b/server/src/routes/expenses.ts @@ -1,12 +1,13 @@ -import { Router, Request, Response } from 'express'; +import { Router, Response } from 'express'; import Expense from '../models/Expense'; +import { AuthRequest } from '../middleware/authenticate'; const router = Router(); -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (req: AuthRequest, res: Response) => { try { const { page = 1, limit = 20, category } = req.query; - const filter: any = {}; + const filter: any = { userId: req.userId }; if (category) filter.category = category; const expenses = await Expense.find(filter) @@ -21,9 +22,9 @@ router.get('/', async (req: Request, res: Response) => { } }); -router.post('/', async (req: Request, res: Response) => { +router.post('/', async (req: AuthRequest, res: Response) => { try { - const expense = new Expense(req.body); + const expense = new Expense({ ...req.body, userId: req.userId }); await expense.save(); res.status(201).json(expense); } catch (err) { @@ -31,9 +32,9 @@ router.post('/', async (req: Request, res: Response) => { } }); -router.get('/:id', async (req: Request, res: Response) => { +router.get('/:id', async (req: AuthRequest, res: Response) => { try { - const expense = await Expense.findById(req.params.id); + const expense = await Expense.findOne({ _id: req.params.id, userId: req.userId }); if (!expense) return res.status(404).json({ message: 'Expense not found' }); res.json(expense); } catch (err) { @@ -41,9 +42,13 @@ router.get('/:id', async (req: Request, res: Response) => { } }); -router.put('/:id', async (req: Request, res: Response) => { +router.put('/:id', async (req: AuthRequest, res: Response) => { try { - const expense = await Expense.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + const expense = await Expense.findOneAndUpdate( + { _id: req.params.id, userId: req.userId }, + req.body, + { new: true, runValidators: true } + ); if (!expense) return res.status(404).json({ message: 'Expense not found' }); res.json(expense); } catch (err) { @@ -51,9 +56,9 @@ router.put('/:id', async (req: Request, res: Response) => { } }); -router.delete('/:id', async (req: Request, res: Response) => { +router.delete('/:id', async (req: AuthRequest, res: Response) => { try { - const expense = await Expense.findByIdAndDelete(req.params.id); + const expense = await Expense.findOneAndDelete({ _id: req.params.id, userId: req.userId }); if (!expense) return res.status(404).json({ message: 'Expense not found' }); res.json({ message: 'Expense deleted' }); } catch (err) { diff --git a/server/src/routes/orders.ts b/server/src/routes/orders.ts index e7eb909..ec5ff79 100644 --- a/server/src/routes/orders.ts +++ b/server/src/routes/orders.ts @@ -1,13 +1,14 @@ -import { Router, Request, Response } from 'express'; +import { Router, Response } from 'express'; import Order from '../models/Order'; import Customer from '../models/Customer'; +import { AuthRequest } from '../middleware/authenticate'; const router = Router(); -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (req: AuthRequest, res: Response) => { try { const { page = 1, limit = 20, status, paymentStatus } = req.query; - const filter: any = {}; + const filter: any = { userId: req.userId }; if (status) filter.status = status; if (paymentStatus) filter.paymentStatus = paymentStatus; @@ -24,14 +25,15 @@ router.get('/', async (req: Request, res: Response) => { } }); -router.post('/', async (req: Request, res: Response) => { +router.post('/', async (req: AuthRequest, res: Response) => { try { - const order = new Order(req.body); + const order = new Order({ ...req.body, userId: req.userId }); await order.save(); - await Customer.findByIdAndUpdate(order.customerId, { - $inc: { totalOrders: 1, totalSpent: order.total }, - }); + await Customer.findOneAndUpdate( + { _id: order.customerId, userId: req.userId }, + { $inc: { totalOrders: 1, totalSpent: order.total } } + ); res.status(201).json(order); } catch (err) { @@ -39,9 +41,10 @@ router.post('/', async (req: Request, res: Response) => { } }); -router.get('/:id', async (req: Request, res: Response) => { +router.get('/:id', async (req: AuthRequest, res: Response) => { try { - const order = await Order.findById(req.params.id).populate('customerId', 'name email'); + const order = await Order.findOne({ _id: req.params.id, userId: req.userId }) + .populate('customerId', 'name email'); if (!order) return res.status(404).json({ message: 'Order not found' }); res.json(order); } catch (err) { @@ -49,9 +52,13 @@ router.get('/:id', async (req: Request, res: Response) => { } }); -router.put('/:id', async (req: Request, res: Response) => { +router.put('/:id', async (req: AuthRequest, res: Response) => { try { - const order = await Order.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + const order = await Order.findOneAndUpdate( + { _id: req.params.id, userId: req.userId }, + req.body, + { new: true, runValidators: true } + ); if (!order) return res.status(404).json({ message: 'Order not found' }); res.json(order); } catch (err) { diff --git a/server/src/routes/products.ts b/server/src/routes/products.ts index 98361a7..894bbf1 100644 --- a/server/src/routes/products.ts +++ b/server/src/routes/products.ts @@ -1,12 +1,13 @@ -import { Router, Request, Response } from 'express'; +import { Router, Response } from 'express'; import Product from '../models/Product'; +import { AuthRequest } from '../middleware/authenticate'; const router = Router(); -router.get('/', async (req: Request, res: Response) => { +router.get('/', async (req: AuthRequest, res: Response) => { try { const { page = 1, limit = 20, category, active } = req.query; - const filter: any = {}; + const filter: any = { userId: req.userId }; if (category) filter.category = category; if (active !== undefined) filter.isActive = active === 'true'; @@ -22,9 +23,9 @@ router.get('/', async (req: Request, res: Response) => { } }); -router.post('/', async (req: Request, res: Response) => { +router.post('/', async (req: AuthRequest, res: Response) => { try { - const product = new Product(req.body); + const product = new Product({ ...req.body, userId: req.userId }); await product.save(); res.status(201).json(product); } catch (err) { @@ -32,9 +33,9 @@ router.post('/', async (req: Request, res: Response) => { } }); -router.get('/:id', async (req: Request, res: Response) => { +router.get('/:id', async (req: AuthRequest, res: Response) => { try { - const product = await Product.findById(req.params.id); + const product = await Product.findOne({ _id: req.params.id, userId: req.userId }); if (!product) return res.status(404).json({ message: 'Product not found' }); res.json(product); } catch (err) { @@ -42,9 +43,13 @@ router.get('/:id', async (req: Request, res: Response) => { } }); -router.put('/:id', async (req: Request, res: Response) => { +router.put('/:id', async (req: AuthRequest, res: Response) => { try { - const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + const product = await Product.findOneAndUpdate( + { _id: req.params.id, userId: req.userId }, + req.body, + { new: true, runValidators: true } + ); if (!product) return res.status(404).json({ message: 'Product not found' }); res.json(product); } catch (err) { @@ -52,9 +57,9 @@ router.put('/:id', async (req: Request, res: Response) => { } }); -router.delete('/:id', async (req: Request, res: Response) => { +router.delete('/:id', async (req: AuthRequest, res: Response) => { try { - const product = await Product.findByIdAndDelete(req.params.id); + const product = await Product.findOneAndDelete({ _id: req.params.id, userId: req.userId }); if (!product) return res.status(404).json({ message: 'Product not found' }); res.json({ message: 'Product deleted' }); } catch (err) {