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:
parent
42f0587cf6
commit
0d42d97d70
15 changed files with 304 additions and 94 deletions
|
|
@ -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() {
|
|||
<Toaster position="top-right" />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Navigate to="/analytics" replace />} />
|
||||
<Route path="analytics" element={<Analytics />} />
|
||||
<Route path="profit-analysis" element={<ProfitAnalysis />} />
|
||||
<Route path="products" element={<Products />} />
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="data-import" element={<DataImport />} />
|
||||
<Route path="expenses" element={<Expenses />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Navigate to="/analytics" replace />} />
|
||||
<Route path="analytics" element={<Analytics />} />
|
||||
<Route path="profit-analysis" element={<ProfitAnalysis />} />
|
||||
<Route path="products" element={<Products />} />
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="data-import" element={<DataImport />} />
|
||||
<Route path="expenses" element={<Expenses />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</div>
|
||||
|
|
|
|||
10
client/src/components/PrivateRoute.tsx
Normal file
10
client/src/components/PrivateRoute.tsx
Normal 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;
|
||||
|
|
@ -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"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</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 ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
86
client/src/pages/Register.tsx
Normal file
86
client/src/pages/Register.tsx
Normal 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;
|
||||
|
|
@ -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
27
client/src/utils/api.ts
Normal 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;
|
||||
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue