Files
SkyArtShop/backend/routes/admin.js

1309 lines
34 KiB
JavaScript
Raw Normal View History

const express = require("express");
const { query } = require("../config/database");
const { requireAuth } = require("../middleware/auth");
2026-01-01 22:24:30 -06:00
const { cache } = require("../middleware/cache");
2026-01-04 17:52:37 -06:00
const { apiLimiter } = require("../config/rateLimiter");
2026-01-01 22:24:30 -06:00
const {
invalidateProductCache,
invalidateBlogCache,
invalidatePortfolioCache,
invalidateHomepageCache,
2026-01-18 02:22:05 -06:00
invalidatePagesCache,
2026-01-01 22:24:30 -06:00
} = require("../utils/cacheInvalidation");
2025-12-19 20:44:46 -06:00
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
2025-12-24 00:13:23 -06:00
const {
sendSuccess,
sendError,
sendNotFound,
} = require("../utils/responseHelpers");
2026-01-18 02:22:05 -06:00
const {
getById,
deleteById,
countRecords,
batchInsert,
getProductWithImages,
} = require("../utils/queryHelpers");
2025-12-19 20:44:46 -06:00
const { HTTP_STATUS } = require("../config/constants");
const router = express.Router();
2026-01-04 17:52:37 -06:00
// Apply rate limiting to all admin routes
router.use(apiLimiter);
// Dashboard stats API
2025-12-24 00:13:23 -06:00
router.get(
"/dashboard/stats",
requireAuth,
asyncHandler(async (req, res) => {
const [productsCount, projectsCount, blogCount, pagesCount] =
await Promise.all([
countRecords("products"),
countRecords("portfolioprojects"),
countRecords("blogposts"),
countRecords("pages"),
]);
sendSuccess(res, {
stats: {
products: productsCount,
projects: projectsCount,
blog: blogCount,
pages: pagesCount,
},
user: {
name: req.session.name,
email: req.session.email,
role: req.session.role,
},
});
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
// Generic CRUD factory function
const createCRUDRoutes = (config) => {
const { table, resourceName, listFields = "*", requiresAuth = true } = config;
const auth = requiresAuth ? requireAuth : (req, res, next) => next();
// List all
2025-12-24 00:13:23 -06:00
router.get(
`/${resourceName}`,
auth,
asyncHandler(async (req, res) => {
const result = await query(
2026-01-18 02:22:05 -06:00
`SELECT ${listFields} FROM ${table} ORDER BY createdat DESC`,
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { [resourceName]: result.rows });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
// Get by ID
2025-12-24 00:13:23 -06:00
router.get(
`/${resourceName}/:id`,
auth,
asyncHandler(async (req, res) => {
const item = await getById(table, req.params.id);
if (!item) {
return sendNotFound(res, resourceName);
}
const responseKey = resourceName.slice(0, -1); // Remove 's' for singular
sendSuccess(res, { [responseKey]: item });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
// Delete
2025-12-24 00:13:23 -06:00
router.delete(
`/${resourceName}/:id`,
auth,
asyncHandler(async (req, res) => {
const deleted = await deleteById(table, req.params.id);
if (!deleted) {
return sendNotFound(res, resourceName);
}
sendSuccess(res, { message: `${resourceName} deleted successfully` });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
};
// Helper function to generate slug
const generateSlug = (name) => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim();
2025-12-19 20:44:46 -06:00
};
// Products CRUD
2025-12-24 00:13:23 -06:00
router.get(
"/products",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
`SELECT p.id, p.name, p.price, p.stockquantity, p.isactive, p.isfeatured,
p.isbestseller, p.category, p.createdat,
(SELECT COUNT(*) FROM product_images WHERE product_id = p.id) as image_count
FROM products p
2026-01-18 02:22:05 -06:00
ORDER BY p.createdat DESC`,
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { products: result.rows });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.get(
"/products/:id",
requireAuth,
asyncHandler(async (req, res) => {
// Get product details
const product = await getById("products", req.params.id);
if (!product) {
return sendNotFound(res, "Product");
}
2025-12-19 20:44:46 -06:00
2025-12-24 00:13:23 -06:00
// Get associated images with color variants
const imagesResult = await query(
2026-01-18 02:22:05 -06:00
`SELECT id, image_url, color_variant, color_code, alt_text, display_order, is_primary, variant_price, variant_stock
2025-12-24 00:13:23 -06:00
FROM product_images
WHERE product_id = $1
ORDER BY display_order ASC, created_at ASC`,
2026-01-18 02:22:05 -06:00
[req.params.id],
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
2025-12-24 00:13:23 -06:00
product.images = imagesResult.rows;
sendSuccess(res, { product });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.post(
"/products",
requireAuth,
asyncHandler(async (req, res) => {
const {
name,
shortdescription,
description,
price,
stockquantity,
category,
sku,
weight,
dimensions,
material,
isactive,
isfeatured,
isbestseller,
images,
} = req.body;
// Generate unique ID and slug from name
const productId =
"prod-" + Date.now() + "-" + Math.random().toString(36).substr(2, 9);
const slug = generateSlug(name);
// Insert product
const productResult = await query(
`INSERT INTO products (
id, name, slug, shortdescription, description, price, stockquantity,
category, sku, weight, dimensions, material, isactive, isfeatured,
isbestseller, createdat
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW())
RETURNING *`,
[
productId,
name,
slug,
shortdescription,
description,
price,
stockquantity || 0,
category,
sku,
weight,
dimensions,
material,
isactive !== false,
isfeatured || false,
isbestseller || false,
2026-01-18 02:22:05 -06:00
],
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
2025-12-24 00:13:23 -06:00
const product = productResult.rows[0];
2026-01-18 02:22:05 -06:00
// Insert images with color variants if provided (batch insert for performance)
2025-12-24 00:13:23 -06:00
if (images && Array.isArray(images) && images.length > 0) {
2026-01-18 02:22:05 -06:00
const imageRecords = images.map((img, i) => ({
product_id: product.id,
image_url: img.image_url,
color_variant: img.color_variant || null,
color_code: img.color_code || null,
alt_text: img.alt_text || name,
display_order: img.display_order !== undefined ? img.display_order : i,
is_primary: img.is_primary !== undefined ? img.is_primary : i === 0,
variant_price: img.variant_price || null,
variant_stock: img.variant_stock || 0,
}));
await batchInsert("product_images", imageRecords, [
"product_id",
"image_url",
"color_variant",
"color_code",
"alt_text",
"display_order",
"is_primary",
"variant_price",
"variant_stock",
]);
2025-12-24 00:13:23 -06:00
}
2026-01-18 02:22:05 -06:00
// Fetch complete product with images using helper
const completeProduct = await getProductWithImages(product.id);
// Invalidate product cache so new product appears immediately
invalidateProductCache();
2025-12-24 00:13:23 -06:00
sendSuccess(
res,
{
2026-01-18 02:22:05 -06:00
product: completeProduct,
2025-12-24 00:13:23 -06:00
message: "Product created successfully",
},
2026-01-18 02:22:05 -06:00
HTTP_STATUS.CREATED,
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.put(
"/products/:id",
requireAuth,
asyncHandler(async (req, res) => {
2026-01-01 22:24:30 -06:00
console.log("=== UPDATE PRODUCT API CALLED ===");
console.log("Product ID:", req.params.id);
console.log("Request body:", JSON.stringify(req.body, null, 2));
2025-12-24 00:13:23 -06:00
const {
name,
shortdescription,
description,
price,
stockquantity,
category,
sku,
weight,
dimensions,
material,
isactive,
isfeatured,
isbestseller,
images,
} = req.body;
2026-01-01 22:24:30 -06:00
console.log("Images to save:", images);
2025-12-24 00:13:23 -06:00
// Generate slug if name is provided
const slug = name ? generateSlug(name) : null;
// Build dynamic update query
const updates = [];
const values = [];
let paramIndex = 1;
if (name !== undefined) {
updates.push(`name = $${paramIndex++}`);
values.push(name);
updates.push(`slug = $${paramIndex++}`);
values.push(slug);
}
if (shortdescription !== undefined) {
updates.push(`shortdescription = $${paramIndex++}`);
values.push(shortdescription);
}
if (description !== undefined) {
updates.push(`description = $${paramIndex++}`);
values.push(description);
}
if (price !== undefined) {
updates.push(`price = $${paramIndex++}`);
values.push(price);
}
if (stockquantity !== undefined) {
updates.push(`stockquantity = $${paramIndex++}`);
values.push(stockquantity);
}
if (category !== undefined) {
updates.push(`category = $${paramIndex++}`);
values.push(category);
}
if (sku !== undefined) {
updates.push(`sku = $${paramIndex++}`);
values.push(sku);
}
if (weight !== undefined) {
updates.push(`weight = $${paramIndex++}`);
values.push(weight);
}
if (dimensions !== undefined) {
updates.push(`dimensions = $${paramIndex++}`);
values.push(dimensions);
}
if (material !== undefined) {
updates.push(`material = $${paramIndex++}`);
values.push(material);
}
if (isactive !== undefined) {
updates.push(`isactive = $${paramIndex++}`);
values.push(isactive);
}
if (isfeatured !== undefined) {
updates.push(`isfeatured = $${paramIndex++}`);
values.push(isfeatured);
}
if (isbestseller !== undefined) {
updates.push(`isbestseller = $${paramIndex++}`);
values.push(isbestseller);
}
updates.push(`updatedat = NOW()`);
values.push(req.params.id);
2025-12-19 20:44:46 -06:00
2025-12-24 00:13:23 -06:00
const updateQuery = `UPDATE products SET ${updates.join(
2026-01-18 02:22:05 -06:00
", ",
2025-12-24 00:13:23 -06:00
)} WHERE id = $${paramIndex} RETURNING *`;
const result = await query(updateQuery, values);
2025-12-14 01:54:40 -06:00
2025-12-24 00:13:23 -06:00
if (result.rows.length === 0) {
return sendNotFound(res, "Product");
}
2026-01-01 22:24:30 -06:00
console.log("Product updated in database:", result.rows[0].id);
2025-12-24 00:13:23 -06:00
// Update images if provided
if (images && Array.isArray(images)) {
2026-01-01 22:24:30 -06:00
console.log("Updating images, count:", images.length);
2025-12-24 00:13:23 -06:00
// Delete existing images for this product
2026-01-01 22:24:30 -06:00
const deleteResult = await query(
"DELETE FROM product_images WHERE product_id = $1",
2026-01-18 02:22:05 -06:00
[req.params.id],
2026-01-01 22:24:30 -06:00
);
console.log("Deleted existing images, count:", deleteResult.rowCount);
2025-12-24 00:13:23 -06:00
2026-01-18 02:22:05 -06:00
// Insert new images (batch insert for performance)
const imageRecords = images.map((img, i) => ({
product_id: req.params.id,
image_url: img.image_url,
color_variant: img.color_variant || null,
color_code: img.color_code || null,
alt_text: img.alt_text || result.rows[0].name,
display_order: img.display_order !== undefined ? img.display_order : i,
is_primary: img.is_primary !== undefined ? img.is_primary : i === 0,
variant_price: img.variant_price || null,
variant_stock: img.variant_stock || 0,
}));
await batchInsert("product_images", imageRecords, [
"product_id",
"image_url",
"color_variant",
"color_code",
"alt_text",
"display_order",
"is_primary",
"variant_price",
"variant_stock",
]);
console.log("All images inserted successfully (batch)");
2026-01-01 22:24:30 -06:00
} else {
console.log("No images to update");
2025-12-24 00:13:23 -06:00
}
2026-01-18 02:22:05 -06:00
// Fetch complete product with images using helper
const completeProduct = await getProductWithImages(req.params.id);
console.log("Final product with images:", completeProduct);
2026-01-01 22:24:30 -06:00
console.log("=== PRODUCT UPDATE COMPLETE ===");
// Invalidate product cache
invalidateProductCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, {
2026-01-18 02:22:05 -06:00
product: completeProduct,
2025-12-24 00:13:23 -06:00
message: "Product updated successfully",
});
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.delete(
"/products/:id",
requireAuth,
asyncHandler(async (req, res) => {
2026-01-18 02:22:05 -06:00
const productId = req.params.id;
console.log(`[DELETE] Attempting to delete product with ID: ${productId}`);
// First, check if product exists
const existingProduct = await getById("products", productId);
if (!existingProduct) {
console.log(`[DELETE] Product not found with ID: ${productId}`);
return sendNotFound(res, "Product");
}
console.log(`[DELETE] Found product: ${existingProduct.name}`);
2025-12-24 00:13:23 -06:00
// Product images will be deleted automatically via CASCADE
2026-01-18 02:22:05 -06:00
const deleted = await deleteById("products", productId);
console.log(`[DELETE] Delete result: ${deleted}`);
2025-12-24 00:13:23 -06:00
if (!deleted) {
2026-01-18 02:22:05 -06:00
console.log(`[DELETE] Failed to delete product: ${productId}`);
2025-12-24 00:13:23 -06:00
return sendNotFound(res, "Product");
}
2026-01-18 02:22:05 -06:00
// Invalidate product cache so deletion reflects immediately
invalidateProductCache();
console.log(`[DELETE] Product deleted successfully: ${productId}`);
2025-12-24 00:13:23 -06:00
sendSuccess(res, { message: "Product deleted successfully" });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
// Portfolio Projects CRUD
2025-12-24 00:13:23 -06:00
router.get(
"/portfolio/projects",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
2026-01-18 02:22:05 -06:00
"SELECT id, title, description, imageurl, category, isactive, createdat FROM portfolioprojects ORDER BY createdat DESC",
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { projects: result.rows });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.get(
"/portfolio/projects/:id",
requireAuth,
asyncHandler(async (req, res) => {
const project = await getById("portfolioprojects", req.params.id);
if (!project) {
return sendNotFound(res, "Project");
}
sendSuccess(res, { project });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.post(
"/portfolio/projects",
requireAuth,
asyncHandler(async (req, res) => {
2026-01-18 02:22:05 -06:00
const { title, description, category, isactive, imageurl, images } =
req.body;
const imagesJson =
images && images.length > 0 ? JSON.stringify(images) : "[]";
2025-12-24 00:13:23 -06:00
const result = await query(
2026-01-18 02:22:05 -06:00
`INSERT INTO portfolioprojects (id, title, description, category, isactive, imageurl, images, createdat)
VALUES ('portfolio-' || gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, NOW()) RETURNING *`,
[
title,
description,
category,
isactive !== false,
imageurl || null,
imagesJson,
],
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
// Invalidate portfolio cache
invalidatePortfolioCache();
2025-12-24 00:13:23 -06:00
sendSuccess(
res,
{
project: result.rows[0],
message: "Project created successfully",
},
2026-01-18 02:22:05 -06:00
HTTP_STATUS.CREATED,
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.put(
"/portfolio/projects/:id",
requireAuth,
asyncHandler(async (req, res) => {
2026-01-18 02:22:05 -06:00
const { title, description, category, isactive, imageurl, images } =
req.body;
const imagesJson =
images && images.length > 0 ? JSON.stringify(images) : "[]";
2025-12-24 00:13:23 -06:00
const result = await query(
`UPDATE portfolioprojects
2026-01-18 02:22:05 -06:00
SET title = $1, description = $2, category = $3, isactive = $4, imageurl = $5, images = $6, updatedat = NOW()
WHERE id = $7 RETURNING *`,
2025-12-24 00:13:23 -06:00
[
title,
description,
category,
isactive !== false,
imageurl || null,
2026-01-18 02:22:05 -06:00
imagesJson,
2025-12-24 00:13:23 -06:00
req.params.id,
2026-01-18 02:22:05 -06:00
],
2025-12-24 00:13:23 -06:00
);
if (result.rows.length === 0) {
return sendNotFound(res, "Project");
}
2026-01-18 02:22:05 -06:00
// Invalidate portfolio cache
invalidatePortfolioCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, {
project: result.rows[0],
message: "Project updated successfully",
});
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.delete(
"/portfolio/projects/:id",
requireAuth,
asyncHandler(async (req, res) => {
const deleted = await deleteById("portfolioprojects", req.params.id);
if (!deleted) {
return sendNotFound(res, "Project");
}
2026-01-18 02:22:05 -06:00
// Invalidate portfolio cache
invalidatePortfolioCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, { message: "Project deleted successfully" });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
2025-12-19 20:44:46 -06:00
// Blog Posts CRUD
2025-12-24 00:13:23 -06:00
router.get(
"/blog",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
2026-01-18 02:22:05 -06:00
"SELECT id, title, slug, excerpt, ispublished, createdat FROM blogposts ORDER BY createdat DESC",
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { posts: result.rows });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.get(
"/blog/:id",
requireAuth,
asyncHandler(async (req, res) => {
const post = await getById("blogposts", req.params.id);
if (!post) {
return sendNotFound(res, "Blog post");
}
sendSuccess(res, { post });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.post(
"/blog",
requireAuth,
asyncHandler(async (req, res) => {
const {
title,
slug,
excerpt,
content,
2026-01-18 02:22:05 -06:00
featuredimage,
images,
videourl,
poll,
2025-12-24 00:13:23 -06:00
metatitle,
metadescription,
ispublished,
} = req.body;
const result = await query(
2026-01-18 02:22:05 -06:00
`INSERT INTO blogposts (id, title, slug, excerpt, content, featuredimage, imageurl, images, videourl, poll, metatitle, metadescription, ispublished, createdat)
VALUES ('blog-' || gen_random_uuid()::text, $1, $2, $3, $4, $5, $5, $6, $7, $8, $9, $10, $11, NOW()) RETURNING *`,
2025-12-24 00:13:23 -06:00
[
title,
slug,
excerpt,
content,
2026-01-18 02:22:05 -06:00
featuredimage || null,
images || "[]",
videourl || null,
poll || null,
2025-12-24 00:13:23 -06:00
metatitle,
metadescription,
ispublished || false,
2026-01-18 02:22:05 -06:00
],
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
// Invalidate blog cache
invalidateBlogCache();
2025-12-24 00:13:23 -06:00
sendSuccess(
res,
{
post: result.rows[0],
message: "Blog post created successfully",
},
2026-01-18 02:22:05 -06:00
HTTP_STATUS.CREATED,
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.put(
"/blog/:id",
requireAuth,
asyncHandler(async (req, res) => {
const {
title,
slug,
excerpt,
content,
2026-01-18 02:22:05 -06:00
featuredimage,
images,
videourl,
poll,
2025-12-24 00:13:23 -06:00
metatitle,
metadescription,
ispublished,
} = req.body;
const result = await query(
`UPDATE blogposts
2026-01-18 02:22:05 -06:00
SET title = $1, slug = $2, excerpt = $3, content = $4, featuredimage = $5, imageurl = $5,
images = $6, videourl = $7, poll = $8, metatitle = $9, metadescription = $10,
ispublished = $11, updatedat = NOW()
WHERE id = $12 RETURNING *`,
2025-12-24 00:13:23 -06:00
[
title,
slug,
excerpt,
content,
2026-01-18 02:22:05 -06:00
featuredimage || null,
images || "[]",
videourl || null,
poll || null,
2025-12-24 00:13:23 -06:00
metatitle,
metadescription,
ispublished || false,
req.params.id,
2026-01-18 02:22:05 -06:00
],
2025-12-24 00:13:23 -06:00
);
if (result.rows.length === 0) {
return sendNotFound(res, "Blog post");
}
2026-01-18 02:22:05 -06:00
// Invalidate blog cache
invalidateBlogCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, {
post: result.rows[0],
message: "Blog post updated successfully",
});
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.delete(
"/blog/:id",
requireAuth,
asyncHandler(async (req, res) => {
const deleted = await deleteById("blogposts", req.params.id);
if (!deleted) {
return sendNotFound(res, "Blog post");
}
2026-01-18 02:22:05 -06:00
// Invalidate blog cache
invalidateBlogCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, { message: "Blog post deleted successfully" });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
2025-12-14 01:54:40 -06:00
// Custom Pages CRUD
2025-12-24 00:13:23 -06:00
router.get(
"/pages",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
2026-01-18 02:22:05 -06:00
"SELECT id, title, slug, ispublished, createdat FROM pages ORDER BY createdat DESC",
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { pages: result.rows });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.get(
"/pages/:id",
requireAuth,
asyncHandler(async (req, res) => {
const page = await getById("pages", req.params.id);
if (!page) {
return sendNotFound(res, "Page");
}
sendSuccess(res, { page });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.post(
"/pages",
requireAuth,
asyncHandler(async (req, res) => {
const {
title,
slug,
content,
contenthtml,
metatitle,
metadescription,
ispublished,
pagedata,
} = req.body;
2026-01-01 22:24:30 -06:00
// Generate readable ID from slug
const pageId = `page-${slug}`;
2025-12-24 00:13:23 -06:00
const result = await query(
2026-01-01 22:24:30 -06:00
`INSERT INTO pages (id, title, slug, content, pagecontent, metatitle, metadescription, ispublished, isactive, pagedata, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) RETURNING *`,
2025-12-24 00:13:23 -06:00
[
2026-01-01 22:24:30 -06:00
pageId,
2025-12-24 00:13:23 -06:00
title,
slug,
content,
contenthtml || content,
metatitle,
metadescription,
ispublished !== false,
ispublished !== false,
pagedata ? JSON.stringify(pagedata) : null,
2026-01-18 02:22:05 -06:00
],
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
// Invalidate pages cache
invalidatePagesCache();
2025-12-24 00:13:23 -06:00
sendSuccess(
res,
{
page: result.rows[0],
message: "Page created successfully",
},
2026-01-18 02:22:05 -06:00
HTTP_STATUS.CREATED,
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.put(
"/pages/:id",
requireAuth,
asyncHandler(async (req, res) => {
const {
title,
slug,
content,
contenthtml,
metatitle,
metadescription,
ispublished,
pagedata,
} = req.body;
2026-01-18 02:22:05 -06:00
console.log("=== PAGE UPDATE REQUEST ===");
console.log("Page ID:", req.params.id);
console.log("Slug:", slug);
console.log("Title:", title);
console.log(
"PageData:",
pagedata ? JSON.stringify(pagedata).substring(0, 200) + "..." : "null",
);
2025-12-24 00:13:23 -06:00
const result = await query(
`UPDATE pages
SET title = $1, slug = $2, content = $3, pagecontent = $4, metatitle = $5,
metadescription = $6, ispublished = $7, isactive = $8, pagedata = $9, updatedat = NOW()
WHERE id = $10 RETURNING *`,
[
title,
slug,
content,
contenthtml || content,
metatitle,
metadescription,
ispublished !== false,
ispublished !== false,
pagedata ? JSON.stringify(pagedata) : null,
req.params.id,
2026-01-18 02:22:05 -06:00
],
2025-12-24 00:13:23 -06:00
);
if (result.rows.length === 0) {
return sendNotFound(res, "Page");
}
2026-01-18 02:22:05 -06:00
console.log("=== PAGE UPDATED SUCCESSFULLY ===");
console.log(
"Updated pagedata:",
result.rows[0].pagedata
? JSON.stringify(result.rows[0].pagedata).substring(0, 200) + "..."
: "null",
);
// Invalidate pages cache
invalidatePagesCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, {
page: result.rows[0],
message: "Page updated successfully",
});
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.delete(
"/pages/:id",
requireAuth,
asyncHandler(async (req, res) => {
const deleted = await deleteById("pages", req.params.id);
if (!deleted) {
return sendNotFound(res, "Page");
}
2026-01-18 02:22:05 -06:00
// Invalidate pages cache
invalidatePagesCache();
2025-12-24 00:13:23 -06:00
sendSuccess(res, { message: "Page deleted successfully" });
2026-01-18 02:22:05 -06:00
}),
);
// Homepage Settings - custom handler with cache invalidation
router.get(
"/homepage/settings",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
"SELECT settings FROM site_settings WHERE key = $1",
["homepage"],
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
sendSuccess(res, { settings });
}),
);
router.post(
"/homepage/settings",
requireAuth,
asyncHandler(async (req, res) => {
const settings = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ($1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $2, updatedat = NOW()`,
["homepage", JSON.stringify(settings)],
);
// Invalidate homepage cache
invalidateHomepageCache();
sendSuccess(res, { message: "Homepage settings saved successfully" });
}),
2025-12-24 00:13:23 -06:00
);
2025-12-14 01:54:40 -06:00
2026-01-18 02:22:05 -06:00
// General Settings
2025-12-19 20:44:46 -06:00
const settingsHandler = (key) => ({
get: asyncHandler(async (req, res) => {
2025-12-14 01:54:40 -06:00
const result = await query(
2025-12-19 20:44:46 -06:00
"SELECT settings FROM site_settings WHERE key = $1",
2026-01-18 02:22:05 -06:00
[key],
2025-12-14 01:54:40 -06:00
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
2025-12-19 20:44:46 -06:00
sendSuccess(res, { settings });
}),
post: asyncHandler(async (req, res) => {
2026-01-19 01:17:43 -06:00
const newSettings = req.body;
// Get existing settings first and merge
const existingResult = await query(
"SELECT settings FROM site_settings WHERE key = $1",
[key],
);
const existingSettings =
existingResult.rows.length > 0 ? existingResult.rows[0].settings : {};
// Merge new settings with existing (new settings overwrite existing for same keys)
const mergedSettings = { ...existingSettings, ...newSettings };
2025-12-14 01:54:40 -06:00
await query(
`INSERT INTO site_settings (key, settings, updatedat)
2025-12-19 20:44:46 -06:00
VALUES ($1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $2, updatedat = NOW()`,
2026-01-19 01:17:43 -06:00
[key, JSON.stringify(mergedSettings)],
2025-12-14 01:54:40 -06:00
);
2025-12-19 20:44:46 -06:00
sendSuccess(res, { message: `${key} settings saved successfully` });
}),
2025-12-14 01:54:40 -06:00
});
2025-12-19 20:44:46 -06:00
const generalSettings = settingsHandler("general");
router.get("/settings", requireAuth, generalSettings.get);
router.post("/settings", requireAuth, generalSettings.post);
2025-12-14 01:54:40 -06:00
// Menu Management
2025-12-24 00:13:23 -06:00
router.get(
"/menu",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
2026-01-18 02:22:05 -06:00
"SELECT settings FROM site_settings WHERE key = 'menu'",
2025-12-24 00:13:23 -06:00
);
const items =
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
sendSuccess(res, { items });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
router.post(
"/menu",
requireAuth,
asyncHandler(async (req, res) => {
const { items } = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
2025-12-19 20:44:46 -06:00
VALUES ('menu', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
2026-01-18 02:22:05 -06:00
[JSON.stringify({ items })],
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { message: "Menu saved successfully" });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
// ==================== TEAM MEMBERS CRUD ====================
// Get all team members
router.get(
"/team-members",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
2026-01-18 02:22:05 -06:00
"SELECT * FROM team_members ORDER BY display_order ASC, created_at DESC",
2025-12-24 00:13:23 -06:00
);
sendSuccess(res, { teamMembers: result.rows });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
// Get single team member
router.get(
"/team-members/:id",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query("SELECT * FROM team_members WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return sendNotFound(res, "Team member");
}
sendSuccess(res, { teamMember: result.rows[0] });
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
// Create team member
router.post(
"/team-members",
requireAuth,
asyncHandler(async (req, res) => {
const { name, position, bio, image_url, display_order } = req.body;
if (!name || !position) {
return sendError(
res,
"Name and position are required",
2026-01-18 02:22:05 -06:00
HTTP_STATUS.BAD_REQUEST,
2025-12-24 00:13:23 -06:00
);
}
const result = await query(
`INSERT INTO team_members (name, position, bio, image_url, display_order, updated_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
RETURNING *`,
2026-01-18 02:22:05 -06:00
[name, position, bio || null, image_url || null, display_order || 0],
2025-12-24 00:13:23 -06:00
);
sendSuccess(
res,
{
teamMember: result.rows[0],
message: "Team member created successfully",
},
2026-01-18 02:22:05 -06:00
HTTP_STATUS.CREATED,
2025-12-24 00:13:23 -06:00
);
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
// Update team member
router.put(
"/team-members/:id",
requireAuth,
asyncHandler(async (req, res) => {
const { name, position, bio, image_url, display_order } = req.body;
if (!name || !position) {
return sendError(
res,
"Name and position are required",
2026-01-18 02:22:05 -06:00
HTTP_STATUS.BAD_REQUEST,
2025-12-24 00:13:23 -06:00
);
}
const result = await query(
`UPDATE team_members
SET name = $1, position = $2, bio = $3, image_url = $4, display_order = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = $6
RETURNING *`,
2026-01-18 02:22:05 -06:00
[name, position, bio, image_url, display_order || 0, req.params.id],
2025-12-24 00:13:23 -06:00
);
if (result.rows.length === 0) {
return sendNotFound(res, "Team member");
}
sendSuccess(res, {
teamMember: result.rows[0],
message: "Team member updated successfully",
});
2026-01-18 02:22:05 -06:00
}),
2025-12-24 00:13:23 -06:00
);
// Delete team member
router.delete(
"/team-members/:id",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
"DELETE FROM team_members WHERE id = $1 RETURNING *",
2026-01-18 02:22:05 -06:00
[req.params.id],
2025-12-24 00:13:23 -06:00
);
if (result.rows.length === 0) {
return sendNotFound(res, "Team member");
}
sendSuccess(res, { message: "Team member deleted successfully" });
2026-01-18 02:22:05 -06:00
}),
);
// ===========================
// CUSTOMERS MANAGEMENT
// ===========================
// Get all customers (for newsletter management)
router.get(
"/customers",
requireAuth,
asyncHandler(async (req, res) => {
const {
page = 1,
limit = 50,
newsletter = "all",
search = "",
status = "all",
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
let whereClause = "WHERE 1=1";
const params = [];
let paramIndex = 1;
// Filter by verification status
if (status === "verified") {
whereClause += " AND email_verified = TRUE";
} else if (status === "unverified") {
whereClause += " AND email_verified = FALSE";
}
// 'all' shows everyone
// Filter by newsletter subscription
if (newsletter === "subscribed") {
whereClause += " AND newsletter_subscribed = TRUE";
} else if (newsletter === "unsubscribed") {
whereClause += " AND newsletter_subscribed = FALSE";
}
// Search by name or email
if (search) {
whereClause += ` AND (
LOWER(first_name) LIKE $${paramIndex} OR
LOWER(last_name) LIKE $${paramIndex} OR
LOWER(email) LIKE $${paramIndex}
)`;
params.push(`%${search.toLowerCase()}%`);
paramIndex++;
}
// Get total count
const countResult = await query(
`SELECT COUNT(*) FROM customers ${whereClause}`,
params,
);
const total = parseInt(countResult.rows[0].count);
// Get customers with cart and wishlist counts
params.push(parseInt(limit), offset);
const result = await query(
`SELECT c.id, c.first_name, c.last_name, c.email, c.newsletter_subscribed,
c.last_login, c.login_count, c.created_at, c.is_active, c.email_verified,
COALESCE((SELECT SUM(quantity) FROM customer_cart WHERE customer_id = c.id), 0) as cart_count,
COALESCE((SELECT COUNT(*) FROM customer_wishlist WHERE customer_id = c.id), 0) as wishlist_count
FROM customers c
${whereClause.replace(/customers/g, "c")}
ORDER BY c.created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
params,
);
sendSuccess(res, {
customers: result.rows,
pagination: {
total,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(total / parseInt(limit)),
},
});
}),
);
// Get single customer details
router.get(
"/customers/:id",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
`SELECT id, first_name, last_name, email, newsletter_subscribed,
email_verified, last_login, login_count, created_at, updated_at, is_active
FROM customers WHERE id = $1`,
[req.params.id],
);
if (result.rows.length === 0) {
return sendNotFound(res, "Customer");
}
sendSuccess(res, { customer: result.rows[0] });
}),
);
// Update customer status (activate/deactivate)
router.patch(
"/customers/:id/status",
requireAuth,
asyncHandler(async (req, res) => {
const { is_active } = req.body;
const result = await query(
`UPDATE customers SET is_active = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2 RETURNING id, first_name, last_name, email, is_active`,
[is_active, req.params.id],
);
if (result.rows.length === 0) {
return sendNotFound(res, "Customer");
}
sendSuccess(res, {
message: `Customer ${
is_active ? "activated" : "deactivated"
} successfully`,
customer: result.rows[0],
});
}),
);
// Export customers for newsletter (CSV format data)
router.get(
"/customers/export/newsletter",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
`SELECT first_name, last_name, email
FROM customers
WHERE email_verified = TRUE AND newsletter_subscribed = TRUE AND is_active = TRUE
ORDER BY created_at DESC`,
);
// Return as JSON array - frontend can convert to CSV if needed
sendSuccess(res, {
customers: result.rows,
count: result.rows.length,
});
}),
);
// Get customer statistics
router.get(
"/customers/stats/overview",
requireAuth,
asyncHandler(async (req, res) => {
const [totalResult, verifiedResult, newsletterResult, activeResult] =
await Promise.all([
query("SELECT COUNT(*) FROM customers"),
query("SELECT COUNT(*) FROM customers WHERE email_verified = TRUE"),
query(
"SELECT COUNT(*) FROM customers WHERE newsletter_subscribed = TRUE AND email_verified = TRUE",
),
query(
"SELECT COUNT(*) FROM customers WHERE is_active = TRUE AND email_verified = TRUE",
),
]);
sendSuccess(res, {
stats: {
total: parseInt(totalResult.rows[0].count),
verified: parseInt(verifiedResult.rows[0].count),
newsletterSubscribed: parseInt(newsletterResult.rows[0].count),
active: parseInt(activeResult.rows[0].count),
},
});
}),
);
// Get customer's cart items (admin view)
router.get(
"/customers/:id/cart",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
`SELECT cc.id, cc.product_id, cc.quantity, cc.variant_color, cc.variant_size, cc.added_at,
p.name, p.price, p.imageurl
FROM customer_cart cc
JOIN products p ON p.id = cc.product_id
WHERE cc.customer_id = $1
ORDER BY cc.added_at DESC`,
[req.params.id],
);
const items = result.rows.map((row) => ({
id: row.id,
productId: row.product_id,
name: row.name,
price: parseFloat(row.price),
image: row.imageurl,
quantity: row.quantity,
variantColor: row.variant_color,
variantSize: row.variant_size,
addedAt: row.added_at,
}));
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
sendSuccess(res, {
items,
itemCount: items.length,
total: total.toFixed(2),
});
}),
);
// Get customer's wishlist items (admin view)
router.get(
"/customers/:id/wishlist",
requireAuth,
asyncHandler(async (req, res) => {
const result = await query(
`SELECT cw.id, cw.product_id, cw.added_at,
p.name, p.price, p.imageurl
FROM customer_wishlist cw
JOIN products p ON p.id = cw.product_id
WHERE cw.customer_id = $1
ORDER BY cw.added_at DESC`,
[req.params.id],
);
const items = result.rows.map((row) => ({
id: row.id,
productId: row.product_id,
name: row.name,
price: parseFloat(row.price),
image: row.imageurl,
addedAt: row.added_at,
}));
sendSuccess(res, {
items,
itemCount: items.length,
});
}),
2025-12-24 00:13:23 -06:00
);
2025-12-14 01:54:40 -06:00
module.exports = router;