Updatweb
This commit is contained in:
84
backend/config/constants.js
Normal file
84
backend/config/constants.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const path = require("path");
|
||||
|
||||
const ENVIRONMENTS = {
|
||||
DEVELOPMENT: "development",
|
||||
PRODUCTION: "production",
|
||||
};
|
||||
|
||||
const HTTP_STATUS = {
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
CONFLICT: 409,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
INTERNAL_ERROR: 500,
|
||||
SERVICE_UNAVAILABLE: 503,
|
||||
};
|
||||
|
||||
const RATE_LIMITS = {
|
||||
API: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100,
|
||||
},
|
||||
AUTH: {
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
},
|
||||
UPLOAD: {
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const SESSION_CONFIG = {
|
||||
COOKIE_MAX_AGE: 24 * 60 * 60 * 1000, // 24 hours
|
||||
SESSION_NAME: "skyartshop.sid",
|
||||
};
|
||||
|
||||
const BODY_PARSER_LIMITS = {
|
||||
JSON: "10mb",
|
||||
URLENCODED: "10mb",
|
||||
};
|
||||
|
||||
const isDevelopment = () => process.env.NODE_ENV !== ENVIRONMENTS.PRODUCTION;
|
||||
|
||||
const getBaseDir = () =>
|
||||
isDevelopment()
|
||||
? path.join(__dirname, "..", "..", "website")
|
||||
: "/var/www/skyartshop";
|
||||
|
||||
const CRITICAL_IMAGES = [
|
||||
"/assets/images/hero-image.jpg",
|
||||
"/assets/images/products/placeholder.jpg",
|
||||
];
|
||||
|
||||
const STATIC_ASSET_EXTENSIONS =
|
||||
/\.(jpg|jpeg|png|gif|svg|css|js|ico|webp|woff|woff2|ttf|eot)$/i;
|
||||
|
||||
const PG_ERROR_CODES = {
|
||||
UNIQUE_VIOLATION: "23505",
|
||||
FOREIGN_KEY_VIOLATION: "23503",
|
||||
INVALID_TEXT: "22P02",
|
||||
};
|
||||
|
||||
const MULTER_ERROR_CODES = {
|
||||
FILE_SIZE: "LIMIT_FILE_SIZE",
|
||||
FILE_COUNT: "LIMIT_FILE_COUNT",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ENVIRONMENTS,
|
||||
HTTP_STATUS,
|
||||
RATE_LIMITS,
|
||||
SESSION_CONFIG,
|
||||
BODY_PARSER_LIMITS,
|
||||
CRITICAL_IMAGES,
|
||||
STATIC_ASSET_EXTENSIONS,
|
||||
PG_ERROR_CODES,
|
||||
MULTER_ERROR_CODES,
|
||||
isDevelopment,
|
||||
getBaseDir,
|
||||
};
|
||||
@@ -1,31 +1,69 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
const { Pool } = require("pg");
|
||||
const logger = require("./logger");
|
||||
require("dotenv").config();
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
host: process.env.DB_HOST || "localhost",
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'skyartshop',
|
||||
user: process.env.DB_USER || 'skyartapp',
|
||||
database: process.env.DB_NAME || "skyartshop",
|
||||
user: process.env.DB_USER || "skyartapp",
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
pool.on('connect', () => console.log('✓ PostgreSQL connected'));
|
||||
pool.on('error', (err) => console.error('PostgreSQL error:', err));
|
||||
pool.on("connect", () => logger.info("✓ PostgreSQL connected"));
|
||||
pool.on("error", (err) => logger.error("PostgreSQL error:", err));
|
||||
|
||||
const query = async (text, params) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
console.log('Executed query', { text, duration, rows: res.rowCount });
|
||||
logger.debug("Executed query", { duration, rows: res.rowCount });
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Query error:', error);
|
||||
logger.error("Query error:", { text, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { pool, query };
|
||||
// Transaction helper
|
||||
const transaction = async (callback) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const result = await callback(client);
|
||||
await client.query("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("Transaction rolled back:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
// Health check
|
||||
const healthCheck = async () => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT NOW() as time, current_database() as database"
|
||||
);
|
||||
return {
|
||||
healthy: true,
|
||||
database: result.rows[0].database,
|
||||
timestamp: result.rows[0].time,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Database health check failed:", error);
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { pool, query, transaction, healthCheck };
|
||||
|
||||
69
backend/config/logger.js
Normal file
69
backend/config/logger.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const winston = require("winston");
|
||||
const path = require("path");
|
||||
require("dotenv").config();
|
||||
|
||||
// Define log format
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
// Console format for development
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
let msg = `${timestamp} [${level}]: ${message}`;
|
||||
if (Object.keys(meta).length > 0) {
|
||||
msg += ` ${JSON.stringify(meta)}`;
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
);
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const fs = require("fs");
|
||||
const logsDir = path.join(__dirname, "..", "logs");
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create logger instance
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
format: logFormat,
|
||||
defaultMeta: { service: "skyartshop" },
|
||||
transports: [
|
||||
// Error logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, "error.log"),
|
||||
level: "error",
|
||||
maxsize: 10485760, // 10MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
// Combined logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, "combined.log"),
|
||||
maxsize: 10485760, // 10MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Add console transport in non-production
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
logger.add(
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Create a stream for Morgan HTTP logger
|
||||
logger.stream = {
|
||||
write: (message) => logger.info(message.trim()),
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
66
backend/config/rateLimiter.js
Normal file
66
backend/config/rateLimiter.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const rateLimit = require("express-rate-limit");
|
||||
const logger = require("./logger");
|
||||
const { RATE_LIMITS, HTTP_STATUS } = require("./constants");
|
||||
|
||||
const createRateLimiter = (config, limitType = "API") => {
|
||||
return rateLimit({
|
||||
windowMs: config.windowMs,
|
||||
max: config.max,
|
||||
skipSuccessfulRequests: config.skipSuccessfulRequests || false,
|
||||
message: {
|
||||
success: false,
|
||||
message: config.message,
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
logger.warn(`${limitType} rate limit exceeded`, {
|
||||
ip: req.ip,
|
||||
path: req.path,
|
||||
email: req.body?.email,
|
||||
});
|
||||
res.status(HTTP_STATUS.TOO_MANY_REQUESTS).json({
|
||||
success: false,
|
||||
message: config.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// General API rate limiter
|
||||
const apiLimiter = createRateLimiter(
|
||||
{
|
||||
windowMs:
|
||||
parseInt(process.env.RATE_LIMIT_WINDOW_MS) || RATE_LIMITS.API.windowMs,
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || RATE_LIMITS.API.max,
|
||||
message: "Too many requests from this IP, please try again later.",
|
||||
},
|
||||
"API"
|
||||
);
|
||||
|
||||
// Strict limiter for authentication endpoints
|
||||
const authLimiter = createRateLimiter(
|
||||
{
|
||||
windowMs: RATE_LIMITS.AUTH.windowMs,
|
||||
max: RATE_LIMITS.AUTH.max,
|
||||
skipSuccessfulRequests: true,
|
||||
message: "Too many login attempts, please try again after 15 minutes.",
|
||||
},
|
||||
"Auth"
|
||||
);
|
||||
|
||||
// File upload limiter
|
||||
const uploadLimiter = createRateLimiter(
|
||||
{
|
||||
windowMs: RATE_LIMITS.UPLOAD.windowMs,
|
||||
max: RATE_LIMITS.UPLOAD.max,
|
||||
message: "Upload limit reached, please try again later.",
|
||||
},
|
||||
"Upload"
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
apiLimiter,
|
||||
authLimiter,
|
||||
uploadLimiter,
|
||||
};
|
||||
28
backend/media-folders-schema.sql
Normal file
28
backend/media-folders-schema.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Create media_folders table for organizing uploads
|
||||
CREATE TABLE IF NOT EXISTS media_folders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
parent_id INTEGER REFERENCES media_folders(id) ON DELETE CASCADE,
|
||||
path VARCHAR(1000) NOT NULL, -- Full path like /folder1/subfolder2
|
||||
created_by INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(parent_id, name) -- Prevent duplicate folder names in same parent
|
||||
);
|
||||
|
||||
-- Add folder_id to uploads table
|
||||
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS folder_id INTEGER REFERENCES media_folders(id) ON DELETE SET NULL;
|
||||
|
||||
-- Create indexes for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_media_folders_parent_id ON media_folders(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_media_folders_path ON media_folders(path);
|
||||
CREATE INDEX IF NOT EXISTS idx_uploads_folder_id ON uploads(folder_id);
|
||||
|
||||
-- Add is_folder and folder_name columns to handle folder-like behavior
|
||||
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS is_folder BOOLEAN DEFAULT FALSE;
|
||||
|
||||
COMMENT ON TABLE media_folders IS 'Organizes uploaded media files into folders/directories';
|
||||
COMMENT ON COLUMN media_folders.name IS 'Folder name (not full path)';
|
||||
COMMENT ON COLUMN media_folders.parent_id IS 'Parent folder ID for nested folders, NULL for root';
|
||||
COMMENT ON COLUMN media_folders.path IS 'Full path from root (e.g., /photos/2024)';
|
||||
COMMENT ON COLUMN uploads.folder_id IS 'Folder containing this file, NULL for root';
|
||||
@@ -1,19 +1,32 @@
|
||||
const logger = require("../config/logger");
|
||||
const { sendUnauthorized, sendForbidden } = require("../utils/responseHelpers");
|
||||
|
||||
const isAuthenticated = (req) => {
|
||||
return req.session?.user?.id;
|
||||
};
|
||||
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (req.session && req.session.user && req.session.user.id) {
|
||||
if (isAuthenticated(req)) {
|
||||
return next();
|
||||
}
|
||||
res.status(401).json({ success: false, message: "Authentication required" });
|
||||
|
||||
logger.warn("Unauthorized access attempt", {
|
||||
path: req.path,
|
||||
ip: req.ip,
|
||||
});
|
||||
sendUnauthorized(res);
|
||||
};
|
||||
|
||||
const requireRole = (allowedRoles) => {
|
||||
// Allow single role or array of roles
|
||||
const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
|
||||
|
||||
return (req, res, next) => {
|
||||
if (!req.session || !req.session.user || !req.session.user.id) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "Authentication required" });
|
||||
if (!isAuthenticated(req)) {
|
||||
logger.warn("Unauthorized access attempt", {
|
||||
path: req.path,
|
||||
ip: req.ip,
|
||||
});
|
||||
return sendUnauthorized(res);
|
||||
}
|
||||
|
||||
const userRole = req.session.user.role_id || "role-admin";
|
||||
@@ -22,12 +35,14 @@ const requireRole = (allowedRoles) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "Access denied. Insufficient permissions.",
|
||||
required_role: roles,
|
||||
your_role: userRole,
|
||||
logger.warn("Forbidden access attempt", {
|
||||
path: req.path,
|
||||
ip: req.ip,
|
||||
userRole,
|
||||
requiredRoles: roles,
|
||||
});
|
||||
|
||||
sendForbidden(res, "Access denied. Insufficient permissions.");
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
109
backend/middleware/errorHandler.js
Normal file
109
backend/middleware/errorHandler.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const logger = require("../config/logger");
|
||||
const {
|
||||
isDevelopment,
|
||||
PG_ERROR_CODES,
|
||||
MULTER_ERROR_CODES,
|
||||
STATIC_ASSET_EXTENSIONS,
|
||||
} = require("../config/constants");
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode, isOperational = true) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.isOperational = isOperational;
|
||||
this.timestamp = new Date().toISOString();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
const ERROR_MAPPINGS = {
|
||||
[PG_ERROR_CODES.UNIQUE_VIOLATION]: {
|
||||
message: "Duplicate entry: Resource already exists",
|
||||
statusCode: 409,
|
||||
},
|
||||
[PG_ERROR_CODES.FOREIGN_KEY_VIOLATION]: {
|
||||
message: "Referenced resource does not exist",
|
||||
statusCode: 400,
|
||||
},
|
||||
[PG_ERROR_CODES.INVALID_TEXT]: {
|
||||
message: "Invalid data format",
|
||||
statusCode: 400,
|
||||
},
|
||||
[MULTER_ERROR_CODES.FILE_SIZE]: {
|
||||
message: "File too large. Maximum size is 5MB",
|
||||
statusCode: 400,
|
||||
},
|
||||
[MULTER_ERROR_CODES.FILE_COUNT]: {
|
||||
message: "Too many files. Maximum is 10 files per upload",
|
||||
statusCode: 400,
|
||||
},
|
||||
};
|
||||
|
||||
// Global error handler middleware
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
error.statusCode = err.statusCode || 500;
|
||||
|
||||
// Log error
|
||||
logger.error("Error occurred", {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
stack: err.stack,
|
||||
});
|
||||
|
||||
// Map known error codes
|
||||
const errorMapping = ERROR_MAPPINGS[err.code];
|
||||
if (errorMapping) {
|
||||
error.message = errorMapping.message;
|
||||
error.statusCode = errorMapping.statusCode;
|
||||
}
|
||||
|
||||
res.status(error.statusCode).json({
|
||||
success: false,
|
||||
message: error.message || "Server error",
|
||||
...(isDevelopment() && {
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
// 404 handler
|
||||
const notFoundHandler = (req, res) => {
|
||||
const isStaticAsset = STATIC_ASSET_EXTENSIONS.test(req.path);
|
||||
|
||||
if (!isStaticAsset) {
|
||||
logger.warn("Route not found", {
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
});
|
||||
} else {
|
||||
logger.debug("Static asset not found", {
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "Route not found",
|
||||
path: req.path,
|
||||
});
|
||||
};
|
||||
|
||||
// Async handler wrapper to catch errors in async routes
|
||||
const asyncHandler = (fn) => (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
asyncHandler,
|
||||
};
|
||||
161
backend/middleware/validators.js
Normal file
161
backend/middleware/validators.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const { body, param, query, validationResult } = require("express-validator");
|
||||
const logger = require("../config/logger");
|
||||
|
||||
// Validation error handler middleware
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
logger.warn("Validation error", {
|
||||
path: req.path,
|
||||
errors: errors.array(),
|
||||
body: req.body,
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Validation failed",
|
||||
errors: errors.array().map((err) => ({
|
||||
field: err.param,
|
||||
message: err.msg,
|
||||
})),
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Common validation rules
|
||||
const validators = {
|
||||
// Auth validators
|
||||
login: [
|
||||
body("email")
|
||||
.isEmail()
|
||||
.withMessage("Valid email is required")
|
||||
.normalizeEmail()
|
||||
.trim(),
|
||||
body("password")
|
||||
.isLength({ min: 8 })
|
||||
.withMessage("Password must be at least 8 characters"),
|
||||
],
|
||||
|
||||
// User validators
|
||||
createUser: [
|
||||
body("email")
|
||||
.isEmail()
|
||||
.withMessage("Valid email is required")
|
||||
.normalizeEmail()
|
||||
.trim(),
|
||||
body("username")
|
||||
.isLength({ min: 3, max: 50 })
|
||||
.matches(/^[a-zA-Z0-9_-]+$/)
|
||||
.withMessage(
|
||||
"Username must be 3-50 characters and contain only letters, numbers, hyphens, and underscores"
|
||||
)
|
||||
.trim(),
|
||||
body("password")
|
||||
.isLength({ min: 8 })
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||
.withMessage(
|
||||
"Password must be at least 8 characters with uppercase, lowercase, and number"
|
||||
),
|
||||
body("role_id").notEmpty().withMessage("Role is required").trim(),
|
||||
],
|
||||
|
||||
updateUser: [
|
||||
param("id")
|
||||
.matches(/^user-[a-f0-9-]+$/)
|
||||
.withMessage("Invalid user ID format"),
|
||||
body("email")
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage("Valid email is required")
|
||||
.normalizeEmail()
|
||||
.trim(),
|
||||
body("username")
|
||||
.optional()
|
||||
.isLength({ min: 3, max: 50 })
|
||||
.withMessage("Username must be 3-50 characters")
|
||||
.matches(/^[a-zA-Z0-9_-]+$/)
|
||||
.trim(),
|
||||
],
|
||||
|
||||
// Product validators
|
||||
createProduct: [
|
||||
body("name")
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage("Product name is required (max 255 characters)")
|
||||
.trim()
|
||||
.escape(),
|
||||
body("description")
|
||||
.optional()
|
||||
.isString()
|
||||
.withMessage("Description must be text")
|
||||
.trim(),
|
||||
body("price")
|
||||
.isFloat({ min: 0 })
|
||||
.withMessage("Price must be a positive number"),
|
||||
body("stockquantity")
|
||||
.optional()
|
||||
.isInt({ min: 0 })
|
||||
.withMessage("Stock quantity must be a non-negative integer"),
|
||||
body("category")
|
||||
.optional()
|
||||
.isString()
|
||||
.withMessage("Category must be text")
|
||||
.trim()
|
||||
.escape(),
|
||||
],
|
||||
|
||||
updateProduct: [
|
||||
param("id").isUUID().withMessage("Invalid product ID"),
|
||||
body("name")
|
||||
.optional()
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage("Product name must be 1-255 characters")
|
||||
.trim()
|
||||
.escape(),
|
||||
body("price")
|
||||
.optional()
|
||||
.isFloat({ min: 0 })
|
||||
.withMessage("Price must be a positive number"),
|
||||
body("stockquantity")
|
||||
.optional()
|
||||
.isInt({ min: 0 })
|
||||
.withMessage("Stock quantity must be a non-negative integer"),
|
||||
],
|
||||
|
||||
// Blog validators
|
||||
createBlogPost: [
|
||||
body("title")
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage("Title is required (max 255 characters)")
|
||||
.trim()
|
||||
.escape(),
|
||||
body("slug")
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.matches(/^[a-z0-9-]+$/)
|
||||
.withMessage(
|
||||
"Slug must contain only lowercase letters, numbers, and hyphens"
|
||||
)
|
||||
.trim(),
|
||||
body("content").notEmpty().withMessage("Content is required").trim(),
|
||||
],
|
||||
|
||||
// Generic ID validator
|
||||
idParam: [param("id").notEmpty().withMessage("ID is required").trim()],
|
||||
|
||||
// Pagination validators
|
||||
pagination: [
|
||||
query("page")
|
||||
.optional()
|
||||
.isInt({ min: 1 })
|
||||
.withMessage("Page must be a positive integer"),
|
||||
query("limit")
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 100 })
|
||||
.withMessage("Limit must be between 1 and 100"),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validators,
|
||||
handleValidationErrors,
|
||||
};
|
||||
316
backend/node_modules/.package-lock.json
generated
vendored
316
backend/node_modules/.package-lock.json
generated
vendored
@@ -4,6 +4,26 @@
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
@@ -24,6 +44,22 @@
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -256,6 +292,52 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^3.1.3",
|
||||
"color-string": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
|
||||
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
@@ -334,6 +416,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
@@ -346,6 +447,19 @@
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -442,6 +556,12 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -501,6 +621,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -542,6 +663,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
@@ -574,6 +713,12 @@
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
@@ -601,6 +746,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -801,6 +952,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -886,6 +1046,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -904,6 +1073,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -927,12 +1108,41 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/logform/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -1232,6 +1442,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1515,6 +1734,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -1766,6 +1994,15 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -1853,6 +2090,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1868,6 +2111,15 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1979,6 +2231,70 @@
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
325
backend/package-lock.json
generated
325
backend/package-lock.json
generated
@@ -10,14 +10,39 @@
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^7.0.1",
|
||||
"express-validator": "^7.3.1",
|
||||
"helmet": "^8.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pg": "^8.11.3",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
@@ -40,6 +65,22 @@
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -272,6 +313,52 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^3.1.3",
|
||||
"color-string": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
|
||||
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
@@ -350,6 +437,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
@@ -362,6 +468,19 @@
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -458,6 +577,12 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -517,6 +642,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -558,6 +684,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
@@ -590,6 +734,12 @@
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
@@ -617,6 +767,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -817,6 +973,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -902,6 +1067,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -920,6 +1094,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -943,12 +1129,41 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/logform/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -1248,6 +1463,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1531,6 +1755,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -1782,6 +2015,15 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -1869,6 +2111,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1884,6 +2132,15 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1995,6 +2252,70 @@
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
@@ -10,13 +10,18 @@
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^7.0.1",
|
||||
"express-validator": "^7.3.1",
|
||||
"helmet": "^8.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pg": "^8.11.3",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
611
backend/routes/admin_backup.js
Normal file
611
backend/routes/admin_backup.js
Normal 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;
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,27 +2,98 @@ const express = require("express");
|
||||
const session = require("express-session");
|
||||
const pgSession = require("connect-pg-simple")(session);
|
||||
const path = require("path");
|
||||
const { pool } = require("./config/database");
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
const cors = require("cors");
|
||||
const { pool, healthCheck } = require("./config/database");
|
||||
const logger = require("./config/logger");
|
||||
const { apiLimiter, authLimiter } = require("./config/rateLimiter");
|
||||
const { errorHandler, notFoundHandler } = require("./middleware/errorHandler");
|
||||
const {
|
||||
isDevelopment,
|
||||
getBaseDir,
|
||||
SESSION_CONFIG,
|
||||
BODY_PARSER_LIMITS,
|
||||
} = require("./config/constants");
|
||||
require("dotenv").config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
const baseDir = getBaseDir();
|
||||
|
||||
// Development mode - Serve static files from development directory
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
const baseDir = isDevelopment
|
||||
? path.join(__dirname, "..", "website")
|
||||
: "/var/www/skyartshop";
|
||||
logger.info(`📁 Serving from: ${baseDir}`);
|
||||
|
||||
console.log(`📁 Serving from: ${baseDir}`);
|
||||
// Security middleware
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
|
||||
imgSrc: ["'self'", "data:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdn.jsdelivr.net"],
|
||||
},
|
||||
},
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// CORS configuration
|
||||
if (process.env.CORS_ORIGIN) {
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN.split(","),
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Trust proxy for rate limiting behind nginx
|
||||
app.set("trust proxy", 1);
|
||||
|
||||
// Body parsers
|
||||
app.use(express.json({ limit: BODY_PARSER_LIMITS.JSON }));
|
||||
app.use(
|
||||
express.urlencoded({ extended: true, limit: BODY_PARSER_LIMITS.URLENCODED })
|
||||
);
|
||||
|
||||
// Fallback middleware for missing product images
|
||||
const productImageFallback = (req, res, next) => {
|
||||
const imagePath = path.join(
|
||||
baseDir,
|
||||
"assets",
|
||||
"images",
|
||||
"products",
|
||||
req.path
|
||||
);
|
||||
|
||||
if (fs.existsSync(imagePath)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const placeholderPath = path.join(
|
||||
baseDir,
|
||||
"assets",
|
||||
"images",
|
||||
"products",
|
||||
"placeholder.jpg"
|
||||
);
|
||||
logger.debug("Serving placeholder image", { requested: req.path });
|
||||
res.sendFile(placeholderPath);
|
||||
};
|
||||
|
||||
app.use("/assets/images/products", productImageFallback);
|
||||
|
||||
app.use(express.static(path.join(baseDir, "public")));
|
||||
app.use("/assets", express.static(path.join(baseDir, "assets")));
|
||||
app.use("/uploads", express.static(path.join(baseDir, "uploads")));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Session middleware
|
||||
app.use(
|
||||
session({
|
||||
store: new pgSession({
|
||||
@@ -30,20 +101,30 @@ app.use(
|
||||
tableName: "session",
|
||||
createTableIfMissing: true,
|
||||
}),
|
||||
secret: process.env.SESSION_SECRET || "skyart-shop-secret-2025",
|
||||
secret: process.env.SESSION_SECRET || "change-this-secret",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Always false for localhost development
|
||||
secure: !isDevelopment(),
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000,
|
||||
maxAge: SESSION_CONFIG.COOKIE_MAX_AGE,
|
||||
sameSite: "lax",
|
||||
},
|
||||
proxy: false, // No proxy in development
|
||||
name: "skyartshop.sid",
|
||||
proxy: !isDevelopment(),
|
||||
name: SESSION_CONFIG.SESSION_NAME,
|
||||
})
|
||||
);
|
||||
|
||||
// Request logging
|
||||
app.use((req, res, next) => {
|
||||
logger.info("Request received", {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
ip: req.ip,
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.session = req.session;
|
||||
res.locals.currentPath = req.path;
|
||||
@@ -66,6 +147,11 @@ app.get("/admin/", (req, res) => {
|
||||
res.redirect("/admin/login.html");
|
||||
});
|
||||
|
||||
// Apply rate limiting to API routes
|
||||
app.use("/api/admin/login", authLimiter);
|
||||
app.use("/api/admin/logout", authLimiter);
|
||||
app.use("/api", apiLimiter);
|
||||
|
||||
// API Routes
|
||||
app.use("/api/admin", authRoutes);
|
||||
app.use("/api/admin", adminRoutes);
|
||||
@@ -81,37 +167,88 @@ app.get("/", (req, res) => {
|
||||
res.sendFile(path.join(baseDir, "public", "index.html"));
|
||||
});
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
database: "connected",
|
||||
// Health check endpoint
|
||||
const { CRITICAL_IMAGES } = require("./config/constants");
|
||||
|
||||
app.get("/health", async (req, res) => {
|
||||
try {
|
||||
const dbHealth = await healthCheck();
|
||||
const missingImages = CRITICAL_IMAGES.filter(
|
||||
(img) => !fs.existsSync(path.join(baseDir, img))
|
||||
);
|
||||
|
||||
const assetsHealthy = missingImages.length === 0;
|
||||
const overallHealthy = dbHealth.healthy && assetsHealthy;
|
||||
const status = overallHealthy ? 200 : 503;
|
||||
|
||||
res.status(status).json({
|
||||
status: overallHealthy ? "ok" : "degraded",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
database: dbHealth,
|
||||
assets: {
|
||||
healthy: assetsHealthy,
|
||||
missingCritical: missingImages,
|
||||
},
|
||||
memory: {
|
||||
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Health check failed:", error);
|
||||
res.status(503).json({
|
||||
status: "error",
|
||||
timestamp: new Date().toISOString(),
|
||||
error: "Health check failed",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use(notFoundHandler);
|
||||
|
||||
// Global error handler
|
||||
app.use(errorHandler);
|
||||
|
||||
const server = app.listen(PORT, "0.0.0.0", () => {
|
||||
logger.info("========================================");
|
||||
logger.info(" SkyArtShop Backend Server");
|
||||
logger.info("========================================");
|
||||
logger.info(`🚀 Server running on http://localhost:${PORT}`);
|
||||
logger.info(`📦 Environment: ${process.env.NODE_ENV || "development"}`);
|
||||
logger.info(`🗄️ Database: PostgreSQL (${process.env.DB_NAME})`);
|
||||
logger.info("========================================");
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const gracefulShutdown = (signal) => {
|
||||
logger.info(`${signal} received, shutting down gracefully...`);
|
||||
|
||||
server.close(() => {
|
||||
logger.info("HTTP server closed");
|
||||
|
||||
pool.end(() => {
|
||||
logger.info("Database pool closed");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Force close after 10 seconds
|
||||
setTimeout(() => {
|
||||
logger.error("Forced shutdown after timeout");
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
||||
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
logger.error("Unhandled Rejection at:", { promise, reason });
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: "Not found" });
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error("Error:", err);
|
||||
res.status(500).json({ error: "Server error" });
|
||||
});
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log("========================================");
|
||||
console.log(" SkyArtShop Backend Server");
|
||||
console.log("========================================");
|
||||
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
||||
console.log(`📦 Environment: ${process.env.NODE_ENV || "development"}`);
|
||||
console.log(`🗄️ Database: PostgreSQL (${process.env.DB_NAME})`);
|
||||
console.log("========================================");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("SIGTERM received, closing server...");
|
||||
pool.end(() => {
|
||||
console.log("Database pool closed");
|
||||
process.exit(0);
|
||||
});
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught Exception:", error);
|
||||
gracefulShutdown("UNCAUGHT_EXCEPTION");
|
||||
});
|
||||
|
||||
45
backend/utils/queryHelpers.js
Normal file
45
backend/utils/queryHelpers.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { query } = require("../config/database");
|
||||
|
||||
const buildSelectQuery = (
|
||||
table,
|
||||
conditions = [],
|
||||
orderBy = "createdat DESC"
|
||||
) => {
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
return `SELECT * FROM ${table} ${whereClause} ORDER BY ${orderBy}`;
|
||||
};
|
||||
|
||||
const getById = async (table, id) => {
|
||||
const result = await query(`SELECT * FROM ${table} WHERE id = $1`, [id]);
|
||||
return result.rows[0] || null;
|
||||
};
|
||||
|
||||
const getAllActive = async (table, orderBy = "createdat DESC") => {
|
||||
const result = await query(
|
||||
`SELECT * FROM ${table} WHERE isactive = true ORDER BY ${orderBy}`
|
||||
);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
const deleteById = async (table, id) => {
|
||||
const result = await query(
|
||||
`DELETE FROM ${table} WHERE id = $1 RETURNING id`,
|
||||
[id]
|
||||
);
|
||||
return result.rowCount > 0;
|
||||
};
|
||||
|
||||
const countRecords = async (table, condition = "") => {
|
||||
const whereClause = condition ? `WHERE ${condition}` : "";
|
||||
const result = await query(`SELECT COUNT(*) FROM ${table} ${whereClause}`);
|
||||
return parseInt(result.rows[0].count);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
buildSelectQuery,
|
||||
getById,
|
||||
getAllActive,
|
||||
deleteById,
|
||||
countRecords,
|
||||
};
|
||||
48
backend/utils/responseHelpers.js
Normal file
48
backend/utils/responseHelpers.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const { HTTP_STATUS } = require("../config/constants");
|
||||
|
||||
const sendSuccess = (res, data = {}, statusCode = HTTP_STATUS.OK) => {
|
||||
res.status(statusCode).json({
|
||||
success: true,
|
||||
...data,
|
||||
});
|
||||
};
|
||||
|
||||
const sendError = (
|
||||
res,
|
||||
message = "Server error",
|
||||
statusCode = HTTP_STATUS.INTERNAL_ERROR
|
||||
) => {
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const sendNotFound = (res, resource = "Resource") => {
|
||||
res.status(HTTP_STATUS.NOT_FOUND).json({
|
||||
success: false,
|
||||
message: `${resource} not found`,
|
||||
});
|
||||
};
|
||||
|
||||
const sendUnauthorized = (res, message = "Authentication required") => {
|
||||
res.status(HTTP_STATUS.UNAUTHORIZED).json({
|
||||
success: false,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const sendForbidden = (res, message = "Access denied") => {
|
||||
res.status(HTTP_STATUS.FORBIDDEN).json({
|
||||
success: false,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendSuccess,
|
||||
sendError,
|
||||
sendNotFound,
|
||||
sendUnauthorized,
|
||||
sendForbidden,
|
||||
};
|
||||
Reference in New Issue
Block a user