bliss/index.js
Your Name 386e65fa88 refactor: use model.getRoute in the catch-all instead of raw SQL
The app.all("*") dispatcher hand-rolled a SELECT * FROM routes while
every other read goes through the model layer. Use model.getRoute for
consistency; the handler only reads structure_id/id/verb/handler, all
of which it returns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 22:32:30 -04:00

861 lines
26 KiB
JavaScript

const fs = require("fs");
const util = require("util");
const vm = require("node:vm");
const path = require("path");
require('dotenv').config()
const express = require("express");
const session = require("express-session");
const fileUpload = require("express-fileupload");
const SQLiteStore = require("better-sqlite3-session-store")(session);
const { Eta } = require("eta");
const { match } = require("path-to-regexp");
const bcrypt = require("bcrypt");
const cheerio = require("cheerio");
const webPush = require("web-push");
const app = express();
const _expressWs = require("express-ws")(app);
const bodyParser = require("body-parser");
const model = require("./db");
const PORT = 3000;
const db = model.db;
const wsRouter = express.Router()
let viewpath = path.join(__dirname, "views");
let eta = new Eta({ views: viewpath, cache: false, autoEscape: true });
let routes = { GET: [], POST: [], PUT: [], DELETE: [] };
const wsRoutes = {}
const wsConnections = {}
const INSPECT_OPTS = { showHidden: false, depth: null, colors: false, compact: false };
function inspectArgs(args) {
return args.map((v) => util.inspect(v, INSPECT_OPTS)).join(" ");
}
function formatError(e) {
return e.stack ? `${e}\n\n${e.stack}` : `${e}`;
}
// Record a handler error in the structure's logs table.
function logError(structureId, routeId, e) {
model.createLog(structureId, routeId, formatError(e), true);
}
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static("public"));
app.use(fileUpload());
app.use(
session({
store: new SQLiteStore({ client: db, expired: { clear: true } }),
secret: "your secret key",
resave: false,
saveUninitialized: true,
cookie: { secure: false },
}),
);
app.use("/", wsRouter);
const vapidPublicKey = process.env.VAPID_PUBLIC_KEY
const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY
// Configure web-push with your VAPID details
webPush.setVapidDetails(
"mailto:signups@sheepmail.net", // a mailto URL or URL
vapidPublicKey,
vapidPrivateKey,
);
async function saveFile(structureId, req, uploadedFile, asset=false) {
const name = uploadedFile.name;
const [mime_type, mime_subtype] = uploadedFile.mimetype.split("/");
const uploadPath = path.join(__dirname, "public", structureId);
const storedPath = path.join(structureId, uploadedFile.name);
fs.mkdirSync(uploadPath, { recursive: true });
await uploadedFile.mv(path.join(uploadPath, name));
let id = model.createFile(
structureId,
name,
storedPath,
mime_type,
mime_subtype,
asset,
);
let file = model.getFile(id);
file.url = prefixUrlWithHost(req, file.path);
return file
}
// A console whose log() mirrors to stdout and to the structure's logs table.
function makeConsole(structureId, routeId) {
return {
log: function (...content) {
content.forEach((c) => console.log(c));
model.createLog(structureId, routeId, inspectArgs(content));
},
};
}
// Evaluate a db's "library" script with `sql` and `console` bound, returning
// its exports. Each library runs in its own context so nothing leaks between dbs.
function runLibrary(sql, librarySource, console) {
const libContext = vm.createContext({ sql, console, module: { exports: null } });
vm.runInContext(librarySource, libContext);
return libContext.module.exports;
}
// The `require(name)` targets available to a user handler.
function makeLibs(structureId, console) {
const dbs = {};
for (let appDb of model.getDbsForStructure(structureId)) {
const sql = model.getDbInstance(appDb.id);
dbs[appDb.alias] = { library: runLibrary(sql, appDb.library, console), sql };
}
return {
eta: model.getTemplater(structureId),
db: (alias) => dbs[alias],
push: webPush,
files: { saveFile: (...args) => saveFile(structureId, ...args) },
};
}
function bootstrapContext(structureId, routeId, initContext) {
const structure = model.getStructure(structureId);
const console = makeConsole(structureId, routeId);
const libs = makeLibs(structureId, console);
return vm.createContext({
...initContext,
require: (name) => libs[name],
module: { exports: null },
console,
vapidPublicKey: vapidPublicKey,
fetch: fetch,
clearTimeout: clearTimeout,
setTimeout: setTimeout,
route: makeRoute(structure),
});
}
function withPrefix(prefix, url) {
return prefix ? path.join(prefix, url) : url;
}
function routeWithPrefix(route) {
return withPrefix(route.route_prefix, route.path);
}
function bootstrapWebsocketHandler(route) {
// todo only expose app when running the handler, should not be available to the handler itself
// need to move to runscript or whatever...or remove app because we can just call it using the handler?
try {
const context = bootstrapContext(route.structure_id, route.id, { app });
let handler = vm.runInContext(`${route.handler}\n\nhandler;`, context);
if (!wsRoutes[routeWithPrefix(route)]) {
wsConnections[routeWithPrefix(route)] = new Set();
// initialize ws router. future updates will only require updating
// the wsRoutes dict, not create a whole new route
wsRouter.ws(routeWithPrefix(route), (ws, req) => {
const myOn = ws.on.bind(ws)
ws.on = (thing, cb) => {
myOn(thing, (...args) => {
try {
cb(...args)
}
catch (e) {
logError(route.structure_id, route.id, e);
}
})
}
ws.render = (template, context = {}) => {
const structure = model.getStructure(route.structure_id);
context.route = makeRoute(structure);
ws.send(
decorate(renderTemplate(route.structure_id, template, context), {
headInjection: structure.head_injection,
source: sourceFor(route, req),
fragment: true,
}),
);
};
ws.clients = wsConnections[routeWithPrefix(route)];
return wsRoutes[routeWithPrefix(route)](ws, req)
});
}
wsRoutes[routeWithPrefix(route)] = (ws, req) => {
try {
return handler(ws, req)
} catch (e) {
logError(route.structure_id, route.id, e);
}
}
model.updateRoute({ ...route, error: null });
} catch (e) {
logError(route.structure_id, route.id, e);
model.updateRoute({ ...route, error: e.stack });
}
}
function buildRoutes() {
let newRoutes = {
GET: [],
POST: [],
PUT: [],
DELETE: [],
};
for (let route of model.getAllRoutes()) {
const p = routeWithPrefix(route);
if (route.verb == "WS") {
bootstrapWebsocketHandler(route);
} else {
newRoutes[route.verb].push({
matcher: match(p, { decode: decodeURIComponent }),
id: route.id,
path: route.path,
});
}
}
routes = newRoutes;
}
model.applyMigrations();
buildRoutes();
// __ __ _______ _______ ______
// | | | || || || _ |
// | | | || _____|| ___|| | ||
// | |_| || |_____ | |___ | |_||_
// | ||_____ || ___|| __ |
// | | _____| || |___ | | | |
// |_______||_______||_______||___| |_|
app.post("/register", async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the saltRounds
const userId = model.createUser(username, hashedPassword);
req.session.userId = userId;
res.redirect("/workshop");
} catch (e) {
return res.send(eta.render("auth/register", { error: e }));
}
});
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = model.getUser(username);
if (user && (await bcrypt.compare(password, user.password))) {
req.session.userId = user.id;
return res.redirect("/");
}
return res.send(
eta.render("auth/login", {
error: "are you sure you entered that right?",
}),
);
});
app.get("/register", async (req, res) => {
if (req.session.userId) {
return res.redirect("/");
}
return res.send(eta.render("auth/register", { error: null }));
});
app.get("/login", async (req, res) => {
if (req.session.userId) {
return res.redirect("/");
}
return res.send(eta.render("auth/login", { error: null }));
});
app.all("/logout", async (req, res) => {
return req.session.destroy(() => {
res.redirect("/");
});
});
// _ _ _______ ______ ___ _ _______ __ __ _______ _______
// | | _ | || || _ | | | | || || | | || || |
// | || || || _ || | || | |_| || _____|| |_| || _ || _ |
// | || | | || |_||_ | _|| |_____ | || | | || |_| |
// | || |_| || __ || |_ |_____ || || |_| || ___|
// | _ || || | | || _ | _____| || _ || || |
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
//
// The chrome injected into the <head> of every rendered page: the client-side
// stack (htmx/hyperscript/tailwind) plus the in-page editor overlay.
function headChrome(headInjection) {
return `
<script src="/js/hyperscript.js"></script>
<script src="/js/tailwind.js"></script>
<script src="/js/htmx.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js"></script>
<script src="/js/bliss_inspector.js"></script>
<script>
tailwind.config = {
corePlugins: {
preflight: false
}
}
</script>
<style> body { margin: 0; }</style>
${headInjection || ""}
`;
}
// A `source` is the provenance of a rendered fragment: which structure/route
// produced it, and (for GET routes) the snippet that re-embeds it. It becomes
// the data-bliss-* attributes the inspector reads.
function blissAttrs(source) {
if (!source) return null;
const attrs = {
"data-bliss-route": `/workshop/${source.structureId}/route/${source.routeId}`,
"data-bliss-clone": `/workshop/${source.structureId}/clone_modal/`,
};
if (source.copyUrl) attrs["data-bliss-copy"] = source.copyUrl;
return attrs;
}
// Full HTML document: inject the head chrome and stamp provenance on <body>.
function decoratePage(html, { headInjection, source } = {}) {
const $ = cheerio.load(html);
let head = $("head");
if (head.length === 0) {
$("html").prepend("<head></head>");
head = $("head");
}
head.attr("id", "head");
head.append(headChrome(headInjection));
const attrs = blissAttrs(source);
if (attrs) {
for (const [k, v] of Object.entries(attrs)) $("body").attr(k, v);
}
return $.html();
}
// HTMX partial: stamp provenance on the swapped-in elements (an oob swap loses
// its own attrs, so we descend into its children) and append an out-of-band
// <head> so head injection still reaches the page.
function decorateFragment(html, { headInjection, source } = {}) {
const $ = cheerio.load(html, null, false);
const attrs = blissAttrs(source);
if (attrs) {
const targets = [];
for (const child of $.root().children()) {
if (child.attribs && child.attribs["hx-swap-oob"]) {
for (const grandchild of child.children) {
if (grandchild.attribs) targets.push(grandchild);
}
} else {
targets.push(child);
}
}
for (const target of targets) Object.assign(target.attribs, attrs);
}
return (
$.html() +
`<head id="head" hx-oob-swap="beforeend">${headInjection || ""}</head>`
);
}
// Single entry point for turning rendered HTML into a response body. A full
// document (or any non-htmx request) becomes a decorated page; an htmx partial
// carrying provenance becomes a decorated fragment; anything else passes through.
function decorate(html, { headInjection, source, fragment } = {}) {
const lower = html.toLowerCase();
const isFullDoc =
lower.startsWith("<html>") || lower.startsWith("<!doctype");
if (isFullDoc || !fragment) {
return decoratePage(html, { headInjection, source });
}
if (source) return decorateFragment(html, { headInjection, source });
return html;
}
// One prefix-bound route() helper per structure, shared by every render site.
function makeRoute(structure) {
return (url) => withPrefix(structure.route_prefix, url);
}
function renderTemplate(structureId, templateName, context) {
return model.getTemplater(structureId).render(templateName, context);
}
// Provenance for a rendered response: GET routes are re-embeddable, so they
// carry the copy-embed snippet; other verbs don't.
function sourceFor(route, req) {
return {
structureId: route.structure_id,
routeId: route.id,
copyUrl: route.verb === "GET" ? embedHTML(req.originalUrl) : null,
};
}
app.post("/workshop", (req, res) => {
let structId = model.createStructure(req.body.name);
return res.redirect("/workshop/" + structId);
});
app.get("/workshop", (req, res) => {
return renderWorkshop(res, "workshop/index", {
structures: model.getStructures(),
});
});
function renderWorkshop(res, template, context) {
res.send(decorate(eta.render(template, context)));
}
function smartRedirect(req, res, redirectUrl) {
if (req.headers["hx-request"]) {
res.set("HX-Redirect", redirectUrl);
res.send();
} else {
res.redirect(redirectUrl);
}
}
function sidebarStuff(structId) {
return {
structure: model.getStructure(structId),
routes: model.getRoutes(structId),
templates: model.getTemplates(structId),
dbs: model.getDbsForStructure(structId),
};
}
app.get("/workshop/:structure_id", (req, res) => {
return renderWorkshop(res, "workshop/editor", sidebarStuff(req.params.structure_id));
});
app.post("/workshop/:structure_id/clone", (req, res) => {
let routePrefix = req.body.route_prefix;
routePrefix = routePrefix[0] == "/" ? routePrefix : "/" + routePrefix;
let newStructureId;
try {
newStructureId = model.cloneStructure(
req.params.structure_id,
req.body.name,
1,
// req.session.userId,
routePrefix,
req.body.clone_dbs,
);
buildRoutes();
} catch (e) {
return res.send(e.stack);
}
return smartRedirect(req, res, `/workshop/${newStructureId}`);
});
app.post("/workshop/:structure_id/db", (req, res) => {
const dbId = model.createDb(req.params.structure_id, req.body.name);
const redirectUrl = `/workshop/${req.params.structure_id}/db/${dbId}`;
return smartRedirect(req, res, redirectUrl);
});
app.post("/workshop/:structure_id/db/attach", (req, res) => {
model.attachDb(req.params.structure_id, req.body.db_id, req.body.alias);
const redirectUrl = `/workshop/${req.params.structure_id}/db/${req.body.db_id}`;
return smartRedirect(req, res, redirectUrl);
});
app.get("/workshop/:structure_id/db/:db_id", (req, res) => {
const structdb = model.getDbForStructure(
req.params.structure_id,
req.params.db_id,
);
if (!structdb) {
return res.send("uh oh");
}
return renderWorkshop(res, "workshop/db_garden", {
db: structdb,
...sidebarStuff(req.params.structure_id),
});
});
app.post("/workshop/:structure_id/route", (req, res) => {
let p = req.body.path;
let dummyHandler =
"// put your handler code here\n\nfunction handler(req, res) {\n res.send('henlo world') \n}";
if (req.body.verb == "WS") {
dummyHandler = `// put your websocket handler code here\n\nfunction handler(ws, req) {\n ws.on('message', function(msg) {\n ws.send(msg);\n })\n}`;
}
if (p[0] == "/") {
p = p.substring(1);
}
if (p[p.length - 1] == "/") {
p = p.substring(0, p.length - 1);
}
const route = model.createRoute(
req.body.verb,
p[0] == "/" ? p.substring(1) : "/" + p,
req.params.structure_id,
dummyHandler,
);
// todo optimize by only adding new, don't just rebuild all
buildRoutes();
return smartRedirect(
req,
res,
`/workshop/${req.params.structure_id}/route/${route}`,
);
});
app.put("/workshop/:structure_id/route/:route_id", (req, res) => {
const route = model.getRoute(req.params.route_id);
model.updateRoute({ ...route, ...req.body });
if (req.body.scaffold_page) {
// TODO: make this send over scaffold page id too someday
const latestPage = model.getLatestScaffoldPage(req.params.route_id)
model.updateScaffoldPage({ ...latestPage, content: req.body.scaffold_page })
}
if (route.verb == "WS") {
bootstrapWebsocketHandler({ ...route, ...req.body })
}
return res.send("good");
});
app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => {
try {
let dbId = req.params.db_id;
let appDb = model.getDb(dbId);
model.updateDb({ ...appDb, library: req.body.library });
appDb = model.getDb(dbId);
let dbInstance = model.getDbInstance(dbId);
let capturedOutput = [];
let context = vm.createContext({
module: { exports: null },
sql: dbInstance,
console: {
log: (...args) => capturedOutput.push(inspectArgs(args)),
},
});
let evaledCode = vm.runInContext(appDb.library, context);
let stdout = capturedOutput.join("\n");
return res.send(`${stdout}\n\n> ${evaledCode}`.trim());
} catch (e) {
return res.send(`${e}\n\n${e.stack}`);
}
});
app.post("/workshop/:structure_id/db/:db_id/repl", (req, res) => {
try {
let dbId = req.params.db_id;
let appDb = model.getDb(dbId);
let dbInstance = model.getDbInstance(dbId);
let capturedOutput = [];
let context = vm.createContext({
module: { exports: null },
sql: dbInstance,
console: {
log: (...args) => capturedOutput.push(inspectArgs(args)),
},
});
const libraryScript = new vm.Script(appDb.library);
libraryScript.runInContext(context);
context.library = context.module.exports;
const replScript = new vm.Script(req.body.code);
let evaledCode = JSON.stringify(replScript.runInContext(context));
let stdout = capturedOutput.join("\n");
return res.send(`${stdout}\n\n> ${evaledCode}`.trim());
} catch (e) {
return res.send(`${e}\n\n${e.stack}`);
}
});
app.post("/workshop/:structure_id/template", (req, res) => {
let name = req.body.name;
const template = model.createTemplate(
req.params.structure_id,
name,
"<div>henlo <%= it.name %></div>",
"it = { name: 'templates!' };",
);
return smartRedirect(
req,
res,
`/workshop/${req.params.structure_id}/template/${template}`,
);
});
app.put("/workshop/:structure_id/template/:template_id", (req, res) => {
let id = req.params.template_id;
let content = req.body.content;
let test_object = req.body.test_object;
model.updateTemplate({
...model.getTemplate(id),
content,
test_object,
});
return res.send("good");
});
app.get("/workshop/:structure_id/template/:template_id", (req, res) => {
const template = model.getTemplate(req.params.template_id);
return renderWorkshop(res, "workshop/template", {
template: template,
...sidebarStuff(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => {
const template = model.getTemplate(req.params.template_id);
const struct = model.getStructure(req.params.structure_id);
const context = vm.createContext({ it: null });
vm.runInContext(template.test_object, context);
context.it.route = makeRoute(struct);
return res.send(
decorate(renderTemplate(req.params.structure_id, template.name, context.it), {
headInjection: struct.head_injection,
}),
);
});
app.get("/workshop/:structure_id/route/:id", (req, res) => {
const route = model.getRoute(req.params.id);
let routePrefix = model.getStructure(req.params.structure_id).route_prefix;
let previewUrl = null;
if (route["verb"] == "GET" ) {
previewUrl = routePrefix
? path.join(routePrefix || "", route.path)
: route.path;
}
else {
previewUrl = `/workshop/${req.params.structure_id}/route/${req.params.id}/preview`;
}
const template = route["verb"] == "GET" ? "workshop/route" : "workshop/cud_route";
return renderWorkshop(res, template, {
route: route,
previewUrl: previewUrl,
...sidebarStuff(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/route/:route_id/preview", (req, res) => {
const route = model.getRoute(req.params.route_id);
const struct = model.getStructure(req.params.structure_id);
const it = { route: makeRoute(struct) };
return res.send(
decorate(eta.renderString(route.scaffold_page_content, it), {
headInjection: struct.head_injection,
}),
);
});
app.get("/workshop/:structure_id/route/:route_id/logs", (req, res) => {
const since = req.query.since;
let logs = [];
if (since != undefined) {
logs = model.getNewLogsByRoute(req.params.route_id, since);
} else {
logs = model.getLogsByRoute(req.params.route_id);
}
const lastId = model.getMostRecentLogIdByRoute(req.params.route_id);
return renderWorkshop(res, "workshop/logs", {
logs: logs,
structId: req.params.structure_id,
routeId: req.params.route_id,
since: lastId,
});
});
// POST route to handle file upload
app.post("/workshop/:structure_id/files", async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send("failed to upload that file");
}
const { structure_id } = req.params;
const targetFile = req.files.file;
try {
const uploadedFile = await saveFile(structure_id, req, targetFile)
return res.send(eta.render("workshop/file_detail", { file: uploadedFile }));
}
catch (e) {
console.log(e)
res.status(500);
return res.send(e);
}
});
function prefixUrlWithHost(req, path) {
return req.protocol + "://" + req.get("host") + "/" + path;
}
// POST route to handle file upload
app.get("/workshop/:structure_id/files", (req, res) => {
const { structure_id } = req.params;
const files = model.getFilesForStruct(structure_id);
files.map((it) => {
it.url = prefixUrlWithHost(req, it.path);
});
return renderWorkshop(res, "workshop/files", {
files,
...sidebarStuff(structure_id),
});
});
app.get("/workshop/:structure_id/settings", (req, res) => {
return renderWorkshop(res, "workshop/settings", {
...sidebarStuff(req.params.structure_id),
});
});
app.put("/workshop/:structure_id/settings", (req, res) => {
let structId = req.params.structure_id;
let struct = model.getStructure(structId);
let routePrefix = req.body.route_prefix;
if (routePrefix[0] != "/") {
routePrefix = "/" + routePrefix;
}
if (routePrefix[routePrefix.length - 1] == "/") {
routePrefix = routePrefix.substring(0, routePrefix.length - 1);
}
model.updateStruct({
...struct,
route_prefix: routePrefix,
head_injection: req.body.head_injection,
});
struct = model.getStructure(structId);
res.send("success!");
});
app.get("/workshop/:structure_id/new_template_modal", (req, res) => {
return renderWorkshop(res, "workshop/new_template_modal", {
structure: model.getStructure(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/new_route_modal", (req, res) => {
return renderWorkshop(res, "workshop/new_route_modal", {
structure: model.getStructure(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/new_db_modal", (req, res) => {
return renderWorkshop(res, "workshop/new_db_modal", {
structure: model.getStructure(req.params.structure_id),
});
});
app.get("/workshop/:structure_id/clone_modal", (req, res) => {
const structure = model.getStructure(req.params.structure_id);
const dbs = model.getDbsForStructure(req.params.structure_id);
return renderWorkshop(res, "workshop/clone_structure_modal", {
structure,
dbs,
defaultRoutePrefix: path.join(structure.route_prefix || "", "clone"),
});
});
function embedHTML(url) {
return `<div hx-get="${url}" hx-trigger="load"></div>`;
}
app.all("*", async (req, res) => {
const req_path = req.path;
const verb = req.method;
try {
let routeMatch = null;
// routes[verb] is ordered most-recently-updated first (getAllRoutes sorts
// by updated_at DESC), so the first match is the most recently saved route.
for (let { matcher, id } of routes[verb]) {
let matchFromRoute = matcher(req_path);
if (matchFromRoute) {
routeMatch = { params: matchFromRoute.params, id: id };
break;
}
}
if (routeMatch) {
req.params = routeMatch.params;
const route = model.getRoute(routeMatch.id);
const structure = model.getStructure(route.structure_id);
try {
// todo: only add req res to contexst when running the handler()
// which means moving to runscript instead of runincontext for that
let context = bootstrapContext(route.structure_id, route.id, {
req,
res,
eta,
});
res.render = (template, context = {}) => {
context.route = makeRoute(structure);
res.send(
decorate(renderTemplate(route.structure_id, template, context), {
headInjection: structure.head_injection,
source: sourceFor(route, req),
fragment: req.headers["hx-request"],
}),
);
};
const handlerScript = new vm.Script(route.handler, { filename: `handler_${route.id}.js` });
// Prepare the execution code that invokes `handler(req, res)` and awaits it
const executionScript = new vm.Script(`handler(req, res); `, { filename: `execution_${route.id}.js` });
await handlerScript.runInContext(context);
const result = await executionScript.runInContext(context);
} catch (e) {
logError(structure.id, route.id, e);
return res.status(500).send(formatError(e));
}
} else {
res.status(404).json({ success: false, message: "Path not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ success: false, message: "Internal server error" });
}
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});