1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-04-07 11:16:55 +00:00

remove list users command & added support for database in server options

This commit is contained in:
webplusai 2024-10-10 08:59:05 +00:00
parent f02c8562f0
commit 81f73de87d
17 changed files with 184 additions and 154 deletions

View File

@ -336,7 +336,7 @@ Get the browser location.hash. We don't use location.hash because of the way tha
*/
$tw.utils.getLocationHash = function() {
var href = window.location.href;
var idx = href.indexOf("#");
var idx = href.indexOf('#');
if(idx === -1) {
return "#";
} else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") {
@ -605,7 +605,7 @@ var globalCheck =[
" delete Object.prototype.__temp__;",
" }",
" delete Object.prototype.__temp__;",
].join("\n");
].join('\n');
/*
Run code globally with specified context variables in scope
@ -1997,7 +1997,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
value = path.relative(rootPath, filename).split(path.sep).slice(0, -1);
break;
case "filepath":
value = path.relative(rootPath, filename).split(path.sep).join("/");
value = path.relative(rootPath, filename).split(path.sep).join('/');
break;
case "filename":
value = path.basename(filename);
@ -2623,7 +2623,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
}
taskIndex++;
}
if(typeof callback === "function") {
if(typeof callback === 'function') {
callback();
}
return false;

View File

@ -11,9 +11,6 @@
"tiddlywiki/snowwhite"
],
"build": {
"--mws-list-users": [
"--mws-list-users"
],
"mws-add-user": [
"--mws-add-permission", "READ", "Allows user to create tiddlers",
"--mws-add-permission", "WRITE", "Gives the user the permission to edit and delete tiddlers",

View File

@ -1,6 +1,5 @@
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form
<$macrocall $name="loginForm"/>
<form class="login-form" method="POST" action="/login">
<input type="hidden" name="returnUrl" value=<<returnUrl>>/>
<input type="text" name="username" placeholder="Username"/>

View File

@ -1,46 +0,0 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-list-users.js
type: application/javascript
module-type: command
Command to list users
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.info = {
name: "mws-list-users",
synchronous: false
};
var Command = function(params,commander,callback) {
this.params = params;
this.commander = commander;
this.callback = callback;
};
Command.prototype.execute = function() {
var self = this;
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
return "Error: MultiWikiServer or SQL database not initialized.";
}
var users = $tw.mws.store.sqlTiddlerDatabase.listUsers().map(function(user){
return ({
username: user.username,
email: user.email,
created_at: user.created_at,
})
});
console.log("Users:", users);
self.callback(null, "Users retrieved successfully:\n" + JSON.stringify(users, null, 2));
};
exports.Command = Command;
})();

View File

@ -50,11 +50,18 @@ TestRunner.prototype.runTests = function(callback) {
const self = this;
let currentTestSpec = 0;
let hasFailed = false;
let sessionId;
function runNextTest() {
if(currentTestSpec < testSpecs.length) {
const testSpec = testSpecs[currentTestSpec];
if(!!sessionId) {
testSpec.headers['Cookie'] = `session=${sessionId}; HttpOnly; Path=/`;
}
currentTestSpec += 1;
self.runTest(testSpec,function(err) {
self.runTest(testSpec,function(err, data) {
if(data?.sessionId) {
sessionId = data?.sessionId;
}
if(err) {
hasFailed = true;
console.log(`Failed "${testSpec.description}" with "${err}"`)
@ -96,7 +103,7 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
response.on("end", () => {
const jsonData = $tw.utils.parseJSONSafe(buffer,function() {return undefined;});
const testResult = testSpec.expectedResult(jsonData,buffer,response.headers);
callback(testResult ? null : "Test failed");
callback(testResult ? null : "Test failed", jsonData);
});
});
request.on("error", (e) => {
@ -112,6 +119,20 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
};
const testSpecs = [
{
description: "Login Test User",
method: "POST",
path: "/login",
headers: {
"Accept": 'application/json',
"Content-Type": 'application/x-www-form-urlencoded',
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
},
data: "username=user&password=pass123",
expectedResult: (jsonData,data,headers) => {
return !!jsonData.sessionId;
}
},
{
description: "Check index page",
method: "GET",

View File

@ -19,7 +19,8 @@ if($tw.node) {
path = require("path"),
querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
zlib = require("zlib"),
aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware;
}
/*
@ -34,7 +35,7 @@ function Server(options) {
this.authenticators = options.authenticators || [];
this.wiki = options.wiki;
this.boot = options.boot || $tw.boot;
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase;
this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase;
// Initialise the variables
this.variables = $tw.utils.extend({},this.defaultVariables);
if(options.variables) {
@ -158,9 +159,10 @@ function sendResponse(request,response,statusCode,headers,data,encoding) {
data = zlib.gzipSync(data);
}
}
response.writeHead(statusCode,headers);
response.end(data,encoding);
if(!response.headersSent) {
response.writeHead(statusCode,headers);
response.end(data,encoding);
}
}
function redirect(request,response,statusCode,location) {
@ -351,6 +353,13 @@ Server.prototype.methodMappings = {
"DELETE": "writers"
};
Server.prototype.methodACLPermMappings = {
"GET": "READ",
"PUT": "WRITE",
"POST": "WRITE",
"DELETE": "WRITE"
}
/*
Check whether a given user is authorized for the specified authorizationType ("readers" or "writers"). Pass null or undefined as the username to check for anonymous access
*/
@ -411,8 +420,7 @@ Server.prototype.redirectToLogin = function(response, 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.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`); const loginUrl = '/login';
response.writeHead(302, {
'Location': loginUrl
});
@ -468,6 +476,12 @@ Server.prototype.requestHandler = function(request,response,options) {
// Find the route that matches this path
var route = self.findMatchingRoute(request,state);
// If the route is configured to use ACL middleware, check that the user has permission
if(route?.useACL) {
const permissionName = this.methodACLPermMappings[route.method];
aclMiddleware(request,response,state,route.entityName,permissionName)
}
// Optionally output debug info
if(self.get("debug-level") !== "none") {

View File

@ -13,12 +13,14 @@ GET /bags/:bag_name
/*global $tw: false */
"use strict";
var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware;
exports.method = "GET";
exports.path = /^\/bags\/([^\/]+)(\/?)$/;
exports.useACL = true;
exports.entityName = "bag"
exports.handler = function (request, response, state) {
// Redirect if there is no trailing slash. We do this so that the relative URL specified in the upload form works correctly
if (state.params[1] !== "/") {
@ -33,7 +35,6 @@ exports.handler = function (request, response, state) {
if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8");
} else {
aclMiddleware(request, response, state, 'bag', 'READ');
if (!response.headersSent) {
// This is not a JSON API request, we should return the raw tiddler content
response.writeHead(200, "OK", {

View File

@ -16,14 +16,15 @@ 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.useACL = true;
exports.entityName = "recipe"
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]),

View File

@ -12,8 +12,6 @@ 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\/$/;
@ -22,8 +20,11 @@ exports.bodyFormat = "stream";
exports.csrfDisable = true;
exports.useACL = true;
exports.entityName = "bag"
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;

View File

@ -17,8 +17,6 @@ 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$/;
@ -27,8 +25,11 @@ exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.useACL = true;
exports.entityName = "bag"
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

@ -37,17 +37,29 @@ exports.handler = function(request,response,state) {
var sessionId = auth.createSession(user.user_id);
var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
response.writeHead(302, {
'Location': returnUrl || '/'
});
if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({
"sessionId": sessionId
}));
} else {
response.writeHead(302, {
'Location': returnUrl || '/'
});
}
} else {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: "Invalid username or password"
}));
response.writeHead(302, {
'Location': '/login'
});
if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({
"message": "Invalid username or password"
}));
} else {
response.writeHead(302, {
'Location': '/login'
});
}
}
response.end();
};

View File

@ -18,8 +18,6 @@ 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$/;
@ -28,8 +26,11 @@ exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.useACL = true;
exports.entityName = "recipe"
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,14 +12,15 @@ 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.useACL = true;
exports.entityName = "bag"
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);

View File

@ -12,14 +12,15 @@ PUT /recipes/:recipe_name/tiddlers/:title
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "PUT";
exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
exports.useACL = true;
exports.entityName = "recipe"
exports.handler = function (request, response, state) {
aclMiddleware(request, response, state, "recipe", "WRITE");
// Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]),

View File

@ -12,14 +12,15 @@ PUT /recipes/:recipe_name
/*global $tw: false */
"use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "PUT";
exports.path = /^\/recipes\/(.+)$/;
exports.useACL = true;
exports.entityName = "recipe"
exports.handler = function (request, response, state) {
aclMiddleware(request, response, state, "recipe", "WRITE");
// Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
data = $tw.utils.parseJSONSafe(state.data);

View File

@ -214,15 +214,13 @@ SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscon
$accesscontrol: accesscontrol,
$description: description
});
// update the permissions on ACL records
const admin = this.getRoleByName('ADMIN');
const admin = this.getRoleByName("ADMIN");
if(admin) {
const readPermission = this.getPermissionByName('READ');
const writePermission = this.getPermissionByName('WRITE');
this.createACL(bag_name, 'bag', admin.role_id, readPermission.permission_id);
this.createACL(bag_name, 'bag', admin.role_id, writePermission.permission_id);
const readPermission = this.getPermissionByName("READ");
const writePermission = this.getPermissionByName("WRITE");
this.createACL(bag_name, "bag", admin.role_id, readPermission.permission_id);
this.createACL(bag_name, "bag", admin.role_id, writePermission.permission_id);
}
return updateBags.lastInsertRowid;
};
@ -290,12 +288,12 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr
// update the permissions on ACL records
const admin = this.getRoleByName('ADMIN');
const admin = this.getRoleByName("ADMIN");
if(admin) {
const readPermission = this.getPermissionByName('READ');
const writePermission = this.getPermissionByName('WRITE');
this.createACL(recipe_name, 'recipe', admin.role_id, readPermission.permission_id);
this.createACL(recipe_name, 'recipe', admin.role_id, writePermission.permission_id);
const readPermission = this.getPermissionByName("READ");
const writePermission = this.getPermissionByName("WRITE");
this.createACL(recipe_name, "recipe", admin.role_id, readPermission.permission_id);
this.createACL(recipe_name, "recipe", admin.role_id, writePermission.permission_id);
}
return updateRecipes.lastInsertRowid;
};
@ -495,34 +493,34 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) {
Checks if a user has permission to access a recipe
*/
SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) {
return this.checkACLPermission(userId, 'recipe', recipeName, permissionName)
return this.checkACLPermission(userId, "recipe", recipeName, permissionName)
};
/*
Checks if a user has permission to access a bag
*/
SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permissionName) {
return this.checkACLPermission(userId, 'bag', bagName, permissionName)
return this.checkACLPermission(userId, "bag", bagName, permissionName)
};
SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName) {
const entityTypeToTableMap = {
bag: {
table: 'bags',
column: 'bag_name'
table: "bags",
column: "bag_name"
},
recipe: {
table: 'recipes',
column: 'recipe_name'
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);
}
// if the entityName starts with "$:/", we'll assume its a system tiddler, then grant the user permission
// if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission
if(entityName.startsWith("$:/")){
return true
}

View File

@ -5,43 +5,45 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
<$transclude/>
</$set>
</$tiddler>
<div class="user-profile-container">
<div class="user-profile-header">
<div class="user-profile-avatar">
<$text text={{{ [<user>jsonget[username]substr[0,1]uppercase[]] }}}/>
</div>
<h1 class="user-profile-name"><$text text={{{ [<user>jsonget[username]] }}}/></h1>
<p class="user-profile-email"><$text text={{{ [<user>jsonget[email]] }}}/></p>
</div>
<div class="user-profile-details">
<div class="user-profile-item">
<span class="user-profile-label">User ID:</span>
<span class="user-profile-value"><$text text={{{ [<user>jsonget[user_id]] }}}/></span>
</div>
<div class="user-profile-item">
<span class="user-profile-label">Created At:</span>
<span class="user-profile-value"><$text text={{{ [<user>jsonget[created_at]split[T]first[]] }}}/></span>
</div>
<div class="user-profile-item">
<span class="user-profile-label">Last Login:</span>
<span class="user-profile-value"><$text text={{{ [<user>jsonget[last_login]split[T]first[]] }}}/></span>
<div class="main-wrapper">
<div class="user-profile-container">
<div class="user-profile-header">
<div class="user-profile-avatar">
<$text text={{{ [<user>jsonget[username]substr[0,1]uppercase[]] }}}/>
</div>
<h1 class="user-profile-name"><$text text={{{ [<user>jsonget[username]] }}}/></h1>
<p class="user-profile-email"><$text text={{{ [<user>jsonget[email]] }}}/></p>
</div>
<div class="user-profile-roles">
<h2>User Roles</h2>
<ul>
<$list filter="[<user-roles>jsonindexes[]]" variable="role-index">
<li>
<$text text={{{ [<user-roles>jsonextract<role-index>jsonget[role_name]] }}}/>
</li>
</$list>
</ul>
<div class="user-profile-details">
<div class="user-profile-item">
<span class="user-profile-label">User ID:</span>
<span class="user-profile-value"><$text text={{{ [<user>jsonget[user_id]] }}}/></span>
</div>
<div class="user-profile-item">
<span class="user-profile-label">Created At:</span>
<span class="user-profile-value"><$text text={{{ [<user>jsonget[created_at]split[T]first[]] }}}/></span>
</div>
<div class="user-profile-item">
<span class="user-profile-label">Last Login:</span>
<span class="user-profile-value"><$text text={{{ [<user>jsonget[last_login]split[T]first[]] }}}/></span>
</div>
<div class="user-profile-roles">
<h2>User Roles</h2>
<ul>
<$list filter="[<user-roles>jsonindexes[]]" variable="role-index">
<li>
<$text text={{{ [<user-roles>jsonextract<role-index>jsonget[role_name]] }}}/>
</li>
</$list>
</ul>
</div>
</div>
</div>
<$reveal type="match" state="is-current-user-profile" text="yes">
<!-- <$reveal type="match" state="is-current-user-profile" text="yes"> -->
<div class="user-profile-management">
<h2>Manage Your Account</h2>
<form class="user-profile-form">
@ -53,28 +55,42 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
<label for="email">Email:</label>
<input type="email" id="email" name="email" value={{{ [<user>jsonget[email]] }}} />
</div>
<button type="submit" class="update-profile-btn">Update Profile</button>
</form>
<hr />
<h2>Danger Zone</h2>
<form class="user-profile-form">
<div class="form-group">
<label for="new-password">New Password:</label>
<input type="password" id="new-password" name="new-password" />
<input type="password" id="new-password" name="new-password" required />
</div>
<div class="form-group">
<label for="confirm-password">Confirm New Password:</label>
<input type="password" id="confirm-password" name="confirm-password" />
<input type="password" id="confirm-password" name="confirm-password" required />
</div>
<button type="submit" class="update-profile-btn">Update Profile</button>
<button type="submit" class="update-password-btn">Change Password</button>
</form>
</div>
</$reveal>
<!-- </$reveal> -->
</div>
<style>
.main-wrapper {
display: flex;
flex-direction: row;
gap: 5px;
max-width: 80vw;
margin: auto;
}
.user-profile-container {
max-width: 600px;
flex: 1;
margin: 2rem auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
max-width: 600px;
}
.user-profile-header {
@ -155,9 +171,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
}
.user-profile-management {
margin-top: 2rem;
padding: 2rem;
border-top: 1px solid #e0e0e0;
padding: 20px;
}
.user-profile-management h2 {
@ -166,6 +180,10 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
margin-bottom: 1rem;
}
.user-profile-form {
margin-bottom: 20px;
}
.user-profile-form .form-group {
margin-bottom: 1rem;
}
@ -184,7 +202,8 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
border-radius: 4px;
}
.update-profile-btn {
.update-profile-btn,
.update-password-btn {
background: #3498db;
color: #fff;
border: none;
@ -194,7 +213,15 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
font-size: 1rem;
}
.update-password-btn {
background: #00796b;
}
.update-profile-btn:hover {
background: #2980b9;
}
.update-password-btn:hover {
background: #00695c;
}
</style>