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 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-04-22 08:16:51 +10:00
parent 42f0587cf6
commit 0d42d97d70
15 changed files with 304 additions and 94 deletions

View file

@ -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,6 +23,8 @@ function App() {
<Toaster position="top-right" />
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route element={<PrivateRoute />}>
<Route path="/" element={<Layout />}>
<Route index element={<Navigate to="/analytics" replace />} />
<Route path="analytics" element={<Analytics />} />
@ -31,6 +35,7 @@ function App() {
<Route path="expenses" element={<Expenses />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
</div>
</Router>

View file

@ -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 ? <Outlet /> : <Navigate to="/login" replace />;
};
export default PrivateRoute;

View file

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
@ -6,30 +31,42 @@ const Login = () => {
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Or{' '}
<Link to="/register" className="font-medium text-indigo-600 hover:text-indigo-500">
create a new account
</Link>
</p>
</div>
<form className="mt-8 space-y-6">
<div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">{error}</div>
)}
<div className="space-y-4">
<input
type="email"
required
value={email}
onChange={(e) => 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"
/>
</div>
<div>
<input
type="password"
required
value={password}
onChange={(e) => 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"
/>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
Sign in
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
</form>
</div>
</div>

View file

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{' '}
<Link to="/login" className="font-medium text-indigo-600 hover:text-indigo-500">
Sign in
</Link>
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">{error}</div>
)}
<div className="space-y-4">
<input
type="text"
required
value={name}
onChange={(e) => 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"
/>
<input
type="email"
required
value={email}
onChange={(e) => 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"
/>
<input
type="password"
required
minLength={8}
value={password}
onChange={(e) => 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"
/>
</div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Create account'}
</button>
</form>
</div>
</div>
);
};
export default Register;

View file

@ -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<string>) => {
state.loading = false;
@ -40,6 +51,8 @@ const authSlice = createSlice({
state.user = null;
state.token = null;
state.error = null;
localStorage.removeItem('token');
localStorage.removeItem('user');
},
},
});

27
client/src/utils/api.ts Normal file
View file

@ -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;

View file

@ -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 },
});

View file

@ -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 },
});

View file

@ -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 }
});

View file

@ -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 }
});

View file

@ -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 });

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {