1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-09-06 04:48:01 +00:00

add middleware to route handlers for bags & tiddlers routes

This commit is contained in:
webplusai
2024-09-20 18:22:40 +00:00
parent 0f0d8be425
commit 0885eda67d
12 changed files with 188 additions and 161 deletions

View File

@@ -482,13 +482,21 @@ Server.prototype.requestAuthentication = function(response) {
};
Server.prototype.redirectToLogin = function(response, returnUrl) {
if(!response.headersSent) {
response.setHeader('Set-Cookie', `returnUrl=${returnUrl}; HttpOnly; Path=/`);
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
response.writeHead(302, {
'Location': loginUrl
});
response.end();
if (!response.headersSent) {
const validReturnUrlRegex = /^\/(?!.*\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot)$).*$/;
var sanitizedReturnUrl = '/'; // Default to home page
if (validReturnUrlRegex.test(returnUrl)) {
sanitizedReturnUrl = returnUrl;
} else {
console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`);
}
response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Path=/`);
const loginUrl = '/login';
response.writeHead(302, {
'Location': loginUrl
});
response.end();
}
};

View File

@@ -12,25 +12,32 @@ DELETE /bags/:bag_name/tiddler/:title
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "DELETE";
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "bag", "WRITE");
// Get the parameters
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]);
if(bag_name) {
var result = $tw.mws.store.deleteTiddler(title,bag_name);
response.writeHead(204, "OK", {
"X-Revision-Number": result.tiddler_id.toString(),
Etag: state.makeTiddlerEtag(result),
"Content-Type": "text/plain"
});
response.end();
if(!response.headersSent) {
var result = $tw.mws.store.deleteTiddler(title,bag_name);
response.writeHead(204, "OK", {
"X-Revision-Number": result.tiddler_id.toString(),
Etag: state.makeTiddlerEtag(result),
"Content-Type": "text/plain"
});
response.end();
}
} else {
response.writeHead(404);
response.end();
if(!response.headersSent) {
response.writeHead(404);
response.end();
}
}
};

View File

@@ -12,17 +12,20 @@ GET /bags/:bag_name/tiddler/:title/blob
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "GET";
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/([^\/]+)\/blob$/;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "bag", "READ");
// Get the parameters
const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]);
if(bag_name) {
const result = $tw.mws.store.getBagTiddlerStream(title,bag_name);
if(result) {
if(result && !response.headersSent) {
response.writeHead(200, "OK",{
Etag: state.makeTiddlerEtag(result),
"Content-Type": result.type,
@@ -31,8 +34,10 @@ exports.handler = function(request,response,state) {
return;
}
}
response.writeHead(404);
response.end();
if (!response.headersSent) {
response.writeHead(404);
response.end();
}
};
}());

View File

@@ -16,11 +16,14 @@ fallback=<url> // Optional redirect if the tiddler is not found
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "GET";
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "gab", "READ");
// Get the parameters
const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]),
@@ -37,29 +40,37 @@ exports.handler = function(request,response,state) {
// This is not a JSON API request, we should return the raw tiddler content
const result = $tw.mws.store.getBagTiddlerStream(title,bag_name);
if(result) {
response.writeHead(200, "OK",{
Etag: state.makeTiddlerEtag(result),
"Content-Type": result.type
});
if(!response.headersSent){
response.writeHead(200, "OK",{
Etag: state.makeTiddlerEtag(result),
"Content-Type": result.type
});
}
result.stream.pipe(response);
return;
} else {
response.writeHead(404);
response.end();
if(!response.headersSent){
response.writeHead(404);
response.end();
}
return;
}
}
} else {
// Redirect to fallback URL if tiddler not found
if(state.queryParameters.fallback) {
response.writeHead(302, "OK",{
"Location": state.queryParameters.fallback
});
response.end();
if (!response.headersSent){
response.writeHead(302, "OK",{
"Location": state.queryParameters.fallback
});
response.end();
}
return;
} else {
response.writeHead(404);
response.end();
if(!response.headersSent){
response.writeHead(404);
response.end();
}
return;
}
}

View File

@@ -16,11 +16,14 @@ fallback=<url> // Optional redirect if the tiddler is not found
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "GET";
exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "recipe", "READ");
// Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]),
@@ -38,27 +41,32 @@ exports.handler = function(request,response,state) {
} else {
// This is not a JSON API request, we should return the raw tiddler content
var type = tiddlerInfo.tiddler.type || "text/plain";
response.writeHead(200, "OK",{
if(!response.headersSent) {
response.writeHead(200, "OK",{
Etag: state.makeTiddlerEtag(tiddlerInfo),
"Content-Type": type
});
response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding);
response.end();;
"Content-Type": type
});
response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding);
response.end();
}
return;
}
} else {
// Redirect to fallback URL if tiddler not found
if(state.queryParameters.fallback) {
response.writeHead(302, "OK",{
"Location": state.queryParameters.fallback
});
response.end();
return;
} else {
response.writeHead(404);
response.end();
return;
if(!response.headersSent) {
// Redirect to fallback URL if tiddler not found
if(state.queryParameters.fallback) {
response.writeHead(302, "OK",{
"Location": state.queryParameters.fallback
});
response.end();
return;
} else {
response.writeHead(404);
response.end();
return;
}
}
return;
}
};

View File

@@ -12,27 +12,32 @@ GET /recipes/:recipe_name/tiddlers.json?last_known_tiddler_id=:last_known_tiddle
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "GET";
exports.path = /^\/recipes\/([^\/]+)\/tiddlers.json$/;
exports.handler = function(request,response,state) {
// Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]);
if(recipe_name) {
// Get the tiddlers in the recipe, optionally since the specified last known tiddler_id
var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{
include_deleted: state.queryParameters.include_deleted === "true",
last_known_tiddler_id: state.queryParameters.last_known_tiddler_id
});
if(recipeTiddlers) {
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeTiddlers),"utf8");
return;
aclMiddleware(request, response, state, "recipe", "READ");
if(!response.headersSent) {
// Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]);
if(recipe_name) {
// Get the tiddlers in the recipe, optionally since the specified last known tiddler_id
var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{
include_deleted: state.queryParameters.include_deleted === "true",
last_known_tiddler_id: state.queryParameters.last_known_tiddler_id
});
if(recipeTiddlers) {
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeTiddlers),"utf8");
return;
}
}
// Fail if something went wrong
response.writeHead(404);
response.end();
}
// Fail if something went wrong
response.writeHead(404);
response.end();
};

View File

@@ -12,6 +12,8 @@ POST /bags/:bag_name/tiddlers/
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "POST";
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/$/;
@@ -21,6 +23,7 @@ exports.bodyFormat = "stream";
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "bag", "WRITE");
const path = require("path"),
fs = require("fs"),
processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream;
@@ -39,29 +42,31 @@ exports.handler = function(request,response,state) {
"imported-tiddlers": results
}));
} else {
response.writeHead(200, "OK",{
"Content-Type": "text/html"
});
response.write(`
<!doctype html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
`);
// Render the html
var html = $tw.mws.store.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/post-bag-tiddlers",{
variables: {
"bag-name": bag_name,
"imported-titles": JSON.stringify(results)
}
});
response.write(html);
response.write(`
</body>
</html>
`);
response.end();
if(!response.headersSent) {
response.writeHead(200, "OK",{
"Content-Type": "text/html"
});
response.write(`
<!doctype html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
`);
// Render the html
var html = $tw.mws.store.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/post-bag-tiddlers",{
variables: {
"bag-name": bag_name,
"imported-titles": JSON.stringify(results)
}
});
response.write(html);
response.write(`
</body>
</html>
`);
response.end();
}
}
}
});

View File

@@ -17,6 +17,8 @@ description
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "POST";
exports.path = /^\/bags$/;
@@ -26,6 +28,7 @@ exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "bag", "WRITE");
if(state.data.bag_name) {
const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description);
if(!result) {

View File

@@ -18,6 +18,8 @@ bag_names: space separated list of bags
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "POST";
exports.path = /^\/recipes$/;
@@ -27,6 +29,7 @@ exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "recipe", "WRITE");
if(state.data.recipe_name && state.data.bag_names) {
const result = $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description);
if(!result) {

View File

@@ -12,16 +12,19 @@ PUT /bags/:bag_name
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "PUT";
exports.path = /^\/bags\/(.+)$/;
exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "bag", "WRITE");
// Get the parameters
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
data = $tw.utils.parseJSONSafe(state.data);
if(bag_name && data) {
const result = $tw.mws.store.createBag(bag_name,data.description);
var result = $tw.mws.store.createBag(bag_name,data.description);
if(!result) {
state.sendResponse(204,{
"Content-Type": "text/plain"
@@ -34,8 +37,10 @@ exports.handler = function(request,response,state) {
"utf8");
}
} else {
response.writeHead(404);
response.end();
if(!response.headersSent) {
response.writeHead(404);
response.end();
}
}
};

View File

@@ -12,20 +12,20 @@ PUT /recipes/:recipe_name
/*global $tw: false */
"use strict";
var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware;
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "PUT";
exports.path = /^\/recipes\/(.+)$/;
exports.handler = function (request, response, state) {
aclMiddleware(request, response, state, 'recipe', 'WRITE');
aclMiddleware(request, response, state, "recipe", "WRITE");
// Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
data = $tw.utils.parseJSONSafe(state.data);
if (recipe_name && data) {
const result = $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description);
if (!result) {
if(recipe_name && data) {
var result = $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description);
if(!result) {
state.sendResponse(204, {
"Content-Type": "text/plain"
});
@@ -37,7 +37,7 @@ PUT /recipes/:recipe_name
"utf8");
}
} else {
if (!response.headersSent) {
if(!response.headersSent) {
response.writeHead(404);
response.end();
}

View File

@@ -163,7 +163,7 @@ SqlTiddlerDatabase.prototype.createTables = function() {
-- ACL table (using bag/recipe ids directly)
CREATE TABLE IF NOT EXISTS acl (
acl_id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_id INTEGER NOT NULL,
entity_name TEXT NOT NULL,
entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')),
role_id INTEGER,
permission_id INTEGER,
@@ -178,7 +178,7 @@ SqlTiddlerDatabase.prototype.createTables = function() {
`,`
CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id)
`,`
CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_id)
CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name)
`]);
};
@@ -482,7 +482,7 @@ SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName)
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.permission_id
JOIN acl ON rp.role_id = acl.role_id AND rp.permission_id = acl.permission_id
JOIN recipes r ON acl.entity_id = r.recipe_id
JOIN recipes r ON acl.entity_name = r.recipe_id
WHERE u.user_id = $user_id
AND r.recipe_name = $recipe_name
AND p.permission_name = 'read'
@@ -507,7 +507,7 @@ SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permis
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.permission_id
JOIN acl ON rp.role_id = acl.role_id AND rp.permission_id = acl.permission_id
JOIN bags b ON acl.entity_id = b.bag_id
JOIN bags b ON acl.entity_name = b.bag_id
WHERE u.user_id = $user_id
AND b.bag_name = $bag_name
AND p.permission_name = 'read'
@@ -523,79 +523,46 @@ SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permis
SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName) {
const entityTypeToTableMap = {
bag: {
table: 'bags',
column: 'bag_name'
},
recipe: {
table: 'recipes',
column: 'recipe_name'
}
bag: {
table: 'bags',
column: 'bag_name'
},
recipe: {
table: 'recipes',
column: 'recipe_name'
}
};
const entityInfo = entityTypeToTableMap[entityType];
if (!entityInfo) {
throw new Error('Invalid entity type: ' + entityType);
throw new Error('Invalid entity type: ' + entityType);
}
console.log("Starting ACL permission check:", { userId, entityType, entityName, permissionName, entityInfo });
// Step 1: Get the entity ID
const entityQuery = `
SELECT ${entityInfo.table.slice(0, -1)}_id as entity_id
FROM ${entityInfo.table}
WHERE ${entityInfo.column} = $entity_name
const query = `
SELECT 1
FROM users u
JOIN user_roles ur ON u.user_id = ur.user_id
JOIN roles r ON ur.role_id = r.role_id
JOIN acl a ON r.role_id = a.role_id
JOIN permissions p ON a.permission_id = p.permission_id
WHERE u.user_id = $user_id
AND a.entity_type = $entity_type
AND a.entity_name = $entity_name
AND p.permission_name = $permission_name
LIMIT 1
`;
const entityResult = this.engine.runStatementGet(entityQuery, { $entity_name: entityName });
console.log("Entity query result:", entityResult);
if (!entityResult) {
console.log(`${entityType} not found: ${entityName}`);
return false;
}
const entityId = entityResult.entity_id;
// Step 2: Get user's roles
const userRolesQuery = `
SELECT r.role_id
FROM users u
JOIN user_roles ur ON u.user_id = ur.user_id
JOIN roles r ON ur.role_id = r.role_id
WHERE u.user_id = $user_id
`;
const userRoles = this.engine.runStatementGetAll(userRolesQuery, { $user_id: userId });
console.log("User roles:", userRoles);
if (userRoles.length === 0) {
console.log(`No roles found for user: ${userId}`);
return false;
}
// Step 3: Check for permission
const roleIds = userRoles.map(role => role.role_id);
const permissionQuery = `
SELECT 1
FROM acl a
JOIN permissions p ON a.permission_id = p.permission_id
WHERE a.role_id IN (${roleIds.join(',')})
AND a.entity_type = $entity_type
AND a.entity_id = $entity_id
AND p.permission_name = $permission_name
LIMIT 1
`;
const permissionResult = this.engine.runStatementGet(permissionQuery, {
$entity_type: entityType,
$entity_id: entityId,
$permission_name: permissionName
const result = this.engine.runStatementGet(query, {
$user_id: userId,
$entity_type: entityType,
$entity_name: entityName,
$permission_name: permissionName
});
console.log("Permission query result:", permissionResult);
const hasPermission = permissionResult !== undefined;
console.log("Final permission check result:", hasPermission);
const hasPermission = result !== undefined;
return hasPermission;
};
};;
/*
Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist
@@ -1079,7 +1046,7 @@ SqlTiddlerDatabase.prototype.listPermissions = function() {
// ACL CRUD operations
SqlTiddlerDatabase.prototype.createACL = function(entityId, entityType, roleId, permissionId) {
const result = this.engine.runStatement(`
INSERT INTO acl (entity_id, entity_type, role_id, permission_id)
INSERT INTO acl (entity_name, entity_type, role_id, permission_id)
VALUES ($entityId, $entityType, $roleId, $permissionId)
`, {
$entityId: entityId,
@@ -1101,7 +1068,7 @@ SqlTiddlerDatabase.prototype.getACL = function(aclId) {
SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, roleId, permissionId) {
this.engine.runStatement(`
UPDATE acl
SET entity_id = $entityId, entity_type = $entityType,
SET entity_name = $entityId, entity_type = $entityType,
role_id = $roleId, permission_id = $permissionId
WHERE acl_id = $aclId
`, {
@@ -1123,7 +1090,7 @@ SqlTiddlerDatabase.prototype.deleteACL = function(aclId) {
SqlTiddlerDatabase.prototype.listACLs = function() {
return this.engine.runStatementGetAll(`
SELECT * FROM acl ORDER BY entity_type, entity_id
SELECT * FROM acl ORDER BY entity_type, entity_name
`);
};