private structures
This commit is contained in:
parent
1f7dbf21e0
commit
e179814859
6 changed files with 243 additions and 18 deletions
101
index.js
101
index.js
|
|
@ -72,6 +72,16 @@ app.use(
|
|||
app.use("/", wsRouter);
|
||||
app.use("/plumbing", require("./plumbing"));
|
||||
|
||||
// Every workshop route is scoped by :structure_id, so one param guard covers
|
||||
// the whole editor: a private structure 404s for anyone who isn't on its member
|
||||
// list, exactly like its user-facing routes do.
|
||||
app.param("structure_id", (req, res, next, id) => {
|
||||
if (!model.canAccessStructure(req.session.userId, id)) {
|
||||
return res.status(404).send("Not found");
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
const vapidPublicKey = process.env.VAPID_PUBLIC_KEY;
|
||||
const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY;
|
||||
|
||||
|
|
@ -165,10 +175,21 @@ function runLibrary(sql, librarySource, console) {
|
|||
return libContext.module.exports;
|
||||
}
|
||||
|
||||
// The `require(name)` targets available to a user handler.
|
||||
function makeLibs(structureId, console) {
|
||||
// The `require(name)` targets available to a user handler. `memberUserId` is the
|
||||
// user on whose behalf the handler runs. A structure can always mount its own
|
||||
// dbs, but a *foreign* db aliased in from another structure is only mounted if
|
||||
// that structure is reachable by this user — otherwise require('db')(alias)
|
||||
// throws, so aliasing a private db into a public structure can't leak it. (WS
|
||||
// handlers compile with no user; own dbs still mount, foreign private ones don't.)
|
||||
function makeLibs(structureId, console, memberUserId) {
|
||||
const dbs = {};
|
||||
const forbidden = new Set();
|
||||
for (let appDb of model.getDbsForStructure(structureId)) {
|
||||
const ownDb = String(appDb.db_struct_id) === String(structureId);
|
||||
if (!ownDb && !model.canAccessStructure(memberUserId, appDb.db_struct_id)) {
|
||||
forbidden.add(appDb.alias);
|
||||
continue;
|
||||
}
|
||||
const sql = model.getDbInstance(appDb.id);
|
||||
dbs[appDb.alias] = {
|
||||
library: runLibrary(sql, appDb.library, console),
|
||||
|
|
@ -177,7 +198,12 @@ function makeLibs(structureId, console) {
|
|||
}
|
||||
return {
|
||||
eta: model.getTemplater(structureId),
|
||||
db: (alias) => dbs[alias],
|
||||
db: (alias) => {
|
||||
if (forbidden.has(alias)) {
|
||||
throw new Error(`db '${alias}' is private; you do not have access`);
|
||||
}
|
||||
return dbs[alias];
|
||||
},
|
||||
push: push,
|
||||
files: { saveFile: (...args) => saveFile(structureId, ...args) },
|
||||
};
|
||||
|
|
@ -197,10 +223,10 @@ function currentUserFor(req) {
|
|||
}
|
||||
}
|
||||
|
||||
function bootstrapContext(structureId, routeId, initContext) {
|
||||
function bootstrapContext(structureId, routeId, initContext, memberUserId) {
|
||||
const structure = model.getStructure(structureId);
|
||||
const console = makeConsole(structureId, routeId);
|
||||
const libs = makeLibs(structureId, console);
|
||||
const libs = makeLibs(structureId, console, memberUserId);
|
||||
|
||||
return vm.createContext({
|
||||
...initContext,
|
||||
|
|
@ -227,6 +253,11 @@ function compileWebsocketHandler(route) {
|
|||
try {
|
||||
// Keep the existing WS handler context for compatibility. Restricting the
|
||||
// exposed capabilities is a separate runtime-sandboxing change.
|
||||
//
|
||||
// WS handlers compile once at startup, not per connection, so there is no
|
||||
// requesting user here. With no user, makeLibs still mounts the structure's
|
||||
// own dbs but blocks foreign private ones. Who may actually *open* the socket
|
||||
// is enforced per-connection below.
|
||||
const context = bootstrapContext(route.structure_id, route.id, { app });
|
||||
const handler = vm.runInContext(`${route.handler}\n\nhandler;`, context);
|
||||
model.updateRoute({ ...route, error: null });
|
||||
|
|
@ -284,6 +315,9 @@ wsRouter.ws("*", (ws, req) => {
|
|||
}
|
||||
|
||||
const { route } = found;
|
||||
if (!model.canAccessStructure(req.session && req.session.userId, route.structure_id)) {
|
||||
return ws.close(1008, "WebSocket route not found");
|
||||
}
|
||||
req.params = found.params;
|
||||
let clients = wsConnections.get(route.id);
|
||||
if (!clients) {
|
||||
|
|
@ -594,9 +628,10 @@ app.post("/workshop", (req, res) => {
|
|||
});
|
||||
|
||||
app.get("/workshop", (req, res) => {
|
||||
return renderWorkshop(res, "workshop/index", {
|
||||
structures: model.getStructures(),
|
||||
});
|
||||
const structures = model
|
||||
.getStructures()
|
||||
.filter((s) => model.canAccessStructure(req.session.userId, s.id));
|
||||
return renderWorkshop(res, "workshop/index", { structures });
|
||||
});
|
||||
|
||||
function renderWorkshop(res, template, context) {
|
||||
|
|
@ -928,6 +963,7 @@ app.get("/workshop/:structure_id/files", (req, res) => {
|
|||
|
||||
app.get("/workshop/:structure_id/settings", (req, res) => {
|
||||
return renderWorkshop(res, "workshop/settings", {
|
||||
members: model.getMembers(req.params.structure_id),
|
||||
...sidebarStuff(req.params.structure_id),
|
||||
});
|
||||
});
|
||||
|
|
@ -944,6 +980,15 @@ app.put("/workshop/:structure_id/settings", (req, res) => {
|
|||
routePrefix = routePrefix.substring(0, routePrefix.length - 1);
|
||||
}
|
||||
|
||||
// An unchecked checkbox sends no field at all, so absence means "public".
|
||||
const wantPrivate = req.body.private === "on" || req.body.private === "1";
|
||||
if (wantPrivate && !req.session.userId) {
|
||||
return res
|
||||
.status(403)
|
||||
.send("log in before making a structure private, or you'll lock everyone out");
|
||||
}
|
||||
model.setStructurePrivacy(structId, wantPrivate, req.session.userId);
|
||||
|
||||
model.updateStruct({
|
||||
...struct,
|
||||
route_prefix: routePrefix,
|
||||
|
|
@ -955,6 +1000,29 @@ app.put("/workshop/:structure_id/settings", (req, res) => {
|
|||
res.send("success!");
|
||||
});
|
||||
|
||||
// Invite a member to a private structure by username. Renders the updated
|
||||
// member list back into the settings page (hx-target).
|
||||
app.post("/workshop/:structure_id/members", (req, res) => {
|
||||
const structId = req.params.structure_id;
|
||||
const user = model.getUser((req.body.username || "").trim());
|
||||
if (user) model.addMember(structId, user.id);
|
||||
return renderWorkshop(res, "workshop/members", {
|
||||
structure: model.getStructure(structId),
|
||||
members: model.getMembers(structId),
|
||||
notFound: user ? null : req.body.username,
|
||||
});
|
||||
});
|
||||
|
||||
app.delete("/workshop/:structure_id/members/:user_id", (req, res) => {
|
||||
const structId = req.params.structure_id;
|
||||
model.removeMember(structId, req.params.user_id);
|
||||
return renderWorkshop(res, "workshop/members", {
|
||||
structure: model.getStructure(structId),
|
||||
members: model.getMembers(structId),
|
||||
notFound: null,
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/workshop/:structure_id/new_template_modal", (req, res) => {
|
||||
return renderWorkshop(res, "workshop/new_template_modal", {
|
||||
structure: model.getStructure(req.params.structure_id),
|
||||
|
|
@ -999,6 +1067,12 @@ app.all("*", async (req, res) => {
|
|||
req.params = routeMatch.params;
|
||||
const route = routeMatch.route;
|
||||
|
||||
// Private structures are invisible to uninvited users: 404, so a private
|
||||
// route is indistinguishable from one that doesn't exist.
|
||||
if (!model.canAccessStructure(req.session.userId, route.structure_id)) {
|
||||
return res.status(404).json({ success: false, message: "Path not found" });
|
||||
}
|
||||
|
||||
// Minimal, handcrafted view of the logged-in Bliss user for user routes.
|
||||
// Deliberately NOT the raw session/user object — just {id, username} —
|
||||
// so structures can greet whoever is logged in without exposing internals.
|
||||
|
|
@ -1009,11 +1083,12 @@ app.all("*", async (req, res) => {
|
|||
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,
|
||||
});
|
||||
let context = bootstrapContext(
|
||||
route.structure_id,
|
||||
route.id,
|
||||
{ req, res, eta },
|
||||
req.session.userId,
|
||||
);
|
||||
|
||||
res.render = (template, context = {}) => {
|
||||
context.route = makeRoute(structure);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue