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 = {} app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(express.static("public")); app.use(fileUpload()); app.use("/", wsRouter); app.use( session({ store: new SQLiteStore({ client: db, expired: { clear: true } }), secret: "your secret key", resave: false, saveUninitialized: true, cookie: { secure: false }, }), ); 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( db, structureId, name, storedPath, mime_type, mime_subtype, asset, ); let file = model.getFile(db, id); file.url = prefixUrlWithHost(req, file.path); return file } function bootstrapContext(db, structureId, routeId, initContext) { const allDbInstances = {}; function getDb(alias) { return allDbInstances[alias]; } // todo wrap template in data-bliss-edit-template thing since ws can't take us to the editor on a component basis?? maybe... // for now just designing with component approach const eta = model.getTemplater(structureId); const libs = { eta, db: getDb, push: webPush, files: { saveFile: (...args) => saveFile(structureId, ...args) } }; let dbs = model.getDbsForStructure(db, structureId); let context = vm.createContext({ ...initContext, require: function (str) { return libs[str]; }, module: { exports: null }, console: { log: function (...content) { const s = content.reduce((acc, curr) => { console.log(curr); return ( acc + (acc ? " " : "") + `${util.inspect(curr, { showHidden: false, depth: null, // `null` lets you see the full depth of the object colors: false, // Setting this to true uses ANSI color codes compact: false, })}` ); }, ""); model.createLog(db, structureId, routeId, s); }, }, vapidPublicKey: vapidPublicKey }); for (let appDb of dbs) { let dbInstance = model.getDbInstance(appDb.id); context.sql = dbInstance; vm.runInContext(appDb.library, context); allDbInstances[appDb.alias] = { library: context.module.exports, sql: dbInstance, }; context.module.exports = null; } return context; } function routeWithPrefix(route) { const prefix = route.route_prefix; return prefix ? path.join(prefix, route.path) : 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(db, 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) { const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`; model.createLog(db, route.structure_id, route.id, error, true); } }) } ws.render = (template, context) => { context = context || {}; context.route = function (url) { return __urlPrefix ? path.join(__urlPrefix, url) : url; }; const eta = model.getTemplater(route.structure_id); const structure = model.getStructure(db, route.structure_id); ws.send( bootstrapTemplateWithHTMXetc( eta.render(template, context), `/workshop/${route.structure_id}/route/${route.id}`, `/workshop/${route.structure_id}/clone_modal/`, route.verb == "GET" ? embedHTML(req.originalUrl) : null, structure.head_injection, htmxRequest = true, ), ); }; ws.clients = wsConnections[routeWithPrefix(route)]; return wsRoutes[routeWithPrefix(route)](ws, req) }); } wsRoutes[routeWithPrefix(route)] = (ws, req) => { try { return handler(ws, req) } catch (e) { const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`; model.createLog(db, route.structure_id, route.id, error, true); } } model.updateRoute(db, { ...route, error: null }); } catch (e) { const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`; model.createLog(db, route.structure_id, route.id, error, true); model.updateRoute(db, { ...route, error: e.stack }); } } function buildRoutes() { let newRoutes = { GET: [], POST: [], PUT: [], DELETE: [], }; for (let route of model.getAllRoutes(db)) { 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 userId = model.createUser(db, 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(db, 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("/"); }); }); // _ _ _______ ______ ___ _ _______ __ __ _______ _______ // | | _ | || || _ | | | | || || | | || || | // | || || || _ || | || | |_| || _____|| |_| || _ || _ | // | || | | || |_||_ | _|| |_____ | || | | || |_| | // | || |_| || __ || |_ |_____ || || |_| || ___| // | _ || || | | || _ | _____| || _ || || | // |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___| // function bootstrapTemplateWithHTMXetc( htmlString, blissRoute, blissClone, blissCopy, headInjection, htmxRequest = false, ) { if ( htmlString.toLowerCase().startsWith("") || htmlString.toLowerCase().startsWith(""); head = $("head"); } head.attr("id", "head"); head.append(` ${headInjection || ""} `); if (blissRoute) { $("body").attr("data-bliss-route", blissRoute); $("body").attr("data-bliss-clone", blissClone); if (blissCopy) $("body").attr("data-bliss-copy", blissCopy); } htmlString = $.html(); } else if (htmxRequest && blissRoute) { const $ = cheerio.load(htmlString, null, false); const targets = []; // when we return an oob thing from htmx, we'll lose all the attrs when it swaps // so this adds those attrs to the children of the swap if possible for (let child of $.root().children()) { if (child.attribs && child.attribs["hx-swap-oob"]) { for (let childchild of child.children) { if (childchild.attribs) { targets.push(childchild); } } } else { targets.push(child); } } // Directly update attributes without wrapping in Cheerio for (let target of targets) { target.attribs["data-bliss-route"] = blissRoute; target.attribs["data-bliss-clone"] = blissClone; if (blissCopy) target.attribs["data-bliss-copy"] = blissCopy; } htmlString = $.html(); if (htmxRequest) { htmlString += `${headInjection}`; } } return htmlString; } app.post("/workshop", (req, res) => { let structId = model.createStructure(db, req.body.name); return res.redirect("/workshop/" + structId); }); app.get("/workshop", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/index", { structures: model.getStructures(db), }), ), ); }); function smartRedirect(req, res, redirectUrl) { if (req.headers["hx-request"]) { res.set("HX-Redirect", redirectUrl); res.send(); } else { res.redirect(redirectUrl); } } function sidebarStuff(db, structId) { return { structure: model.getStructure(db, structId), routes: model.getRoutes(db, structId), templates: model.getTemplates(db, structId), dbs: model.getDbsForStructure(db, structId), }; } app.get("/workshop/:structure_id", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/editor", sidebarStuff(db, 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(db, 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(db, 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( db, req.params.structure_id, req.params.db_id, ); if (!structdb) { return res.send("uh oh"); } return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/db_garden", { db: structdb, ...sidebarStuff(db, 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( db, 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(db, req.params.route_id); model.updateRoute(db, { ...route, ...req.body }); if (req.body.scaffold_page) { // TODO: make this send over scaffold page id too someday const latestPage = model.getLatestScaffoldPage(db, req.params.route_id) model.updateScaffoldPage(db, { ...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(db, dbId); model.updateDb(db, { ...appDb, library: req.body.library }); appDb = model.getDb(db, dbId); let dbInstance = model.getDbInstance(dbId); let capturedOutput = []; let context = vm.createContext({ module: { exports: null }, sql: dbInstance, console: { log: (...args) => capturedOutput.push(args.join(" ")), }, }); let evaledCode = vm.runInContext(appDb.library, context); let stdout = capturedOutput .map((v) => { return util.inspect(v, { showHidden: false, depth: null, // `null` lets you see the full depth of the object colors: false, // Setting this to true uses ANSI color codes compact: false, }); }) .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(db, dbId); let dbInstance = model.getDbInstance(dbId); let capturedOutput = []; let context = vm.createContext({ module: { exports: null }, sql: dbInstance, console: { log: (...args) => capturedOutput.push( args .map((v) => { return util.inspect(v, { showHidden: false, depth: null, // `null` lets you see the full depth of the object colors: false, // Setting this to true uses ANSI color codes compact: false, }); }) .join(" "), ), }, }); 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( db, req.params.structure_id, name, "
henlo <%= it.name %>
", "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(db, { ...model.getTemplate(db, id), content, test_object, }); return res.send("good"); }); app.get("/workshop/:structure_id/template/:template_id", (req, res) => { const template = model.getTemplate(db, req.params.template_id); return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/template", { template: template, ...sidebarStuff(db, req.params.structure_id), }), ), ); }); app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => { const template = model.getTemplate(db, req.params.template_id); const struct = model.getStructure(db, req.params.structure_id); const eta = model.getTemplater(req.params.structure_id); const context = vm.createContext({ it: null }); vm.runInContext(template.test_object, context); context.it.route = function (url) { return struct.route_prefix ? path.join(struct.route_prefix, url) : url; }; return res.send( bootstrapTemplateWithHTMXetc( eta.render(template.name, context.it), null, null, null, struct.head_injection, false, ), ); }); app.get("/workshop/:structure_id/route/:id", (req, res) => { const route = model.getRoute(db, req.params.id); let routePrefix = model.getStructure( db, 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`; } template = route["verb"] == "GET" ? "workshop/route" : "workshop/cud_route"; return res.send( bootstrapTemplateWithHTMXetc( eta.render(template, { route: route, previewUrl: previewUrl, ...sidebarStuff(db, req.params.structure_id), }), ), ); }); app.get("/workshop/:structure_id/route/:route_id/preview", (req, res) => { const route = model.getRoute(db, req.params.route_id); const struct = model.getStructure(db, req.params.structure_id); const it = { route: function (url) { return struct.route_prefix ? path.join(struct.route_prefix, url) : url; } } return res.send( bootstrapTemplateWithHTMXetc( eta.renderString(route.scaffold_page_content, it), null, null, null, struct.head_injection, false, ), ); }); app.get("/workshop/:structure_id/route/:route_id/logs", (req, res) => { const since = req.query.since; let logs = []; if (since != undefined) { logs = model.getNewLogsByRoute(db, req.params.route_id, since); } else { logs = model.getLogsByRoute(db, req.params.route_id); } const lastId = model.getMostRecentLogIdByRoute(db, req.params.route_id); return res.send( bootstrapTemplateWithHTMXetc( eta.render("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(db, structure_id); files.map((it) => { it.url = prefixUrlWithHost(req, it.path); }); return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/files", { files, ...sidebarStuff(db, structure_id), }), ), ); }); app.get("/workshop/:structure_id/settings", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/settings", { ...sidebarStuff(db, req.params.structure_id), }), ), ); }); app.put("/workshop/:structure_id/settings", (req, res) => { let structId = req.params.structure_id; let struct = model.getStructure(db, 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(db, { ...struct, route_prefix: routePrefix, head_injection: req.body.head_injection, }); struct = model.getStructure(db, structId); res.send("success!"); }); app.get("/workshop/:structure_id/new_template_modal", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/new_template_modal", { structure: model.getStructure(db, req.params.structure_id), }), ), ); }); app.get("/workshop/:structure_id/new_route_modal", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/new_route_modal", { structure: model.getStructure(db, req.params.structure_id), }), ), ); }); app.get("/workshop/:structure_id/new_db_modal", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/new_db_modal", { structure: model.getStructure(db, req.params.structure_id), }), ), ); }); app.get("/workshop/:structure_id/clone_modal", (req, res) => { const structure = model.getStructure(db, req.params.structure_id); const dbs = model.getDbsForStructure(db, req.params.structure_id); return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/clone_structure_modal", { structure, dbs, defaultRoutePrefix: path.join(structure.route_prefix || "", "clone"), }), ), ); }); function embedHTML(url) { return `
`; } app.all("*", async (req, res) => { const req_path = req.path; const verb = req.method; try { let routeMatch = null; for (let { matcher, id } of routes[verb]) { let matchFromRoute = matcher(req_path); if (matchFromRoute) { routeMatch = { params: matchFromRoute.params, id: id }; } } if (routeMatch) { req.params = routeMatch.params; const route = db .prepare("SELECT * FROM routes WHERE id = ?") .get(routeMatch.id); const structure = model.getStructure(db, route.structure_id); const __urlPrefix = structure.route_prefix; 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(db, route.structure_id, route.id, { req, res, eta, }); res.render = (template, context) => { context = context || {}; context.route = function (url) { return __urlPrefix ? path.join(__urlPrefix, url) : url; }; const eta = model.getTemplater(route.structure_id); res.send( bootstrapTemplateWithHTMXetc( eta.render(template, context), `/workshop/${route.structure_id}/route/${route.id}`, `/workshop/${route.structure_id}/clone_modal/`, verb == "GET" ? embedHTML(req.originalUrl) : null, structure.head_injection, 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) { const error = e.stack ? `${e}\n\n${e.stack}` : `${e}`; model.createLog(db, structure.id, route.id, error, true); return res.status(500).send(error); } } 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}`); });