Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions api/controllers/adminController.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const jwt = require('jsonwebtoken');
const audit = require('../services/auditService');
const { isDbReady } = require('../config/db');
const AuditLog = require('../models/AuditLog');

function login(req, res) {
const { username, password } = req.body || {};
Expand All @@ -23,16 +26,53 @@ function login(req, res) {
: password === ADMIN_PASSWORD;

if (username !== ADMIN_USERNAME || !passwordMatch) {
// Log failed login attempt (non-blocking)
audit.log({
actor: String(username || 'unknown').slice(0, 120),
action: 'ADMIN_LOGIN_FAILED',
resource: 'session',
metadata: { reason: 'invalid_credentials' },
ip: req.ip || null,
});
return res.status(401).json({ success: false, message: 'Invalid credentials' });
}

const token = jwt.sign(
{ username: ADMIN_USERNAME },
ADMIN_JWT_SECRET,
{ expiresIn: ADMIN_JWT_EXPIRES_IN }
{ expiresIn: ADMIN_JWT_EXPIRES_IN, algorithm: 'HS256' }
);

// Log successful login (non-blocking)
audit.log({
actor: ADMIN_USERNAME,
action: 'ADMIN_LOGIN',
resource: 'session',
metadata: { expiresIn: ADMIN_JWT_EXPIRES_IN },
ip: req.ip || null,
});

return res.json({ success: true, token, expiresIn: ADMIN_JWT_EXPIRES_IN });
}

module.exports = { login };
/**
* GET /api/admin/audit-logs
* Returns the most recent audit log entries (max 200).
*/
async function getAuditLogs(req, res) {
try {
if (!isDbReady()) {
return res.json({ success: true, logs: [], note: 'Audit logs only available in DB mode' });
}
const logs = await AuditLog.find()
.sort({ created_at: -1 })
.limit(200)
.lean();
return res.json({ success: true, logs });
} catch (err) {
console.error(err);
return res.status(500).json({ success: false, message: 'Server error' });
}
}

module.exports = { login, getAuditLogs };
162 changes: 113 additions & 49 deletions api/controllers/orderController.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const Order = require('../models/Order');
const Product = require('../models/Product');
const { isDbReady } = require('../config/db');
const audit = require('../services/auditService');
const {
sendOrderReceiptEmail,
isValidEmail,
normalizeEmail,
} = require('../email/mailer');

const memoryOrders = [];
const orderLocks = new Set();

const ALLOWED_ORDER_STATUSES = [
'pending',
Expand Down Expand Up @@ -283,6 +285,7 @@ async function createOrder(req, res) {
let serverTotal = 0;

for (const item of items) {
const qtyRaw = Number(item.qty);
if (!Number.isFinite(qtyRaw) || qtyRaw <= 0 || qtyRaw > 999) {
return res.status(400).json({
success: false,
Expand Down Expand Up @@ -325,44 +328,76 @@ async function createOrder(req, res) {
? Math.round(clientTotal * 100) / 100
: Math.round(computedTotal * 100) / 100;

const order_id = generateOrderId();
const orderDoc = {
order_id,
customer_name: sanitizedCustomerName,
email: customerEmail,
phone: phoneDigits.slice(0, 15),
address: sanitizedAddress,
city: sanitizedCity,
pincode: sanitizedPincode,
items: verifiedItems,
total: finalTotal,
};
const lockKey = `${phoneDigits.slice(0, 15)}_${finalTotal}`;
if (orderLocks.has(lockKey)) {
return res.status(409).json({ success: false, message: 'Your order is currently being processed. Please wait.' });
}

orderLocks.add(lockKey);
try {
// ── DUPLICATE PROTECTION ────────────────────────────────────────────────────
const duplicateWindowMs = 2 * 60 * 1000;
if (isDbReady()) {
const duplicate = await Order.findOne({
phone: phoneDigits.slice(0, 15),
total: finalTotal,
created_at: { $gt: new Date(Date.now() - duplicateWindowMs) }
});
if (duplicate) {
return res.status(409).json({ success: false, message: 'Duplicate order detected. Please wait before placing another order.' });
}
} else {
const duplicate = memoryOrders.find(o =>
o.phone === phoneDigits.slice(0, 15) &&
o.total === finalTotal &&
new Date(o.created_at).getTime() > Date.now() - duplicateWindowMs
);
if (duplicate) {
return res.status(409).json({ success: false, message: 'Duplicate order detected. Please wait before placing another order.' });
}
}

if (!isDbReady()) {
const now = new Date();
memoryOrders.unshift({
...orderDoc,
status: 'pending',
payment_status: 'unpaid',
notes: '',
confirmed_at: null,
created_at: now,
updated_at: now,
});
return res.json({
success: true,
const order_id = generateOrderId();
const orderDoc = {
order_id,
message:
'Order placed successfully (memory mode — add MONGO_URI to persist orders in MongoDB).',
customer_name: sanitizedCustomerName,
email: customerEmail,
phone: phoneDigits.slice(0, 15),
address: sanitizedAddress,
city: sanitizedCity,
pincode: sanitizedPincode,
items: verifiedItems,
total: finalTotal,
};

if (!isDbReady()) {
const now = new Date();
memoryOrders.unshift({
...orderDoc,
status: 'pending',
payment_status: 'unpaid',
notes: '',
confirmed_at: null,
created_at: now,
updated_at: now,
});
return res.json({
success: true,
order_id,
message:
'Order placed successfully (memory mode — add MONGO_URI to persist orders in MongoDB).',
});
}

const order = await Order.create(orderDoc);
res.json({
success: true,
order_id: order.order_id,
message: 'Order placed successfully',
});
} finally {
orderLocks.delete(lockKey);
}

const order = await Order.create(orderDoc);
res.json({
success: true,
order_id: order.order_id,
message: 'Order placed successfully',
});
} catch (err) {
console.error(err);
res
Expand All @@ -373,7 +408,9 @@ async function createOrder(req, res) {

async function getAllOrders(req, res) {
try {
const { status, search, from, to } = req.query;
const status = req.query.status ? String(req.query.status) : undefined;
const search = req.query.search ? String(req.query.search) : undefined;
const { from, to } = req.query;

if (!isDbReady()) {
let list = [...memoryOrders];
Expand Down Expand Up @@ -525,6 +562,16 @@ async function confirmPayment(req, res) {

const receipt_email = await sendOrderReceiptEmail(order);

// Audit: payment confirmation (non-blocking)
audit.log({
actor: req.admin?.username || 'admin',
action: 'PAYMENT_CONFIRMED',
resource: 'order',
resourceId: req.params.orderId,
metadata: { receipt_email, notes: order?.notes },
ip: req.ip || null,
});

res.json({
success: true,
message: 'Payment confirmed',
Expand All @@ -540,6 +587,8 @@ async function confirmPayment(req, res) {

async function updateOrderStatus(req, res) {
try {
const status = String(req.body.status || '').trim();

if (!ALLOWED_ORDER_STATUSES.includes(status)) {
return res.status(400).json({
success: false,
Expand Down Expand Up @@ -567,6 +616,17 @@ async function updateOrderStatus(req, res) {
return res
.status(404)
.json({ success: false, message: 'Order not found' });

// Audit: status change (non-blocking)
audit.log({
actor: req.admin?.username || 'admin',
action: 'ORDER_STATUS_CHANGED',
resource: 'order',
resourceId: req.params.orderId,
metadata: { previousStatus: order.status, newStatus: status },
ip: req.ip || null,
});

res.json({ success: true });
} catch (err) {
res.status(500).json({ success: false, message: 'Server error' });
Expand Down Expand Up @@ -597,24 +657,28 @@ async function getStats(req, res) {
});
}

const [totalOrders, pendingOrders, paidOrders, revenueResult] =
await Promise.all([
Order.countDocuments(),
Order.countDocuments({ status: 'pending' }),
Order.countDocuments({ payment_status: 'paid' }),
Order.aggregate([
{ $match: { payment_status: 'paid' } },
{ $group: { _id: null, total: { $sum: '$total' } } },
]),
]);
// Single $facet pipeline replaces 4 separate DB round-trips
const [facetResult] = await Order.aggregate([
{
$facet: {
total_orders: [{ $count: 'count' }],
pending_orders: [{ $match: { status: 'pending' } }, { $count: 'count' }],
paid_orders: [{ $match: { payment_status: 'paid' } }, { $count: 'count' }],
total_revenue: [
{ $match: { payment_status: 'paid' } },
{ $group: { _id: null, total: { $sum: '$total' } } },
],
},
},
]);

res.json({
success: true,
stats: {
total_orders: totalOrders,
pending_orders: pendingOrders,
paid_orders: paidOrders,
total_revenue: revenueResult[0]?.total || 0,
total_orders: facetResult.total_orders[0]?.count || 0,
pending_orders: facetResult.pending_orders[0]?.count || 0,
paid_orders: facetResult.paid_orders[0]?.count || 0,
total_revenue: facetResult.total_revenue[0]?.total || 0,
},
});
} catch (err) {
Expand Down
29 changes: 24 additions & 5 deletions api/controllers/otpController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ function generateOTP() {

async function sendOtp(req, res) {
try {
const { phone } = req.body;
const phone = String(req.body.phone || '').trim();

if (!phone || phone.length < 10) {
return res.status(400).json({ success: false, message: 'Invalid phone number' });
}

// DB-level rate limiting: prevent generating an OTP if one was created in the last 1 minute
const recentOtp = await Otp.findOne({ phone, created_at: { $gt: new Date(Date.now() - 60 * 1000) } });
if (recentOtp) {
return res.status(429).json({ success: false, message: 'Please wait before requesting a new OTP' });
}

await Otp.updateMany({ phone, used: false }, { used: true });

const otp = generateOTP();
const expires_at = new Date(Date.now() + 5 * 60 * 1000);

await Otp.create({ phone, otp, expires_at });
await Otp.create({ phone, otp, expires_at, attempts: 0 });

const apiKey = process.env.FAST2SMS_API_KEY;
if (apiKey && apiKey !== 'your_actual_api_key_here') {
Expand All @@ -44,17 +50,30 @@ async function sendOtp(req, res) {

async function verifyOtp(req, res) {
try {
const { phone, otp } = req.body;
const phone = String(req.body.phone || '').trim();
const otp = String(req.body.otp || '').trim();

// Find the most recent active OTP for this phone
const record = await Otp.findOne({
phone,
otp,
used: false,
expires_at: { $gt: new Date() },
}).sort({ created_at: -1 });

if (!record) {
return res.status(400).json({ success: false, message: 'Invalid or expired OTP' });
return res.status(400).json({ success: false, message: 'No valid OTP found or OTP expired' });
}

if (record.attempts >= 3) {
record.used = true; // invalidate it
await record.save();
return res.status(429).json({ success: false, message: 'Too many failed attempts, please request a new OTP' });
}

if (record.otp !== otp) {
record.attempts += 1;
await record.save();
return res.status(400).json({ success: false, message: 'Invalid OTP' });
}

record.used = true;
Expand Down
9 changes: 7 additions & 2 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ app.get('*', (req, res) => {
// ─── GLOBAL ERROR HANDLER ──────────────────────────────────────────────────────
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ success: false, message: 'Something went wrong!' });
const isProduction = process.env.NODE_ENV === 'production';
res.status(err.status || 500).json({
success: false,
message: err.message || 'Something went wrong!',
...( !isProduction && { stack: err.stack } )
});
});

// ─── LOCAL SERVER ───────────────────────────────────────────────────────────────
Expand All @@ -88,7 +93,7 @@ function startServer(port) {
});
}

if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
startServer(PORT);
}

Expand Down
Loading