de-com-press-backend/index.js

1029 lines
43 KiB
JavaScript

const express = require('express');
const Database = require('better-sqlite3');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const cors = require('cors');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
const app = express();
const port = 3000;
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const SALT_ROUNDS = 10;
// Configure multer for image uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/uploads/');
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const filetypes = /jpeg|jpg|png|gif/;
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = filetypes.test(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
}
cb(new Error('Images only!'));
},
});
// Middleware to parse JSON bodies and enable CORS
app.use(express.json());
app.use(cors({
origin: 'http://localhost:5174',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
app.use('/uploads', express.static('public/uploads'));
// Initialize SQLite database
const db = new Database('wpclone.db', { verbose: console.log });
// Initialize database tables
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user'
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
structure TEXT NOT NULL,
styles TEXT,
component TEXT DEFAULT 'PageViewer',
form_id INTEGER,
FOREIGN KEY (form_id) REFERENCES forms(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS forms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
fields TEXT NOT NULL,
created_by INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (created_by) REFERENCES users(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
slug TEXT,
data TEXT NOT NULL,
author_id INTEGER NOT NULL,
template_id INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
modified_at TEXT NOT NULL DEFAULT (datetime('now')),
created_by INTEGER NOT NULL,
FOREIGN KEY (author_id) REFERENCES users(id),
FOREIGN KEY (template_id) REFERENCES templates(id),
FOREIGN KEY (created_by) REFERENCES users(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
author_id INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
modified_at TEXT NOT NULL DEFAULT (datetime('now')),
created_by INTEGER NOT NULL,
FOREIGN KEY (author_id) REFERENCES users(id),
FOREIGN KEY (created_by) REFERENCES users(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT NOT NULL,
price REAL NOT NULL,
image TEXT,
created_by INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (created_by) REFERENCES users(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
total REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS fulfillments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (order_id) REFERENCES orders(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS shipments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
address TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (order_id) REFERENCES orders(id)
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
path TEXT NOT NULL,
uploaded_by INTEGER NOT NULL,
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (uploaded_by) REFERENCES users(id)
)
`);
// Check and update schema
const checkRoleColumn = db.prepare("PRAGMA table_info(users)").all();
if (!checkRoleColumn.some(column => column.name === 'role')) {
db.exec(`ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'`);
console.log('Added role column to users table');
}
const checkTemplateIdColumn = db.prepare("PRAGMA table_info(pages)").all();
if (!checkTemplateIdColumn.some(column => column.name === 'template_id')) {
db.exec(`ALTER TABLE pages ADD COLUMN template_id INTEGER`);
console.log('Added template_id column to pages table');
}
const checkTemplateStylesColumn = db.prepare("PRAGMA table_info(templates)").all();
if (!checkTemplateStylesColumn.some(column => column.name === 'styles')) {
db.exec(`ALTER TABLE templates ADD COLUMN styles TEXT`);
console.log('Added styles column to templates table');
}
const checkTemplateComponentColumn = db.prepare("PRAGMA table_info(templates)").all();
if (!checkTemplateComponentColumn.some(column => column.name === 'component')) {
db.exec(`ALTER TABLE templates ADD COLUMN component TEXT DEFAULT 'PageViewer'`);
console.log('Added component column to templates table');
}
const checkTemplateFormIdColumn = db.prepare("PRAGMA table_info(templates)").all();
if (!checkTemplateFormIdColumn.some(column => column.name === 'form_id')) {
db.exec(`ALTER TABLE templates ADD COLUMN form_id INTEGER`);
console.log('Added form_id column to templates table');
}
const checkFormCreatedByColumn = db.prepare("PRAGMA table_info(forms)").all();
if (!checkFormCreatedByColumn.some(column => column.name === 'created_by')) {
db.exec(`ALTER TABLE forms ADD COLUMN created_by INTEGER`);
console.log('Added created_by column to forms table');
db.exec(`UPDATE forms SET created_by = 1 WHERE created_by IS NULL`);
}
const checkPageSlugColumn = db.prepare("PRAGMA table_info(pages)").all();
if (!checkPageSlugColumn.some(column => column.name === 'slug')) {
db.exec(`ALTER TABLE pages ADD COLUMN slug TEXT`);
console.log('Added slug column to pages table');
const pages = db.prepare('SELECT id, title FROM pages WHERE slug IS NULL').all();
for (const page of pages) {
let slug = page.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
let baseSlug = slug || 'page-' + page.id;
let counter = 1;
while (db.prepare('SELECT id FROM pages WHERE slug = ?').get(slug)) {
slug = `${baseSlug}-${counter++}`;
}
db.prepare('UPDATE pages SET slug = ? WHERE id = ?').run(slug, page.id);
}
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug)`);
console.log('Added unique index on pages.slug');
}
// Migrate pages table
const checkPageColumns = db.prepare("PRAGMA table_info(pages)").all();
if (!checkPageColumns.some(column => column.name === 'created_at')) {
db.exec(`
BEGIN TRANSACTION;
CREATE TABLE pages_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
slug TEXT,
data TEXT NOT NULL,
author_id INTEGER NOT NULL,
template_id INTEGER,
created_at TEXT NOT NULL,
modified_at TEXT NOT NULL,
created_by INTEGER NOT NULL,
FOREIGN KEY (author_id) REFERENCES users(id),
FOREIGN KEY (template_id) REFERENCES templates(id),
FOREIGN KEY (created_by) REFERENCES users(id)
);
INSERT INTO pages_new (id, title, slug, data, author_id, template_id, created_at, modified_at, created_by)
SELECT id, title, 'page-' || id AS slug, content AS data, author_id, template_id, datetime('now'), datetime('now'), author_id
FROM pages;
DROP TABLE pages;
ALTER TABLE pages_new RENAME TO pages;
CREATE UNIQUE INDEX idx_pages_slug ON pages(slug);
COMMIT;
`);
console.log('Migrated pages table schema with slug');
}
// Migrate posts table
const checkPostColumns = db.prepare("PRAGMA table_info(posts)").all();
if (!checkPostColumns.some(column => column.name === 'created_at')) {
db.exec(`
BEGIN TRANSACTION;
CREATE TABLE posts_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
author_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
modified_at TEXT NOT NULL,
created_by INTEGER NOT NULL,
FOREIGN KEY (author_id) REFERENCES users(id),
FOREIGN KEY (created_by) REFERENCES users(id)
);
INSERT INTO posts_new (id, title, body, author_id, created_at, modified_at, created_by)
SELECT id, title, content AS body, author_id, datetime('now'), datetime('now'), author_id
FROM posts;
DROP TABLE posts;
ALTER TABLE posts_new RENAME TO posts;
COMMIT;
`);
console.log('Migrated posts table schema');
}
// Middleware to verify JWT
const authenticateJWT = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
};
const restrictToAdmin = (req, res, next) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin access required' });
next();
};
// Order endpoints
app.post('/orders', authenticateJWT, (req, res) => {
const { cart, shippingAddress } = req.body;
if (!cart || typeof cart !== 'object' || Object.keys(cart).length === 0) {
return res.status(400).json({ error: 'Cart cannot be empty' });
}
if (!shippingAddress || typeof shippingAddress !== 'string' || shippingAddress.trim() === '') {
return res.status(400).json({ error: 'Valid shipping address is required' });
}
try {
// Calculate total
let total = 0;
const productIds = Object.keys(cart).map(id => parseInt(id));
const products = db.prepare('SELECT id, price FROM products WHERE id IN (' + productIds.map(() => '?').join(',') + ')').all(...productIds);
for (const item of Object.values(cart)) {
const product = products.find(p => p.id === item.product.id);
if (!product) {
return res.status(400).json({ error: `Product ID ${item.product.id} not found` });
}
if (!item.quantity || item.quantity <= 0) {
return res.status(400).json({ error: `Invalid quantity for product ID ${item.product.id}` });
}
total += product.price * item.quantity;
}
const now = new Date().toISOString();
// Create order
const orderStmt = db.prepare('INSERT INTO orders (user_id, total, status, created_at) VALUES (?, ?, ?, ?)');
const orderResult = orderStmt.run(req.user.id, total, 'pending', now);
const orderId = orderResult.lastInsertRowid;
// Create fulfillment
const fulfillmentStmt = db.prepare('INSERT INTO fulfillments (order_id, status, created_at) VALUES (?, ?, ?)');
fulfillmentStmt.run(orderId, 'pending', now);
// Create shipment
const shipmentStmt = db.prepare('INSERT INTO shipments (order_id, address, status, created_at) VALUES (?, ?, ?, ?)');
shipmentStmt.run(orderId, shippingAddress, 'pending', now);
res.json({ message: 'Order created successfully', orderId });
} catch (err) {
console.error('Order creation error:', err);
res.status(500).json({ error: 'Failed to create order' });
}
});
// Form endpoints
app.post('/admin/forms', authenticateJWT, restrictToAdmin, (req, res) => {
const { name, fields } = req.body;
if (!name || !fields || typeof fields !== 'object') {
return res.status(400).json({ error: 'Name and valid fields object are required' });
}
const validFieldTypes = ['input', 'textarea', 'select', 'enum'];
for (const field in fields) {
if (!validFieldTypes.includes(fields[field].type)) {
return res.status(400).json({ error: `Invalid field type: ${fields[field].type}` });
}
if (fields[field].type === 'input' && !['text', 'password', 'email'].includes(fields[field].inputType)) {
return res.status(400).json({ error: `Invalid input type: ${fields[field].inputType}` });
}
if (fields[field].type === 'select' && !Array.isArray(fields[field].options)) {
return res.status(400).json({ error: `Select field ${field} requires an options array` });
}
if (fields[field].type === 'enum' && !Array.isArray(fields[field].values)) {
return res.status(400).json({ error: `Enum field ${field} requires a values array` });
}
if (fields[field].required && typeof fields[field].required !== 'boolean') {
return res.status(400).json({ error: `Required property for ${field} must be a boolean` });
}
if (fields[field].maxlength && !Number.isInteger(fields[field].maxlength)) {
return res.status(400).json({ error: `Maxlength for ${field} must be an integer` });
}
if (fields[field].minlength && !Number.isInteger(fields[field].minlength)) {
return res.status(400).json({ error: `Minlength for ${field} must be an integer` });
}
}
try {
const now = new Date().toISOString();
const stmt = db.prepare('INSERT INTO forms (name, fields, created_by, created_at) VALUES (?, ?, ?, ?)');
stmt.run(name, JSON.stringify(fields), req.user.id, now);
res.json({ message: 'Form created successfully' });
} catch (err) {
console.error('Form creation error:', err);
res.status(500).json({ error: 'Failed to create form' });
}
});
app.get('/admin/forms', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const forms = db.prepare('SELECT f.*, u.username AS created_by_username FROM forms f LEFT JOIN users u ON f.created_by = u.id').all();
res.json(forms.map(f => ({ ...f, fields: JSON.parse(f.fields) })));
} catch (err) {
console.error('Form fetch error:', err);
res.status(500).json({ error: 'Failed to fetch forms' });
}
});
app.put('/admin/forms/:id', authenticateJWT, restrictToAdmin, (req, res) => {
const { name, fields } = req.body;
if (!name || !fields || typeof fields !== 'object') {
return res.status(400).json({ error: 'Name and valid fields object are required' });
}
const validFieldTypes = ['input', 'textarea', 'select', 'enum'];
for (const field in fields) {
if (!validFieldTypes.includes(fields[field].type)) {
return res.status(400).json({ error: `Invalid field type: ${fields[field].type}` });
}
if (fields[field].type === 'input' && !['text', 'password', 'email'].includes(fields[field].inputType)) {
return res.status(400).json({ error: `Invalid input type: ${fields[field].inputType}` });
}
if (fields[field].type === 'select' && !Array.isArray(fields[field].options)) {
return res.status(400).json({ error: `Select field ${field} requires an options array` });
}
if (fields[field].type === 'enum' && !Array.isArray(fields[field].values)) {
return res.status(400).json({ error: `Enum field ${field} requires a values array` });
}
if (fields[field].required && typeof fields[field].required !== 'boolean') {
return res.status(400).json({ error: `Required property for ${field} must be a boolean` });
}
if (fields[field].maxlength && !Number.isInteger(fields[field].maxlength)) {
return res.status(400).json({ error: `Maxlength for ${field} must be an integer` });
}
if (fields[field].minlength && !Number.isInteger(fields[field].minlength)) {
return res.status(400).json({ error: `Minlength for ${field} must be an integer` });
}
}
try {
const stmt = db.prepare('UPDATE forms SET name = ?, fields = ? WHERE id = ?');
const result = stmt.run(name, JSON.stringify(fields), req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Form not found' });
res.json({ message: 'Form updated successfully' });
} catch (err) {
console.error('Form update error:', err);
res.status(500).json({ error: 'Failed to update form' });
}
});
app.delete('/admin/forms/:id', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const stmt = db.prepare('DELETE FROM forms WHERE id = ?');
const result = stmt.run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Form not found' });
res.json({ message: 'Form deleted successfully' });
} catch (err) {
console.error('Form deletion error:', err);
res.status(500).json({ error: 'Failed to delete form' });
}
});
// Product endpoints
app.post('/admin/products', authenticateJWT, restrictToAdmin, upload.single('image'), (req, res) => {
const { name, description, price } = req.body;
if (!name || !description || !price || isNaN(parseFloat(price))) {
return res.status(400).json({ error: 'Name, description, and valid price are required' });
}
try {
const imagePath = req.file ? `/uploads/${req.file.filename}` : null;
const now = new Date().toISOString();
const stmt = db.prepare('INSERT INTO products (name, description, price, image, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?)');
stmt.run(name, description, parseFloat(price), imagePath, req.user.id, now);
res.json({ message: 'Product created successfully' });
} catch (err) {
console.error('Product creation error:', err);
res.status(500).json({ error: 'Failed to create product' });
}
});
app.get('/products', (req, res) => {
try {
const products = db.prepare('SELECT p.*, u.username AS created_by_username FROM products p LEFT JOIN users u ON p.created_by = u.id').all();
res.json(products);
} catch (err) {
console.error('Product fetch error:', err);
res.status(500).json({ error: 'Failed to fetch products' });
}
});
app.put('/admin/products/:id', authenticateJWT, restrictToAdmin, upload.single('image'), (req, res) => {
const { name, description, price } = req.body;
if (!name || !description || !price || isNaN(parseFloat(price))) {
return res.status(400).json({ error: 'Name, description, and valid price are required' });
}
try {
const imagePath = req.file ? `/uploads/${req.file.filename}` : null;
const existing = db.prepare('SELECT image FROM products WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Product not found' });
const stmt = db.prepare('UPDATE products SET name = ?, description = ?, price = ?, image = ? WHERE id = ?');
stmt.run(name, description, parseFloat(price), imagePath || existing.image, req.params.id);
if (imagePath && existing.image) {
fs.unlink(path.join(__dirname, 'public', existing.image), (err) => {
if (err) console.error('Failed to delete old image:', err);
});
}
res.json({ message: 'Product updated successfully' });
} catch (err) {
console.error('Product update error:', err);
res.status(500).json({ error: 'Failed to update product' });
}
});
app.delete('/admin/products/:id', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const product = db.prepare('SELECT image FROM products WHERE id = ?').get(req.params.id);
if (!product) return res.status(404).json({ error: 'Product not found' });
const stmt = db.prepare('DELETE FROM products WHERE id = ?');
const result = stmt.run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Product not found' });
if (product.image) {
fs.unlink(path.join(__dirname, 'public', product.image), (err) => {
if (err) console.error('Failed to delete image:', err);
});
}
res.json({ message: 'Product deleted successfully' });
} catch (err) {
console.error('Product deletion error:', err);
res.status(500).json({ error: 'Failed to delete product' });
}
});
app.post('/admin/products/import', authenticateJWT, restrictToAdmin, (req, res) => {
const { products } = req.body;
if (!products || !Array.isArray(products)) {
return res.status(400).json({ error: 'Products must be an array' });
}
try {
const stmt = db.prepare('INSERT INTO products (name, description, price, image, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?)');
const now = new Date().toISOString();
let importedCount = 0;
for (const product of products) {
if (!product.name || !product.description || !product.price || isNaN(parseFloat(product.price))) {
console.warn('Skipping invalid product:', product);
continue;
}
stmt.run(
product.name,
product.description,
parseFloat(product.price),
product.image || null,
req.user.id,
now
);
importedCount++;
}
res.json({ message: `Imported ${importedCount} products successfully` });
} catch (err) {
console.error('Product import error:', err);
res.status(500).json({ error: 'Failed to import products' });
}
});
// Media endpoints
app.post('/admin/media', authenticateJWT, restrictToAdmin, upload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
const imagePath = `/uploads/${req.file.filename}`;
const stmt = db.prepare('INSERT INTO media (filename, path, uploaded_by) VALUES (?, ?, ?)');
const result = stmt.run(req.file.originalname, imagePath, req.user.id);
res.json({ id: result.lastInsertRowid, filename: req.file.originalname, path: imagePath });
} catch (err) {
console.error('Media upload error:', err);
res.status(500).json({ error: 'Failed to upload media' });
}
});
app.get('/admin/media', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const media = db.prepare('SELECT m.*, u.username AS uploaded_by_username FROM media m LEFT JOIN users u ON m.uploaded_by = u.id').all();
res.json(media);
} catch (err) {
console.error('Media fetch error:', err);
res.status(500).json({ error: 'Failed to fetch media' });
}
});
app.delete('/admin/media/:id', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const media = db.prepare('SELECT path FROM media WHERE id = ?').get(req.params.id);
if (!media) {
return res.status(404).json({ error: 'Media not found' });
}
fs.unlinkSync(path.join(__dirname, 'public', media.path));
const stmt = db.prepare('DELETE FROM media WHERE id = ?');
const result = stmt.run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Media not found' });
res.json({ message: 'Media deleted successfully' });
} catch (err) {
console.error('Media deletion error:', err);
res.status(500).json({ error: 'Failed to delete media' });
}
});
// Image upload endpoint (for backward compatibility)
app.post('/admin/images', authenticateJWT, restrictToAdmin, upload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No image uploaded' });
}
const imagePath = `/uploads/${req.file.filename}`;
const stmt = db.prepare('INSERT INTO media (filename, path, uploaded_by) VALUES (?, ?, ?)');
stmt.run(req.file.originalname, imagePath, req.user.id);
res.json({ imagePath });
} catch (err) {
console.error('Image upload error:', err);
res.status(500).json({ error: 'Failed to upload image' });
}
});
// Login endpoint
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(403).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
});
// User registration endpoint
app.post('/users', async (req, res) => {
const { username, password, role } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
if (typeof username !== 'string' || username.trim() === '') {
return res.status(400).json({ error: 'Invalid username' });
}
try {
const existingUser = db.prepare('SELECT username FROM users WHERE username = ?').get(username);
if (existingUser) {
return res.status(409).json({ error: 'Username already exists' });
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const stmt = db.prepare('INSERT INTO users (username, password, role) VALUES (?, ?, ?)');
stmt.run(username.trim(), hashedPassword, role || 'user');
res.json({ message: 'User registered successfully' });
} catch (err) {
console.error('Registration error:', err);
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('UNIQUE')) {
return res.status(409).json({ error: 'Username already exists' });
}
res.status(500).json({ error: 'Failed to register user' });
}
});
// Template management endpoints
app.post('/admin/templates', authenticateJWT, restrictToAdmin, (req, res) => {
const { name, structure, styles, component, form_id } = req.body;
console.log('Received template creation payload:', { name, structure, styles, component, form_id });
if (!name || !structure || typeof structure !== 'object') {
return res.status(400).json({ error: 'Name and valid structure object are required' });
}
if (styles && typeof styles !== 'object') {
return res.status(400).json({ error: 'Styles must be a valid object' });
}
if (component && typeof component !== 'string') {
return res.status(400).json({ error: 'Component must be a valid string' });
}
if (form_id && isNaN(parseInt(form_id))) {
return res.status(400).json({ error: 'Valid form_id is required' });
}
const validFieldTypes = ['text', 'textarea', 'select', 'enum', 'repeater', 'grid', 'image', 'richtext', 'number', 'date'];
for (const field in structure) {
if (!validFieldTypes.includes(structure[field].type)) {
return res.status(400).json({ error: `Invalid field type: ${structure[field].type}` });
}
if (structure[field].type === 'select' && !Array.isArray(structure[field].options)) {
return res.status(400).json({ error: `Select field ${field} requires an options array` });
}
if (structure[field].type === 'enum' && !Array.isArray(structure[field].values)) {
return res.status(400).json({ error: `Enum field ${field} requires a values array` });
}
if (structure[field].type === 'repeater' && (!structure[field].subTemplate || typeof structure[field].subTemplate !== 'object')) {
return res.status(400).json({ error: `Repeater field ${field} requires a valid subTemplate object` });
}
if (structure[field].type === 'grid' && (!structure[field].columns || !Array.isArray(structure[field].columns))) {
return res.status(400).json({ error: `Grid field ${field} requires a columns array` });
}
}
try {
const stmt = db.prepare('INSERT INTO templates (name, structure, styles, component, form_id) VALUES (?, ?, ?, ?, ?)');
const result = stmt.run(name, JSON.stringify(structure), styles ? JSON.stringify(styles) : null, component || 'PageViewer', form_id ? parseInt(form_id) : null);
console.log('Template created with ID:', result.lastInsertRowid);
res.json({ message: 'Template created successfully' });
} catch (err) {
console.error('Template creation error:', err);
res.status(500).json({ error: 'Failed to create template' });
}
});
app.get('/admin/templates', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const templates = db.prepare('SELECT * FROM templates').all();
res.json(templates.map(t => ({
...t,
structure: JSON.parse(t.structure),
styles: t.styles ? JSON.parse(t.styles) : null,
})));
} catch (err) {
console.error('Template fetch error:', err);
res.status(500).json({ error: 'Failed to fetch templates' });
}
});
app.put('/admin/templates/:id', authenticateJWT, restrictToAdmin, (req, res) => {
const { name, structure, styles, component, form_id } = req.body;
console.log('Received template update payload:', { name, structure, styles, component, form_id });
if (!name || !structure || typeof structure !== 'object') {
return res.status(400).json({ error: 'Name and valid structure object are required' });
}
if (styles && typeof styles !== 'object') {
return res.status(400).json({ error: 'Styles must be a valid object' });
}
if (component && typeof component !== 'string') {
return res.status(400).json({ error: 'Component must be a valid string' });
}
if (form_id && isNaN(parseInt(form_id))) {
return res.status(400).json({ error: 'Valid form_id is required' });
}
const validFieldTypes = ['text', 'textarea', 'select', 'enum', 'repeater', 'grid', 'image', 'richtext', 'number', 'date'];
for (const field in structure) {
if (!validFieldTypes.includes(structure[field].type)) {
return res.status(400).json({ error: `Invalid field type: ${structure[field].type}` });
}
if (structure[field].type === 'select' && !Array.isArray(structure[field].options)) {
return res.status(400).json({ error: `Select field ${field} requires an options array` });
}
if (structure[field].type === 'enum' && !Array.isArray(structure[field].values)) {
return res.status(400).json({ error: `Enum field ${field} requires a values array` });
}
if (structure[field].type === 'repeater' && (!structure[field].subTemplate || typeof structure[field].subTemplate !== 'object')) {
return res.status(400).json({ error: `Repeater field ${field} requires a valid subTemplate object` });
}
if (structure[field].type === 'grid' && (!structure[field].columns || !Array.isArray(structure[field].columns))) {
return res.status(400).json({ error: `Grid field ${field} requires a columns array` });
}
}
try {
const stmt = db.prepare('UPDATE templates SET name = ?, structure = ?, styles = ?, component = ?, form_id = ? WHERE id = ?');
const result = stmt.run(name, JSON.stringify(structure), styles ? JSON.stringify(styles) : null, component || 'PageViewer', form_id ? parseInt(form_id) : null, req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Template not found' });
console.log('Template updated with ID:', req.params.id);
res.json({ message: 'Template updated successfully' });
} catch (err) {
console.error('Template update error:', err);
res.status(500).json({ error: 'Failed to update template' });
}
});
app.delete('/admin/templates/:id', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const stmt = db.prepare('DELETE FROM templates WHERE id = ?');
const result = stmt.run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Template not found' });
res.json({ message: 'Template deleted successfully' });
} catch (err) {
console.error('Template deletion error:', err);
res.status(500).json({ error: 'Failed to delete template' });
}
});
// Page endpoints
app.post('/pages', authenticateJWT, restrictToAdmin, (req, res) => {
const { title, slug, data, template_id } = req.body;
if (!title || typeof title !== 'string' || title.trim() === '') {
return res.status(400).json({ error: 'Valid title is required' });
}
if (!slug || typeof slug !== 'string' || !/^[a-z0-9-]+$/i.test(slug)) {
return res.status(400).json({ error: 'Valid slug is required (letters, numbers, and hyphens only)' });
}
if (!data || typeof data !== 'object') {
return res.status(400).json({ error: 'Valid data object is required' });
}
if (template_id && isNaN(parseInt(template_id))) {
return res.status(400).json({ error: 'Valid template_id is required' });
}
try {
if (template_id) {
const template = db.prepare('SELECT id, structure FROM templates WHERE id = ?').get(parseInt(template_id));
if (!template) {
return res.status(400).json({ error: 'Invalid template_id' });
}
const structure = JSON.parse(template.structure);
for (const field in structure) {
if (structure[field].type === 'number' && data[field] && isNaN(parseFloat(data[field]))) {
return res.status(400).json({ error: `Field ${field} must be a number` });
}
if (structure[field].type === 'date' && data[field] && !/^\d{4}-\d{2}-\d{2}$/.test(data[field])) {
return res.status(400).json({ error: `Field ${field} must be a valid date (YYYY-MM-DD)` });
}
if (structure[field].type === 'select' && data[field] && !structure[field].options.includes(data[field])) {
return res.status(400).json({ error: `Field ${field} must be a valid option` });
}
if (structure[field].type === 'enum' && data[field] && !structure[field].values.includes(data[field])) {
return res.status(400).json({ error: `Field ${field} must be a valid enum value` });
}
if (structure[field].type === 'image' && data[field] && !data[field].startsWith('/uploads/')) {
return res.status(400).json({ error: `Field ${field} must be a valid image path` });
}
if (structure[field].type === 'repeater' && data[field] && !Array.isArray(data[field])) {
return res.status(400).json({ error: `Field ${field} must be an array` });
}
if (structure[field].type === 'grid' && data[field] && typeof data[field] !== 'object') {
return res.status(400).json({ error: `Field ${field} must be an object` });
}
}
}
const existingSlug = db.prepare('SELECT id FROM pages WHERE slug = ?').get(slug);
if (existingSlug) {
return res.status(409).json({ error: 'Slug already exists' });
}
const user = db.prepare('SELECT id FROM users WHERE id = ?').get(req.user.id);
if (!user) {
return res.status(400).json({ error: 'Invalid author_id' });
}
const now = new Date().toISOString();
const stmt = db.prepare('INSERT INTO pages (title, slug, data, author_id, template_id, created_by, created_at, modified_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
stmt.run(title.trim(), slug, JSON.stringify(data), req.user.id, template_id ? parseInt(template_id) : null, req.user.id, now, now);
const pageId = db.prepare('SELECT last_insert_rowid() as id').get().id;
res.json({ message: 'Page created successfully', pageId });
} catch (err) {
console.error('Page creation error:', err);
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('FOREIGN KEY')) {
return res.status(400).json({ error: 'Invalid template_id or author_id' });
}
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('UNIQUE')) {
return res.status(409).json({ error: 'Slug already exists' });
}
res.status(500).json({ error: 'Failed to create page' });
}
});
app.get('/pages/:slug', (req, res) => {
try {
const page = db.prepare(`
SELECT p.*, t.structure, t.styles, t.component, t.form_id, f.fields AS form_fields, u.username as created_by_username
FROM pages p
LEFT JOIN templates t ON p.template_id = t.id
LEFT JOIN forms f ON t.form_id = f.id
LEFT JOIN users u ON p.created_by = u.id
WHERE p.slug = ?
`).get(req.params.slug);
if (!page) return res.status(404).json({ error: 'Page not found' });
console.log('Fetched page:', { slug: req.params.slug, form_id: page.form_id, form_fields: page.form_fields });
res.json({
...page,
data: JSON.parse(page.data),
structure: page.structure ? JSON.parse(page.structure) : null,
styles: page.styles ? JSON.parse(page.styles) : null,
form_fields: page.form_fields ? JSON.parse(page.form_fields) : null,
});
} catch (err) {
console.error('Page retrieval error:', err);
res.status(500).json({ error: 'Failed to retrieve page' });
}
});
app.get('/admin/pages', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const pages = db.prepare(`
SELECT p.*, u.username as created_by_username
FROM pages p
LEFT JOIN users u ON p.created_by = u.id
`).all();
res.json(pages.map(p => ({ ...p, data: JSON.parse(p.data) })));
} catch (err) {
console.error('Pages fetch error:', err);
res.status(500).json({ error: 'Failed to fetch pages' });
}
});
app.put('/admin/pages/:id', authenticateJWT, restrictToAdmin, (req, res) => {
const { title, slug, data, template_id } = req.body;
if (!title || typeof title !== 'string' || title.trim() === '') {
return res.status(400).json({ error: 'Valid title is required' });
}
if (!slug || typeof slug !== 'string' || !/^[a-z0-9-]+$/i.test(slug)) {
return res.status(400).json({ error: 'Valid slug is required (letters, numbers, and hyphens only)' });
}
if (!data || typeof data !== 'object') {
return res.status(400).json({ error: 'Valid data object is required' });
}
if (template_id && isNaN(parseInt(template_id))) {
return res.status(400).json({ error: 'Valid template_id is required' });
}
try {
if (template_id) {
const template = db.prepare('SELECT id, structure FROM templates WHERE id = ?').get(parseInt(template_id));
if (!template) {
return res.status(400).json({ error: 'Invalid template_id' });
}
const structure = JSON.parse(template.structure);
for (const field in structure) {
if (structure[field].type === 'number' && data[field] && isNaN(parseFloat(data[field]))) {
return res.status(400).json({ error: `Field ${field} must be a number` });
}
if (structure[field].type === 'date' && data[field] && !/^\d{4}-\d{2}-\d{2}$/.test(data[field])) {
return res.status(400).json({ error: `Field ${field} must be a valid date (YYYY-MM-DD)` });
}
if (structure[field].type === 'select' && data[field] && !structure[field].options.includes(data[field])) {
return res.status(400).json({ error: `Field ${field} must be a valid option` });
}
if (structure[field].type === 'enum' && data[field] && !structure[field].values.includes(data[field])) {
return res.status(400).json({ error: `Field ${field} must be a valid enum value` });
}
if (structure[field].type === 'image' && data[field] && !data[field].startsWith('/uploads/')) {
return res.status(400).json({ error: `Field ${field} must be a valid image path` });
}
if (structure[field].type === 'repeater' && data[field] && !Array.isArray(data[field])) {
return res.status(400).json({ error: `Field ${field} must be an array` });
}
if (structure[field].type === 'grid' && data[field] && typeof data[field] !== 'object') {
return res.status(400).json({ error: `Field ${field} must be an object` });
}
}
}
const existingSlug = db.prepare('SELECT id FROM pages WHERE slug = ? AND id != ?').get(slug, req.params.id);
if (existingSlug) {
return res.status(409).json({ error: 'Slug already exists' });
}
const now = new Date().toISOString();
const stmt = db.prepare(`
UPDATE pages
SET title = ?, slug = ?, data = ?, template_id = ?, modified_at = ?
WHERE id = ?
`);
const result = stmt.run(title.trim(), slug, JSON.stringify(data), template_id ? parseInt(template_id) : null, now, req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Page not found' });
res.json({ message: 'Page updated successfully' });
} catch (err) {
console.error('Page update error:', err);
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('FOREIGN KEY')) {
return res.status(400).json({ error: 'Invalid template_id' });
}
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('UNIQUE')) {
return res.status(409).json({ error: 'Slug already exists' });
}
res.status(500).json({ error: 'Failed to update page' });
}
});
app.delete('/admin/pages/:id', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const stmt = db.prepare('DELETE FROM pages WHERE id = ?');
const result = stmt.run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Page not found' });
res.json({ message: 'Page deleted successfully' });
} catch (err) {
console.error('Page deletion error:', err);
res.status(500).json({ error: 'Failed to delete page' });
}
});
// Post endpoints
app.get('/admin/posts', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const posts = db.prepare(`
SELECT p.*, u.username as created_by_username
FROM posts p
LEFT JOIN users u ON p.created_by = u.id
`).all();
res.json(posts);
} catch (err) {
console.error('Posts fetch error:', err);
res.status(500).json({ error: 'Failed to fetch posts' });
}
});
app.post('/posts', authenticateJWT, restrictToAdmin, (req, res) => {
const { title, body } = req.body;
if (!title || typeof title !== 'string' || title.trim() === '') {
return res.status(400).json({ error: 'Valid title is required' });
}
if (!body || typeof body !== 'string' || body.trim() === '') {
return res.status(400).json({ error: 'Valid body is required' });
}
try {
const user = db.prepare('SELECT id FROM users WHERE id = ?').get(req.user.id);
if (!user) {
return res.status(400).json({ error: 'Invalid author_id' });
}
const now = new Date().toISOString();
const stmt = db.prepare('INSERT INTO posts (title, body, author_id, created_by, created_at, modified_at) VALUES (?, ?, ?, ?, ?, ?)');
stmt.run(title.trim(), body, req.user.id, req.user.id, now, now);
const postId = db.prepare('SELECT last_insert_rowid() as id').get().id;
res.json({ message: 'Post created successfully', postId });
} catch (err) {
console.error('Post creation error:', err);
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('NOT NULL')) {
return res.status(400).json({ error: 'Missing required fields' });
}
res.status(500).json({ error: 'Failed to create post' });
}
});
// Other endpoints
app.get('/admin/orders', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const orders = db.prepare('SELECT o.*, u.username AS user_username FROM orders o LEFT JOIN users u ON o.user_id = u.id').all();
res.json(orders);
} catch (err) {
console.error('Orders fetch error:', err);
res.status(500).json({ error: 'Failed to fetch orders' });
}
});
app.get('/admin/fulfillments', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const fulfillments = db.prepare('SELECT f.*, o.user_id, u.username AS user_username FROM fulfillments f JOIN orders o ON f.order_id = o.id LEFT JOIN users u ON o.user_id = u.id').all();
res.json(fulfillments);
} catch (err) {
console.error('Fulfillments fetch error:', err);
res.status(500).json({ error: 'Failed to fetch fulfillments' });
}
});
app.get('/admin/shipments', authenticateJWT, restrictToAdmin, (req, res) => {
try {
const shipments = db.prepare('SELECT s.*, o.user_id, u.username AS user_username FROM shipments s JOIN orders o ON s.order_id = o.id LEFT JOIN users u ON o.user_id = u.id').all();
res.json(shipments);
} catch (err) {
console.error('Shipments fetch error:', err);
res.status(500).json({ error: 'Failed to fetch shipments' });
}
});
// Start the server
app.listen(port, () => {
console.log(`DeComPress server listening on http://localhost:${port}`);
});