1029 lines
43 KiB
JavaScript
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}`);
|
|
}); |