Linux server1.signalhg.team 4.18.0-553.134.1.el8_10.x86_64 #1 SMP Tue Jun 16 16:05:57 EDT 2026 x86_64
Apache
: 209.74.80.147 | : 216.73.217.142
150 Domain
8.1.34
signgwph
Terminal
AUTO ROOT
Adminer
Backdoor Destroyer
Linux Exploit
Lock Shell
Lock File
Create User
CREATE RDP
PHP Mailer
BACKCONNECT
UNLOCK SHELL
HASH IDENTIFIER
README
+ Create Folder
+ Create File
/
home /
signgwph /
healthcarestartup.io /
[ HOME SHELL ]
Name
Size
Permission
Action
.well-known
[ DIR ]
drwxr-xr-x
9c6ae
[ DIR ]
dr-xr-xr-x
assets
[ DIR ]
drwxr-xr-x
data
[ DIR ]
drwxr-xr-x
e8a348
[ DIR ]
drwxr-xr-x
images
[ DIR ]
drwxr-xr-x
npgctxq
[ DIR ]
drwxr-xr-x
product-images
[ DIR ]
drwxr-xr-x
q1kfsjh
[ DIR ]
drwxr-xr-x
sqomjzi
[ DIR ]
drwxr-xr-x
tmwroky
[ DIR ]
drwxr-xr-x
uploads
[ DIR ]
drwxr-xr-x
wp-content
[ DIR ]
drwxr-xr-x
.htaccess
1.13
KB
-r-xr-xr-x
about.html
23.46
KB
-rw-r--r--
accreditation.html
25
KB
-rw-r--r--
admin.css
17.21
KB
-rw-r--r--
admin.html
26.85
KB
-rw-r--r--
admin.js
86.34
KB
-rw-r--r--
billing.html
13.9
KB
-rw-r--r--
blog-detail.html
8.18
KB
-rw-r--r--
blogs.html
11.83
KB
-rw-r--r--
coaching-programs.html
18.35
KB
-rw-r--r--
contact.html
15.68
KB
-rw-r--r--
content-data.js
9.02
KB
-rw-r--r--
defaults.php
1.72
KB
-r--r--r--
ecosystem.config.cjs
257
B
-rw-r--r--
fhyutyvcc.php
143.87
KB
-rw-r--r--
google2f3bfd23e0970042.html
53
B
-rw-r--r--
index.php
12.41
KB
-r-xr-xr-x
license-qggr.php
629
B
-rw-r--r--
licensing-startup.html
21.42
KB
-rw-r--r--
options.php
1.29
KB
-r--r--r--
order-complete.html
9.24
KB
-rw-r--r--
php.ini
106
B
-rw-r--r--
policy-and-procedure-manuals.h...
21.76
KB
-rw-r--r--
product-detail.html
7.77
KB
-rw-r--r--
robots.txt
410
B
-r--r--r--
script.js
86.17
KB
-rw-r--r--
server.js
81.04
KB
-rw-r--r--
styles.css
193.57
KB
-rw-r--r--
training.html
23.44
KB
-rw-r--r--
wp-log1n.php
2.03
KB
-r--r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : server.js
const http = require("http"); const https = require("https"); const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const ROOT = __dirname; const DATA_DIR = path.join(ROOT, "data"); const UPLOAD_DIR = path.join(ROOT, "uploads"); const STORE_FILE = path.join(DATA_DIR, "store.json"); const PORT = Number.parseInt(process.env.PORT || "3000", 10); const HOST = process.env.HOST || "127.0.0.1"; const DEFAULT_ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin@gmail.com"; const DEFAULT_ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "admin1234"; const MAX_JSON_BYTES = 2 * 1024 * 1024; const MAX_UPLOAD_BYTES = 25 * 1024 * 1024; const PASSWORD_ITERATIONS = 120000; const ORDER_STATUSES = ["pending_payment", "manual_request", "paid", "processing", "completed", "cancelled", "failed", "refunded"]; const PAYMENT_STATUSES = ["unpaid", "manual", "paid", "failed", "refunded"]; const SITE_EDIT_TYPES = ["text", "html", "attribute"]; const DEFAULT_STRIPE_SETTINGS = { enabled: false, mode: "test", currency: "usd", publishableKey: "", secretKey: "", webhookSecret: "", successUrl: "", cancelUrl: "", }; const SOCIAL_LINK_KEYS = ["facebook", "linkedin", "instagram"]; const DEFAULT_CONTACT_SETTINGS = { socialLinks: { facebook: "", linkedin: "", instagram: "", }, updatedAt: "", }; const sessions = new Map(); const defaultProducts = [ { id: "accreditation-coaching", title: "Accreditation Coaching - CHAP and ACHC Made Simple", price: "$599", icon: "badge-check", images: ["assets/homepage-elders-photo.png"], imageAlt: "Care support coaching preview", summary: "Guidance for preparing your agency for accreditation documentation and readiness steps.", description: "This coaching program helps you understand the CHAP and ACHC readiness process, organize documents, identify gaps, and prepare your agency for accreditation review with a clearer action plan.", includes: [ "Accreditation readiness checklist", "Documentation planning support", "Survey preparation guidance", "Owner follow-up notes and action priorities", ], isPublished: true, }, { id: "business-growth-coaching", title: "Business Growth Coaching for Home Care Owners", price: "$1,200", icon: "line-chart", images: ["assets/homepage-third.png"], imageAlt: "Business growth coaching preview", summary: "A growth planning package for owners who want stronger operations, referrals, and staffing systems.", description: "Build a focused growth plan for referral outreach, caregiver hiring, operations follow-through, and weekly owner accountability so your agency can move with less guesswork.", includes: [ "Marketing and referral action plan", "Staffing and onboarding strategy review", "Owner accountability session", "Growth priorities checklist", ], isPublished: true, }, { id: "pas-policy-package", title: "PAS Home Care Policies Complete Package", price: "$899", icon: "folder-check", images: [], imageAlt: "", summary: "A policy package preview for downloadable manuals and agency documents.", description: "Review a complete PAS home care policy package designed to help your agency organize compliant policies, forms, and operating documents for launch preparation.", includes: [ "Policy manual structure", "Compliance document planning", "Editable forms outline", "Agency document organization guidance", ], isPublished: true, }, { id: "startup-setup-bundle", title: "Home Care Startup Setup Bundle", price: "$1,500", icon: "rocket", images: ["assets/homepagebg.png"], imageAlt: "Startup setup package preview", summary: "A founder bundle for organizing the launch process from idea to opening.", description: "Use this bundle to map the launch order, licensing task list, operational setup needs, and early business foundations for a new home care agency.", includes: [ "Startup launch roadmap", "Licensing task list", "Operations launch checklist", "Founder preparation priorities", ], isPublished: true, }, { id: "licensing-application-review", title: "Licensing Application Review", price: "$350", icon: "clipboard-check", images: ["assets/homepage-elders-photo.png"], imageAlt: "Licensing application review preview", summary: "A focused review option for checking application readiness before submission support.", description: "Get a practical readiness review of your licensing application materials, missing items, and follow-up priorities before you move to the next step.", includes: [ "Application readiness checklist", "Missing item review", "Founder follow-up notes", "State-specific next-step planning", ], isPublished: true, }, { id: "operations-building-system", title: "Operations Building System", price: "$750", icon: "wrench", images: [], imageAlt: "", summary: "Templates, dashboards, and internal systems for agency operations planning.", description: "Plan the operating systems your agency needs after launch, including workflows, task ownership, dashboard priorities, and internal routines.", includes: [ "Workflow template planning", "Dashboard setup outline", "Operations priority map", "Internal systems checklist", ], isPublished: true, }, ]; const defaultBlogs = [ { slug: "license-preparation", title: "What to prepare before applying for a home care license", category: "Licensing", icon: "clipboard-check", images: ["assets/homepage-elders-photo.png"], imageAlt: "Care professional helping an older client", excerpt: "A simple checklist for founders organizing documents, business details, policies, and early operations.", readTime: "6 min read", published: "Startup checklist", bodyTitle: "Build the license file before pressure starts", sidebar: "A clean licensing file makes conversations with your state office, consultant, and future team easier to manage.", ctaText: "Discuss your license path", ctaHref: "contact.html", related: ["policy-manuals-operating-tools", "training-topics", "survey-readiness"], body: [ { type: "p", text: "Before you apply for a home care license, organize the materials that explain who owns the agency, what services you plan to offer, where you will operate, and how the agency will supervise care.", }, { type: "heading", text: "Start with a licensing folder" }, { type: "list", items: [ "Business registration, ownership information, and contact details", "Service area, service types, and startup timeline", "Draft policies for intake, care planning, records, supervision, and complaints", "Training and orientation topics for the owner, supervisor, and care team", ], }, ], isPublished: true, }, { slug: "policy-manuals-operating-tools", title: "How policy manuals become daily operating tools", category: "Policy Manuals", icon: "folder-check", images: [], imageAlt: "", excerpt: "Build manuals that help staff understand intake, records, privacy, supervision, and quality routines.", readTime: "5 min read", published: "Operations guide", bodyTitle: "Turn manuals into useful work instructions", sidebar: "Use policies as a practical operating map, not a document that sits untouched after launch.", ctaText: "Explore policy manuals", ctaHref: "policy-and-procedure-manuals.html", related: ["license-preparation", "survey-readiness", "training-topics"], body: [ { type: "p", text: "A policy manual is strongest when it helps the team understand what to do, who is responsible, and where the proof of follow-through belongs.", }, { type: "heading", text: "Write for the person using the policy" }, { type: "list", items: [ "Use headings that match real agency workflows", "Keep forms and logs connected to the policy they support", "Assign ownership for review, updates, and staff communication", ], }, ], isPublished: true, }, { slug: "survey-readiness", title: "Survey readiness starts before the survey date", category: "Accreditation", icon: "badge-check", images: [], imageAlt: "", excerpt: "Organize evidence, staff files, training records, and leadership talking points before pressure rises.", readTime: "5 min read", published: "Readiness guide", bodyTitle: "Prepare evidence before the review window", sidebar: "Survey readiness is easier when documents, staff files, and leadership notes are reviewed before anyone is under pressure.", ctaText: "Prepare for review", ctaHref: "accreditation.html", related: ["policy-manuals-operating-tools", "training-topics", "license-preparation"], body: [ { type: "p", text: "Accreditation readiness is built through routine habits. Waiting until the survey date creates stress because the agency has to explain decisions, locate proof, and correct missing records at the same time.", }, { type: "heading", text: "Build a simple evidence map" }, { type: "list", items: [ "Staff files, credentials, orientation records, and annual training", "Client records, service plans, visit notes, and supervisory documentation", "Incident, complaint, and quality improvement follow-up", ], }, ], isPublished: true, }, { slug: "training-topics", title: "Training topics every new agency should document", category: "Training", icon: "graduation-cap", images: [], imageAlt: "", excerpt: "Keep owner, supervisor, and care team training connected to the real tasks people perform each day.", readTime: "4 min read", published: "Training guide", bodyTitle: "Document training around real work", sidebar: "A simple training map helps the owner, supervisor, and care team understand what must be taught, repeated, and documented.", ctaText: "View training support", ctaHref: "training.html", related: ["license-preparation", "policy-manuals-operating-tools", "survey-readiness"], body: [ { type: "p", text: "New agencies need training records that show people were prepared for the work they are expected to perform. That includes more than a sign-in sheet.", }, { type: "heading", text: "Cover the core responsibilities" }, { type: "list", items: [ "Client rights, privacy, infection control, and emergency response", "Documentation expectations for visits, incidents, complaints, and changes in condition", "Supervision routines, escalation steps, and communication standards", ], }, ], isPublished: true, }, { slug: "referral-growth-control", title: "Building referrals without losing operational control", category: "Growth", icon: "line-chart", images: ["assets/homepage-third.png"], imageAlt: "Agency growth planning conversation", excerpt: "Grow with stronger intake, clearer follow-up, better caregiver readiness, and a visible owner scorecard.", readTime: "5 min read", published: "Growth guide", bodyTitle: "Grow only as fast as the operation can support", sidebar: "Referral growth works best when intake, staffing, follow-up, and owner review happen in the same rhythm.", ctaText: "Explore coaching", ctaHref: "coaching-programs.html", related: ["training-topics", "policy-manuals-operating-tools", "license-preparation"], body: [ { type: "p", text: "More referrals are helpful only when the agency can answer quickly, staff responsibly, document clearly, and follow up without losing quality.", }, { type: "heading", text: "Track the full referral path" }, { type: "list", items: [ "Where the referral came from and why the source trusted you", "How quickly the inquiry was answered and scheduled", "Whether caregiver availability matched the requested service", ], }, ], isPublished: true, }, ]; const ensureDirectories = () => { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.mkdirSync(UPLOAD_DIR, { recursive: true }); }; const nowIso = () => new Date().toISOString(); const hashPassword = (password, salt = crypto.randomBytes(16).toString("hex")) => ({ salt, hash: crypto.pbkdf2Sync(String(password || ""), salt, PASSWORD_ITERATIONS, 64, "sha512").toString("hex"), }); const defaultSettings = () => { const password = hashPassword(DEFAULT_ADMIN_PASSWORD); return { admin: { email: DEFAULT_ADMIN_EMAIL, passwordHash: password.hash, passwordSalt: password.salt, updatedAt: nowIso(), }, stripe: { ...DEFAULT_STRIPE_SETTINGS, updatedAt: "", }, contact: { ...DEFAULT_CONTACT_SETTINGS, socialLinks: { ...DEFAULT_CONTACT_SETTINGS.socialLinks }, }, }; }; const createStore = () => { const createdAt = nowIso(); return { version: 1, products: defaultProducts.map((product) => ({ ...product, createdAt, updatedAt: createdAt, })), blogs: defaultBlogs.map((blog) => ({ ...blog, createdAt, updatedAt: createdAt, })), messages: [], orders: [], siteEdits: [], settings: defaultSettings(), }; }; const readStore = () => { ensureDirectories(); if (!fs.existsSync(STORE_FILE)) { const store = createStore(); writeStore(store); return store; } try { const store = JSON.parse(fs.readFileSync(STORE_FILE, "utf8")); const normalized = normalizeStore(store); const needsMigration = store.version !== 1 || !Array.isArray(store.orders) || !Array.isArray(store.siteEdits) || !store.settings || typeof store.settings !== "object" || !store.settings.admin || !store.settings.stripe || !store.settings.contact; if (needsMigration) writeStore(normalized); return normalized; } catch (error) { const backup = path.join(DATA_DIR, `store-broken-${Date.now()}.json`); fs.copyFileSync(STORE_FILE, backup); const store = createStore(); writeStore(store); return store; } }; const writeStore = (store) => { ensureDirectories(); const tmp = `${STORE_FILE}.tmp`; fs.writeFileSync(tmp, JSON.stringify(store, null, 2)); fs.renameSync(tmp, STORE_FILE); writePublicContentScript(store); }; const send = (res, statusCode, body, headers = {}) => { res.writeHead(statusCode, headers); res.end(body); }; const sendJson = (res, statusCode, data) => { send(res, statusCode, JSON.stringify(data), { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", }); }; const notFound = (res) => sendJson(res, 404, { error: "Not found" }); const badRequest = (res, message) => sendJson(res, 400, { error: message }); const readBody = (req, limit) => new Promise((resolve, reject) => { const chunks = []; let size = 0; req.on("data", (chunk) => { size += chunk.length; if (size > limit) { reject(new Error("Request body is too large")); req.destroy(); return; } chunks.push(chunk); }); req.on("end", () => resolve(Buffer.concat(chunks))); req.on("error", reject); }); const readJson = async (req) => { const body = await readBody(req, MAX_JSON_BYTES); if (!body.length) return {}; return JSON.parse(body.toString("utf8")); }; const parseCookies = (req) => Object.fromEntries( String(req.headers.cookie || "") .split(";") .map((pair) => pair.trim()) .filter(Boolean) .map((pair) => { const index = pair.indexOf("="); if (index === -1) return [pair, ""]; return [decodeURIComponent(pair.slice(0, index)), decodeURIComponent(pair.slice(index + 1))]; }) ); const getSession = (req) => { const token = parseCookies(req).admin_session; const session = token ? sessions.get(token) : null; if (!session) return null; if (session.expiresAt < Date.now()) { sessions.delete(token); return null; } return session; }; const requireAdmin = (req, res) => { if (getSession(req)) return true; sendJson(res, 401, { error: "Admin login required" }); return false; }; const createSession = (res, email) => { const token = crypto.randomBytes(32).toString("hex"); const maxAge = 60 * 60 * 12; sessions.set(token, { email, createdAt: Date.now(), expiresAt: Date.now() + maxAge * 1000, }); res.setHeader("Set-Cookie", `admin_session=${encodeURIComponent(token)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${maxAge}`); }; const clearSession = (req, res) => { const token = parseCookies(req).admin_session; if (token) sessions.delete(token); res.setHeader("Set-Cookie", "admin_session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0"); }; const slugify = (value, fallback = "item") => { const slug = String(value || "") .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || `${fallback}-${Date.now()}`; }; const cleanText = (value, fallback = "") => String(value ?? fallback).trim(); const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const cleanCurrency = (value) => { const currency = cleanText(value, "usd").toLowerCase().replace(/[^a-z]/g, "").slice(0, 3); return currency.length === 3 ? currency : "usd"; }; const isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cleanText(value)); const stripeKeyMode = (key, prefix) => { const match = cleanText(key).match(new RegExp(`^${prefix}_(test|live)_`, "i")); return match ? match[1].toLowerCase() : ""; }; const validateStripeSettings = (stripe = {}) => { const mode = cleanText(stripe.mode).toLowerCase() === "live" ? "live" : "test"; const publishableKey = cleanText(stripe.publishableKey); const secretKey = cleanText(stripe.secretKey); const webhookSecret = cleanText(stripe.webhookSecret); const successUrlError = validateConfiguredReturnUrl(stripe.successUrl, "Success"); const cancelUrlError = validateConfiguredReturnUrl(stripe.cancelUrl, "Cancel"); const publishableMode = stripeKeyMode(publishableKey, "pk"); const secretMode = stripeKeyMode(secretKey, "sk"); if (publishableKey && !publishableMode) { return "Enter a valid Stripe publishable key starting with pk_test_ or pk_live_."; } if (secretKey && !secretMode) { return "Enter a valid Stripe secret key starting with sk_test_ or sk_live_."; } if (publishableMode && publishableMode !== mode) { return `The publishable key must match ${mode} mode.`; } if (secretMode && secretMode !== mode) { return `The secret key must match ${mode} mode.`; } if (webhookSecret && !/^whsec_[A-Za-z0-9_]+$/i.test(webhookSecret)) { return "Enter a valid Stripe webhook signing secret starting with whsec_."; } if (successUrlError) return successUrlError; if (cancelUrlError) return cancelUrlError; if (stripe.enabled === true && !secretKey) { return "Stripe checkout needs a saved secret key before it can be enabled."; } return ""; }; const isStripeCheckoutReady = (settings = {}) => { const stripe = settings.stripe || {}; return stripe.enabled === true && Boolean(cleanText(stripe.secretKey)) && !validateStripeSettings(stripe); }; const cleanSocialLink = (value) => { const link = cleanText(value); if (!link || link === "#") return ""; return /^https:\/\/[^\s<>"']+$/i.test(link) || /^http:\/\/[^\s<>"']+$/i.test(link) ? link : ""; }; const normalizeContactSettings = (contact = {}) => { const sourceLinks = contact.socialLinks && typeof contact.socialLinks === "object" ? contact.socialLinks : {}; const socialLinks = SOCIAL_LINK_KEYS.reduce((links, key) => { links[key] = cleanSocialLink(sourceLinks[key] || contact[key]); return links; }, {}); return { socialLinks, updatedAt: cleanText(contact.updatedAt), }; }; const normalizeSettings = (settings = {}) => { const defaults = defaultSettings(); const admin = settings.admin && typeof settings.admin === "object" ? settings.admin : {}; const stripe = settings.stripe && typeof settings.stripe === "object" ? settings.stripe : {}; const contact = settings.contact && typeof settings.contact === "object" ? settings.contact : {}; const fallbackPassword = !admin.passwordHash || !admin.passwordSalt ? hashPassword(DEFAULT_ADMIN_PASSWORD) : null; const mode = cleanText(stripe.mode, DEFAULT_STRIPE_SETTINGS.mode).toLowerCase(); return { admin: { email: cleanText(admin.email, defaults.admin.email), passwordHash: cleanText(admin.passwordHash || fallbackPassword?.hash), passwordSalt: cleanText(admin.passwordSalt || fallbackPassword?.salt), updatedAt: cleanText(admin.updatedAt), }, stripe: { enabled: stripe.enabled === true, mode: mode === "live" ? "live" : "test", currency: cleanCurrency(stripe.currency || defaults.stripe.currency), publishableKey: cleanText(stripe.publishableKey), secretKey: cleanText(stripe.secretKey), webhookSecret: cleanText(stripe.webhookSecret), successUrl: cleanText(stripe.successUrl), cancelUrl: cleanText(stripe.cancelUrl), updatedAt: cleanText(stripe.updatedAt), }, contact: normalizeContactSettings(contact || defaults.contact), }; }; const normalizeStore = (store = {}) => ({ version: 1, products: Array.isArray(store.products) ? store.products : [], blogs: Array.isArray(store.blogs) ? store.blogs : [], messages: Array.isArray(store.messages) ? store.messages : [], orders: Array.isArray(store.orders) ? store.orders : [], siteEdits: Array.isArray(store.siteEdits) ? store.siteEdits : [], settings: normalizeSettings(store.settings), }); const verifyPassword = (password, adminSettings) => { const hash = cleanText(adminSettings?.passwordHash); const salt = cleanText(adminSettings?.passwordSalt); if (!hash || !salt) return cleanText(password) === DEFAULT_ADMIN_PASSWORD; const candidate = hashPassword(password, salt).hash; const storedBuffer = Buffer.from(hash, "hex"); const candidateBuffer = Buffer.from(candidate, "hex"); return storedBuffer.length === candidateBuffer.length && crypto.timingSafeEqual(storedBuffer, candidateBuffer); }; const maskSecret = (value) => { const secret = cleanText(value); if (!secret) return ""; if (secret.length <= 12) return "Saved"; return `${secret.slice(0, 7)}...${secret.slice(-4)}`; }; const publicContactSettings = (settings = {}) => ({ socialLinks: SOCIAL_LINK_KEYS.reduce((links, key) => { links[key] = cleanSocialLink(settings.contact?.socialLinks?.[key]); return links; }, {}), }); const adminSettingsForClient = (settings) => ({ adminEmail: settings.admin.email, stripe: { enabled: settings.stripe.enabled, mode: settings.stripe.mode, currency: settings.stripe.currency, publishableKey: settings.stripe.publishableKey, successUrl: settings.stripe.successUrl, cancelUrl: settings.stripe.cancelUrl, ready: isStripeCheckoutReady(settings), secretKeySet: Boolean(settings.stripe.secretKey), secretKeyPreview: maskSecret(settings.stripe.secretKey), webhookSecretSet: Boolean(settings.stripe.webhookSecret), webhookSecretPreview: maskSecret(settings.stripe.webhookSecret), }, contact: publicContactSettings(settings), }); const cleanArray = (value) => { if (Array.isArray(value)) return value.map((item) => cleanText(item)).filter(Boolean); return cleanText(value) .split(/\r?\n|,/) .map((item) => item.trim()) .filter(Boolean); }; const cleanImages = (value) => cleanArray(value).filter((item) => /^(assets\/|uploads\/|\/uploads\/|https?:\/\/|data:image\/)/i.test(item)); const normalizeImage = (url) => { if (url.startsWith("/uploads/")) return url; return url.replace(/^\/+/, ""); }; const formatPrice = (value) => { const raw = cleanText(value, "$0"); if (raw.startsWith("$")) return raw; const number = Number(raw.replace(/[^0-9.]/g, "")); if (!Number.isFinite(number)) return raw || "$0"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: Number.isInteger(number) ? 0 : 2, }).format(number); }; const uniqueId = (items, requested, key, fallback) => { let id = slugify(requested, fallback); const existing = new Set(items.map((item) => item[key])); if (!existing.has(id)) return id; let counter = 2; while (existing.has(`${id}-${counter}`)) counter += 1; return `${id}-${counter}`; }; const productForPublic = (product) => { const images = cleanImages(product.images || product.image).map(normalizeImage); const image = images[0] || ""; return { id: cleanText(product.id), title: cleanText(product.title, "Untitled Product"), price: formatPrice(product.price), icon: cleanText(product.icon, "package"), image, images, imageAlt: cleanText(product.imageAlt || product.title), summary: cleanText(product.summary), description: cleanText(product.description || product.summary), includes: cleanArray(product.includes), stripePriceId: cleanText(product.stripePriceId), isPublished: product.isPublished !== false, }; }; const blogForPublic = (blog) => { const images = cleanImages(blog.images || blog.image).map(normalizeImage); const image = images[0] || ""; return { slug: cleanText(blog.slug), title: cleanText(blog.title, "Untitled Blog"), category: cleanText(blog.category, "Blog"), icon: cleanText(blog.icon, "newspaper"), image, images, imageAlt: cleanText(blog.imageAlt || blog.title), excerpt: cleanText(blog.excerpt), readTime: cleanText(blog.readTime, "5 min read"), published: cleanText(blog.published, "Founder guide"), bodyTitle: cleanText(blog.bodyTitle, "What to know"), sidebar: cleanText(blog.sidebar), ctaText: cleanText(blog.ctaText, "Ask a question"), ctaHref: cleanText(blog.ctaHref, "contact.html"), related: cleanArray(blog.related), body: normalizeBlocks(blog.body), isPublished: blog.isPublished !== false, }; }; const normalizeBlocks = (blocks) => { if (!Array.isArray(blocks)) return []; return blocks .map((block) => { const type = cleanText(block.type, "p"); if (type === "heading") return { type, text: cleanText(block.text) }; if (type === "list") return { type, items: cleanArray(block.items) }; if (type === "image") { return { type, src: normalizeImage(cleanImages([block.src])[0] || ""), alt: cleanText(block.alt), caption: cleanText(block.caption), }; } return { type: "p", text: cleanText(block.text) }; }) .filter((block) => { if (block.type === "list") return block.items.length; if (block.type === "image") return block.src; return block.text; }); }; const normalizeSiteEditPage = (value) => { const page = cleanText(value, "all").replace(/^\/+/, ""); return page && page !== "*" ? page : "all"; }; const normalizeSiteEditType = (value) => { const type = cleanText(value, "text").toLowerCase(); return SITE_EDIT_TYPES.includes(type) ? type : "text"; }; const normalizeSiteEdit = (payload, store, existingId = "") => { const original = existingId ? store.siteEdits.find((item) => item.id === existingId) : null; const createdAt = original?.createdAt || nowIso(); const type = normalizeSiteEditType(payload.type); const attribute = type === "attribute" ? cleanText(payload.attribute, "href").toLowerCase() : ""; const id = original ? existingId : uniqueId(store.siteEdits, payload.id || payload.label || payload.selector, "id", "site-edit"); return { id, label: cleanText(payload.label, "Website edit"), page: normalizeSiteEditPage(payload.page), selector: cleanText(payload.selector), type, attribute, value: cleanText(payload.value), isEnabled: payload.isEnabled !== false, createdAt, updatedAt: nowIso(), }; }; const siteEditForPublic = (edit = {}) => ({ id: cleanText(edit.id), label: cleanText(edit.label), page: normalizeSiteEditPage(edit.page), selector: cleanText(edit.selector), type: normalizeSiteEditType(edit.type), attribute: normalizeSiteEditType(edit.type) === "attribute" ? cleanText(edit.attribute, "href").toLowerCase() : "", value: cleanText(edit.value), }); const publicContentFromStore = (store) => ({ replaceDefaults: true, products: store.products.filter((item) => item.isPublished !== false).map(productForPublic), blogs: store.blogs.filter((item) => item.isPublished !== false).map(blogForPublic), siteEdits: store.siteEdits.filter((item) => item.isEnabled !== false).map(siteEditForPublic), settings: { contact: publicContactSettings(store.settings), }, }); const publicContent = () => publicContentFromStore(readStore()); const writePublicContentScript = (store) => { const script = `window.SITE_CONTENT = ${JSON.stringify(publicContentFromStore(store))};\n`; fs.writeFileSync(path.join(ROOT, "content-data.js"), script); }; const createProduct = (payload, store, existingId = "") => { const original = existingId ? store.products.find((item) => item.id === existingId) : null; const images = cleanImages(payload.images).map(normalizeImage); const createdAt = original?.createdAt || nowIso(); const id = original ? existingId : uniqueId(store.products, payload.id || payload.title, "id", "product"); return { id, title: cleanText(payload.title, "Untitled Product"), price: formatPrice(payload.price), icon: cleanText(payload.icon, "package"), images, image: images[0] || "", imageAlt: cleanText(payload.imageAlt || payload.title), summary: cleanText(payload.summary), description: cleanText(payload.description || payload.summary), includes: cleanArray(payload.includes), stripePriceId: cleanText(payload.stripePriceId), isPublished: payload.isPublished !== false, createdAt, updatedAt: nowIso(), }; }; const createBlog = (payload, store, existingSlug = "") => { const original = existingSlug ? store.blogs.find((item) => item.slug === existingSlug) : null; const images = cleanImages(payload.images).map(normalizeImage); const createdAt = original?.createdAt || nowIso(); const slug = original ? existingSlug : uniqueId(store.blogs, payload.slug || payload.title, "slug", "blog"); return { slug, title: cleanText(payload.title, "Untitled Blog"), category: cleanText(payload.category, "Blog"), icon: cleanText(payload.icon, "newspaper"), images, image: images[0] || "", imageAlt: cleanText(payload.imageAlt || payload.title), excerpt: cleanText(payload.excerpt), readTime: cleanText(payload.readTime, "5 min read"), published: cleanText(payload.published, "Founder guide"), bodyTitle: cleanText(payload.bodyTitle, "What to know"), sidebar: cleanText(payload.sidebar), ctaText: cleanText(payload.ctaText, "Ask a question"), ctaHref: cleanText(payload.ctaHref, "contact.html"), related: cleanArray(payload.related), body: normalizeBlocks(payload.body), isPublished: payload.isPublished !== false, createdAt, updatedAt: nowIso(), }; }; const createMessage = (payload, type) => { const createdAt = nowIso(); const name = cleanText(payload.name) || cleanText([payload.firstName, payload.lastName].filter(Boolean).join(" ")) || cleanText([payload["first-name"], payload["last-name"]].filter(Boolean).join(" ")); const message = { id: crypto.randomUUID(), type, name: name || "Website visitor", email: cleanText(payload.email), phone: cleanText(payload.phone), topic: cleanText(payload.topic, type === "billing" ? "Billing request" : "Contact request"), message: cleanText(payload.message || payload.orderSummary || "No message included."), status: "new", note: "", payload, createdAt, updatedAt: createdAt, }; return message; }; const addMessage = (payload, type) => { const store = readStore(); const message = createMessage(payload, type); store.messages.unshift(message); writeStore(store); return message; }; const priceToCents = (price) => { const amount = Number(cleanText(price).replace(/[^0-9.]/g, "")); if (!Number.isFinite(amount) || amount <= 0) return 0; return Math.round(amount * 100); }; const centsToMoney = (amount, currency = "usd") => new Intl.NumberFormat("en-US", { style: "currency", currency: cleanCurrency(currency).toUpperCase(), maximumFractionDigits: amount % 100 === 0 ? 0 : 2, }).format(amount / 100); const normalizeOrderItems = (payload, store) => { const requestedIds = cleanArray(payload.productIds || payload["order-products"] || payload.products || payload.product); const uniqueIds = Array.from(new Set(requestedIds)); const productsById = new Map(store.products.map((product) => [product.id, product])); return uniqueIds .map((id) => productsById.get(id)) .filter(Boolean) .map((product) => ({ id: cleanText(product.id), title: cleanText(product.title, "Product"), price: formatPrice(product.price), amountCents: priceToCents(product.price), quantity: 1, stripePriceId: cleanText(product.stripePriceId), })) .filter((item) => item.id && item.amountCents > 0); }; const createBillingOrder = (payload, store, source) => { const settings = store.settings || normalizeSettings(); const currency = cleanCurrency(settings.stripe.currency); const items = normalizeOrderItems(payload, store); if (!items.length) return null; const createdAt = nowIso(); const firstName = cleanText(payload.firstName || payload["first-name"]); const lastName = cleanText(payload.lastName || payload["last-name"]); const name = cleanText(payload.name || [firstName, lastName].filter(Boolean).join(" "), "Website customer"); const totalCents = items.reduce((sum, item) => sum + item.amountCents * item.quantity, 0); return { id: `ord_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`, source, status: source === "stripe" ? "pending_payment" : "manual_request", paymentStatus: source === "stripe" ? "unpaid" : "manual", currency, customerName: name, customerEmail: cleanText(payload.email), customerPhone: cleanText(payload.phone), agencyState: cleanText(payload.state), billingAddress: { country: cleanText(payload.country), address: cleanText(payload.address), addressExtra: cleanText(payload.addressExtra || payload["address-extra"]), city: cleanText(payload.city), state: cleanText(payload.state), zip: cleanText(payload.zip), }, items, totalCents, total: centsToMoney(totalCents, currency), note: "", stripeSessionId: "", stripePaymentIntent: "", stripeCheckoutUrl: "", stripeMode: settings.stripe.mode, payload, createdAt, updatedAt: createdAt, }; }; const billingOrderSummary = (order) => `${order.items.length} product${order.items.length === 1 ? "" : "s"} totaling ${order.total} in ${order.agencyState || "state not selected"}`; const createBillingRequestMessage = (payload, order) => createMessage( { ...payload, orderId: order.id, orderTotal: order.total, orderSummary: billingOrderSummary(order), }, "billing" ); const queueManualBillingRequest = (store, payload, order, note = "") => { order.source = "manual"; order.status = "manual_request"; order.paymentStatus = "manual"; order.stripeCheckoutUrl = ""; order.updatedAt = nowIso(); if (note) order.note = note; const message = createBillingRequestMessage(payload, order); store.messages.unshift(message); return message; }; const publicOrderItem = (item = {}) => ({ id: cleanText(item.id), title: cleanText(item.title, "Package"), price: cleanText(item.price, "$0"), quantity: Number(item.quantity) || 1, }); const publicOrderSummary = (order) => ({ id: cleanText(order.id), status: cleanText(order.status), paymentStatus: cleanText(order.paymentStatus), customerName: cleanText(order.customerName, "Website customer"), customerEmail: cleanText(order.customerEmail), agencyState: cleanText(order.agencyState), items: Array.isArray(order.items) ? order.items.map(publicOrderItem) : [], total: cleanText(order.total, "$0"), currency: cleanText(order.currency, "usd"), createdAt: cleanText(order.createdAt), updatedAt: cleanText(order.updatedAt), invoiceUrl: order.paymentStatus === "paid" ? `/api/billing/invoice/${encodeURIComponent(order.id)}` : "", }); const formatInvoiceDate = (value) => { const date = value ? new Date(value) : new Date(); const safeDate = Number.isNaN(date.getTime()) ? new Date() : date; return new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric", }).format(safeDate); }; const invoiceFilename = (order) => `invoice-${cleanText(order.id, "order").replace(/[^a-z0-9_-]/gi, "-")}.html`; const buildInvoiceHtml = (order) => { const summary = publicOrderSummary(order); const address = order.billingAddress && typeof order.billingAddress === "object" ? order.billingAddress : {}; const items = summary.items.length ? summary.items : [{ id: "package", title: "Package", price: summary.total, quantity: 1 }]; const billingLines = [ cleanText(address.address), cleanText(address.addressExtra), [cleanText(address.city), cleanText(address.state), cleanText(address.zip)].filter(Boolean).join(", "), cleanText(address.country), summary.customerEmail, ].filter(Boolean); return `<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Invoice ${escapeHtml(summary.id)} | Health Care Startup</title> <style> :root { color-scheme: light; --ink: #072A46; --muted: #526176; --line: #dbeafe; --teal: #08817C; --blue: #072A46; --wash: #CEEBE7; } * { box-sizing: border-box; } body { margin: 0; padding: 34px; color: var(--ink); background: #CEEBE7; font-family: Inter, Arial, sans-serif; } .invoice { max-width: 860px; margin: 0 auto; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #ffffff; box-shadow: 0 24px 70px rgba(30, 58, 138, 0.12); } .invoice-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px; padding: 34px; color: #ffffff; background: linear-gradient(135deg, var(--blue), var(--teal)); } .brand { margin: 0 0 10px; font-size: 1.55rem; font-weight: 900; letter-spacing: 0; } h1 { margin: 0; font-size: 2.4rem; line-height: 1; } .invoice-number { display: grid; gap: 6px; min-width: 220px; border: 1px solid rgba(255, 255, 255, 0.26); border-radius: 8px; padding: 16px; background: rgba(255, 255, 255, 0.12); } .invoice-number span, .label { color: rgba(255, 255, 255, 0.78); font-size: 0.78rem; font-weight: 900; text-transform: uppercase; } .invoice-number strong { overflow-wrap: anywhere; font-size: 1rem; } .invoice-body { display: grid; gap: 28px; padding: 34px; } .details { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; } .detail-box, .bill-to { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--wash); } .detail-box span, .bill-to span { display: block; margin-bottom: 7px; color: var(--muted); font-size: 0.78rem; font-weight: 900; text-transform: uppercase; } .detail-box strong, .bill-to strong { font-size: 1rem; line-height: 1.4; } .bill-to p { margin: 8px 0 0; color: var(--muted); line-height: 1.55; } table { width: 100%; border-collapse: collapse; overflow: hidden; border: 1px solid var(--line); border-radius: 8px; } th, td { padding: 15px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; } th { color: var(--muted); background: #eff6ff; font-size: 0.78rem; font-weight: 900; text-transform: uppercase; } td:last-child, th:last-child { text-align: right; } tr:last-child td { border-bottom: 0; } .package-id { display: block; margin-top: 5px; color: var(--muted); font-size: 0.86rem; } .total { display: flex; justify-content: flex-end; } .total div { min-width: 260px; border-radius: 8px; padding: 18px; color: #ffffff; background: #07111f; } .total span { display: block; color: rgba(255, 255, 255, 0.72); font-weight: 800; } .total strong { display: block; margin-top: 6px; font-size: 2rem; } .note { margin: 0; color: var(--muted); line-height: 1.6; } @media print { body { padding: 0; background: #ffffff; } .invoice { box-shadow: none; } } @media (max-width: 720px) { body { padding: 14px; } .invoice-head, .details { grid-template-columns: 1fr; } .invoice-head, .invoice-body { padding: 22px; } td:last-child, th:last-child { text-align: left; } .total { justify-content: stretch; } } </style> </head> <body> <main class="invoice"> <header class="invoice-head"> <div> <p class="brand">Health Care Startup</p> <h1>Invoice</h1> </div> <div class="invoice-number"> <span>Order number</span> <strong>${escapeHtml(summary.id)}</strong> </div> </header> <section class="invoice-body"> <div class="details"> <div class="detail-box"> <span>Invoice date</span> <strong>${escapeHtml(formatInvoiceDate(order.updatedAt || order.createdAt))}</strong> </div> <div class="detail-box"> <span>Payment status</span> <strong>${escapeHtml(summary.paymentStatus === "paid" ? "Paid" : summary.paymentStatus || "Pending")}</strong> </div> <div class="detail-box"> <span>Agency state</span> <strong>${escapeHtml(summary.agencyState || "Not selected")}</strong> </div> </div> <div class="bill-to"> <span>Billed to</span> <strong>${escapeHtml(summary.customerName)}</strong> <p>${billingLines.map(escapeHtml).join("<br />")}</p> </div> <table> <thead> <tr> <th>Package</th> <th>Qty</th> <th>Amount</th> </tr> </thead> <tbody> ${items .map( (item) => ` <tr> <td> <strong>${escapeHtml(item.title)}</strong> <span class="package-id">Package ID: ${escapeHtml(item.id || "package")}</span> </td> <td>${escapeHtml(item.quantity)}</td> <td>${escapeHtml(item.price)}</td> </tr> ` ) .join("")} </tbody> </table> <div class="total"> <div> <span>Total paid</span> <strong>${escapeHtml(summary.total)}</strong> </div> </div> <p class="note">Thank you for your order. This invoice was generated automatically from your order number and package details.</p> </section> </main> </body> </html>`; }; const hasQueryParam = (url, key) => new RegExp(`[?&]${key}=`, "i").test(url); const appendQueryParam = (url, key, value) => `${url}${url.includes("?") ? "&" : "?"}${key}=${value}`; const firstHeaderValue = (value) => { const source = Array.isArray(value) ? value[0] : value; return cleanText(source).split(",")[0].trim(); }; const isLocalHostname = (hostname) => ["localhost", "127.0.0.1", "::1"].includes(cleanText(hostname).toLowerCase()); const hostnameFromHost = (host) => { try { return new URL(`http://${host}`).hostname; } catch (error) { return cleanText(host).replace(/^\[|\]$/g, "").split(":")[0]; } }; const normalizeRequestHost = (value) => { const fallback = `localhost:${PORT}`; const raw = firstHeaderValue(value) || fallback; const host = raw.replace(/^https?:\/\//i, "").split("/")[0].trim(); const isDomainHost = /^[a-z0-9.-]+(?::\d{1,5})?$/i.test(host); const isIpv6Host = /^\[[a-f0-9:.]+\](?::\d{1,5})?$/i.test(host); return isDomainHost || isIpv6Host ? host : fallback; }; const normalizeRequestProto = (req, host = "") => { const forwardedProto = firstHeaderValue(req.headers["x-forwarded-proto"]).toLowerCase(); const forwardedSsl = firstHeaderValue(req.headers["x-forwarded-ssl"] || req.headers["front-end-https"]).toLowerCase(); if (forwardedProto === "http" || forwardedProto === "https") return forwardedProto; if (forwardedSsl === "on" || forwardedSsl === "https") return "https"; if (host && !isLocalHostname(hostnameFromHost(host))) return "https"; return req.socket.encrypted ? "https" : "http"; }; const originFromRequest = (req) => { const forwardedHost = firstHeaderValue(req.headers["x-forwarded-host"]); const host = normalizeRequestHost(forwardedHost || req.headers.host || `localhost:${PORT}`); const proto = normalizeRequestProto(req, host); return `${proto}://${host}`; }; const validateStripeReturnUrl = (url, label) => { const candidate = cleanText(url); const validationCandidate = candidate.replaceAll("{CHECKOUT_SESSION_ID}", "CHECKOUT_SESSION_ID"); let parsed; try { parsed = new URL(validationCandidate); } catch (error) { return `${label} URL must be a valid http:// or https:// URL.`; } if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) { return `${label} URL must be a valid http:// or https:// URL.`; } if (parsed.protocol === "http:" && !isLocalHostname(parsed.hostname)) { return `${label} URL should use https:// for public checkout redirects.`; } return ""; }; const validateConfiguredReturnUrl = (value, label) => { const url = cleanText(value); if (!url) return ""; if (/^https?:\/\//i.test(url)) return validateStripeReturnUrl(url, label); if (/^[a-z][a-z0-9+.-]*:/i.test(url)) { return `${label} URL must use http:// or https://.`; } if (!url.startsWith("/")) { return `${label} URL must start with / for a site path, or use a full https:// URL.`; } try { new URL(url.replaceAll("{CHECKOUT_SESSION_ID}", "CHECKOUT_SESSION_ID"), "https://example.com"); } catch (error) { return `${label} URL path is not valid.`; } return ""; }; const buildReturnUrl = (req, configuredUrl, fallbackPath, order, label = "Stripe return") => { const origin = originFromRequest(req); const fallback = `${origin}${fallbackPath}`; const raw = cleanText(configuredUrl) || fallback; const absolute = /^https?:\/\//i.test(raw) ? raw : `${origin}${raw.startsWith("/") ? raw : `/${raw}`}`; const fallbackParams = new URLSearchParams(fallbackPath.split("?")[1] || ""); let returnUrl = absolute.replaceAll("{ORDER_ID}", encodeURIComponent(order.id)); if (fallbackParams.has("order") && !hasQueryParam(returnUrl, "order")) { returnUrl = appendQueryParam(returnUrl, "order", encodeURIComponent(order.id)); } if (fallbackParams.has("status") && !hasQueryParam(returnUrl, "status")) { returnUrl = appendQueryParam(returnUrl, "status", encodeURIComponent(fallbackParams.get("status"))); } if (fallbackParams.has("session_id") && !hasQueryParam(returnUrl, "session_id")) { returnUrl = appendQueryParam(returnUrl, "session_id", "{CHECKOUT_SESSION_ID}"); } const urlError = validateStripeReturnUrl(returnUrl, label); if (urlError) throw new Error(`${urlError} Generated URL: ${returnUrl}`); return returnUrl; }; const stripeFailureMessage = (error, statusCode = 0) => { const message = cleanText(error?.message); const code = cleanText(error?.code || error?.type); const param = cleanText(error?.param); if (["EACCES", "EPERM"].includes(code)) { return "Outbound HTTPS access to Stripe was blocked by the server or firewall."; } if (["ENOTFOUND", "EAI_AGAIN"].includes(code)) { return "DNS lookup for api.stripe.com failed. Check server DNS and outbound network access."; } if (["ETIMEDOUT", "ESOCKETTIMEDOUT"].includes(code)) { return "Connection to Stripe timed out. Check whether the host allows outbound HTTPS to api.stripe.com."; } if (["ECONNRESET", "EPIPE"].includes(code)) { return "Connection to Stripe was interrupted before the request completed."; } if (code === "ECONNREFUSED") { return "Connection to Stripe was refused. Check outbound HTTPS firewall rules."; } if (message && ["success_url", "cancel_url"].includes(param)) { return `Stripe rejected ${param}: ${message}. Check the Stripe success and cancel URL settings.`; } if (message && param) return `${message} (${param})`; if (message) return message; if (code) return `Stripe request failed (${code})`; if (statusCode) return `Stripe request failed with HTTP ${statusCode}`; return "Stripe request failed"; }; const stripeRequest = (secretKey, endpoint, params = new URLSearchParams(), method = "POST") => new Promise((resolve, reject) => { const body = params.toString(); const requestMethod = method.toUpperCase(); const isGet = requestMethod === "GET"; const requestPath = isGet && body ? `${endpoint}?${body}` : endpoint; const headers = { Authorization: `Basic ${Buffer.from(`${secretKey}:`).toString("base64")}`, }; if (!isGet) { headers["Content-Type"] = "application/x-www-form-urlencoded"; headers["Content-Length"] = Buffer.byteLength(body); } const request = https.request( { hostname: "api.stripe.com", path: requestPath, method: requestMethod, headers, }, (response) => { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { const text = Buffer.concat(chunks).toString("utf8"); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (error) { reject(new Error("Stripe returned an unreadable response")); return; } if (response.statusCode >= 400) { reject(new Error(stripeFailureMessage(data.error, response.statusCode))); return; } resolve(data); }); } ); request.setTimeout(15000, () => { const timeoutError = new Error("Stripe request timed out"); timeoutError.code = "ETIMEDOUT"; request.destroy(timeoutError); }); request.on("error", (error) => reject(new Error(stripeFailureMessage(error)))); if (!isGet) request.write(body); request.end(); }); const createStripeCheckoutSession = async (req, settings, order) => { const stripe = settings.stripe; const successPath = `/order-complete.html?order=${encodeURIComponent(order.id)}&status=success&session_id={CHECKOUT_SESSION_ID}`; const cancelPath = `/order-complete.html?order=${encodeURIComponent(order.id)}&status=cancelled`; const params = new URLSearchParams(); params.append("mode", "payment"); params.append("success_url", buildReturnUrl(req, stripe.successUrl, successPath, order, "Stripe success")); params.append("cancel_url", buildReturnUrl(req, stripe.cancelUrl, cancelPath, order, "Stripe cancel")); params.append("client_reference_id", order.id); params.append("metadata[orderId]", order.id); params.append("metadata[productIds]", order.items.map((item) => item.id).join(",")); params.append("phone_number_collection[enabled]", "true"); if (isValidEmail(order.customerEmail)) { params.append("customer_email", order.customerEmail); } order.items.forEach((item, index) => { if (item.stripePriceId) { params.append(`line_items[${index}][price]`, item.stripePriceId); } else { params.append(`line_items[${index}][price_data][currency]`, order.currency); params.append(`line_items[${index}][price_data][product_data][name]`, item.title); params.append(`line_items[${index}][price_data][unit_amount]`, String(item.amountCents)); } params.append(`line_items[${index}][quantity]`, String(item.quantity)); }); return stripeRequest(stripe.secretKey, "/v1/checkout/sessions", params); }; const retrieveStripeCheckoutSession = (settings, sessionId) => { const id = cleanText(sessionId); if (!/^cs_(test|live)_[A-Za-z0-9_]+$/.test(id)) { throw new Error("Invalid Stripe checkout session"); } return stripeRequest(settings.stripe.secretKey, `/v1/checkout/sessions/${encodeURIComponent(id)}`, new URLSearchParams(), "GET"); }; const updateOrderFromStripeSession = (store, session, eventType, fallbackOrderId = "") => { const orderId = cleanText(session.metadata?.orderId || session.client_reference_id || fallbackOrderId); if (!orderId) return false; const index = store.orders.findIndex((order) => order.id === orderId); if (index === -1) return false; const order = store.orders[index]; const stripePaymentStatus = cleanText(session.payment_status).toLowerCase(); const stripeSessionStatus = cleanText(session.status).toLowerCase(); const validPaymentStatus = PAYMENT_STATUSES.includes(stripePaymentStatus) ? stripePaymentStatus : ""; const next = { ...order, stripeSessionId: cleanText(session.id || order.stripeSessionId), stripePaymentIntent: cleanText(session.payment_intent || order.stripePaymentIntent), stripeCheckoutUrl: cleanText(session.url || order.stripeCheckoutUrl), updatedAt: nowIso(), }; if ( eventType === "checkout.session.completed" || eventType === "checkout.session.async_payment_succeeded" || eventType === "checkout.session.return" ) { if (stripePaymentStatus === "paid") { next.paymentStatus = "paid"; next.status = "paid"; } else if (validPaymentStatus) { next.paymentStatus = validPaymentStatus; next.status = stripeSessionStatus === "complete" ? "processing" : "pending_payment"; } } else if (eventType === "checkout.session.async_payment_failed") { next.paymentStatus = "failed"; next.status = "failed"; } else if (eventType === "checkout.session.expired") { next.status = "cancelled"; } store.orders[index] = next; return true; }; const verifyStripeSignature = (rawBody, signatureHeader, endpointSecret) => { const signature = cleanText(signatureHeader); const secret = cleanText(endpointSecret); if (!signature || !secret) return false; const parts = signature.split(",").reduce( (acc, part) => { const [key, value] = part.split("="); if (key === "t") acc.timestamp = value; if (key === "v1") acc.signatures.push(value); return acc; }, { timestamp: "", signatures: [] } ); if (!parts.timestamp || !parts.signatures.length) return false; if (Math.abs(Date.now() / 1000 - Number(parts.timestamp)) > 300) return false; const expected = crypto.createHmac("sha256", secret).update(`${parts.timestamp}.${rawBody}`, "utf8").digest("hex"); const expectedBuffer = Buffer.from(expected, "hex"); return parts.signatures.some((value) => { const candidateBuffer = Buffer.from(value, "hex"); return candidateBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(candidateBuffer, expectedBuffer); }); }; const parseMultipart = async (req) => { const contentType = req.headers["content-type"] || ""; const match = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i); if (!match) throw new Error("Missing multipart boundary"); const boundary = `--${match[1] || match[2]}`; const raw = (await readBody(req, MAX_UPLOAD_BYTES)).toString("latin1"); const parts = raw.split(boundary).slice(1, -1); const files = []; for (const part of parts) { const cleanPart = part.replace(/^\r\n/, ""); const splitIndex = cleanPart.indexOf("\r\n\r\n"); if (splitIndex === -1) continue; const headerText = cleanPart.slice(0, splitIndex); let content = cleanPart.slice(splitIndex + 4); if (content.endsWith("\r\n")) content = content.slice(0, -2); const disposition = headerText.match(/content-disposition:[^\r\n]+/i)?.[0] || ""; const filename = disposition.match(/filename="([^"]*)"/i)?.[1] || ""; const contentTypeHeader = headerText.match(/content-type:\s*([^\r\n]+)/i)?.[1] || ""; if (!filename || !contentTypeHeader.startsWith("image/")) continue; const originalExtension = path.extname(filename).toLowerCase(); const extensionByType = { "image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "image/gif": ".gif", "image/svg+xml": ".svg", }; const extension = extensionByType[contentTypeHeader] || originalExtension || ".img"; const safeName = `${Date.now()}-${crypto.randomUUID()}${extension}`; const target = path.join(UPLOAD_DIR, safeName); const buffer = Buffer.from(content, "latin1"); fs.writeFileSync(target, buffer); files.push({ url: `/uploads/${safeName}`, name: filename, mime: contentTypeHeader, size: buffer.length, }); } return files; }; const serveContentScript = (res) => { const script = `window.SITE_CONTENT = ${JSON.stringify(publicContent())};\n`; send(res, 200, script, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-store", }); }; const mimeTypes = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", }; const serveStatic = (res, pathname) => { const cleanPath = decodeURIComponent(pathname === "/" ? "/index.html" : pathname); const staticPath = cleanPath === "/admin" ? "/admin.html" : cleanPath; if (staticPath.startsWith("/data/") || staticPath === "/data") { send(res, 403, "Forbidden", { "Content-Type": "text/plain; charset=utf-8" }); return; } const filePath = path.normalize(path.join(ROOT, staticPath)); const relativePath = path.relative(ROOT, filePath); if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { send(res, 403, "Forbidden", { "Content-Type": "text/plain; charset=utf-8" }); return; } if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) { send(res, 404, "Not found", { "Content-Type": "text/plain; charset=utf-8" }); return; } const type = mimeTypes[path.extname(filePath).toLowerCase()] || "application/octet-stream"; const cache = staticPath.startsWith("/uploads/") || staticPath.startsWith("/assets/") ? "public, max-age=86400" : "no-store"; send(res, 200, fs.readFileSync(filePath), { "Content-Type": type, "Cache-Control": cache, }); }; const routeAdmin = async (req, res, pathname) => { if (pathname === "/api/admin/login" && req.method === "POST") { const payload = await readJson(req); const store = readStore(); const settings = store.settings; if ( cleanText(payload.email).toLowerCase() !== settings.admin.email.toLowerCase() || !verifyPassword(payload.password, settings.admin) ) { sendJson(res, 401, { error: "Invalid admin email or password" }); return; } createSession(res, settings.admin.email); sendJson(res, 200, { ok: true, email: settings.admin.email }); return; } if (pathname === "/api/admin/logout" && req.method === "POST") { clearSession(req, res); sendJson(res, 200, { ok: true }); return; } if (!requireAdmin(req, res)) return; if (pathname === "/api/admin/me" && req.method === "GET") { const store = readStore(); sendJson(res, 200, { email: store.settings.admin.email }); return; } if (pathname === "/api/admin/data" && req.method === "GET") { const store = readStore(); const newOrderStatuses = new Set(["pending_payment", "manual_request", "paid"]); sendJson(res, 200, { products: store.products, blogs: store.blogs, messages: store.messages, orders: store.orders, siteEdits: store.siteEdits, settings: adminSettingsForClient(store.settings), stats: { products: store.products.length, blogs: store.blogs.length, messages: store.messages.length, orders: store.orders.length, newMessages: store.messages.filter((item) => item.status === "new").length, newOrders: store.orders.filter((item) => newOrderStatuses.has(item.status)).length, }, }); return; } if (pathname === "/api/admin/settings" && req.method === "GET") { const store = readStore(); sendJson(res, 200, adminSettingsForClient(store.settings)); return; } if (pathname === "/api/admin/settings" && ["PUT", "PATCH"].includes(req.method)) { const store = readStore(); const payload = await readJson(req); const settings = store.settings; const nextEmail = cleanText(payload.adminEmail || settings.admin.email); const currentPassword = cleanText(payload.currentPassword); const newPassword = cleanText(payload.newPassword); if (!isValidEmail(nextEmail)) { return badRequest(res, "Enter a valid admin email address"); } if (newPassword) { if (newPassword.length < 8) return badRequest(res, "New password must be at least 8 characters"); if (!verifyPassword(currentPassword, settings.admin)) return sendJson(res, 403, { error: "Current password is required to change password" }); const password = hashPassword(newPassword); settings.admin.passwordHash = password.hash; settings.admin.passwordSalt = password.salt; settings.admin.updatedAt = nowIso(); } if (nextEmail !== settings.admin.email) { settings.admin.email = nextEmail; settings.admin.updatedAt = nowIso(); } if (payload.stripe && typeof payload.stripe === "object") { const stripe = payload.stripe; settings.stripe.enabled = stripe.enabled === true; settings.stripe.mode = cleanText(stripe.mode).toLowerCase() === "live" ? "live" : "test"; settings.stripe.currency = cleanCurrency(stripe.currency || settings.stripe.currency); settings.stripe.publishableKey = cleanText(stripe.publishableKey); settings.stripe.successUrl = cleanText(stripe.successUrl); settings.stripe.cancelUrl = cleanText(stripe.cancelUrl); if (stripe.clearSecretKey === true) settings.stripe.secretKey = ""; else if (cleanText(stripe.secretKey)) settings.stripe.secretKey = cleanText(stripe.secretKey); if (stripe.clearWebhookSecret === true) settings.stripe.webhookSecret = ""; else if (cleanText(stripe.webhookSecret)) settings.stripe.webhookSecret = cleanText(stripe.webhookSecret); settings.stripe.updatedAt = nowIso(); } const stripeError = validateStripeSettings(settings.stripe); if (stripeError) return badRequest(res, stripeError); if (payload.contact && typeof payload.contact === "object") { const contact = payload.contact; const nextContact = normalizeContactSettings({ socialLinks: contact.socialLinks, updatedAt: nowIso(), }); const rawLinks = contact.socialLinks && typeof contact.socialLinks === "object" ? contact.socialLinks : {}; const invalidSocialLink = SOCIAL_LINK_KEYS.find((key) => cleanText(rawLinks[key]) && !nextContact.socialLinks[key]); if (invalidSocialLink) { return badRequest(res, "Social links must be full http:// or https:// URLs."); } settings.contact = nextContact; } store.settings = normalizeSettings(settings); writeStore(store); sendJson(res, 200, adminSettingsForClient(store.settings)); return; } if (pathname === "/api/admin/stripe/test" && req.method === "POST") { const store = readStore(); const settings = normalizeSettings(store.settings); const stripeError = validateStripeSettings(settings.stripe); if (stripeError) return badRequest(res, stripeError); if (!isStripeCheckoutReady(settings)) return badRequest(res, "Stripe checkout is not enabled or the secret key is missing."); try { const balance = await stripeRequest(settings.stripe.secretKey, "/v1/balance", new URLSearchParams(), "GET"); sendJson(res, 200, { ok: true, mode: settings.stripe.mode, livemode: balance.livemode === true, }); } catch (error) { const message = stripeFailureMessage(error); console.error(`[stripe] connection test failed: ${message}`); sendJson(res, 502, { error: `Stripe connection failed: ${message}` }); } return; } if (pathname === "/api/admin/upload" && req.method === "POST") { const files = await parseMultipart(req); sendJson(res, 200, { files }); return; } const siteEditMatch = pathname.match(/^\/api\/admin\/site-edits(?:\/([^/]+))?$/); if (siteEditMatch) { const id = siteEditMatch[1] ? decodeURIComponent(siteEditMatch[1]) : ""; const store = readStore(); if (req.method === "POST" && !id) { const payload = await readJson(req); if (!cleanText(payload.selector)) return badRequest(res, "Enter a CSS selector for this website edit"); const edit = normalizeSiteEdit(payload, store); store.siteEdits.unshift(edit); writeStore(store); sendJson(res, 201, edit); return; } const index = store.siteEdits.findIndex((item) => item.id === id); if (index === -1) return notFound(res); if (["PUT", "PATCH"].includes(req.method)) { const payload = await readJson(req); if (!cleanText(payload.selector)) return badRequest(res, "Enter a CSS selector for this website edit"); store.siteEdits[index] = normalizeSiteEdit(payload, store, id); writeStore(store); sendJson(res, 200, store.siteEdits[index]); return; } if (req.method === "DELETE") { const [removed] = store.siteEdits.splice(index, 1); writeStore(store); sendJson(res, 200, { ok: true, removed }); return; } } const productMatch = pathname.match(/^\/api\/admin\/products(?:\/([^/]+))?$/); if (productMatch) { const id = productMatch[1] ? decodeURIComponent(productMatch[1]) : ""; const store = readStore(); if (req.method === "POST" && !id) { const product = createProduct(await readJson(req), store); store.products.unshift(product); writeStore(store); sendJson(res, 201, product); return; } const index = store.products.findIndex((item) => item.id === id); if (index === -1) return notFound(res); if (["PUT", "PATCH"].includes(req.method)) { store.products[index] = createProduct(await readJson(req), store, id); writeStore(store); sendJson(res, 200, store.products[index]); return; } if (req.method === "DELETE") { const [removed] = store.products.splice(index, 1); writeStore(store); sendJson(res, 200, { ok: true, removed }); return; } } const blogMatch = pathname.match(/^\/api\/admin\/blogs(?:\/([^/]+))?$/); if (blogMatch) { const slug = blogMatch[1] ? decodeURIComponent(blogMatch[1]) : ""; const store = readStore(); if (req.method === "POST" && !slug) { const blog = createBlog(await readJson(req), store); store.blogs.unshift(blog); writeStore(store); sendJson(res, 201, blog); return; } const index = store.blogs.findIndex((item) => item.slug === slug); if (index === -1) return notFound(res); if (["PUT", "PATCH"].includes(req.method)) { store.blogs[index] = createBlog(await readJson(req), store, slug); writeStore(store); sendJson(res, 200, store.blogs[index]); return; } if (req.method === "DELETE") { const [removed] = store.blogs.splice(index, 1); writeStore(store); sendJson(res, 200, { ok: true, removed }); return; } } const orderMatch = pathname.match(/^\/api\/admin\/orders\/([^/]+)$/); if (orderMatch) { const id = decodeURIComponent(orderMatch[1]); const store = readStore(); const index = store.orders.findIndex((item) => item.id === id); if (index === -1) return notFound(res); if (["PUT", "PATCH"].includes(req.method)) { const payload = await readJson(req); const status = cleanText(payload.status); const paymentStatus = cleanText(payload.paymentStatus); const verifyStripe = payload.verifyStripe === true; if (status && !ORDER_STATUSES.includes(status)) { return badRequest(res, "Invalid order status"); } if (paymentStatus && !PAYMENT_STATUSES.includes(paymentStatus)) { return badRequest(res, "Invalid payment status"); } if (verifyStripe) { const order = store.orders[index]; if (!order.stripeSessionId) { return badRequest(res, "This order does not have a Stripe session"); } if (!store.settings.stripe.secretKey) { return badRequest(res, "Stripe secret key is required to verify this order"); } const session = await retrieveStripeCheckoutSession(store.settings, order.stripeSessionId); const sessionOrderId = cleanText(session.metadata?.orderId || session.client_reference_id); if (sessionOrderId && sessionOrderId !== order.id) { return badRequest(res, "Stripe session belongs to a different order"); } updateOrderFromStripeSession(store, session, "checkout.session.return", order.id); } store.orders[index] = { ...store.orders[index], status: status || store.orders[index].status, paymentStatus: paymentStatus || store.orders[index].paymentStatus, note: payload.note === undefined ? store.orders[index].note : cleanText(payload.note), updatedAt: nowIso(), }; writeStore(store); sendJson(res, 200, store.orders[index]); return; } if (req.method === "DELETE") { const [removed] = store.orders.splice(index, 1); writeStore(store); sendJson(res, 200, { ok: true, removed }); return; } } const messageMatch = pathname.match(/^\/api\/admin\/messages\/([^/]+)$/); if (messageMatch) { const id = decodeURIComponent(messageMatch[1]); const store = readStore(); const index = store.messages.findIndex((item) => item.id === id); if (index === -1) return notFound(res); if (["PUT", "PATCH"].includes(req.method)) { const payload = await readJson(req); const status = cleanText(payload.status); if (status && !["new", "delayed", "archived"].includes(status)) { return badRequest(res, "Invalid message status"); } store.messages[index] = { ...store.messages[index], status: status || store.messages[index].status, note: payload.note === undefined ? store.messages[index].note : cleanText(payload.note), updatedAt: nowIso(), }; writeStore(store); sendJson(res, 200, store.messages[index]); return; } if (req.method === "DELETE") { const [removed] = store.messages.splice(index, 1); writeStore(store); sendJson(res, 200, { ok: true, removed }); return; } } return notFound(res); }; const server = http.createServer(async (req, res) => { try { const url = new URL(req.url, `http://${req.headers.host || "localhost"}`); const pathname = url.pathname; if (pathname === "/content-data.js" && req.method === "GET") { serveContentScript(res); return; } if (pathname === "/api/content" && req.method === "GET") { sendJson(res, 200, publicContent()); return; } if (pathname === "/api/stripe/webhook" && req.method === "POST") { const store = readStore(); const rawBody = (await readBody(req, MAX_JSON_BYTES)).toString("utf8"); const webhookSecret = store.settings.stripe.webhookSecret; if (webhookSecret && !verifyStripeSignature(rawBody, req.headers["stripe-signature"], webhookSecret)) { sendJson(res, 400, { error: "Invalid Stripe signature" }); return; } const event = JSON.parse(rawBody); const session = event?.data?.object || {}; const changed = session.object === "checkout.session" && updateOrderFromStripeSession(store, session, event.type); if (changed) writeStore(store); sendJson(res, 200, { received: true }); return; } if (pathname === "/api/billing/options" && req.method === "GET") { const store = readStore(); sendJson(res, 200, { stripeReady: isStripeCheckoutReady(store.settings), manualReady: true, }); return; } if (pathname === "/api/billing/confirm" && req.method === "POST") { const payload = await readJson(req); const orderId = cleanText(payload.order || payload.orderId); const sessionId = cleanText(payload.sessionId || payload.session_id); if (!orderId || !sessionId) { return badRequest(res, "Order and Stripe session are required"); } const store = readStore(); const index = store.orders.findIndex((order) => order.id === orderId); if (index === -1) return notFound(res); const order = store.orders[index]; if (order.stripeSessionId && order.stripeSessionId !== sessionId) { return badRequest(res, "Stripe session does not match this order"); } if (order.paymentStatus !== "paid") { if (!store.settings.stripe.secretKey) { sendJson(res, 409, { error: "Stripe secret key is required to verify this payment" }); return; } const session = await retrieveStripeCheckoutSession(store.settings, sessionId); const sessionOrderId = cleanText(session.metadata?.orderId || session.client_reference_id); if (sessionOrderId && sessionOrderId !== orderId) { return badRequest(res, "Stripe session belongs to a different order"); } const changed = updateOrderFromStripeSession(store, session, "checkout.session.return", orderId); if (changed) writeStore(store); } const updatedOrder = store.orders.find((item) => item.id === orderId) || order; sendJson(res, 200, { ok: true, ...publicOrderSummary(updatedOrder) }); return; } const publicOrderMatch = pathname.match(/^\/api\/billing\/orders\/([^/]+)$/); if (publicOrderMatch && req.method === "GET") { const id = decodeURIComponent(publicOrderMatch[1]); const store = readStore(); const order = store.orders.find((item) => item.id === id); if (!order) return notFound(res); sendJson(res, 200, { ok: true, order: publicOrderSummary(order) }); return; } const invoiceMatch = pathname.match(/^\/api\/billing\/invoice\/([^/]+)$/); if (invoiceMatch && req.method === "GET") { const id = decodeURIComponent(invoiceMatch[1]); const store = readStore(); const order = store.orders.find((item) => item.id === id); if (!order) return notFound(res); if (order.paymentStatus !== "paid") { sendJson(res, 409, { error: "Invoice is available after payment is confirmed" }); return; } send(res, 200, buildInvoiceHtml(order), { "Content-Type": "text/html; charset=utf-8", "Content-Disposition": `attachment; filename="${invoiceFilename(order)}"`, "Cache-Control": "no-store", }); return; } if (pathname === "/api/contact" && req.method === "POST") { const message = addMessage(await readJson(req), "contact"); sendJson(res, 201, { ok: true, id: message.id }); return; } if (pathname === "/api/billing" && req.method === "POST") { const payload = await readJson(req); const store = readStore(); const stripeReady = isStripeCheckoutReady(store.settings); const order = createBillingOrder(payload, store, stripeReady ? "stripe" : "manual"); if (!order) { return badRequest(res, "Select at least one priced product before checkout"); } store.orders.unshift(order); if (stripeReady) { try { const session = await createStripeCheckoutSession(req, store.settings, order); order.stripeSessionId = cleanText(session.id); order.stripeCheckoutUrl = cleanText(session.url); order.paymentStatus = cleanText(session.payment_status, order.paymentStatus); order.updatedAt = nowIso(); writeStore(store); sendJson(res, 201, { ok: true, id: order.id, checkoutUrl: order.stripeCheckoutUrl, stripeSessionId: order.stripeSessionId }); return; } catch (error) { const message = stripeFailureMessage(error); const note = `Stripe checkout could not start: ${message}`; console.error(`[stripe] checkout fallback for ${order.id}: ${message}`); queueManualBillingRequest(store, payload, order, note); writeStore(store); sendJson(res, 201, { ok: true, id: order.id, manual: true, stripeFallback: true, message: "Thank you. Your order was received and our team will follow up with secure payment instructions.", }); return; } } queueManualBillingRequest(store, payload, order); writeStore(store); sendJson(res, 201, { ok: true, id: order.id, manual: true }); return; } if (pathname.startsWith("/api/admin/")) { await routeAdmin(req, res, pathname); return; } if (req.method !== "GET" && req.method !== "HEAD") { sendJson(res, 405, { error: "Method not allowed" }); return; } serveStatic(res, pathname); } catch (error) { sendJson(res, 500, { error: error.message || "Server error" }); } }); ensureDirectories(); const initialStore = readStore(); writePublicContentScript(initialStore); server.on("error", (error) => { if (error.code === "EADDRINUSE") { console.error(`Port ${PORT} is already in use. Set PORT to a free port before starting the server.`); } else { console.error(error); } process.exit(1); }); server.listen(PORT, HOST, () => { console.log(`Health Care Startup admin server running at http://${HOST}:${PORT}`); console.log(`Admin login: ${readStore().settings.admin.email}`); });
Close