This commit is contained in:
Local Server
2025-12-19 20:44:46 -06:00
parent 701f799cde
commit e4b3de4a46
113 changed files with 16673 additions and 2174 deletions

View File

@@ -1,609 +1,350 @@
const express = require("express");
const { query } = require("../config/database");
const { requireAuth } = require("../middleware/auth");
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
const { sendSuccess, sendError, sendNotFound } = require("../utils/responseHelpers");
const { getById, deleteById, countRecords } = require("../utils/queryHelpers");
const { HTTP_STATUS } = require("../config/constants");
const router = express.Router();
// Dashboard stats API
router.get("/dashboard/stats", requireAuth, async (req, res) => {
try {
const productsCount = await query("SELECT COUNT(*) FROM products");
const projectsCount = await query("SELECT COUNT(*) FROM portfolioprojects");
const blogCount = await query("SELECT COUNT(*) FROM blogposts");
const pagesCount = await query("SELECT COUNT(*) FROM pages");
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"),
]);
res.json({
success: true,
stats: {
products: parseInt(productsCount.rows[0].count),
projects: parseInt(projectsCount.rows[0].count),
blog: parseInt(blogCount.rows[0].count),
pages: parseInt(pagesCount.rows[0].count),
},
user: {
name: req.session.name,
email: req.session.email,
role: req.session.role,
},
});
} catch (error) {
console.error("Dashboard error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, {
stats: {
products: productsCount,
projects: projectsCount,
blog: blogCount,
pages: pagesCount,
},
user: {
name: req.session.name,
email: req.session.email,
role: req.session.role,
},
});
}));
// Products API
router.get("/products", requireAuth, async (req, res) => {
try {
// Generic CRUD factory function
const createCRUDRoutes = (config) => {
const { table, resourceName, listFields = "*", requiresAuth = true } = config;
const auth = requiresAuth ? requireAuth : (req, res, next) => next();
// List all
router.get(`/${resourceName}`, auth, asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, name, price, stockquantity, isactive, createdat FROM products ORDER BY createdat DESC"
`SELECT ${listFields} FROM ${table} ORDER BY createdat DESC`
);
res.json({
success: true,
products: result.rows,
});
} catch (error) {
console.error("Products error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { [resourceName]: result.rows });
}));
// Portfolio Projects API
router.get("/portfolio/projects", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, title, description, imageurl, categoryid, createdat FROM portfolioprojects ORDER BY createdat DESC"
);
res.json({
success: true,
projects: result.rows,
});
} catch (error) {
console.error("Portfolio error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Blog Posts API
router.get("/blog", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, title, slug, excerpt, ispublished, createdat FROM blogposts ORDER BY createdat DESC"
);
res.json({
success: true,
posts: result.rows,
});
} catch (error) {
console.error("Blog error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Pages API
router.get("/pages", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, title, slug, ispublished, createdat FROM pages ORDER BY createdat DESC"
);
res.json({
success: true,
pages: result.rows,
});
} catch (error) {
console.error("Pages error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Get single product
router.get("/products/:id", requireAuth, async (req, res) => {
try {
const result = await query("SELECT * FROM products WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
// Get by ID
router.get(`/${resourceName}/:id`, auth, asyncHandler(async (req, res) => {
const item = await getById(table, req.params.id);
if (!item) {
return sendNotFound(res, resourceName);
}
res.json({
success: true,
product: result.rows[0],
});
} catch (error) {
console.error("Product error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
const responseKey = resourceName.slice(0, -1); // Remove 's' for singular
sendSuccess(res, { [responseKey]: item });
}));
// Create product
router.post("/products", requireAuth, async (req, res) => {
try {
const {
name,
description,
price,
stockquantity,
category,
isactive,
isbestseller,
} = req.body;
const result = await query(
`INSERT INTO products (name, description, price, stockquantity, category, isactive, isbestseller, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
RETURNING *`,
[
name,
description,
price,
stockquantity || 0,
category,
isactive !== false,
isbestseller || false,
]
);
res.json({
success: true,
product: result.rows[0],
message: "Product created successfully",
});
} catch (error) {
console.error("Create product error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Update product
router.put("/products/:id", requireAuth, async (req, res) => {
try {
const {
name,
description,
price,
stockquantity,
category,
isactive,
isbestseller,
} = req.body;
const result = await query(
`UPDATE products
SET name = $1, description = $2, price = $3, stockquantity = $4,
category = $5, isactive = $6, isbestseller = $7, updatedat = NOW()
WHERE id = $8
RETURNING *`,
[
name,
description,
price,
stockquantity || 0,
category,
isactive !== false,
isbestseller || false,
req.params.id,
]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
// Delete
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` });
}));
};
res.json({
success: true,
product: result.rows[0],
message: "Product updated successfully",
});
} catch (error) {
console.error("Update product error:", error);
res.status(500).json({ success: false, message: "Server error" });
// Products CRUD
router.get("/products", requireAuth, asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, name, price, stockquantity, isactive, createdat FROM products ORDER BY createdat DESC"
);
sendSuccess(res, { products: result.rows });
}));
router.get("/products/:id", requireAuth, asyncHandler(async (req, res) => {
const product = await getById("products", req.params.id);
if (!product) {
return sendNotFound(res, "Product");
}
});
sendSuccess(res, { product });
}));
// Delete product
router.delete("/products/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"DELETE FROM products WHERE id = $1 RETURNING id",
[req.params.id]
);
router.post("/products", requireAuth, asyncHandler(async (req, res) => {
const { name, description, price, stockquantity, category, isactive, isbestseller } = req.body;
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
}
const result = await query(
`INSERT INTO products (name, description, price, stockquantity, category, isactive, isbestseller, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`,
[name, description, price, stockquantity || 0, category, isactive !== false, isbestseller || false]
);
res.json({
success: true,
message: "Product deleted successfully",
});
} catch (error) {
console.error("Delete product error:", error);
res.status(500).json({ success: false, message: "Server error" });
sendSuccess(res, {
product: result.rows[0],
message: "Product created successfully",
}, HTTP_STATUS.CREATED);
}));
router.put("/products/:id", requireAuth, asyncHandler(async (req, res) => {
const { name, description, price, stockquantity, category, isactive, isbestseller } = req.body;
const result = await query(
`UPDATE products
SET name = $1, description = $2, price = $3, stockquantity = $4,
category = $5, isactive = $6, isbestseller = $7, updatedat = NOW()
WHERE id = $8 RETURNING *`,
[name, description, price, stockquantity || 0, category, isactive !== false, isbestseller || false, req.params.id]
);
if (result.rows.length === 0) {
return sendNotFound(res, "Product");
}
});
// Portfolio Project CRUD
router.get("/portfolio/projects/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT * FROM portfolioprojects WHERE id = $1",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Project not found" });
}
res.json({ success: true, project: result.rows[0] });
} catch (error) {
console.error("Portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, {
product: result.rows[0],
message: "Product updated successfully",
});
}));
router.post("/portfolio/projects", requireAuth, async (req, res) => {
try {
const { title, description, category, isactive } = req.body;
const result = await query(
`INSERT INTO portfolioprojects (title, description, category, isactive, createdat)
VALUES ($1, $2, $3, $4, NOW()) RETURNING *`,
[title, description, category, isactive !== false]
);
res.json({
success: true,
project: result.rows[0],
message: "Project created successfully",
});
} catch (error) {
console.error("Create portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
router.delete("/products/:id", requireAuth, asyncHandler(async (req, res) => {
const deleted = await deleteById("products", req.params.id);
if (!deleted) {
return sendNotFound(res, "Product");
}
});
sendSuccess(res, { message: "Product deleted successfully" });
}));
router.put("/portfolio/projects/:id", requireAuth, async (req, res) => {
try {
const { title, description, category, isactive } = req.body;
const result = await query(
`UPDATE portfolioprojects
SET title = $1, description = $2, category = $3, isactive = $4, updatedat = NOW()
WHERE id = $5 RETURNING *`,
[title, description, category, isactive !== false, req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Project not found" });
}
res.json({
success: true,
project: result.rows[0],
message: "Project updated successfully",
});
} catch (error) {
console.error("Update portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Portfolio Projects CRUD
router.get("/portfolio/projects", requireAuth, asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, description, imageurl, categoryid, createdat FROM portfolioprojects ORDER BY createdat DESC"
);
sendSuccess(res, { projects: result.rows });
}));
router.delete("/portfolio/projects/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"DELETE FROM portfolioprojects WHERE id = $1 RETURNING id",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Project not found" });
}
res.json({ success: true, message: "Project deleted successfully" });
} catch (error) {
console.error("Delete portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
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 });
}));
// Blog Post CRUD
router.get("/blog/:id", requireAuth, async (req, res) => {
try {
const result = await query("SELECT * FROM blogposts WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
}
res.json({ success: true, post: result.rows[0] });
} catch (error) {
console.error("Blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.post("/portfolio/projects", requireAuth, asyncHandler(async (req, res) => {
const { title, description, category, isactive } = req.body;
const result = await query(
`INSERT INTO portfolioprojects (title, description, category, isactive, createdat)
VALUES ($1, $2, $3, $4, NOW()) RETURNING *`,
[title, description, category, isactive !== false]
);
sendSuccess(res, {
project: result.rows[0],
message: "Project created successfully",
}, HTTP_STATUS.CREATED);
}));
router.post("/blog", requireAuth, async (req, res) => {
try {
const {
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished,
} = req.body;
const result = await query(
`INSERT INTO blogposts (title, slug, excerpt, content, metatitle, metadescription, ispublished, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`,
[
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished || false,
]
);
res.json({
success: true,
post: result.rows[0],
message: "Blog post created successfully",
});
} catch (error) {
console.error("Create blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
router.put("/portfolio/projects/:id", requireAuth, asyncHandler(async (req, res) => {
const { title, description, category, isactive } = req.body;
const result = await query(
`UPDATE portfolioprojects
SET title = $1, description = $2, category = $3, isactive = $4, updatedat = NOW()
WHERE id = $5 RETURNING *`,
[title, description, category, isactive !== false, req.params.id]
);
if (result.rows.length === 0) {
return sendNotFound(res, "Project");
}
});
sendSuccess(res, {
project: result.rows[0],
message: "Project updated successfully",
});
}));
router.put("/blog/:id", requireAuth, async (req, res) => {
try {
const {
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished,
} = req.body;
const result = await query(
`UPDATE blogposts
SET title = $1, slug = $2, excerpt = $3, content = $4, metatitle = $5,
metadescription = $6, ispublished = $7, updatedat = NOW()
WHERE id = $8 RETURNING *`,
[
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished || false,
req.params.id,
]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
}
res.json({
success: true,
post: result.rows[0],
message: "Blog post updated successfully",
});
} catch (error) {
console.error("Update blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
router.delete("/portfolio/projects/:id", requireAuth, asyncHandler(async (req, res) => {
const deleted = await deleteById("portfolioprojects", req.params.id);
if (!deleted) {
return sendNotFound(res, "Project");
}
});
sendSuccess(res, { message: "Project deleted successfully" });
}));
router.delete("/blog/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"DELETE FROM blogposts WHERE id = $1 RETURNING id",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
}
res.json({ success: true, message: "Blog post deleted successfully" });
} catch (error) {
console.error("Delete blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
// Blog Posts CRUD
router.get("/blog", requireAuth, asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, slug, excerpt, ispublished, createdat FROM blogposts ORDER BY createdat DESC"
);
sendSuccess(res, { posts: result.rows });
}));
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 });
}));
router.post("/blog", requireAuth, asyncHandler(async (req, res) => {
const { title, slug, excerpt, content, metatitle, metadescription, ispublished } = req.body;
const result = await query(
`INSERT INTO blogposts (title, slug, excerpt, content, metatitle, metadescription, ispublished, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`,
[title, slug, excerpt, content, metatitle, metadescription, ispublished || false]
);
sendSuccess(res, {
post: result.rows[0],
message: "Blog post created successfully",
}, HTTP_STATUS.CREATED);
}));
router.put("/blog/:id", requireAuth, asyncHandler(async (req, res) => {
const { title, slug, excerpt, content, metatitle, metadescription, ispublished } = req.body;
const result = await query(
`UPDATE blogposts
SET title = $1, slug = $2, excerpt = $3, content = $4, metatitle = $5,
metadescription = $6, ispublished = $7, updatedat = NOW()
WHERE id = $8 RETURNING *`,
[title, slug, excerpt, content, metatitle, metadescription, ispublished || false, req.params.id]
);
if (result.rows.length === 0) {
return sendNotFound(res, "Blog post");
}
sendSuccess(res, {
post: result.rows[0],
message: "Blog post updated successfully",
});
}));
router.delete("/blog/:id", requireAuth, asyncHandler(async (req, res) => {
const deleted = await deleteById("blogposts", req.params.id);
if (!deleted) {
return sendNotFound(res, "Blog post");
}
sendSuccess(res, { message: "Blog post deleted successfully" });
}));
// Custom Pages CRUD
router.get("/pages/:id", requireAuth, async (req, res) => {
try {
const result = await query("SELECT * FROM pages WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
}
res.json({ success: true, page: result.rows[0] });
} catch (error) {
console.error("Page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.get("/pages", requireAuth, asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, slug, ispublished, createdat FROM pages ORDER BY createdat DESC"
);
sendSuccess(res, { pages: result.rows });
}));
router.post("/pages", requireAuth, async (req, res) => {
try {
const { title, slug, content, metatitle, metadescription, ispublished } =
req.body;
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 });
}));
router.post("/pages", requireAuth, asyncHandler(async (req, res) => {
const { title, slug, content, metatitle, metadescription, ispublished } = req.body;
const result = await query(
`INSERT INTO pages (title, slug, content, metatitle, metadescription, ispublished, createdat)
VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING *`,
[title, slug, content, metatitle, metadescription, ispublished !== false]
);
sendSuccess(res, {
page: result.rows[0],
message: "Page created successfully",
}, HTTP_STATUS.CREATED);
}));
router.put("/pages/:id", requireAuth, asyncHandler(async (req, res) => {
const { title, slug, content, metatitle, metadescription, ispublished } = req.body;
const result = await query(
`UPDATE pages
SET title = $1, slug = $2, content = $3, metatitle = $4,
metadescription = $5, ispublished = $6, updatedat = NOW()
WHERE id = $7 RETURNING *`,
[title, slug, content, metatitle, metadescription, ispublished !== false, req.params.id]
);
if (result.rows.length === 0) {
return sendNotFound(res, "Page");
}
sendSuccess(res, {
page: result.rows[0],
message: "Page updated successfully",
});
}));
router.delete("/pages/:id", requireAuth, asyncHandler(async (req, res) => {
const deleted = await deleteById("pages", req.params.id);
if (!deleted) {
return sendNotFound(res, "Page");
}
sendSuccess(res, { message: "Page deleted successfully" });
}));
// Settings Management
const settingsHandler = (key) => ({
get: asyncHandler(async (req, res) => {
const result = await query(
`INSERT INTO pages (title, slug, content, metatitle, metadescription, ispublished, createdat)
VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING *`,
[title, slug, content, metatitle, metadescription, ispublished !== false]
"SELECT settings FROM site_settings WHERE key = $1",
[key]
);
res.json({
success: true,
page: result.rows[0],
message: "Page created successfully",
});
} catch (error) {
console.error("Create page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.put("/pages/:id", requireAuth, async (req, res) => {
try {
const { title, slug, content, metatitle, metadescription, ispublished } =
req.body;
const result = await query(
`UPDATE pages
SET title = $1, slug = $2, content = $3, metatitle = $4,
metadescription = $5, ispublished = $6, updatedat = NOW()
WHERE id = $7 RETURNING *`,
[
title,
slug,
content,
metatitle,
metadescription,
ispublished !== false,
req.params.id,
]
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
sendSuccess(res, { settings });
}),
post: 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()`,
[key, JSON.stringify(settings)]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
}
res.json({
success: true,
page: result.rows[0],
message: "Page updated successfully",
});
} catch (error) {
console.error("Update page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.delete("/pages/:id", requireAuth, async (req, res) => {
try {
const result = await query("DELETE FROM pages WHERE id = $1 RETURNING id", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
}
res.json({ success: true, message: "Page deleted successfully" });
} catch (error) {
console.error("Delete page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
sendSuccess(res, { message: `${key} settings saved successfully` });
}),
});
// Homepage Settings
router.get("/homepage/settings", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'homepage'"
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
res.json({ success: true, settings });
} catch (error) {
console.error("Homepage settings error:", error);
res.json({ success: true, settings: {} });
}
});
router.post("/homepage/settings", requireAuth, async (req, res) => {
try {
const settings = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('homepage', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify(settings)]
);
res.json({
success: true,
message: "Homepage settings saved successfully",
});
} catch (error) {
console.error("Save homepage settings error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
const homepageSettings = settingsHandler("homepage");
router.get("/homepage/settings", requireAuth, homepageSettings.get);
router.post("/homepage/settings", requireAuth, homepageSettings.post);
// General Settings
router.get("/settings", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'general'"
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
res.json({ success: true, settings });
} catch (error) {
console.error("Settings error:", error);
res.json({ success: true, settings: {} });
}
});
router.post("/settings", requireAuth, async (req, res) => {
try {
const settings = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('general', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify(settings)]
);
res.json({ success: true, message: "Settings saved successfully" });
} catch (error) {
console.error("Save settings error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
const generalSettings = settingsHandler("general");
router.get("/settings", requireAuth, generalSettings.get);
router.post("/settings", requireAuth, generalSettings.post);
// Menu Management
router.get("/menu", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'menu'"
);
const items =
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
res.json({ success: true, items });
} catch (error) {
console.error("Menu error:", error);
res.json({ success: true, items: [] });
}
});
router.get("/menu", requireAuth, asyncHandler(async (req, res) => {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'menu'"
);
const items = result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
sendSuccess(res, { items });
}));
router.post("/menu", requireAuth, async (req, res) => {
try {
const { items } = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('menu', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify({ items })]
);
res.json({ success: true, message: "Menu saved successfully" });
} catch (error) {
console.error("Save menu error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.post("/menu", requireAuth, asyncHandler(async (req, res) => {
const { items } = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('menu', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify({ items })]
);
sendSuccess(res, { message: "Menu saved successfully" });
}));
module.exports = router;

View File

@@ -0,0 +1,611 @@
const express = require("express");
const { query } = require("../config/database");
const { requireAuth } = require("../middleware/auth");
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
const router = express.Router();
// Dashboard stats API
router.get("/dashboard/stats", requireAuth, async (req, res) => {
try {
const productsCount = await query("SELECT COUNT(*) FROM products");
const projectsCount = await query("SELECT COUNT(*) FROM portfolioprojects");
const blogCount = await query("SELECT COUNT(*) FROM blogposts");
const pagesCount = await query("SELECT COUNT(*) FROM pages");
res.json({
success: true,
stats: {
products: parseInt(productsCount.rows[0].count),
projects: parseInt(projectsCount.rows[0].count),
blog: parseInt(blogCount.rows[0].count),
pages: parseInt(pagesCount.rows[0].count),
},
user: {
name: req.session.name,
email: req.session.email,
role: req.session.role,
},
});
} catch (error) {
logger.error("Dashboard error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Products API
router.get("/products", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, name, price, stockquantity, isactive, createdat FROM products ORDER BY createdat DESC"
);
res.json({
success: true,
products: result.rows,
});
} catch (error) {
logger.error("Products error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Portfolio Projects API
router.get("/portfolio/projects", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, title, description, imageurl, categoryid, createdat FROM portfolioprojects ORDER BY createdat DESC"
);
res.json({
success: true,
projects: result.rows,
});
} catch (error) {
logger.error("Portfolio error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Blog Posts API
router.get("/blog", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, title, slug, excerpt, ispublished, createdat FROM blogposts ORDER BY createdat DESC"
);
res.json({
success: true,
posts: result.rows,
});
} catch (error) {
logger.error("Blog error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Pages API
router.get("/pages", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT id, title, slug, ispublished, createdat FROM pages ORDER BY createdat DESC"
);
res.json({
success: true,
pages: result.rows,
});
} catch (error) {
logger.error("Pages error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Get single product
router.get("/products/:id", requireAuth, async (req, res) => {
try {
const result = await query("SELECT * FROM products WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
}
res.json({
success: true,
product: result.rows[0],
});
} catch (error) {
logger.error("Product error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Create product
router.post("/products", requireAuth, async (req, res) => {
try {
const {
name,
description,
price,
stockquantity,
category,
isactive,
isbestseller,
} = req.body;
const result = await query(
`INSERT INTO products (name, description, price, stockquantity, category, isactive, isbestseller, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
RETURNING *`,
[
name,
description,
price,
stockquantity || 0,
category,
isactive !== false,
isbestseller || false,
]
);
res.json({
success: true,
product: result.rows[0],
message: "Product created successfully",
});
} catch (error) {
logger.error("Create product error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Update product
router.put("/products/:id", requireAuth, async (req, res) => {
try {
const {
name,
description,
price,
stockquantity,
category,
isactive,
isbestseller,
} = req.body;
const result = await query(
`UPDATE products
SET name = $1, description = $2, price = $3, stockquantity = $4,
category = $5, isactive = $6, isbestseller = $7, updatedat = NOW()
WHERE id = $8
RETURNING *`,
[
name,
description,
price,
stockquantity || 0,
category,
isactive !== false,
isbestseller || false,
req.params.id,
]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
}
res.json({
success: true,
product: result.rows[0],
message: "Product updated successfully",
});
} catch (error) {
logger.error("Update product error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Delete product
router.delete("/products/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"DELETE FROM products WHERE id = $1 RETURNING id",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
}
res.json({
success: true,
message: "Product deleted successfully",
});
} catch (error) {
logger.error("Delete product error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Portfolio Project CRUD
router.get("/portfolio/projects/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT * FROM portfolioprojects WHERE id = $1",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Project not found" });
}
res.json({ success: true, project: result.rows[0] });
} catch (error) {
logger.error("Portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.post("/portfolio/projects", requireAuth, async (req, res) => {
try {
const { title, description, category, isactive } = req.body;
const result = await query(
`INSERT INTO portfolioprojects (title, description, category, isactive, createdat)
VALUES ($1, $2, $3, $4, NOW()) RETURNING *`,
[title, description, category, isactive !== false]
);
res.json({
success: true,
project: result.rows[0],
message: "Project created successfully",
});
} catch (error) {
logger.error("Create portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.put("/portfolio/projects/:id", requireAuth, async (req, res) => {
try {
const { title, description, category, isactive } = req.body;
const result = await query(
`UPDATE portfolioprojects
SET title = $1, description = $2, category = $3, isactive = $4, updatedat = NOW()
WHERE id = $5 RETURNING *`,
[title, description, category, isactive !== false, req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Project not found" });
}
res.json({
success: true,
project: result.rows[0],
message: "Project updated successfully",
});
} catch (error) {
logger.error("Update portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.delete("/portfolio/projects/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"DELETE FROM portfolioprojects WHERE id = $1 RETURNING id",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Project not found" });
}
res.json({ success: true, message: "Project deleted successfully" });
} catch (error) {
logger.error("Delete portfolio project error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Blog Post CRUD
router.get("/blog/:id", requireAuth, async (req, res) => {
try {
const result = await query("SELECT * FROM blogposts WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
}
res.json({ success: true, post: result.rows[0] });
} catch (error) {
logger.error("Blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.post("/blog", requireAuth, async (req, res) => {
try {
const {
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished,
} = req.body;
const result = await query(
`INSERT INTO blogposts (title, slug, excerpt, content, metatitle, metadescription, ispublished, createdat)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`,
[
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished || false,
]
);
res.json({
success: true,
post: result.rows[0],
message: "Blog post created successfully",
});
} catch (error) {
logger.error("Create blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.put("/blog/:id", requireAuth, async (req, res) => {
try {
const {
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished,
} = req.body;
const result = await query(
`UPDATE blogposts
SET title = $1, slug = $2, excerpt = $3, content = $4, metatitle = $5,
metadescription = $6, ispublished = $7, updatedat = NOW()
WHERE id = $8 RETURNING *`,
[
title,
slug,
excerpt,
content,
metatitle,
metadescription,
ispublished || false,
req.params.id,
]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
}
res.json({
success: true,
post: result.rows[0],
message: "Blog post updated successfully",
});
} catch (error) {
logger.error("Update blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.delete("/blog/:id", requireAuth, async (req, res) => {
try {
const result = await query(
"DELETE FROM blogposts WHERE id = $1 RETURNING id",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
}
res.json({ success: true, message: "Blog post deleted successfully" });
} catch (error) {
logger.error("Delete blog post error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Custom Pages CRUD
router.get("/pages/:id", requireAuth, async (req, res) => {
try {
const result = await query("SELECT * FROM pages WHERE id = $1", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
}
res.json({ success: true, page: result.rows[0] });
} catch (error) {
logger.error("Page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.post("/pages", requireAuth, async (req, res) => {
try {
const { title, slug, content, metatitle, metadescription, ispublished } =
req.body;
const result = await query(
`INSERT INTO pages (title, slug, content, metatitle, metadescription, ispublished, createdat)
VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING *`,
[title, slug, content, metatitle, metadescription, ispublished !== false]
);
res.json({
success: true,
page: result.rows[0],
message: "Page created successfully",
});
} catch (error) {
logger.error("Create page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.put("/pages/:id", requireAuth, async (req, res) => {
try {
const { title, slug, content, metatitle, metadescription, ispublished } =
req.body;
const result = await query(
`UPDATE pages
SET title = $1, slug = $2, content = $3, metatitle = $4,
metadescription = $5, ispublished = $6, updatedat = NOW()
WHERE id = $7 RETURNING *`,
[
title,
slug,
content,
metatitle,
metadescription,
ispublished !== false,
req.params.id,
]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
}
res.json({
success: true,
page: result.rows[0],
message: "Page updated successfully",
});
} catch (error) {
logger.error("Update page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
router.delete("/pages/:id", requireAuth, async (req, res) => {
try {
const result = await query("DELETE FROM pages WHERE id = $1 RETURNING id", [
req.params.id,
]);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
}
res.json({ success: true, message: "Page deleted successfully" });
} catch (error) {
logger.error("Delete page error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Homepage Settings
router.get("/homepage/settings", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'homepage'"
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
res.json({ success: true, settings });
} catch (error) {
logger.error("Homepage settings error:", error);
res.json({ success: true, settings: {} });
}
});
router.post("/homepage/settings", requireAuth, async (req, res) => {
try {
const settings = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('homepage', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify(settings)]
);
res.json({
success: true,
message: "Homepage settings saved successfully",
});
} catch (error) {
logger.error("Save homepage settings error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// General Settings
router.get("/settings", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'general'"
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
res.json({ success: true, settings });
} catch (error) {
logger.error("Settings error:", error);
res.json({ success: true, settings: {} });
}
});
router.post("/settings", requireAuth, async (req, res) => {
try {
const settings = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('general', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify(settings)]
);
res.json({ success: true, message: "Settings saved successfully" });
} catch (error) {
logger.error("Save settings error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Menu Management
router.get("/menu", requireAuth, async (req, res) => {
try {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'menu'"
);
const items =
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
res.json({ success: true, items });
} catch (error) {
logger.error("Menu error:", error);
res.json({ success: true, items: [] });
}
});
router.post("/menu", requireAuth, async (req, res) => {
try {
const { items } = req.body;
await query(
`INSERT INTO site_settings (key, settings, updatedat)
VALUES ('menu', $1, NOW())
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
[JSON.stringify({ items })]
);
res.json({ success: true, message: "Menu saved successfully" });
} catch (error) {
logger.error("Save menu error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
module.exports = router;

View File

@@ -1,100 +1,112 @@
const express = require("express");
const bcrypt = require("bcrypt");
const { query } = require("../config/database");
const logger = require("../config/logger");
const {
validators,
handleValidationErrors,
} = require("../middleware/validators");
const { asyncHandler } = require("../middleware/errorHandler");
const {
sendSuccess,
sendError,
sendUnauthorized,
} = require("../utils/responseHelpers");
const { HTTP_STATUS } = require("../config/constants");
const router = express.Router();
// Login endpoint (JSON API)
router.post("/login", async (req, res) => {
const { email, password } = req.body;
try {
const result = await query(
`
SELECT u.id, u.email, u.username, u.passwordhash, u.role_id, u.isactive,
r.name as role_name, r.permissions
FROM adminusers u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.email = $1
`,
[email]
);
const getUserByEmail = async (email) => {
const result = await query(
`SELECT u.id, u.email, u.username, u.passwordhash, u.role_id, u.isactive,
r.name as role_name, r.permissions
FROM adminusers u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.email = $1`,
[email]
);
return result.rows[0] || null;
};
if (result.rows.length === 0) {
return res
.status(401)
.json({ success: false, message: "Invalid email or password" });
const updateLastLogin = async (userId) => {
await query("UPDATE adminusers SET last_login = NOW() WHERE id = $1", [
userId,
]);
};
const createUserSession = (req, user) => {
req.session.user = {
id: user.id,
email: user.email,
username: user.username,
role_id: user.role_id,
role_name: user.role_name,
permissions: user.permissions,
};
};
// Login endpoint
router.post(
"/login",
validators.login,
handleValidationErrors,
asyncHandler(async (req, res) => {
const { email, password } = req.body;
const admin = await getUserByEmail(email);
if (!admin) {
logger.warn("Login attempt with invalid email", { email });
return sendUnauthorized(res, "Invalid email or password");
}
const admin = result.rows[0];
// Check if user is active
if (!admin.isactive) {
return res
.status(401)
.json({ success: false, message: "Account is deactivated" });
logger.warn("Login attempt with deactivated account", { email });
return sendUnauthorized(res, "Account is deactivated");
}
const validPassword = await bcrypt.compare(password, admin.passwordhash);
if (!validPassword) {
return res
.status(401)
.json({ success: false, message: "Invalid email or password" });
logger.warn("Login attempt with invalid password", { email });
return sendUnauthorized(res, "Invalid email or password");
}
// Update last login
await query("UPDATE adminusers SET last_login = NOW() WHERE id = $1", [
admin.id,
]);
await updateLastLogin(admin.id);
createUserSession(req, admin);
// Store user info in session
req.session.user = {
id: admin.id,
email: admin.email,
username: admin.username,
role_id: admin.role_id,
role_name: admin.role_name,
permissions: admin.permissions,
};
// Save session before responding
req.session.save((err) => {
if (err) {
console.error("Session save error:", err);
return res
.status(500)
.json({ success: false, message: "Session error" });
logger.error("Session save error:", err);
return sendError(res, "Session error");
}
res.json({
success: true,
user: req.session.user,
logger.info("User logged in successfully", {
userId: admin.id,
email: admin.email,
});
sendSuccess(res, { user: req.session.user });
});
} catch (error) {
console.error("Login error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
})
);
// Check session endpoint
router.get("/session", (req, res) => {
if (req.session && req.session.user) {
res.json({
authenticated: true,
user: req.session.user,
});
} else {
res.status(401).json({ authenticated: false });
if (req.session?.user) {
return sendSuccess(res, { authenticated: true, user: req.session.user });
}
res.status(HTTP_STATUS.UNAUTHORIZED).json({ authenticated: false });
});
// Logout endpoint
router.post("/logout", (req, res) => {
const userId = req.session?.user?.id;
req.session.destroy((err) => {
if (err) {
console.error("Logout error:", err);
return res.status(500).json({ success: false, message: "Logout failed" });
logger.error("Logout error:", err);
return sendError(res, "Logout failed");
}
res.json({ success: true, message: "Logged out successfully" });
logger.info("User logged out", { userId });
sendSuccess(res, { message: "Logged out successfully" });
});
});

View File

@@ -1,220 +1,179 @@
const express = require("express");
const { query } = require("../config/database");
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
const {
sendSuccess,
sendError,
sendNotFound,
} = require("../utils/responseHelpers");
const router = express.Router();
const handleDatabaseError = (res, error, context) => {
logger.error(`${context} error:`, error);
sendError(res);
};
// Get all products
router.get("/products", async (req, res) => {
try {
router.get(
"/products",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, name, description, shortdescription, price, imageurl, images, category, color, stockquantity, isactive, createdat FROM products WHERE isactive = true ORDER BY createdat DESC"
`SELECT id, name, description, shortdescription, price, imageurl, images,
category, color, stockquantity, isactive, createdat
FROM products WHERE isactive = true ORDER BY createdat DESC`
);
res.json({
success: true,
products: result.rows,
});
} catch (error) {
console.error("Products API error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { products: result.rows });
})
);
// Get featured products
router.get("/products/featured", async (req, res) => {
try {
router.get(
"/products/featured",
asyncHandler(async (req, res) => {
const limit = parseInt(req.query.limit) || 4;
const result = await query(
"SELECT id, name, description, price, imageurl, images FROM products WHERE isactive = true ORDER BY createdat DESC LIMIT $1",
`SELECT id, name, description, price, imageurl, images
FROM products WHERE isactive = true ORDER BY createdat DESC LIMIT $1`,
[limit]
);
res.json({
success: true,
products: result.rows,
});
} catch (error) {
console.error("Featured products error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { products: result.rows });
})
);
// Get single product
router.get("/products/:id", async (req, res) => {
try {
router.get(
"/products/:id",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT * FROM products WHERE id = $1 AND isactive = true",
[req.params.id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Product not found" });
return sendNotFound(res, "Product");
}
res.json({
success: true,
product: result.rows[0],
});
} catch (error) {
console.error("Product detail error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { product: result.rows[0] });
})
);
// Get site settings
router.get("/settings", async (req, res) => {
try {
router.get(
"/settings",
asyncHandler(async (req, res) => {
const result = await query("SELECT * FROM sitesettings LIMIT 1");
res.json({
success: true,
settings: result.rows[0] || {},
});
} catch (error) {
console.error("Settings error:", error);
res.json({ success: true, settings: {} });
}
});
sendSuccess(res, { settings: result.rows[0] || {} });
})
);
// Get homepage sections
router.get("/homepage/sections", async (req, res) => {
try {
router.get(
"/homepage/sections",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT * FROM homepagesections ORDER BY displayorder ASC"
);
res.json({
success: true,
sections: result.rows,
});
} catch (error) {
console.error("Homepage sections error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { sections: result.rows });
})
);
// Get portfolio projects
router.get("/portfolio/projects", async (req, res) => {
try {
router.get(
"/portfolio/projects",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, description, featuredimage, images, category, categoryid, isactive, createdat FROM portfolioprojects WHERE isactive = true ORDER BY displayorder ASC, createdat DESC"
`SELECT id, title, description, featuredimage, images, category,
categoryid, isactive, createdat
FROM portfolioprojects WHERE isactive = true
ORDER BY displayorder ASC, createdat DESC`
);
res.json({
success: true,
projects: result.rows,
});
} catch (error) {
console.error("Portfolio error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { projects: result.rows });
})
);
// Get blog posts
router.get("/blog/posts", async (req, res) => {
try {
router.get(
"/blog/posts",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, slug, excerpt, content, imageurl, ispublished, createdat FROM blogposts WHERE ispublished = true ORDER BY createdat DESC"
`SELECT id, title, slug, excerpt, content, imageurl, ispublished, createdat
FROM blogposts WHERE ispublished = true ORDER BY createdat DESC`
);
res.json({
success: true,
posts: result.rows,
});
} catch (error) {
console.error("Blog posts error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { posts: result.rows });
})
);
// Get single blog post by slug
router.get("/blog/posts/:slug", async (req, res) => {
try {
router.get(
"/blog/posts/:slug",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT * FROM blogposts WHERE slug = $1 AND ispublished = true",
[req.params.slug]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Blog post not found" });
return sendNotFound(res, "Blog post");
}
res.json({
success: true,
post: result.rows[0],
});
} catch (error) {
console.error("Blog post detail error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { post: result.rows[0] });
})
);
// Get custom pages
router.get("/pages", async (req, res) => {
try {
router.get(
"/pages",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, title, slug, content, metatitle, metadescription, isactive, createdat FROM pages WHERE isactive = true ORDER BY createdat DESC"
`SELECT id, title, slug, content, metatitle, metadescription, isactive, createdat
FROM pages WHERE isactive = true ORDER BY createdat DESC`
);
res.json({
success: true,
pages: result.rows,
});
} catch (error) {
console.error("Pages error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { pages: result.rows });
})
);
// Get single page by slug
router.get("/pages/:slug", async (req, res) => {
try {
router.get(
"/pages/:slug",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT * FROM pages WHERE slug = $1 AND isactive = true",
[req.params.slug]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ success: false, message: "Page not found" });
return sendNotFound(res, "Page");
}
res.json({
success: true,
page: result.rows[0],
});
} catch (error) {
console.error("Page detail error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
sendSuccess(res, { page: result.rows[0] });
})
);
// Get menu items for frontend navigation
router.get("/menu", async (req, res) => {
try {
router.get(
"/menu",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'menu'"
);
const items =
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
// Filter only visible items
const visibleItems = items.filter((item) => item.visible !== false);
res.json({
success: true,
items: visibleItems,
});
} catch (error) {
console.error("Menu error:", error);
res.json({ success: true, items: [] });
}
});
sendSuccess(res, { items: visibleItems });
})
);
// Get homepage settings for frontend
router.get("/homepage/settings", async (req, res) => {
try {
router.get(
"/homepage/settings",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'homepage'"
);
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
res.json({
success: true,
settings,
});
} catch (error) {
console.error("Homepage settings error:", error);
res.json({ success: true, settings: {} });
}
});
sendSuccess(res, { settings });
})
);
module.exports = router;

View File

@@ -5,6 +5,15 @@ const path = require("path");
const fs = require("fs").promises;
const { requireAuth } = require("../middleware/auth");
const { pool } = require("../config/database");
const logger = require("../config/logger");
const { uploadLimiter } = require("../config/rateLimiter");
require("dotenv").config();
// Allowed file types
const ALLOWED_MIME_TYPES = (
process.env.ALLOWED_FILE_TYPES || "image/jpeg,image/png,image/gif,image/webp"
).split(",");
const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024; // 5MB default
// Configure multer for file uploads
const storage = multer.diskStorage({
@@ -14,17 +23,19 @@ const storage = multer.diskStorage({
await fs.mkdir(uploadDir, { recursive: true });
cb(null, uploadDir);
} catch (error) {
logger.error("Error creating upload directory:", error);
cb(error);
}
},
filename: function (req, file, cb) {
// Generate unique filename
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
const ext = path.extname(file.originalname).toLowerCase();
const name = path
.basename(file.originalname, ext)
.replace(/[^a-z0-9]/gi, "-")
.toLowerCase();
.toLowerCase()
.substring(0, 50); // Limit filename length
cb(null, name + "-" + uniqueSuffix + ext);
},
});
@@ -32,13 +43,37 @@ const storage = multer.diskStorage({
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB limit
fileSize: MAX_FILE_SIZE,
files: 10, // Max 10 files per request
},
fileFilter: function (req, file, cb) {
// Accept images only
if (!file.mimetype.startsWith("image/")) {
return cb(new Error("Only image files are allowed!"), false);
// Validate MIME type
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
logger.warn("File upload rejected - invalid type", {
mimetype: file.mimetype,
userId: req.session?.user?.id,
});
return cb(
new Error(
`File type not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(
", "
)}`
),
false
);
}
// Validate file extension
const ext = path.extname(file.originalname).toLowerCase();
const allowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
if (!allowedExtensions.includes(ext)) {
logger.warn("File upload rejected - invalid extension", {
extension: ext,
userId: req.session?.user?.id,
});
return cb(new Error("Invalid file extension"), false);
}
cb(null, true);
},
});
@@ -47,37 +82,72 @@ const upload = multer({
router.post(
"/upload",
requireAuth,
uploadLimiter,
upload.array("files", 10),
async (req, res) => {
async (req, res, next) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: "No files uploaded",
});
}
const uploadedBy = req.session.user?.id || null;
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
const files = [];
// Insert each file into database
for (const file of req.files) {
const result = await pool.query(
`INSERT INTO uploads
(filename, original_name, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING id, filename, original_name, file_path, file_size, mime_type, created_at`,
[
file.filename,
file.originalname,
`/uploads/${file.filename}`,
file.size,
file.mimetype,
uploadedBy,
]
);
try {
const result = await pool.query(
`INSERT INTO uploads
(filename, original_name, file_path, file_size, mime_type, uploaded_by, folder_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
RETURNING id, filename, original_name, file_path, file_size, mime_type, folder_id, created_at`,
[
file.filename,
file.originalname,
`/uploads/${file.filename}`,
file.size,
file.mimetype,
uploadedBy,
folderId,
]
);
files.push({
id: result.rows[0].id,
filename: result.rows[0].filename,
originalName: result.rows[0].original_name,
size: result.rows[0].file_size,
mimetype: result.rows[0].mime_type,
path: result.rows[0].file_path,
uploadDate: result.rows[0].created_at,
files.push({
id: result.rows[0].id,
filename: result.rows[0].filename,
originalName: result.rows[0].original_name,
size: result.rows[0].file_size,
mimetype: result.rows[0].mime_type,
path: result.rows[0].file_path,
uploadDate: result.rows[0].created_at,
folderId: result.rows[0].folder_id,
});
logger.info("File uploaded successfully", {
fileId: result.rows[0].id,
filename: file.filename,
userId: uploadedBy,
});
} catch (dbError) {
logger.error("Database insert failed for file:", {
filename: file.filename,
error: dbError.message,
});
// Clean up this specific file
await fs
.unlink(file.path)
.catch((err) => logger.error("Failed to clean up file:", err));
}
}
if (files.length === 0) {
return res.status(500).json({
success: false,
message: "Failed to save uploaded files",
});
}
@@ -87,23 +157,19 @@ router.post(
files: files,
});
} catch (error) {
console.error("Upload error:", error);
logger.error("Upload error:", error);
// If database insert fails, clean up uploaded files
// Clean up all uploaded files on error
if (req.files) {
for (const file of req.files) {
try {
await fs.unlink(file.path);
} catch (unlinkError) {
console.error("Error cleaning up file:", unlinkError);
logger.error("Error cleaning up file:", unlinkError);
}
}
}
res.status(500).json({
success: false,
error: error.message,
});
next(error);
}
}
);
@@ -111,9 +177,9 @@ router.post(
// Get all uploaded files
router.get("/uploads", requireAuth, async (req, res) => {
try {
// Query files from database
const result = await pool.query(
`SELECT
const folderId = req.query.folder_id;
let query = `SELECT
id,
filename,
original_name,
@@ -121,13 +187,27 @@ router.get("/uploads", requireAuth, async (req, res) => {
file_size,
mime_type,
uploaded_by,
folder_id,
created_at,
updated_at,
used_in_type,
used_in_id
FROM uploads
ORDER BY created_at DESC`
);
FROM uploads`;
const params = [];
if (folderId !== undefined) {
if (folderId === "null" || folderId === "") {
query += ` WHERE folder_id IS NULL`;
} else {
query += ` WHERE folder_id = $1`;
params.push(parseInt(folderId));
}
}
query += ` ORDER BY created_at DESC`;
const result = await pool.query(query, params);
const files = result.rows.map((row) => ({
id: row.id,
@@ -138,6 +218,7 @@ router.get("/uploads", requireAuth, async (req, res) => {
path: row.file_path,
uploadDate: row.created_at,
uploadedBy: row.uploaded_by,
folderId: row.folder_id,
usedInType: row.used_in_type,
usedInId: row.used_in_id,
}));
@@ -147,7 +228,7 @@ router.get("/uploads", requireAuth, async (req, res) => {
files: files,
});
} catch (error) {
console.error("Error listing files:", error);
logger.error("Error listing files:", error);
res.status(500).json({
success: false,
error: error.message,
@@ -187,7 +268,7 @@ router.delete("/uploads/:filename", requireAuth, async (req, res) => {
try {
await fs.unlink(filePath);
} catch (fileError) {
console.warn("File already deleted from disk:", filename);
logger.warn("File already deleted from disk:", filename);
// Continue anyway since database record is deleted
}
@@ -196,7 +277,339 @@ router.delete("/uploads/:filename", requireAuth, async (req, res) => {
message: "File deleted successfully",
});
} catch (error) {
console.error("Error deleting file:", error);
logger.error("Error deleting file:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Delete file by ID
router.delete("/uploads/id/:id", requireAuth, async (req, res) => {
try {
const fileId = parseInt(req.params.id);
// Get file info first
const fileResult = await pool.query(
"SELECT filename FROM uploads WHERE id = $1",
[fileId]
);
if (fileResult.rows.length === 0) {
return res.status(404).json({
success: false,
error: "File not found",
});
}
const filename = fileResult.rows[0].filename;
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
const filePath = path.join(uploadDir, filename);
// Delete from database
await pool.query("DELETE FROM uploads WHERE id = $1", [fileId]);
// Delete physical file
try {
await fs.unlink(filePath);
} catch (fileError) {
logger.warn("File already deleted from disk:", filename);
}
res.json({
success: true,
message: "File deleted successfully",
});
} catch (error) {
logger.error("Error deleting file:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// ===== FOLDER MANAGEMENT ROUTES =====
// Create a new folder
router.post("/folders", requireAuth, async (req, res) => {
try {
const { name, parent_id } = req.body;
if (!name || name.trim() === "") {
return res.status(400).json({
success: false,
error: "Folder name is required",
});
}
// Sanitize folder name
const sanitizedName = name.trim().replace(/[^a-zA-Z0-9\s\-_]/g, "");
// Build path
let path = `/${sanitizedName}`;
if (parent_id) {
const parentResult = await pool.query(
"SELECT path FROM media_folders WHERE id = $1",
[parent_id]
);
if (parentResult.rows.length === 0) {
return res.status(404).json({
success: false,
error: "Parent folder not found",
});
}
path = `${parentResult.rows[0].path}/${sanitizedName}`;
}
const createdBy = req.session.user?.id || null;
const result = await pool.query(
`INSERT INTO media_folders (name, parent_id, path, created_by)
VALUES ($1, $2, $3, $4)
RETURNING id, name, parent_id, path, created_at`,
[sanitizedName, parent_id || null, path, createdBy]
);
res.json({
success: true,
folder: {
id: result.rows[0].id,
name: result.rows[0].name,
parentId: result.rows[0].parent_id,
path: result.rows[0].path,
createdAt: result.rows[0].created_at,
},
});
} catch (error) {
if (error.code === "23505") {
// Unique constraint violation
return res.status(400).json({
success: false,
error: "A folder with this name already exists in this location",
});
}
logger.error("Error creating folder:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Get all folders
router.get("/folders", requireAuth, async (req, res) => {
try {
const result = await pool.query(
`SELECT
f.id,
f.name,
f.parent_id,
f.path,
f.created_at,
(SELECT COUNT(*) FROM uploads WHERE folder_id = f.id) as file_count,
(SELECT COUNT(*) FROM media_folders WHERE parent_id = f.id) as subfolder_count
FROM media_folders f
ORDER BY f.path ASC`
);
const folders = result.rows.map((row) => ({
id: row.id,
name: row.name,
parentId: row.parent_id,
path: row.path,
createdAt: row.created_at,
fileCount: parseInt(row.file_count),
subfolderCount: parseInt(row.subfolder_count),
}));
res.json({
success: true,
folders: folders,
});
} catch (error) {
logger.error("Error listing folders:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Delete a folder (and optionally its contents)
router.delete("/folders/:id", requireAuth, async (req, res) => {
try {
const folderId = parseInt(req.params.id);
const deleteContents = req.query.delete_contents === "true";
// Check if folder exists
const folderResult = await pool.query(
"SELECT id, name FROM media_folders WHERE id = $1",
[folderId]
);
if (folderResult.rows.length === 0) {
return res.status(404).json({
success: false,
error: "Folder not found",
});
}
if (deleteContents) {
// Delete all files in this folder and subfolders
const filesResult = await pool.query(
`SELECT u.filename FROM uploads u
WHERE u.folder_id = $1 OR u.folder_id IN (
SELECT id FROM media_folders WHERE path LIKE (
SELECT path || '%' FROM media_folders WHERE id = $1
)
)`,
[folderId]
);
// Delete physical files
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
for (const row of filesResult.rows) {
try {
await fs.unlink(path.join(uploadDir, row.filename));
} catch (err) {
logger.warn(`Could not delete file: ${row.filename}`, err);
}
}
// Delete folder (cascade will delete subfolders and DB records)
await pool.query("DELETE FROM media_folders WHERE id = $1", [folderId]);
} else {
// Check if folder has contents
const contentsCheck = await pool.query(
`SELECT
(SELECT COUNT(*) FROM uploads WHERE folder_id = $1) as file_count,
(SELECT COUNT(*) FROM media_folders WHERE parent_id = $1) as subfolder_count`,
[folderId]
);
const fileCount = parseInt(contentsCheck.rows[0].file_count);
const subfolderCount = parseInt(contentsCheck.rows[0].subfolder_count);
if (fileCount > 0 || subfolderCount > 0) {
return res.status(400).json({
success: false,
error: `Folder contains ${fileCount} file(s) and ${subfolderCount} subfolder(s). Delete contents first or use delete_contents=true`,
});
}
await pool.query("DELETE FROM media_folders WHERE id = $1", [folderId]);
}
res.json({
success: true,
message: "Folder deleted successfully",
});
} catch (error) {
logger.error("Error deleting folder:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Move files to a folder
router.patch("/uploads/move", requireAuth, async (req, res) => {
try {
const { file_ids, folder_id } = req.body;
if (!Array.isArray(file_ids) || file_ids.length === 0) {
return res.status(400).json({
success: false,
error: "file_ids array is required",
});
}
const targetFolderId = folder_id || null;
// Verify folder exists if provided
if (targetFolderId) {
const folderCheck = await pool.query(
"SELECT id FROM media_folders WHERE id = $1",
[targetFolderId]
);
if (folderCheck.rows.length === 0) {
return res.status(404).json({
success: false,
error: "Target folder not found",
});
}
}
// Move files
const result = await pool.query(
`UPDATE uploads
SET folder_id = $1, updated_at = NOW()
WHERE id = ANY($2::int[])
RETURNING id`,
[targetFolderId, file_ids]
);
res.json({
success: true,
message: `${result.rowCount} file(s) moved successfully`,
movedCount: result.rowCount,
});
} catch (error) {
logger.error("Error moving files:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Bulk delete files
router.post("/uploads/bulk-delete", requireAuth, async (req, res) => {
try {
const { file_ids } = req.body;
if (!Array.isArray(file_ids) || file_ids.length === 0) {
return res.status(400).json({
success: false,
error: "file_ids array is required",
});
}
// Get filenames first
const filesResult = await pool.query(
"SELECT filename FROM uploads WHERE id = ANY($1::int[])",
[file_ids]
);
// Delete from database
const result = await pool.query(
"DELETE FROM uploads WHERE id = ANY($1::int[])",
[file_ids]
);
// Delete physical files
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
for (const row of filesResult.rows) {
try {
await fs.unlink(path.join(uploadDir, row.filename));
} catch (err) {
logger.warn(`Could not delete file: ${row.filename}`, err);
}
}
res.json({
success: true,
message: `${result.rowCount} file(s) deleted successfully`,
deletedCount: result.rowCount,
});
} catch (error) {
logger.error("Error bulk deleting files:", error);
res.status(500).json({
success: false,
error: error.message,

View File

@@ -2,6 +2,12 @@ const express = require("express");
const bcrypt = require("bcrypt");
const { query } = require("../config/database");
const { requireAuth, requireRole } = require("../middleware/auth");
const logger = require("../config/logger");
const {
validators,
handleValidationErrors,
} = require("../middleware/validators");
const { asyncHandler } = require("../middleware/errorHandler");
const router = express.Router();
// Require admin role for all routes
@@ -24,7 +30,7 @@ router.get("/", async (req, res) => {
users: result.rows,
});
} catch (error) {
console.error("Get users error:", error);
logger.error("Get users error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
@@ -43,7 +49,7 @@ router.get("/roles", async (req, res) => {
roles: result.rows,
});
} catch (error) {
console.error("Get roles error:", error);
logger.error("Get roles error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
@@ -115,7 +121,7 @@ router.post("/", async (req, res) => {
user: result.rows[0],
});
} catch (error) {
console.error("Create user error:", error);
logger.error("Create user error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
@@ -184,7 +190,7 @@ router.put("/:id", async (req, res) => {
user: result.rows[0],
});
} catch (error) {
console.error("Update user error:", error);
logger.error("Update user error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
@@ -244,7 +250,7 @@ router.post("/:id/reset-password", async (req, res) => {
message: "Password reset successfully",
});
} catch (error) {
console.error("Reset password error:", error);
logger.error("Reset password error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
@@ -279,7 +285,7 @@ router.delete("/:id", async (req, res) => {
message: "User deleted successfully",
});
} catch (error) {
console.error("Delete user error:", error);
logger.error("Delete user error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
@@ -323,7 +329,7 @@ router.post("/:id/toggle-status", async (req, res) => {
isactive: result.rows[0].isactive,
});
} catch (error) {
console.error("Toggle status error:", error);
logger.error("Toggle status error:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});