1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-07-04 02:52:52 +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() { $tw.utils.getLocationHash = function() {
var href = window.location.href; var href = window.location.href;
var idx = href.indexOf("#"); var idx = href.indexOf('#');
if(idx === -1) { if(idx === -1) {
return "#"; return "#";
} else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") { } 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__;",
" }", " }",
" delete Object.prototype.__temp__;", " delete Object.prototype.__temp__;",
].join("\n"); ].join('\n');
/* /*
Run code globally with specified context variables in scope 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); value = path.relative(rootPath, filename).split(path.sep).slice(0, -1);
break; break;
case "filepath": case "filepath":
value = path.relative(rootPath, filename).split(path.sep).join("/"); value = path.relative(rootPath, filename).split(path.sep).join('/');
break; break;
case "filename": case "filename":
value = path.basename(filename); value = path.basename(filename);
@ -2623,7 +2623,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
} }
taskIndex++; taskIndex++;
} }
if(typeof callback === "function") { if(typeof callback === 'function') {
callback(); callback();
} }
return false; return false;

View File

@ -11,9 +11,6 @@
"tiddlywiki/snowwhite" "tiddlywiki/snowwhite"
], ],
"build": { "build": {
"--mws-list-users": [
"--mws-list-users"
],
"mws-add-user": [ "mws-add-user": [
"--mws-add-permission", "READ", "Allows user to create tiddlers", "--mws-add-permission", "READ", "Allows user to create tiddlers",
"--mws-add-permission", "WRITE", "Gives the user the permission to edit and delete 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 title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form
<$macrocall $name="loginForm"/>
<form class="login-form" method="POST" action="/login"> <form class="login-form" method="POST" action="/login">
<input type="hidden" name="returnUrl" value=<<returnUrl>>/> <input type="hidden" name="returnUrl" value=<<returnUrl>>/>
<input type="text" name="username" placeholder="Username"/> <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; const self = this;
let currentTestSpec = 0; let currentTestSpec = 0;
let hasFailed = false; let hasFailed = false;
let sessionId;
function runNextTest() { function runNextTest() {
if(currentTestSpec < testSpecs.length) { if(currentTestSpec < testSpecs.length) {
const testSpec = testSpecs[currentTestSpec]; const testSpec = testSpecs[currentTestSpec];
if(!!sessionId) {
testSpec.headers['Cookie'] = `session=${sessionId}; HttpOnly; Path=/`;
}
currentTestSpec += 1; currentTestSpec += 1;
self.runTest(testSpec,function(err) { self.runTest(testSpec,function(err, data) {
if(data?.sessionId) {
sessionId = data?.sessionId;
}
if(err) { if(err) {
hasFailed = true; hasFailed = true;
console.log(`Failed "${testSpec.description}" with "${err}"`) console.log(`Failed "${testSpec.description}" with "${err}"`)
@ -96,7 +103,7 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
response.on("end", () => { response.on("end", () => {
const jsonData = $tw.utils.parseJSONSafe(buffer,function() {return undefined;}); const jsonData = $tw.utils.parseJSONSafe(buffer,function() {return undefined;});
const testResult = testSpec.expectedResult(jsonData,buffer,response.headers); const testResult = testSpec.expectedResult(jsonData,buffer,response.headers);
callback(testResult ? null : "Test failed"); callback(testResult ? null : "Test failed", jsonData);
}); });
}); });
request.on("error", (e) => { request.on("error", (e) => {
@ -112,6 +119,20 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
}; };
const testSpecs = [ 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", description: "Check index page",
method: "GET", method: "GET",

View File

@ -19,7 +19,8 @@ if($tw.node) {
path = require("path"), path = require("path"),
querystring = require("querystring"), querystring = require("querystring"),
crypto = require("crypto"), 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.authenticators = options.authenticators || [];
this.wiki = options.wiki; this.wiki = options.wiki;
this.boot = options.boot || $tw.boot; this.boot = options.boot || $tw.boot;
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase; this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase;
// Initialise the variables // Initialise the variables
this.variables = $tw.utils.extend({},this.defaultVariables); this.variables = $tw.utils.extend({},this.defaultVariables);
if(options.variables) { if(options.variables) {
@ -158,9 +159,10 @@ function sendResponse(request,response,statusCode,headers,data,encoding) {
data = zlib.gzipSync(data); data = zlib.gzipSync(data);
} }
} }
if(!response.headersSent) {
response.writeHead(statusCode,headers); response.writeHead(statusCode,headers);
response.end(data,encoding); response.end(data,encoding);
}
} }
function redirect(request,response,statusCode,location) { function redirect(request,response,statusCode,location) {
@ -351,6 +353,13 @@ Server.prototype.methodMappings = {
"DELETE": "writers" "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 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 { } else {
console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`); console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`);
} }
response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Path=/`); response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`); const loginUrl = '/login';
const loginUrl = '/login';
response.writeHead(302, { response.writeHead(302, {
'Location': loginUrl 'Location': loginUrl
}); });
@ -469,6 +477,12 @@ Server.prototype.requestHandler = function(request,response,options) {
// Find the route that matches this path // Find the route that matches this path
var route = self.findMatchingRoute(request,state); 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 // Optionally output debug info
if(self.get("debug-level") !== "none") { if(self.get("debug-level") !== "none") {
console.log("Request path:",JSON.stringify(state.urlInfo)); console.log("Request path:",JSON.stringify(state.urlInfo));

View File

@ -13,12 +13,14 @@ GET /bags/:bag_name
/*global $tw: false */ /*global $tw: false */
"use strict"; "use strict";
var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware;
exports.method = "GET"; exports.method = "GET";
exports.path = /^\/bags\/([^\/]+)(\/?)$/; exports.path = /^\/bags\/([^\/]+)(\/?)$/;
exports.useACL = true;
exports.entityName = "bag"
exports.handler = function (request, response, state) { 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 // 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] !== "/") { 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) { if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8"); state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8");
} else { } else {
aclMiddleware(request, response, state, 'bag', 'READ');
if (!response.headersSent) { if (!response.headersSent) {
// This is not a JSON API request, we should return the raw tiddler content // This is not a JSON API request, we should return the raw tiddler content
response.writeHead(200, "OK", { response.writeHead(200, "OK", {

View File

@ -16,14 +16,15 @@ fallback=<url> // Optional redirect if the tiddler is not found
/*global $tw: false */ /*global $tw: false */
"use strict"; "use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "GET"; exports.method = "GET";
exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
exports.useACL = true;
exports.entityName = "recipe"
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "recipe", "READ");
// Get the parameters // Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]), title = $tw.utils.decodeURIComponentSafe(state.params[1]),

View File

@ -12,8 +12,6 @@ POST /bags/:bag_name/tiddlers/
/*global $tw: false */ /*global $tw: false */
"use strict"; "use strict";
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
exports.method = "POST"; exports.method = "POST";
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/$/; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/$/;
@ -22,8 +20,11 @@ exports.bodyFormat = "stream";
exports.csrfDisable = true; exports.csrfDisable = true;
exports.useACL = true;
exports.entityName = "bag"
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
aclMiddleware(request, response, state, "bag", "WRITE");
const path = require("path"), const path = require("path"),
fs = require("fs"), fs = require("fs"),
processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream; processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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