make em private

This commit is contained in:
Your Name 2026-08-21 09:37:19 -04:00
parent 1394ce1e0c
commit 02cdca748d
4 changed files with 123 additions and 37 deletions

105
db.js
View file

@ -126,7 +126,10 @@ function inspectableContext(data) {
function annotateTemplateHtml(html, template, data) {
const context = inspectableContext(data);
const contextId = randomUUID();
templateContextCache.set(contextId, { templateId: String(template.id), context });
templateContextCache.set(contextId, {
templateId: String(template.id),
context,
});
function annotate($, element) {
const current = $(element).attr("data-bliss-templates");
let templates = [];
@ -147,7 +150,10 @@ function annotateTemplateHtml(html, template, data) {
} catch (_) {}
}
contextIds[String(template.id)] = contextId;
$(element).attr("data-bliss-template-context-ids", JSON.stringify(contextIds));
$(element).attr(
"data-bliss-template-context-ids",
JSON.stringify(contextIds),
);
if (!$(element).attr("data-bliss-template-id")) {
$(element).attr({
"data-bliss-template-id": String(template.id),
@ -207,14 +213,20 @@ function createStructure(name, userId) {
return info.lastInsertRowid; // Returns the structure_id of the newly created structure
}
// ---- access control (private structures) --------------------------------
// ---- access control -------------------------------------------------------
//
// A structure is public unless `private` is set. Access to a private structure
// is exactly its member list (structure_members) — a flat set of equals, no
// creator or owner. Anyone on the list can reach it, edit it, manage the list,
// or make it public again. This is the single source of truth the platform
// consults before serving a user route, mounting a db, or opening the workshop
// — never the structure's own code.
// Two independent flags, three tiers of reach:
//
// public = 1 -> anyone, no login (opt-in, with a warning)
// public = 0, private = 0 -> any logged-in user (the default)
// public = 0, private = 1 -> invited members only
//
// So login is required by default; making a structure public is the explicit
// way back onto the open web. `private` layers a stricter members-only gate on
// top: access is then exactly its member list (structure_members) — a flat set
// of equals, no creator or owner. This is the single source of truth the
// platform consults before serving a user route, mounting a db, or opening the
// workshop — never the structure's own code.
function isMember(structureId, userId) {
return !!db
@ -227,9 +239,10 @@ function isMember(structureId, userId) {
function canAccessStructure(userId, structureId) {
const s = getStructure(structureId);
if (!s) return false;
if (!s.private) return true;
if (userId == null) return false;
return isMember(structureId, userId);
if (s.public) return true; // open web, no login
if (userId == null) return false; // everything else needs a login
if (!s.private) return true; // any logged-in user
return isMember(structureId, userId); // members-only
}
// Members of a structure, with their usernames, for the settings UI.
@ -271,16 +284,28 @@ function setStructurePrivacy(structureId, isPrivate, actorUserId) {
if (isPrivate && actorUserId != null) addMember(structureId, actorUserId);
}
// Put a structure on the open web (public = 1) or take it back off (login
// required). Independent of the members-only `private` flag; public wins in
// canAccessStructure, so a public structure is reachable regardless of private.
function setStructurePublic(structureId, isPublic) {
const s = getStructure(structureId);
if (!s) return;
db.prepare("UPDATE structures SET public = ? WHERE id = ?").run(
isPublic ? 1 : 0,
structureId,
);
}
function createRoute(verb, path, structureId, handler) {
path = encodeURI(path);
const stmt = db.prepare(
"INSERT INTO routes (verb, path, structure_id, handler) VALUES (?, ?, ?, ?)"
"INSERT INTO routes (verb, path, structure_id, handler) VALUES (?, ?, ?, ?)",
);
const info = stmt.run(verb, path, structureId, handler);
const routeId = info.lastInsertRowid; // Get the newly created route ID
if (verb !== 'GET') {
if (verb !== "GET") {
createScaffoldPage(routeId);
}
@ -306,13 +331,17 @@ const ROUTE_SELECT = `
function getRoutes(structureId) {
return db
.prepare(`${ROUTE_SELECT} WHERE r.structure_id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC`)
.prepare(
`${ROUTE_SELECT} WHERE r.structure_id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC`,
)
.all(structureId);
}
function getRoute(routeId) {
return db
.prepare(`${ROUTE_SELECT} WHERE r.id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC LIMIT 1`)
.prepare(
`${ROUTE_SELECT} WHERE r.id = ? ORDER BY sp.created_at DESC, sp2.created_at DESC LIMIT 1`,
)
.get(routeId);
}
@ -321,7 +350,9 @@ function update(table, fields, obj) {
const placeholders = fields.map((field) => `${field} = ?`).join(", ");
const values = fields.map((field) => obj[field]);
values.push(obj.id);
return db.prepare(`UPDATE ${table} SET ${placeholders} WHERE id = ?`).run(...values);
return db
.prepare(`UPDATE ${table} SET ${placeholders} WHERE id = ?`)
.run(...values);
}
// ---- version history (append-only) --------------------------------------
@ -337,7 +368,11 @@ function snapshotFor(entityType, obj) {
case "route":
return { verb: obj.verb, path: obj.path, handler: obj.handler };
case "template":
return { name: obj.name, content: obj.content, test_object: obj.test_object };
return {
name: obj.name,
content: obj.content,
test_object: obj.test_object,
};
case "db":
return { name: obj.name, library: obj.library };
default:
@ -377,7 +412,11 @@ function getVersion(versionId) {
}
function updateRoute(route) {
update("routes", ["verb", "path", "structure_id", "handler", "updated_at", "error"], route);
update(
"routes",
["verb", "path", "structure_id", "handler", "updated_at", "error"],
route,
);
recordVersion("route", route.id, route.structure_id, route);
}
@ -689,18 +728,25 @@ function getMostRecentLogIdByRoute(routeId) {
}
function buildScaffoldUrl(endpoint, urlParams, queryString) {
let populatedUrl = endpoint.replace(/:([^/]+)/g, () => urlParams.shift() || '');
let populatedUrl = endpoint.replace(
/:([^/]+)/g,
() => urlParams.shift() || "",
);
return queryString ? `${populatedUrl}?${queryString}` : populatedUrl;
}
function createScaffoldPage(routeId, content=null) {
function createScaffoldPage(routeId, content = null) {
if (!content) {
const route = getRoute(routeId)
const endpoint = buildScaffoldUrl(route.path, route.url_params, route.query_params);
const route = getRoute(routeId);
const endpoint = buildScaffoldUrl(
route.path,
route.url_params,
route.query_params,
);
content = getDefaultScaffoldContentByVerb(endpoint, route.verb);
}
const stmt = db.prepare(
"INSERT INTO scaffold_pages (route_id, content) VALUES (?, ?)"
"INSERT INTO scaffold_pages (route_id, content) VALUES (?, ?)",
);
const info = stmt.run(routeId, content);
return info.lastInsertRowid; // Returns the scaffold_page id of the newly created scaffold page
@ -708,7 +754,7 @@ function createScaffoldPage(routeId, content=null) {
function getLatestScaffoldPage(routeId) {
const stmt = db.prepare(
"SELECT * FROM scaffold_pages WHERE route_id = ? ORDER BY created_at DESC LIMIT 1"
"SELECT * FROM scaffold_pages WHERE route_id = ? ORDER BY created_at DESC LIMIT 1",
);
return stmt.get(routeId);
}
@ -819,11 +865,9 @@ function generateWSPage(endpoint) {
function getDefaultScaffoldContentByVerb(url, verb) {
if (verb == "POST") {
return generatePostForm(url);
}
else if (verb == "PUT" || verb == "DELETE") {
} else if (verb == "PUT" || verb == "DELETE") {
return generateModifyForm(url, verb);
}
else if (verb == "WS") {
} else if (verb == "WS") {
return generateWSPage(url);
}
}
@ -845,6 +889,7 @@ module.exports = {
addMember,
removeMember,
setStructurePrivacy,
setStructurePublic,
createRoute,
getRoutes,
getRoute,
@ -877,5 +922,5 @@ module.exports = {
getMostRecentLogIdByRoute,
createScaffoldPage,
getLatestScaffoldPage,
updateScaffoldPage
updateScaffoldPage,
};