Fix: stock update override, server-side validation, and admin security improvements
This commit is contained in:
1 parent
beb730cb6c
commit
92792cb2de
3 files changed
+99
-28
No files matched your search
+78
-14
@@ -38,11 +38,12 @@ app.get('/api/data', async (req, res) => {
|
|||||||
for (const item of stock) {
|
for (const item of stock) {
|
||||||
if (item.maxStock !== null && item.maxStock !== undefined) {
|
if (item.maxStock !== null && item.maxStock !== undefined) {
|
||||||
const ordered = orderedQtyById[item.id] || 0;
|
const ordered = orderedQtyById[item.id] || 0;
|
||||||
const shouldBeAvailable = ordered < item.maxStock;
|
const isOutOfStock = ordered >= item.maxStock;
|
||||||
if (item.available !== shouldBeAvailable) {
|
// Only auto-disable if it's currently marked as available but is actually out of stock
|
||||||
|
if (isOutOfStock && item.available) {
|
||||||
await prisma.stockItem.update({
|
await prisma.stockItem.update({
|
||||||
where: { id: item.id },
|
where: { id: item.id },
|
||||||
data: { available: shouldBeAvailable }
|
data: { available: false }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,38 +61,101 @@ app.get('/api/data', async (req, res) => {
|
|||||||
: null
|
: null
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ stock: stockWithRemaining, orders: parsedOrders, config });
|
// Security: Don't send the admin code to the client
|
||||||
|
const configResponse = config ? { ...config, adminCode: undefined } : null;
|
||||||
|
|
||||||
|
res.json({ stock: stockWithRemaining, orders: parsedOrders, config: configResponse });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).json({ error: 'Failed to fetch data' });
|
res.status(500).json({ error: 'Failed to fetch data' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin Login
|
||||||
|
app.post('/api/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { code } = req.body;
|
||||||
|
const config = await prisma.config.findFirst();
|
||||||
|
if (config && config.adminCode === code) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ error: 'Invalid admin code' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Login failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Submit a new order
|
// Submit a new order
|
||||||
app.post('/api/orders', async (req, res) => {
|
app.post('/api/orders', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, phone, items, totalWeight, estimatedTotal } = req.body;
|
const { name, phone, items, totalWeight, estimatedTotal } = req.body;
|
||||||
|
|
||||||
// Generate simple order number
|
// 1. Validate Stock Availability
|
||||||
const lastOrder = await prisma.order.findFirst({ orderBy: { id: 'desc' } });
|
const stockItems = await prisma.stockItem.findMany();
|
||||||
const nextId = (lastOrder?.id || 0) + 1;
|
const allOrders = await prisma.order.findMany();
|
||||||
const orderNumber = `#${nextId + 1000}`;
|
|
||||||
|
|
||||||
|
const orderedQtyById: Record<number, number> = {};
|
||||||
|
for (const order of allOrders) {
|
||||||
|
const parsedItems = JSON.parse(order.items);
|
||||||
|
for (const item of parsedItems) {
|
||||||
|
orderedQtyById[item.id] = (orderedQtyById[item.id] || 0) + (item.qty || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let computedTotalWeight = 0;
|
||||||
|
let computedEstimatedTotal = 0;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const dbItem = stockItems.find(s => s.id === item.id);
|
||||||
|
if (!dbItem) throw new Error(`Item ${item.name} not found`);
|
||||||
|
if (!dbItem.available) throw new Error(`Sorry, ${dbItem.name} has just sold out!`);
|
||||||
|
|
||||||
|
if (dbItem.maxStock !== null && dbItem.maxStock !== undefined) {
|
||||||
|
const alreadyOrdered = orderedQtyById[dbItem.id] || 0;
|
||||||
|
if (alreadyOrdered + item.qty > dbItem.maxStock) {
|
||||||
|
throw new Error(`Not enough stock for ${dbItem.name}. Only ${Math.max(0, dbItem.maxStock - alreadyOrdered)}${dbItem.unit} remaining.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate prices server-side
|
||||||
|
const getPrice = (stockItem: any, qty: number) => {
|
||||||
|
if (stockItem.bulk2Threshold > 0 && qty >= stockItem.bulk2Threshold && stockItem.bulk2Price > 0) return stockItem.bulk2Price;
|
||||||
|
if (stockItem.bulk1Threshold > 0 && qty >= stockItem.bulk1Threshold && stockItem.bulk1Price > 0) return stockItem.bulk1Price;
|
||||||
|
return stockItem.price;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalPrice = getPrice(dbItem, item.qty);
|
||||||
|
item.finalPricePerUnit = finalPrice;
|
||||||
|
item.lineTotal = finalPrice * item.qty;
|
||||||
|
|
||||||
|
computedTotalWeight += item.qty;
|
||||||
|
computedEstimatedTotal += item.lineTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create the order
|
||||||
const order = await prisma.order.create({
|
const order = await prisma.order.create({
|
||||||
data: {
|
data: {
|
||||||
orderNumber,
|
|
||||||
name,
|
name,
|
||||||
phone,
|
phone,
|
||||||
totalWeight,
|
totalWeight: computedTotalWeight,
|
||||||
estimatedTotal,
|
estimatedTotal: computedEstimatedTotal,
|
||||||
items: JSON.stringify(items),
|
items: JSON.stringify(items),
|
||||||
status: 'Pending'
|
status: 'Pending'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
res.json(order);
|
|
||||||
} catch (error) {
|
// 3. Generate and update order number based on ID (to avoid race conditions)
|
||||||
|
const orderNumber = `#${order.id + 1000}`;
|
||||||
|
const updatedOrder = await prisma.order.update({
|
||||||
|
where: { id: order.id },
|
||||||
|
data: { orderNumber }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(updatedOrder);
|
||||||
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).json({ error: 'Failed to submit order' });
|
res.status(400).json({ error: error.message || 'Failed to submit order' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+11
-13
@@ -16,24 +16,16 @@ function App() {
|
|||||||
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
||||||
const [adminCodeInput, setAdminCodeInput] = useState('');
|
const [adminCodeInput, setAdminCodeInput] = useState('');
|
||||||
const [notification, setNotification] = useState<string | null>(null);
|
const [notification, setNotification] = useState<string | null>(null);
|
||||||
const [adminCode, setAdminCode] = useState('1234');
|
|
||||||
const [config, setConfig] = useState<any>(null);
|
const [config, setConfig] = useState<any>(null);
|
||||||
const [confirmModal, setConfirmModal] = useState<{ show: boolean, message: string, onConfirm: (() => void) | null }>({ show: false, message: '', onConfirm: null });
|
const [confirmModal, setConfirmModal] = useState<{ show: boolean, message: string, onConfirm: (() => void) | null }>({ show: false, message: '', onConfirm: null });
|
||||||
const [pickerItem, setPickerItem] = useState<any>(null);
|
const [pickerItem, setPickerItem] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refreshData();
|
|
||||||
const intervalId = setInterval(refreshData, 5000);
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await API.fetchData();
|
const data = await API.fetchData();
|
||||||
setStock(data.stock);
|
setStock(data.stock);
|
||||||
setOrders(data.orders.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()));
|
setOrders(data.orders.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()));
|
||||||
if (data.config) {
|
if (data.config) {
|
||||||
setAdminCode(data.config.adminCode || '1234');
|
|
||||||
setConfig(data.config);
|
setConfig(data.config);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -42,6 +34,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshData();
|
||||||
|
const intervalId = setInterval(refreshData, 5000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
const showNotification = (msg: string) => {
|
const showNotification = (msg: string) => {
|
||||||
setNotification(msg);
|
setNotification(msg);
|
||||||
setTimeout(() => setNotification(null), 3000);
|
setTimeout(() => setNotification(null), 3000);
|
||||||
@@ -110,19 +107,20 @@ function App() {
|
|||||||
refreshData();
|
refreshData();
|
||||||
showNotification("Order Request Sent!");
|
showNotification("Order Request Sent!");
|
||||||
return order;
|
return order;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
showNotification("Error submitting order.");
|
showNotification(error.message || "Error submitting order.");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdminLogin = () => {
|
const handleAdminLogin = async () => {
|
||||||
if (adminCodeInput === adminCode) {
|
try {
|
||||||
|
await API.login(adminCodeInput);
|
||||||
setView('admin');
|
setView('admin');
|
||||||
setShowAdminLogin(false);
|
setShowAdminLogin(false);
|
||||||
setAdminCodeInput('');
|
setAdminCodeInput('');
|
||||||
showNotification("Logged in as Admin");
|
showNotification("Logged in as Admin");
|
||||||
} else {
|
} catch (err) {
|
||||||
showNotification("Incorrect Admin Code");
|
showNotification("Incorrect Admin Code");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,5 +36,14 @@ export const API = {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(cfg)
|
body: JSON.stringify(cfg)
|
||||||
}),
|
}),
|
||||||
resetDB: () => fetch(`${API_BASE}/api/reset`, { method: 'POST' })
|
resetDB: () => fetch(`${API_BASE}/api/reset`, { method: 'POST' }),
|
||||||
|
login: async (code: string) => {
|
||||||
|
const res = await fetch(`${API_BASE}/api/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ code })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Invalid code');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user