const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const express = require("express");
const session = require("express-session");
const bcrypt = require("bcryptjs");
const multer = require("multer");
const sharp = require("sharp");
const nodemailer = require("nodemailer");
const archiver = require("archiver");
const config = require("./config");
const {
db,
setSetting,
getSetting,
allSettings,
ensureCategory,
saveRevision,
audit
} = require("./database");
const {
escapeHtml,
cleanRichText,
makeSlug,
token,
now,
bytes,
checkbox,
listFromText,
safeJson
} = require("./utils");
const {
publicLayout,
adminLayout,
authLayout,
setting,
button,
itemCard,
serviceCard,
contactForm,
sectionHtml,
mediaOption,
mediaGrid
} = require("./render");
const app = express();
app.set("trust proxy", 1);
class SQLiteSessionStore extends session.Store {
get(sid, cb) {
try {
const row = db.prepare("SELECT sess FROM sessions WHERE sid = ? AND expired_at > ?").get(sid, Date.now());
cb(null, row ? JSON.parse(row.sess) : null);
} catch (error) {
cb(error);
}
}
set(sid, sess, cb) {
try {
const maxAge = sess.cookie?.maxAge || 1000 * 60 * 60 * 8;
db.prepare(`
INSERT INTO sessions (sid, sess, expired_at)
VALUES (?, ?, ?)
ON CONFLICT(sid) DO UPDATE SET sess = excluded.sess, expired_at = excluded.expired_at
`).run(sid, JSON.stringify(sess), Date.now() + maxAge);
cb(null);
} catch (error) {
cb(error);
}
}
destroy(sid, cb) {
try {
db.prepare("DELETE FROM sessions WHERE sid = ?").run(sid);
cb(null);
} catch (error) {
cb(error);
}
}
}
app.use(express.urlencoded({ extended: true, limit: "1mb" }));
app.use(express.json({ limit: "1mb" }));
app.use(session({
store: new SQLiteSessionStore(),
name: "dws.sid",
secret: config.sessionSecret,
resave: false,
saveUninitialized: false,
rolling: true,
cookie: {
httpOnly: true,
sameSite: "lax",
secure: config.production,
maxAge: 1000 * 60 * 60 * 8
}
}));
app.use((req, res, next) => {
if (!req.session.csrfToken) req.session.csrfToken = token(16);
res.locals.csrfToken = req.session.csrfToken;
next();
});
app.use((req, res, next) => {
if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
const submitted = req.body?._csrf || req.headers["x-csrf-token"];
if (!submitted || submitted !== req.session.csrfToken) return res.status(403).send("Security check failed. Please go back and try again.");
}
next();
});
app.use("/css", express.static(path.join(config.root, "public/css"), { maxAge: "7d" }));
app.use("/js", express.static(path.join(config.root, "public/js"), { maxAge: "7d" }));
app.use("/uploads", express.static(config.uploadDir, { maxAge: "30d", fallthrough: true }));
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 8 * 1024 * 1024, files: 10 },
fileFilter(req, file, cb) {
const ok = ["image/jpeg", "image/png", "image/webp"].includes(file.mimetype);
cb(ok ? null : new Error("Only JPG, PNG, and WebP images are allowed."), ok);
}
});
function adminCount() {
return db.prepare("SELECT COUNT(*) AS count FROM administrators").get().count;
}
function currentAdmin(req) {
if (!req.session.adminId) return null;
return db.prepare("SELECT id, name, email, role FROM administrators WHERE id = ?").get(req.session.adminId);
}
function requireAdmin(req, res, next) {
const admin = currentAdmin(req);
if (!admin) return res.redirect("/admin/login");
req.admin = admin;
next();
}
function navLinks() {
return db.prepare("SELECT * FROM navigation WHERE is_published = 1 ORDER BY display_order, id").all();
}
function mediaRows() {
return db.prepare("SELECT * FROM media ORDER BY created_at DESC").all();
}
function pageBySlug(slug) {
return db.prepare("SELECT * FROM pages WHERE slug = ?").get(slug);
}
function pageSections(pageId, includeHidden = false) {
return db.prepare(`
SELECT ps.*, m.url image_url, m.alt_text image_alt, bm.url background_url
FROM page_sections ps
LEFT JOIN media m ON m.id = ps.image_id
LEFT JOIN media bm ON bm.id = ps.background_image_id
WHERE ps.page_id = ? ${includeHidden ? "" : "AND ps.is_published = 1"}
ORDER BY ps.display_order, ps.id
`).all(pageId);
}
function publicItems(where = "", params = []) {
return db.prepare(`
SELECT i.*, c.name category_name, m.url image_url, m.alt_text image_alt
FROM items i
LEFT JOIN categories c ON c.id = i.category_id
LEFT JOIN media m ON m.id = i.main_image_id
WHERE i.is_published = 1 AND i.is_archived = 0 ${where}
ORDER BY i.name COLLATE NOCASE
`).all(...params);
}
function publicServices() {
return db.prepare(`
SELECT s.*, m.url image_url, m.alt_text image_alt
FROM services s
LEFT JOIN media m ON m.id = s.image_id
WHERE s.is_published = 1
ORDER BY s.display_order, s.name
`).all();
}
function renderPublic(req, res, opts) {
const settings = allSettings();
if (maintenanceActive(req, settings)) {
return res.status(503).send(publicLayout({
title: "Website Maintenance",
description: "The website is temporarily in maintenance mode.",
settings,
nav: navLinks(),
csrfToken: res.locals.csrfToken,
currentPath: req.path,
body: maintenancePage(settings)
}));
}
res.send(publicLayout({
...opts,
settings,
nav: navLinks(),
csrfToken: res.locals.csrfToken,
currentPath: req.path
}));
}
function renderAdmin(req, res, title, active, body) {
res.send(adminLayout({ title, active, body, admin: req.admin, csrfToken: res.locals.csrfToken }));
}
function maintenanceActive(req, settings) {
return setting(settings, "maintenance.enabled", "false") === "true" && !req.session.adminId && !req.path.startsWith("/admin") && req.path !== "/healthz";
}
function maintenancePage(settings) {
const artwork = setting(settings, "maintenance.artwork", "");
return `
${artwork ? ` ` : ""}
Website maintenance
${escapeHtml(setting(settings, "business.name", "Debbie Windler Seamstress"))}
${escapeHtml(setting(settings, "maintenance.message", "The website is getting a careful refresh. Please check back soon."))}
${setting(settings, "maintenance.return_note", "") ? `${escapeHtml(setting(settings, "maintenance.return_note"))}
` : ""}
`;
}
function ipHash(req) {
return crypto.createHash("sha256").update(`${req.ip}|${config.sessionSecret}`).digest("hex");
}
function rateLimit(key, limit, windowMs) {
const resetAt = Date.now() + windowMs;
const row = db.prepare("SELECT * FROM rate_limits WHERE key = ?").get(key);
if (!row || row.reset_at < Date.now()) {
db.prepare("INSERT INTO rate_limits (key, count, reset_at) VALUES (?, 1, ?) ON CONFLICT(key) DO UPDATE SET count = 1, reset_at = excluded.reset_at").run(key, resetAt);
return true;
}
if (row.count >= limit) return false;
db.prepare("UPDATE rate_limits SET count = count + 1 WHERE key = ?").run(key);
return true;
}
async function storeImage(file, { title = "", alt = "", category = "" } = {}) {
if (!file) return null;
const metadata = await sharp(file.buffer, { failOn: "error" }).metadata();
if (!["jpeg", "png", "webp"].includes(metadata.format)) throw new Error("That image format is not allowed.");
const base = `${Date.now()}-${token(4)}`;
const webName = `${base}.webp`;
const thumbName = `${base}-thumb.webp`;
const webPath = path.join(config.uploadDir, webName);
const thumbPath = path.join(config.uploadDir, thumbName);
await sharp(file.buffer).rotate().resize({ width: 1800, withoutEnlargement: true }).webp({ quality: 82 }).toFile(webPath);
await sharp(file.buffer).rotate().resize(420, 300, { fit: "cover" }).webp({ quality: 78 }).toFile(thumbPath);
const finalMeta = await sharp(webPath).metadata();
const created = now();
const result = db.prepare(`
INSERT INTO media (file_name, original_name, title, alt_text, category, mime_type, size_bytes, width, height, url, thumb_url, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'image/webp', ?, ?, ?, ?, ?, ?, ?)
`).run(webName, file.originalname, title || file.originalname, alt, category, fs.statSync(webPath).size, finalMeta.width, finalMeta.height, `/uploads/${webName}`, `/uploads/${thumbName}`, created, created);
return db.prepare("SELECT * FROM media WHERE id = ?").get(result.lastInsertRowid);
}
async function notifyOwner(message) {
const ownerEmail = config.smtp.ownerEmail || getSetting("business.email", "");
if (!config.smtp.host || !ownerEmail) return { status: "not_configured", error: "" };
try {
const transporter = nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: config.smtp.secure,
auth: config.smtp.user ? { user: config.smtp.user, pass: config.smtp.pass } : undefined
});
await transporter.sendMail({
from: config.smtp.from,
to: ownerEmail,
replyTo: message.email,
subject: `Website inquiry: ${message.subject}`,
text: [
`Name: ${message.name}`,
`Email: ${message.email}`,
`Phone: ${message.phone || ""}`,
`Preferred contact: ${message.preferred_contact || ""}`,
`Related: ${message.related || ""}`,
`Source: ${message.source_page || ""}`,
"",
message.message
].join("\n")
});
return { status: "sent", error: "" };
} catch (error) {
return { status: "failed", error: error.message.slice(0, 500) };
}
}
app.get("/healthz", (req, res) => res.json({ ok: true, time: now() }));
app.get("/", (req, res) => {
const settings = allSettings();
const featured = publicItems("AND (i.featured = 1 OR i.newly_added = 1 OR i.on_sale = 1)").slice(0, 6);
const home = pageBySlug("home");
const sections = pageSections(home.id);
const overlay = setting(settings, "home.hero.overlay", "light");
const body = `
${escapeHtml(setting(settings, "business.tagline", ""))}
${escapeHtml(setting(settings, "home.hero.title", "Debbie Windler Seamstress"))}
${escapeHtml(setting(settings, "home.hero.subtitle", ""))}
${escapeHtml(setting(settings, "home.hero.contact", ""))}
${button(setting(settings, "home.hero.button1_label"), setting(settings, "home.hero.button1_url"))}
${button(setting(settings, "home.hero.button2_label"), setting(settings, "home.hero.button2_url"), "button secondary")}
Showcase
${escapeHtml(setting(settings, "home.highlight.title", "Featured and Newly Added"))}
${escapeHtml(setting(settings, "home.highlight.intro", ""))}
${featured.map(itemCard).join("")}
${sections.map(sectionHtml).join("")}
`;
res.send(publicLayout({
title: setting(settings, "site.title", "Debbie Windler Seamstress"),
description: setting(settings, "site.description", ""),
settings,
nav: navLinks(),
csrfToken: res.locals.csrfToken,
currentPath: req.path,
body
}));
});
app.get("/items", (req, res) => {
const settings = allSettings();
const q = String(req.query.q || "").trim();
const category = String(req.query.category || "").trim();
const availability = String(req.query.availability || "").trim();
const where = [];
const params = [];
if (q) {
where.push("AND (i.name LIKE ? OR i.short_description LIKE ? OR i.full_description LIKE ?)");
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
}
if (category) {
where.push("AND c.slug = ?");
params.push(category);
}
if (availability) {
where.push("AND i.availability_status = ?");
params.push(availability);
}
const items = publicItems(where.join(" "), params);
const categories = db.prepare("SELECT * FROM categories ORDER BY name").all();
const statuses = db.prepare("SELECT DISTINCT availability_status FROM items WHERE is_published = 1 ORDER BY availability_status").all();
const alphabet = [...new Set(items.map((item) => item.name[0]?.toUpperCase()).filter(Boolean))];
const body = `
Items Available and Custom Orders Browse sample listings alphabetically. Every item can be edited, hidden, marked new, put on sale, or offered as a custom order.
${alphabet.length > 6 ? `${alphabet.map((letter) => `
${letter} `).join("")}
` : ""}
${items.map(itemCard).join("") || "No items match that search yet.
"}
`;
renderPublic(req, res, { title: "Items Available and Custom Orders", description: "Items, custom orders, and sewing examples.", body });
});
app.get("/items/:slug", (req, res) => {
const item = db.prepare(`
SELECT i.*, c.name category_name, m.url image_url, m.alt_text image_alt
FROM items i
LEFT JOIN categories c ON c.id = i.category_id
LEFT JOIN media m ON m.id = i.main_image_id
WHERE i.slug = ? AND i.is_published = 1 AND i.is_archived = 0
`).get(req.params.slug);
if (!item) return res.status(404).send("Item not found");
const gallery = db.prepare(`
SELECT m.* FROM item_images ii JOIN media m ON m.id = ii.media_id
WHERE ii.item_id = ? ORDER BY ii.display_order, ii.id
`).all(item.id);
const body = `
${item.image_url ? `
` : `
`}
${escapeHtml(item.category_name || "Item")}
${escapeHtml(item.name)}
${escapeHtml(item.availability_status)}
${item.contact_for_pricing ? "Contact for pricing" : escapeHtml(item.price || item.starting_price || item.price_range || "")}
${cleanRichText(item.full_description || item.short_description || "")}
${item.material_notes ? `
Materials ${escapeHtml(item.material_notes)}
` : ""}
${item.color_size_notes ? `
Color or Size Notes ${escapeHtml(item.color_size_notes)}
` : ""}
${item.care_instructions ? `
Care Instructions ${escapeHtml(item.care_instructions)}
` : ""}
${button("Ask About This Item", `/about-contact?item=${encodeURIComponent(item.name)}#contact-form`)}
${gallery.length ? `${gallery.map((m) => ` `).join("")} ` : ""}`;
renderPublic(req, res, { title: item.name, description: item.short_description, body });
});
app.get("/services", (req, res) => {
const page = pageBySlug("services");
const services = publicServices();
const sections = pageSections(page.id);
const body = `${escapeHtml(page.title)} ${escapeHtml(page.intro || "")}
${services.map(serviceCard).join("")}
${sections.map(sectionHtml).join("")}`;
renderPublic(req, res, { title: page.title, description: page.meta_description, body });
});
app.get("/about-contact", (req, res) => {
const settings = allSettings();
const page = pageBySlug("about-contact");
const sections = pageSections(page.id);
const equipment = db.prepare(`
SELECT e.*, m.url image_url, m.alt_text image_alt
FROM equipment e LEFT JOIN media m ON m.id = e.image_id
WHERE e.is_published = 1 ORDER BY e.display_order, e.name
`).all();
const body = `${escapeHtml(page.title)} ${escapeHtml(page.intro || "")}
${sections.map(sectionHtml).join("")}
Equipment
Sewing Room and Equipment
${equipment.map((e) => `
${e.image_url ? ` ` : `
`}${escapeHtml(e.name)} ${escapeHtml([e.brand, e.model].filter(Boolean).join(" "))}
${escapeHtml(e.description || "")}
`).join("")}
${contactForm({ csrfToken: res.locals.csrfToken, item: req.query.item, service: req.query.service, success: req.query.sent === "1", settings })}`;
renderPublic(req, res, { title: page.title, description: page.meta_description, body });
});
app.post("/contact", async (req, res) => {
if (!rateLimit(`contact:${ipHash(req)}`, 5, 60 * 60 * 1000)) return res.status(429).send("Too many messages were sent recently. Please try again later.");
if (req.body.website) return res.redirect("/about-contact?sent=1#contact-form");
const fields = {
name: String(req.body.name || "").trim().slice(0, 120),
email: String(req.body.email || "").trim().slice(0, 180),
phone: String(req.body.phone || "").trim().slice(0, 80),
preferred_contact: String(req.body.preferred_contact || "").trim().slice(0, 80),
subject: String(req.body.subject || "").trim().slice(0, 160),
message: String(req.body.message || "").trim().slice(0, 3000),
related: String(req.body.related || "").trim().slice(0, 160),
source_page: String(req.body.source_page || req.get("referer") || "").slice(0, 300)
};
if (!fields.name || !fields.email || !fields.subject || !fields.message || !req.body.consent || !fields.email.includes("@")) {
return res.status(400).send("Please complete the required contact form fields and try again.");
}
const emailResult = await notifyOwner(fields);
const created = now();
db.prepare(`
INSERT INTO messages
(name, email, phone, preferred_contact, subject, message, related_item, consent, source_page, ip_hash, user_agent, email_notification_status, email_error, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
`).run(fields.name, fields.email, fields.phone, fields.preferred_contact, fields.subject, fields.message, fields.related, fields.source_page, ipHash(req), String(req.get("user-agent") || "").slice(0, 300), emailResult.status, emailResult.error, created, created);
res.redirect("/about-contact?sent=1#contact-form");
});
app.get("/robots.txt", (req, res) => {
const settings = allSettings();
const visible = setting(settings, "seo.visible", "true") !== "false";
res.type("text/plain").send(`${visible ? "User-agent: *\nAllow: /" : "User-agent: *\nDisallow: /"}\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
});
app.get("/sitemap.xml", (req, res) => {
const items = publicItems();
const urls = ["/", "/items", "/services", "/about-contact", ...items.map((item) => `/items/${item.slug}`)];
res.type("application/xml").send(`${urls.map((url) => `${escapeHtml(config.baseUrl + url)} `).join("")} `);
});
app.get("/admin", (req, res) => {
if (!adminCount()) return res.redirect("/admin/setup");
if (!currentAdmin(req)) return res.redirect("/admin/login");
res.redirect("/admin/dashboard");
});
app.get("/admin/setup", (req, res) => {
if (adminCount()) return res.redirect("/admin/login");
res.send(authLayout("Create Owner Account", `
Create the first owner login. No password is stored as plain text.
`));
});
app.post("/admin/setup", async (req, res) => {
if (adminCount()) return res.redirect("/admin/login");
const password = String(req.body.password || "");
if (password.length < 10) return res.status(400).send("Please use a password with at least 10 characters.");
const hash = await bcrypt.hash(password, 12);
const ts = now();
const result = db.prepare("INSERT INTO administrators (name, email, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(String(req.body.name || "Owner").trim(), String(req.body.email || "").trim().toLowerCase(), hash, ts, ts);
req.session.adminId = result.lastInsertRowid;
audit(result.lastInsertRowid, "Created owner account", "", req.ip);
res.redirect("/admin/dashboard");
});
app.get("/admin/login", (req, res) => {
if (!adminCount()) return res.redirect("/admin/setup");
res.send(authLayout("Owner Login", ``));
});
app.post("/admin/login", async (req, res) => {
if (!rateLimit(`login:${ipHash(req)}`, 8, 15 * 60 * 1000)) return res.status(429).send("Too many login tries. Please wait and try again.");
const admin = db.prepare("SELECT * FROM administrators WHERE email = ?").get(String(req.body.email || "").trim().toLowerCase());
if (!admin || !(await bcrypt.compare(String(req.body.password || ""), admin.password_hash))) return res.status(401).send("Login failed. Check the email and password.");
req.session.regenerate((error) => {
if (error) return res.status(500).send("Login could not be completed.");
req.session.adminId = admin.id;
req.session.csrfToken = token(16);
db.prepare("UPDATE administrators SET last_login_at = ? WHERE id = ?").run(now(), admin.id);
audit(admin.id, "Logged in", "", req.ip);
res.redirect("/admin/dashboard");
});
});
app.post("/admin/logout", requireAdmin, (req, res) => {
req.session.destroy(() => res.redirect("/admin/login"));
});
app.get("/admin/dashboard", requireAdmin, (req, res) => {
const stats = {
unread: db.prepare("SELECT COUNT(*) count FROM messages WHERE status = 'unread'").get().count,
publishedItems: db.prepare("SELECT COUNT(*) count FROM items WHERE is_published = 1 AND is_archived = 0").get().count,
hiddenItems: db.prepare("SELECT COUNT(*) count FROM items WHERE is_published = 0 OR is_archived = 1").get().count,
newItems: db.prepare("SELECT COUNT(*) count FROM items WHERE newly_added = 1").get().count,
featuredItems: db.prepare("SELECT COUNT(*) count FROM items WHERE featured = 1").get().count,
services: db.prepare("SELECT COUNT(*) count FROM services WHERE is_published = 1").get().count,
media: db.prepare("SELECT COALESCE(SUM(size_bytes),0) bytes FROM media").get().bytes,
backups: db.prepare("SELECT * FROM backups ORDER BY created_at DESC LIMIT 1").get()
};
const messages = db.prepare("SELECT * FROM messages ORDER BY created_at DESC LIMIT 5").all();
const changes = db.prepare("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 8").all();
const body = `
${Object.entries({
"Unread messages": stats.unread,
"Published items": stats.publishedItems,
"Hidden or draft items": stats.hiddenItems,
"New items": stats.newItems,
"Featured items": stats.featuredItems,
"Published services": stats.services,
"Storage used": bytes(stats.media),
"Last backup": stats.backups ? stats.backups.created_at.slice(0, 10) : "No backup yet"
}).map(([label, value]) => `${escapeHtml(value)} ${label} `).join("")}
${button("Add Item", "/admin/items/new")}
${button("Add Service", "/admin/services/new")}
${button("Upload Image", "/admin/media")}
${button("Edit Home Page", "/admin/home")}
${button("Read Messages", "/admin/messages")}
${button("Edit Contact Information", "/admin/business")}
Recent Messages ${messages.map((m) => `
${escapeHtml(m.subject)} ${escapeHtml(m.name)} · ${escapeHtml(m.created_at)}
`).join("") || "
No messages yet.
"}
Recent Website Changes ${changes.map((c) => `
${escapeHtml(c.action)}${escapeHtml(c.created_at)}
`).join("") || "
No changes yet.
"}
`;
renderAdmin(req, res, "Dashboard", "dashboard", body);
});
app.get("/admin/home", requireAdmin, (req, res) => {
const s = allSettings();
const body = ``;
renderAdmin(req, res, "Home Page Editor", "home", body);
});
app.post("/admin/home", requireAdmin, (req, res) => {
Object.keys(req.body).filter((key) => key.startsWith("home.")).forEach((key) => setSetting(key, req.body[key]));
audit(req.admin.id, "Updated home page settings", "", req.ip);
res.redirect("/admin/home?saved=1");
});
app.get("/admin/pages", requireAdmin, (req, res) => {
const pages = db.prepare("SELECT * FROM pages ORDER BY id").all();
renderAdmin(req, res, "Page Editor", "pages", `${pages.map((p) => `
${escapeHtml(p.title)} ${escapeHtml(p.intro || "")}
Edit Page Preview `).join("")}
`);
});
app.get("/admin/pages/:slug", requireAdmin, (req, res) => {
const page = pageBySlug(req.params.slug);
if (!page) return res.status(404).send("Page not found");
const sections = pageSections(page.id, true);
const media = mediaRows();
const body = `
Content Sections
${sections.map((section) => sectionForm(section, media, res.locals.csrfToken, `/admin/pages/${page.slug}/sections/${section.id}`)).join("")}
`;
renderAdmin(req, res, `Edit ${page.title}`, "pages", body);
});
app.post("/admin/pages/:slug", requireAdmin, (req, res) => {
const page = pageBySlug(req.params.slug);
saveRevision("page", page.id, page.title, page, req.admin.id);
db.prepare("UPDATE pages SET title=?, subtitle=?, intro=?, meta_title=?, meta_description=?, search_visible=?, status=?, updated_at=? WHERE id=?")
.run(req.body.title, req.body.subtitle, req.body.intro, req.body.meta_title, req.body.meta_description, checkbox(req.body.search_visible === "1"), req.body.status, now(), page.id);
audit(req.admin.id, `Updated page ${page.title}`, "", req.ip);
res.redirect(`/admin/pages/${page.slug}`);
});
app.post("/admin/pages/:slug/sections/new", requireAdmin, (req, res) => {
const page = pageBySlug(req.params.slug);
db.prepare("INSERT INTO page_sections (page_id, title, body, layout, display_order, is_published, updated_at) VALUES (?, 'New Section', 'Edit this section.
', 'text', (SELECT COALESCE(MAX(display_order),0)+10 FROM page_sections WHERE page_id=?), 0, ?)")
.run(page.id, page.id, now());
res.redirect(`/admin/pages/${page.slug}`);
});
app.post("/admin/pages/:slug/sections/:id", requireAdmin, (req, res) => {
const page = pageBySlug(req.params.slug);
const section = db.prepare("SELECT * FROM page_sections WHERE id = ?").get(req.params.id);
saveRevision("section", section.id, section.title, section, req.admin.id);
db.prepare("UPDATE page_sections SET title=?, body=?, layout=?, image_id=?, background_image_id=?, background_color=?, button_label=?, button_url=?, display_order=?, is_published=?, updated_at=? WHERE id=?")
.run(req.body.title, cleanRichText(req.body.body), req.body.layout, req.body.image_id || null, req.body.background_image_id || null, req.body.background_color, req.body.button_label, req.body.button_url, Number(req.body.display_order || 0), checkbox(req.body.is_published), now(), section.id);
audit(req.admin.id, `Updated section on ${page.title}`, "", req.ip);
res.redirect(`/admin/pages/${page.slug}`);
});
function csrf(res) {
return ` `;
}
function input(label, name, value = "", type = "text", min = "", max = "", step = "") {
return `${escapeHtml(label)} `;
}
function textarea(label, name, value = "") {
return `${escapeHtml(label)} `;
}
function select(label, name, options, selected = "") {
const html = options.map((option) => {
const value = Array.isArray(option) ? option[0] : option;
const text = Array.isArray(option) ? option[1] : option;
return `${escapeHtml(text)} `;
}).join("");
return `${escapeHtml(label)} ${html} `;
}
function checkboxInput(label, name, checked) {
return ` ${escapeHtml(label)} `;
}
function sectionForm(section, media, csrfToken, action) {
return `${escapeHtml(section.title || "Untitled section")} ${section.is_published ? "" : "(hidden)"}
`;
}
app.get("/admin/items", requireAdmin, (req, res) => {
const q = String(req.query.q || "");
const items = db.prepare(`
SELECT i.*, c.name category_name FROM items i LEFT JOIN categories c ON c.id = i.category_id
WHERE i.name LIKE ? OR i.short_description LIKE ?
ORDER BY i.name
`).all(`%${q}%`, `%${q}%`);
const body = `
${items.map((i) => `
${escapeHtml(i.name)} ${escapeHtml(i.category_name || "")} · ${escapeHtml(i.availability_status)} ${i.is_published ? "" : "· Hidden"}
Edit Preview `).join("")}
`;
renderAdmin(req, res, "Items Manager", "items", body);
});
app.get("/admin/items/new", requireAdmin, (req, res) => renderAdmin(req, res, "Add Item", "items", itemEditor(null, res.locals.csrfToken)));
app.get("/admin/items/:id/edit", requireAdmin, (req, res) => {
const item = db.prepare("SELECT i.*, c.name category_name FROM items i LEFT JOIN categories c ON c.id = i.category_id WHERE i.id = ?").get(req.params.id);
if (!item) return res.status(404).send("Item not found");
renderAdmin(req, res, `Edit ${item.name}`, "items", itemEditor(item, res.locals.csrfToken));
});
app.post("/admin/items/new", requireAdmin, upload.array("images", 8), async (req, res, next) => {
try {
const id = await saveItem(req, null);
res.redirect(`/admin/items/${id}/edit`);
} catch (error) { next(error); }
});
app.post("/admin/items/:id/edit", requireAdmin, upload.array("images", 8), async (req, res, next) => {
try {
await saveItem(req, req.params.id);
res.redirect(`/admin/items/${req.params.id}/edit`);
} catch (error) { next(error); }
});
app.post("/admin/items/:id/duplicate", requireAdmin, (req, res) => {
const item = db.prepare("SELECT * FROM items WHERE id=?").get(req.params.id);
if (!item) return res.status(404).send("Item not found");
const copyName = `${item.name} Copy`;
const copySlug = `${item.slug}-copy-${token(3)}`;
const ts = now();
const result = db.prepare(`INSERT INTO items (name, slug, short_description, full_description, category_id, main_image_id, price, starting_price, price_range, contact_for_pricing, quantity, availability_status, custom_order_available, color_size_notes, material_notes, care_instructions, featured, newly_added, on_sale, sale_text, display_order, is_published, is_archived, date_added, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 0, 0, ?, NULL, ?)`)
.run(copyName, copySlug, item.short_description, item.full_description, item.category_id, item.main_image_id, item.price, item.starting_price, item.price_range, item.contact_for_pricing, item.quantity, item.availability_status, item.custom_order_available, item.color_size_notes, item.material_notes, item.care_instructions, item.sale_text, item.display_order + 1, ts, ts);
audit(req.admin.id, "Duplicated item", item.name, req.ip);
res.redirect(`/admin/items/${result.lastInsertRowid}/edit`);
});
app.post("/admin/items/:id/archive", requireAdmin, (req, res) => {
db.prepare("UPDATE items SET is_archived=1, is_published=0, updated_at=? WHERE id=?").run(now(), req.params.id);
audit(req.admin.id, "Archived item", String(req.params.id), req.ip);
res.redirect("/admin/items");
});
app.post("/admin/items/:id/delete", requireAdmin, (req, res) => {
db.prepare("DELETE FROM items WHERE id=?").run(req.params.id);
audit(req.admin.id, "Deleted item", String(req.params.id), req.ip);
res.redirect("/admin/items");
});
async function saveItem(req, id) {
const categoryId = req.body.category ? ensureCategory(req.body.category.trim()) : null;
const ts = now();
const slug = makeSlug(req.body.slug || req.body.name, "item");
if (id) {
const old = db.prepare("SELECT * FROM items WHERE id=?").get(id);
saveRevision("item", id, old.name, old, req.admin.id);
db.prepare(`UPDATE items SET name=?, slug=?, short_description=?, full_description=?, category_id=?, price=?, starting_price=?, price_range=?, contact_for_pricing=?, quantity=?, availability_status=?, custom_order_available=?, color_size_notes=?, material_notes=?, care_instructions=?, featured=?, newly_added=?, on_sale=?, sale_text=?, display_order=?, is_published=?, updated_at=? WHERE id=?`)
.run(req.body.name, slug, req.body.short_description, cleanRichText(req.body.full_description), categoryId, req.body.price, req.body.starting_price, req.body.price_range, checkbox(req.body.contact_for_pricing), req.body.quantity, req.body.availability_status, checkbox(req.body.custom_order_available), req.body.color_size_notes, req.body.material_notes, req.body.care_instructions, checkbox(req.body.featured), checkbox(req.body.newly_added), checkbox(req.body.on_sale), req.body.sale_text, Number(req.body.display_order || 0), checkbox(req.body.is_published), ts, id);
} else {
const result = db.prepare(`INSERT INTO items (name, slug, short_description, full_description, category_id, price, starting_price, price_range, contact_for_pricing, quantity, availability_status, custom_order_available, color_size_notes, material_notes, care_instructions, featured, newly_added, on_sale, sale_text, display_order, is_published, date_added, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(req.body.name, slug, req.body.short_description, cleanRichText(req.body.full_description), categoryId, req.body.price, req.body.starting_price, req.body.price_range, checkbox(req.body.contact_for_pricing), req.body.quantity, req.body.availability_status, checkbox(req.body.custom_order_available), req.body.color_size_notes, req.body.material_notes, req.body.care_instructions, checkbox(req.body.featured), checkbox(req.body.newly_added), checkbox(req.body.on_sale), req.body.sale_text, Number(req.body.display_order || 0), checkbox(req.body.is_published), ts, ts, ts);
id = result.lastInsertRowid;
}
for (const file of req.files || []) {
const media = await storeImage(file, { title: req.body.name, alt: req.body.name, category: "Items" });
db.prepare("INSERT INTO item_images (item_id, media_id, display_order, is_main) VALUES (?, ?, (SELECT COALESCE(MAX(display_order),0)+10 FROM item_images WHERE item_id=?), 0)").run(id, media.id, id);
const item = db.prepare("SELECT main_image_id FROM items WHERE id=?").get(id);
if (!item.main_image_id) db.prepare("UPDATE items SET main_image_id=? WHERE id=?").run(media.id, id);
}
audit(req.admin.id, id ? "Saved item" : "Added item", req.body.name, req.ip);
return id;
}
function itemEditor(item, csrfToken) {
const values = item || { availability_status: "Available", is_published: 1 };
return `
${values.id ? `` : ""}`;
}
app.get("/admin/services", requireAdmin, (req, res) => {
const services = db.prepare("SELECT * FROM services ORDER BY display_order, name").all();
renderAdmin(req, res, "Services Manager", "services", `Add Service
${services.map((s) => `
${escapeHtml(s.name)} ${s.is_published ? "Published" : "Hidden"} · Order ${s.display_order}
Edit `).join("")}
`);
});
app.get("/admin/services/new", requireAdmin, (req, res) => renderAdmin(req, res, "Add Service", "services", serviceEditor(null, res.locals.csrfToken)));
app.get("/admin/services/:id/edit", requireAdmin, (req, res) => {
const service = db.prepare("SELECT * FROM services WHERE id=?").get(req.params.id);
renderAdmin(req, res, `Edit ${service.name}`, "services", serviceEditor(service, res.locals.csrfToken));
});
app.post("/admin/services/new", requireAdmin, upload.single("image"), async (req, res, next) => {
try { const id = await saveService(req, null); res.redirect(`/admin/services/${id}/edit`); } catch (e) { next(e); }
});
app.post("/admin/services/:id/edit", requireAdmin, upload.single("image"), async (req, res, next) => {
try { await saveService(req, req.params.id); res.redirect(`/admin/services/${req.params.id}/edit`); } catch (e) { next(e); }
});
app.post("/admin/services/:id/duplicate", requireAdmin, (req, res) => {
const s = db.prepare("SELECT * FROM services WHERE id=?").get(req.params.id);
if (!s) return res.status(404).send("Service not found");
const result = db.prepare("INSERT INTO services (name, slug, image_id, short_description, full_description, pricing_note, display_order, featured, is_published, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?)")
.run(`${s.name} Copy`, `${s.slug}-copy-${token(3)}`, s.image_id, s.short_description, s.full_description, s.pricing_note, s.display_order + 1, now());
audit(req.admin.id, "Duplicated service", s.name, req.ip);
res.redirect(`/admin/services/${result.lastInsertRowid}/edit`);
});
app.post("/admin/services/:id/delete", requireAdmin, (req, res) => {
db.prepare("DELETE FROM services WHERE id=?").run(req.params.id);
audit(req.admin.id, "Deleted service", String(req.params.id), req.ip);
res.redirect("/admin/services");
});
async function saveService(req, id) {
const media = req.file ? await storeImage(req.file, { title: req.body.name, alt: req.body.name, category: "Services" }) : null;
const ts = now();
const slug = makeSlug(req.body.slug || req.body.name, "service");
if (id) {
const old = db.prepare("SELECT * FROM services WHERE id=?").get(id);
saveRevision("service", id, old.name, old, req.admin.id);
db.prepare("UPDATE services SET name=?, slug=?, image_id=COALESCE(?, image_id), short_description=?, full_description=?, pricing_note=?, display_order=?, featured=?, is_published=?, updated_at=? WHERE id=?")
.run(req.body.name, slug, media?.id || null, req.body.short_description, cleanRichText(req.body.full_description), req.body.pricing_note, Number(req.body.display_order || 0), checkbox(req.body.featured), checkbox(req.body.is_published), ts, id);
} else {
const result = db.prepare("INSERT INTO services (name, slug, image_id, short_description, full_description, pricing_note, display_order, featured, is_published, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
.run(req.body.name, slug, media?.id || null, req.body.short_description, cleanRichText(req.body.full_description), req.body.pricing_note, Number(req.body.display_order || 0), checkbox(req.body.featured), checkbox(req.body.is_published), ts, ts);
id = result.lastInsertRowid;
}
audit(req.admin.id, "Saved service", req.body.name, req.ip);
return id;
}
function serviceEditor(service, csrfToken) {
const s = service || { is_published: 1, display_order: 0 };
return `
${s.id ? `` : ""}`;
}
app.get("/admin/about-equipment", requireAdmin, (req, res) => {
const equipment = db.prepare("SELECT * FROM equipment ORDER BY display_order, name").all();
const body = `Edit About Page Text
Equipment ${equipment.map((e) => equipmentForm(e, res.locals.csrfToken)).join("")}
`;
renderAdmin(req, res, "About and Equipment Manager", "about", body);
});
app.post("/admin/about-equipment/new", requireAdmin, (req, res) => {
db.prepare("INSERT INTO equipment (name, type, description, display_order, is_published, updated_at) VALUES ('New Equipment', '', '', 100, 0, ?)").run(now());
res.redirect("/admin/about-equipment");
});
app.post("/admin/about-equipment/:id", requireAdmin, (req, res) => {
db.prepare("UPDATE equipment SET name=?, type=?, brand=?, model=?, description=?, display_order=?, is_published=?, updated_at=? WHERE id=?")
.run(req.body.name, req.body.type, req.body.brand, req.body.model, req.body.description, Number(req.body.display_order || 0), checkbox(req.body.is_published), now(), req.params.id);
audit(req.admin.id, "Saved equipment", req.body.name, req.ip);
res.redirect("/admin/about-equipment");
});
function equipmentForm(e, csrfToken) {
return `${escapeHtml(e.name)} ${e.is_published ? "" : "(hidden)"} `;
}
app.get("/admin/messages", requireAdmin, (req, res) => {
const status = req.query.status || "";
const rows = db.prepare(`SELECT * FROM messages ${status ? "WHERE status=?" : ""} ORDER BY created_at DESC`).all(...(status ? [status] : []));
const body = `
${rows.map((m) => `
${escapeHtml(m.name)} · ${escapeHtml(m.email)} · ${escapeHtml(m.created_at)} · ${escapeHtml(m.status)}
`).join("") || "
No messages here.
"}
`;
renderAdmin(req, res, "Messages Inbox", "messages", body);
});
app.get("/admin/messages/export", requireAdmin, (req, res) => {
const rows = db.prepare("SELECT created_at,name,email,phone,preferred_contact,subject,message,related_item,source_page,status,email_notification_status FROM messages ORDER BY created_at DESC").all();
const csv = ["created_at,name,email,phone,preferred_contact,subject,message,related_item,source_page,status,email_notification_status", ...rows.map((r) => Object.values(r).map((v) => `"${String(v ?? "").replaceAll('"', '""')}"`).join(","))].join("\n");
res.type("text/csv").attachment("debbie-windler-messages.csv").send(csv);
});
app.get("/admin/messages/:id", requireAdmin, (req, res) => {
const m = db.prepare("SELECT * FROM messages WHERE id=?").get(req.params.id);
if (!m) return res.status(404).send("Message not found");
db.prepare("UPDATE messages SET status='read', updated_at=? WHERE id=? AND status='unread'").run(now(), m.id);
const mailto = `mailto:${encodeURIComponent(m.email)}?subject=${encodeURIComponent(`Re: ${m.subject}`)}`;
renderAdmin(req, res, m.subject, "messages", `From: ${escapeHtml(m.name)} · ${escapeHtml(m.email)} · ${escapeHtml(m.phone || "")}
Preferred: ${escapeHtml(m.preferred_contact || "")}
Related: ${escapeHtml(m.related_item || "")}
Source: ${escapeHtml(m.source_page || "")}
Email notice: ${escapeHtml(m.email_notification_status)} ${m.email_error ? `- ${escapeHtml(m.email_error)}` : ""}
${escapeHtml(m.message)} `);
});
app.post("/admin/messages/:id/status", requireAdmin, (req, res) => {
db.prepare("UPDATE messages SET status=?, updated_at=? WHERE id=?").run(req.body.status, now(), req.params.id);
res.redirect("/admin/messages");
});
app.get("/admin/media", requireAdmin, (req, res) => {
const media = mediaRows();
const body = `${mediaGrid(media)}
${media.map((m) => `
${escapeHtml(m.title || m.original_name)} ${escapeHtml(m.url)} · ${bytes(m.size_bytes)}
`).join("")}
`;
renderAdmin(req, res, "Media Library", "media", body);
});
app.post("/admin/media", requireAdmin, upload.array("images", 10), async (req, res, next) => {
try {
for (const file of req.files || []) await storeImage(file, req.body);
audit(req.admin.id, "Uploaded images", `${req.files?.length || 0} files`, req.ip);
res.redirect("/admin/media");
} catch (e) { next(e); }
});
app.post("/admin/media/:id", requireAdmin, (req, res) => {
db.prepare("UPDATE media SET title=?, alt_text=?, category=?, updated_at=? WHERE id=?").run(req.body.title, req.body.alt_text, req.body.category, now(), req.params.id);
res.redirect("/admin/media");
});
app.post("/admin/media/:id/delete", requireAdmin, (req, res) => {
const id = Number(req.params.id);
const used = [
db.prepare("SELECT COUNT(*) count FROM items WHERE main_image_id=?").get(id).count,
db.prepare("SELECT COUNT(*) count FROM services WHERE image_id=?").get(id).count,
db.prepare("SELECT COUNT(*) count FROM equipment WHERE image_id=?").get(id).count,
db.prepare("SELECT COUNT(*) count FROM page_sections WHERE image_id=? OR background_image_id=?").get(id, id).count
].reduce((sum, count) => sum + count, 0);
if (used) return res.status(400).send("This image is currently used on the website. Remove it from those pages or items before deleting it.");
const media = db.prepare("SELECT * FROM media WHERE id=?").get(id);
if (media) {
for (const url of [media.url, media.thumb_url]) {
if (!url) continue;
const filePath = path.join(config.root, "public", url.replace(/^\//, ""));
if (filePath.startsWith(config.uploadDir) && fs.existsSync(filePath)) fs.unlinkSync(filePath);
}
db.prepare("DELETE FROM media WHERE id=?").run(id);
audit(req.admin.id, "Deleted unused image", media.file_name, req.ip);
}
res.redirect("/admin/media");
});
app.get("/admin/appearance", requireAdmin, (req, res) => settingsForm(req, res, "Appearance and Theme Settings", "appearance", [
["Main background color", "theme.bg", "color"], ["Secondary background color", "theme.bg_alt", "color"], ["Text color", "theme.text", "color"], ["Heading color", "theme.heading", "color"], ["Link color", "theme.link", "color"], ["Button color", "theme.button", "color"], ["Button text color", "theme.button_text", "color"], ["Accent color", "theme.accent", "color"], ["Border radius", "theme.radius", "number"], ["Site width", "theme.width", "number"], ["Heading font", "theme.heading_font"], ["Body font", "theme.body_font"], ["Footer style", "theme.footer_style"]
]));
app.post("/admin/appearance", requireAdmin, (req, res) => saveSettings(req, res, "appearance", "Updated appearance settings"));
app.get("/admin/business", requireAdmin, (req, res) => settingsForm(req, res, "Business Settings", "business", [
["Business name", "business.name"], ["Tagline", "business.tagline"], ["Email", "business.email"], ["Phone", "business.phone"], ["Facebook link", "business.facebook"], ["Service area", "business.service_area"], ["Business hours", "business.hours"], ["Preferred contact method", "business.preferred_contact"], ["Footer text", "business.footer_text"], ["Copyright text", "business.copyright"], ["Website title", "site.title"], ["Website description", "site.description"]
]));
app.post("/admin/business", requireAdmin, (req, res) => saveSettings(req, res, "business", "Updated business settings"));
app.get("/admin/seo", requireAdmin, (req, res) => settingsForm(req, res, "Search Engine and Sharing Settings", "seo", [
["Website title", "site.title"], ["Website description", "site.description"], ["Canonical URL", "seo.canonical"], ["Search-engine visibility", "seo.visible"]
]));
app.post("/admin/seo", requireAdmin, (req, res) => saveSettings(req, res, "seo", "Updated SEO settings"));
app.get("/admin/maintenance", requireAdmin, (req, res) => settingsForm(req, res, "Maintenance Mode", "maintenance", [
["Maintenance enabled (true or false)", "maintenance.enabled"], ["Maintenance message", "maintenance.message"], ["Expected return message", "maintenance.return_note"], ["Artwork URL", "maintenance.artwork"]
]));
app.post("/admin/maintenance", requireAdmin, (req, res) => saveSettings(req, res, "maintenance", "Updated maintenance mode"));
function settingsForm(req, res, title, active, fields) {
const s = allSettings();
const body = ``;
renderAdmin(req, res, title, active, body);
}
function saveSettings(req, res, redirect, action) {
Object.entries(req.body).filter(([k]) => k !== "_csrf").forEach(([key, value]) => setSetting(key, value));
audit(req.admin.id, action, "", req.ip);
res.redirect(`/admin/${redirect}`);
}
app.get("/admin/navigation", requireAdmin, (req, res) => {
const rows = db.prepare("SELECT * FROM navigation ORDER BY display_order, id").all();
const body = `${rows.map((n) => ``).join("")} `;
renderAdmin(req, res, "Navigation Manager", "navigation", body);
});
app.post("/admin/navigation/new", requireAdmin, (req, res) => {
db.prepare("INSERT INTO navigation (label,url,kind,display_order,is_published) VALUES ('New Link','/','internal',100,0)").run();
res.redirect("/admin/navigation");
});
app.post("/admin/navigation/:id", requireAdmin, (req, res) => {
const url = String(req.body.url || "/");
if (url.startsWith("/admin")) return res.status(400).send("Administration links cannot be added to the public menu.");
db.prepare("UPDATE navigation SET label=?, url=?, kind=?, display_order=?, is_published=?, opens_new_tab=? WHERE id=?").run(req.body.label, url, req.body.kind, Number(req.body.display_order || 0), checkbox(req.body.is_published), checkbox(req.body.opens_new_tab), req.params.id);
res.redirect("/admin/navigation");
});
app.get("/admin/security", requireAdmin, (req, res) => {
renderAdmin(req, res, "User and Security Settings", "security", `Security Included Secure password hashing Session expiration Login rate limiting CSRF protection Upload validation Audit log for important changes `);
});
app.post("/admin/security", requireAdmin, async (req, res) => {
const row = db.prepare("SELECT * FROM administrators WHERE id=?").get(req.admin.id);
if (!(await bcrypt.compare(req.body.current_password || "", row.password_hash))) return res.status(400).send("Current password was not correct.");
if (String(req.body.new_password || "").length < 10) return res.status(400).send("Use at least 10 characters.");
db.prepare("UPDATE administrators SET password_hash=?, updated_at=? WHERE id=?").run(await bcrypt.hash(req.body.new_password, 12), now(), req.admin.id);
audit(req.admin.id, "Changed password", "", req.ip);
res.redirect("/admin/security");
});
app.get("/admin/backups", requireAdmin, (req, res) => {
const rows = db.prepare("SELECT * FROM backups ORDER BY created_at DESC").all();
renderAdmin(req, res, "Backup and Restore", "backups", `Backups include the SQLite database and uploaded images. Secret environment files are not included.
${rows.map((b) => `
${escapeHtml(b.file_name)} ${bytes(b.size_bytes)} · ${escapeHtml(b.created_at)}
Download Backup `).join("") || "
No backups yet.
"}
`);
});
app.post("/admin/backups/create", requireAdmin, async (req, res, next) => {
try {
const backup = await createBackup();
audit(req.admin.id, "Created backup", backup.file_name, req.ip);
res.redirect("/admin/backups");
} catch (e) { next(e); }
});
app.get("/admin/backups/:id/download", requireAdmin, (req, res) => {
const backup = db.prepare("SELECT * FROM backups WHERE id=?").get(req.params.id);
if (!backup || !fs.existsSync(backup.file_path)) return res.status(404).send("Backup not found");
res.download(backup.file_path, backup.file_name);
});
async function createBackup() {
fs.mkdirSync(config.backupDir, { recursive: true });
const fileName = `debbie-windler-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.zip`;
const filePath = path.join(config.backupDir, fileName);
await new Promise((resolve, reject) => {
const output = fs.createWriteStream(filePath);
const archive = archiver("zip", { zlib: { level: 9 } });
output.on("close", resolve);
archive.on("error", reject);
archive.pipe(output);
archive.file(config.databasePath, { name: "site.sqlite" });
if (fs.existsSync(config.uploadDir)) archive.directory(config.uploadDir, "uploads");
archive.finalize();
});
const stat = fs.statSync(filePath);
const result = db.prepare("INSERT INTO backups (file_name, file_path, size_bytes, created_at) VALUES (?, ?, ?, ?)").run(fileName, filePath, stat.size, now());
return db.prepare("SELECT * FROM backups WHERE id=?").get(result.lastInsertRowid);
}
app.use((err, req, res, next) => {
console.error(err);
const message = config.production ? "Something went wrong. Please try again." : err.message;
res.status(500).send(`Something went wrong ${escapeHtml(message)}
`);
});
if (require.main === module) {
app.listen(config.port, () => {
console.log(`Debbie Windler Seamstress running at http://localhost:${config.port}`);
});
}
module.exports = { app, createBackup };