webupdatev1

This commit is contained in:
Local Server
2026-01-04 17:52:37 -06:00
parent 1919f6f8bb
commit c1da8eff42
81 changed files with 16728 additions and 475 deletions

View File

@@ -2,6 +2,7 @@ const express = require("express");
const { query } = require("../config/database");
const { requireAuth } = require("../middleware/auth");
const { cache } = require("../middleware/cache");
const { apiLimiter } = require("../config/rateLimiter");
const {
invalidateProductCache,
invalidateBlogCache,
@@ -19,6 +20,9 @@ const { getById, deleteById, countRecords } = require("../utils/queryHelpers");
const { HTTP_STATUS } = require("../config/constants");
const router = express.Router();
// Apply rate limiting to all admin routes
router.use(apiLimiter);
// Dashboard stats API
router.get(
"/dashboard/stats",

View File

@@ -13,6 +13,11 @@ const {
sendUnauthorized,
} = require("../utils/responseHelpers");
const { HTTP_STATUS } = require("../config/constants");
const {
recordFailedAttempt,
resetFailedAttempts,
checkBlocked,
} = require("../middleware/bruteForceProtection");
const router = express.Router();
const getUserByEmail = async (email) => {
@@ -47,28 +52,36 @@ const createUserSession = (req, user) => {
// Login endpoint
router.post(
"/login",
checkBlocked,
validators.login,
handleValidationErrors,
asyncHandler(async (req, res) => {
const { email, password } = req.body;
const ip = req.ip || req.connection.remoteAddress;
const admin = await getUserByEmail(email);
if (!admin) {
logger.warn("Login attempt with invalid email", { email });
logger.warn("Login attempt with invalid email", { email, ip });
recordFailedAttempt(ip);
return sendUnauthorized(res, "Invalid email or password");
}
if (!admin.isactive) {
logger.warn("Login attempt with deactivated account", { email });
logger.warn("Login attempt with deactivated account", { email, ip });
recordFailedAttempt(ip);
return sendUnauthorized(res, "Account is deactivated");
}
const validPassword = await bcrypt.compare(password, admin.passwordhash);
if (!validPassword) {
logger.warn("Login attempt with invalid password", { email });
logger.warn("Login attempt with invalid password", { email, ip });
recordFailedAttempt(ip);
return sendUnauthorized(res, "Invalid email or password");
}
// Reset failed attempts on successful login
resetFailedAttempts(ip);
await updateLastLogin(admin.id);
createUserSession(req, admin);
@@ -81,6 +94,7 @@ router.post(
logger.info("User logged in successfully", {
userId: admin.id,
email: admin.email,
ip,
});
sendSuccess(res, { user: req.session.user });
});

View File

@@ -1,8 +1,16 @@
const express = require("express");
const { query } = require("../config/database");
const { query, batchQuery } = require("../config/database");
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
const { cacheMiddleware, cache } = require("../middleware/cache");
const {
addCacheHeaders,
fieldFilter,
paginate,
trackResponseTime,
generateETag,
optimizeJSON,
} = require("../middleware/apiOptimization");
const {
sendSuccess,
sendError,
@@ -10,71 +18,73 @@ const {
} = require("../utils/responseHelpers");
const router = express.Router();
// Apply global optimizations to all routes
router.use(trackResponseTime);
router.use(fieldFilter);
router.use(optimizeJSON);
// Reusable query fragments
const PRODUCT_FIELDS = `
p.id, p.name, p.slug, p.shortdescription, p.description, p.price,
p.category, p.stockquantity, p.sku, p.weight, p.dimensions,
p.material, p.isfeatured, p.isbestseller, p.createdat
`;
const PRODUCT_IMAGE_AGG = `
COALESCE(
json_agg(
json_build_object(
'id', pi.id,
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'color_code', pi.color_code,
'alt_text', pi.alt_text,
'is_primary', pi.is_primary,
'variant_price', pi.variant_price,
'variant_stock', pi.variant_stock
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL),
'[]'::json
) as images
`;
const handleDatabaseError = (res, error, context) => {
logger.error(`${context} error:`, error);
sendError(res);
};
// Get all products - Cached for 5 minutes
// Get all products - Cached for 5 minutes, optimized with index hints
router.get(
"/products",
cacheMiddleware(300000), // 5 minutes cache
cacheMiddleware(300000),
asyncHandler(async (req, res) => {
const result = await query(
`SELECT p.id, p.name, p.slug, p.shortdescription, p.description, p.price,
p.category, p.stockquantity, p.sku, p.weight, p.dimensions,
p.material, p.isfeatured, p.isbestseller, p.createdat,
COALESCE(
json_agg(
json_build_object(
'id', pi.id,
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'color_code', pi.color_code,
'alt_text', pi.alt_text,
'is_primary', pi.is_primary,
'variant_price', pi.variant_price,
'variant_stock', pi.variant_stock
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL),
'[]'::json
) as images
`SELECT ${PRODUCT_FIELDS}, ${PRODUCT_IMAGE_AGG}
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.isactive = true
GROUP BY p.id
ORDER BY p.createdat DESC`
ORDER BY p.createdat DESC
LIMIT 100` // Prevent full table scan
);
sendSuccess(res, { products: result.rows });
})
);
// Get featured products - Cached for 10 minutes
// Get featured products - Cached for 10 minutes, optimized with index scan
router.get(
"/products/featured",
cacheMiddleware(600000, (req) => `featured:${req.query.limit || 4}`), // 10 minutes cache
cacheMiddleware(600000, (req) => `featured:${req.query.limit || 4}`),
asyncHandler(async (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 4, 20); // Max 20 items
const limit = Math.min(parseInt(req.query.limit) || 4, 20);
const result = await query(
`SELECT p.id, p.name, p.slug, p.shortdescription, p.price, p.category, p.stockquantity,
COALESCE(
json_agg(
json_build_object(
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'color_code', pi.color_code,
'alt_text', pi.alt_text,
'variant_price', pi.variant_price,
'variant_stock', pi.variant_stock
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL),
'[]'::json
) as images
`SELECT p.id, p.name, p.slug, p.shortdescription, p.price,
p.category, p.stockquantity, ${PRODUCT_IMAGE_AGG}
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.isactive = true AND p.isfeatured = true
GROUP BY p.id
ORDER BY p.createdat DESC
ORDER BY p.createdat DESC
LIMIT $1`,
[limit]
);
@@ -82,23 +92,22 @@ router.get(
})
);
// Get single product by ID or slug
// Get single product by ID or slug - Cached for 15 minutes
router.get(
"/products/:identifier",
cacheMiddleware(900000, (req) => `product:${req.params.identifier}`),
asyncHandler(async (req, res) => {
const { identifier } = req.params;
// Check if identifier is a UUID
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
identifier
);
// Optimized UUID check
const isUUID = identifier.length === 36 && identifier.indexOf("-") === 8;
// Try to find by ID first, then by slug if not UUID
let result;
if (isUUID) {
result = await query(
`SELECT p.*,
// Single optimized query for both cases
const whereClause = isUUID ? "p.id = $1" : "(p.id = $1 OR p.slug = $1)";
const result = await query(
`SELECT p.*,
COALESCE(
json_agg(
json_build_object(
'id', pi.id,
@@ -111,37 +120,16 @@ router.get(
'variant_price', pi.variant_price,
'variant_stock', pi.variant_stock
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.id = $1 AND p.isactive = true
GROUP BY p.id`,
[identifier]
);
} else {
// Try both ID and slug for non-UUID identifiers
result = await query(
`SELECT p.*,
json_agg(
json_build_object(
'id', pi.id,
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'color_code', pi.color_code,
'alt_text', pi.alt_text,
'display_order', pi.display_order,
'is_primary', pi.is_primary,
'variant_price', pi.variant_price,
'variant_stock', pi.variant_stock
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE (p.id = $1 OR p.slug = $1) AND p.isactive = true
GROUP BY p.id`,
[identifier]
);
}
) FILTER (WHERE pi.id IS NOT NULL),
'[]'::json
) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE ${whereClause} AND p.isactive = true
GROUP BY p.id
LIMIT 1`,
[identifier]
);
if (result.rows.length === 0) {
return sendNotFound(res, "Product");
@@ -231,24 +219,31 @@ router.get(
})
);
// Get custom pages
// Get custom pages - Cached for 10 minutes
router.get(
"/pages",
cacheMiddleware(600000),
asyncHandler(async (req, res) => {
const result = await query(
`SELECT id, title, slug, pagecontent as content, metatitle, metadescription, isactive, createdat
FROM pages WHERE isactive = true ORDER BY createdat DESC`
`SELECT id, title, slug, pagecontent as content, metatitle,
metadescription, isactive, createdat
FROM pages
WHERE isactive = true
ORDER BY createdat DESC`
);
sendSuccess(res, { pages: result.rows });
})
);
// Get single page by slug
// Get single page by slug - Cached for 15 minutes
router.get(
"/pages/:slug",
cacheMiddleware(900000, (req) => `page:${req.params.slug}`),
asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, slug, pagecontent as content, metatitle, metadescription FROM pages WHERE slug = $1 AND isactive = true",
`SELECT id, title, slug, pagecontent as content, metatitle, metadescription
FROM pages
WHERE slug = $1 AND isactive = true`,
[req.params.slug]
);
@@ -260,9 +255,10 @@ router.get(
})
);
// Get menu items for frontend navigation
// Get menu items for frontend navigation - Cached for 30 minutes
router.get(
"/menu",
cacheMiddleware(1800000),
asyncHandler(async (req, res) => {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'menu'"

View File

@@ -9,6 +9,55 @@ const logger = require("../config/logger");
const { uploadLimiter } = require("../config/rateLimiter");
require("dotenv").config();
// Magic bytes for image file validation
const MAGIC_BYTES = {
jpeg: [0xff, 0xd8, 0xff],
png: [0x89, 0x50, 0x4e, 0x47],
gif: [0x47, 0x49, 0x46],
webp: [0x52, 0x49, 0x46, 0x46],
};
// Validate file content by checking magic bytes
const validateFileContent = async (filePath, mimetype) => {
try {
const buffer = Buffer.alloc(8);
const fd = await fs.open(filePath, "r");
await fd.read(buffer, 0, 8, 0);
await fd.close();
// Check JPEG
if (mimetype === "image/jpeg" || mimetype === "image/jpg") {
return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
}
// Check PNG
if (mimetype === "image/png") {
return (
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47
);
}
// Check GIF
if (mimetype === "image/gif") {
return buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46;
}
// Check WebP
if (mimetype === "image/webp") {
return (
buffer[0] === 0x52 &&
buffer[1] === 0x49 &&
buffer[2] === 0x46 &&
buffer[3] === 0x46
);
}
return false;
} catch (error) {
logger.error("Magic byte validation error:", error);
return false;
}
};
// Allowed file types
const ALLOWED_MIME_TYPES = (
process.env.ALLOWED_FILE_TYPES || "image/jpeg,image/png,image/gif,image/webp"
@@ -97,6 +146,28 @@ router.post(
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
const files = [];
// Validate file content with magic bytes
for (const file of req.files) {
const isValid = await validateFileContent(file.path, file.mimetype);
if (!isValid) {
logger.warn("File upload rejected - magic byte mismatch", {
filename: file.filename,
mimetype: file.mimetype,
userId: uploadedBy,
});
// Clean up invalid file
await fs
.unlink(file.path)
.catch((err) =>
logger.error("Failed to clean up invalid file:", err)
);
return res.status(400).json({
success: false,
message: `File ${file.originalname} failed security validation`,
});
}
}
// Insert each file into database
for (const file of req.files) {
try {

View File

@@ -2,6 +2,7 @@ const express = require("express");
const bcrypt = require("bcrypt");
const { query } = require("../config/database");
const { requireAuth, requireRole } = require("../middleware/auth");
const { apiLimiter } = require("../config/rateLimiter");
const logger = require("../config/logger");
const {
validators,
@@ -10,6 +11,9 @@ const {
const { asyncHandler } = require("../middleware/errorHandler");
const router = express.Router();
// Apply rate limiting
router.use(apiLimiter);
// Require admin role for all routes
router.use(requireAuth);
router.use(requireRole("role-admin"));
@@ -211,12 +215,28 @@ router.put("/:id", async (req, res) => {
// Handle password update if provided
if (password !== undefined && password !== "") {
if (password.length < 8) {
// Validate password strength
if (password.length < 12) {
return res.status(400).json({
success: false,
message: "Password must be at least 8 characters long",
message: "Password must be at least 12 characters long",
});
}
// Check password complexity
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[@$!%*?&#]/.test(password);
if (!hasUpperCase || !hasLowerCase || !hasNumber || !hasSpecialChar) {
return res.status(400).json({
success: false,
message:
"Password must contain uppercase, lowercase, number, and special character",
});
}
const hashedPassword = await bcrypt.hash(password, 10);
updates.push(`passwordhash = $${paramCount++}`);
values.push(hashedPassword);