1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-01-07 07:50:26 +00:00

Add success and error message feedback for user profile operations (#8716)

* mws authentication

* add more tests and permission checkers

* add logic to ensure that only authenticated users' requests are handled

* add custom login page

* Implement user authentication as well as session handling

* work on user operations authorization

* add middleware to route handlers for bags & tiddlers routes

* add feature that only returns the tiddlers and bags which the user has permission to access on index page

* refactor auth routes & added user management page

* fix Ci Test failure issue

* fix users list page, add manage roles page

* add commands and scripts to create new user & assign roles and permissions

* resolved ci-test failure

* add ACL permissions to bags & tiddlers on creation

* fix comments and access control list bug

* fix indentation issues

* working on user profile edit

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

* implement user profile update and password change feature

* update plugin readme

* implement command which triggers protected mode on the server

* revert server-wide auth flag. Implement selective authorization

* ACL management feature

* Complete Access control list implementation

* Added support to manage users' assigned role by admin

* fix comments

* fix comment

* Add user profile management and account deletion functionality

* add success and error message feedback for user profile operations

* fix indentation issues

* Add command to create admin user if none exists when the start command is executed

* refactor annonymous user flow with create admin implementation

* remove mws-add-user from start command
This commit is contained in:
webplusai 2024-11-08 11:09:42 +01:00 committed by GitHub
parent 3a5f67d4f5
commit 316bd65296
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 580 additions and 390 deletions

View File

@ -11,15 +11,6 @@
"tiddlywiki/snowwhite"
],
"build": {
"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",
"--mws-add-role", "ADMIN", "System Administrator",
"--mws-assign-role-permission", "ADMIN", "READ",
"--mws-assign-role-permission", "ADMIN", "WRITE",
"--mws-add-user", "user", "pass123",
"--mws-assign-user-role", "user", "ADMIN"
],
"load-mws-demo-data": [
"--mws-load-wiki-folder","./editions/tw5.com","docs", "TiddlyWiki Documentation from https://tiddlywiki.com","docs","TiddlyWiki Documentation from https://tiddlywiki.com",
"--mws-load-wiki-folder","./editions/dev","dev","TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev","dev-docs", "TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev",

View File

@ -38,8 +38,6 @@ Command.prototype.execute = function() {
var description = this.params[1];
$tw.mws.store.sqlTiddlerDatabase.createPermission(permission_name, description);
console.log(permission_name+" Permission Created Successfully!")
self.callback();
return null;
};

View File

@ -38,8 +38,6 @@ Command.prototype.execute = function() {
var description = this.params[1];
$tw.mws.store.sqlTiddlerDatabase.createRole(role_name, description);
console.log(role_name+" Role Created Successfully!")
self.callback(null, "Role Created Successfully!");
return null;
};

View File

@ -43,11 +43,9 @@ Command.prototype.execute = function() {
var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username);
if(user) {
self.callback("WARNING: An account with the username (" + username + ") already exists");
} else {
if(!user) {
$tw.mws.store.sqlTiddlerDatabase.createUser(username, email, hashedPassword);
console.log("User Account Created Successfully!")
console.log("User Account Created Successfully with username: " + username + " and password: " + password);
self.callback();
}
return null;

View File

@ -51,8 +51,6 @@ Command.prototype.execute = function() {
$tw.mws.store.sqlTiddlerDatabase.addPermissionToRole(role.role_id, permission.permission_id);
console.log(permission_name+" permission assigned to "+role_name+" role successfully!")
self.callback();
return null;
};

View File

@ -119,20 +119,6 @@ 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

@ -441,6 +441,8 @@ Server.prototype.requestHandler = function(request,response,options) {
// Check whether anonymous access is granted
state.allowAnon = false; //this.isAuthorized(state.authorizationType,null);
state.firstGuestUser = this.sqlTiddlerDatabase.listUsers().length === 0 && !state.authenticatedUser;
// Authorize with the authenticated username
if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername) && !response.headersSent) {
response.writeHead(403,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");

View File

@ -22,28 +22,44 @@ exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.handler = function (request, response, state) {
var userId = state.data.userId;
// Clean up any existing error/success messages
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/error");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/success");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/login/error");
if(!state.authenticatedUser) {
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
response.end("Unauthorized");
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: "You must be logged in to change passwords"
}));
response.writeHead(302, { "Location": "/login" });
response.end();
return;
}
var auth = authenticator(state.server.sqlTiddlerDatabase);
var userId = state.data.userId;
var auth = authenticator(state.server.sqlTiddlerDatabase);
var newPassword = state.data.newPassword;
var confirmPassword = state.data.confirmPassword;
var currentUserId = state.authenticatedUser.user_id;
var hasPermission = ($tw.utils.parseInt(userId, 10) === currentUserId) || state.authenticatedUser.isAdmin;
var hasPermission = ($tw.utils.parseInt(userId) === currentUserId) || state.authenticatedUser.isAdmin;
if(!hasPermission) {
response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" });
response.end("Forbidden");
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/change-password/" + userId + "/error",
text: "You don't have permission to change this user's password"
}));
response.writeHead(302, { "Location": "/admin/users/" + userId });
response.end();
return;
}
if(newPassword !== confirmPassword) {
response.setHeader("Set-Cookie", "flashMessage=New passwords do not match; Path=/; HttpOnly; Max-Age=5");
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/change-password/" + userId + "/error",
text: "New passwords do not match"
}));
response.writeHead(302, { "Location": "/admin/users/" + userId });
response.end();
return;
@ -52,7 +68,10 @@ exports.handler = function (request, response, state) {
var userData = state.server.sqlTiddlerDatabase.getUser(userId);
if(!userData) {
response.setHeader("Set-Cookie", "flashMessage=User not found; Path=/; HttpOnly; Max-Age=5");
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/change-password/" + userId + "/error",
text: "User not found"
}));
response.writeHead(302, { "Location": "/admin/users/" + userId });
response.end();
return;
@ -61,7 +80,10 @@ exports.handler = function (request, response, state) {
var newHash = auth.hashPassword(newPassword);
var result = state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash);
response.setHeader("Set-Cookie", `flashMessage=${result.message}; Path=/; HttpOnly; Max-Age=5`);
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/change-password/" + userId + "/success",
text: result.message
}));
response.writeHead(302, { "Location": "/admin/users/" + userId });
response.end();
};

View File

@ -8,51 +8,86 @@ POST /delete-user-account
\*/
(function () {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.method = "POST";
exports.path = /^\/delete-user-account\/?$/;
exports.path = /^\/delete-user-account\/?$/;
exports.bodyFormat = "www-form-urlencoded";
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.csrfDisable = true;
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var userId = state.data.userId;
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var userId = state.data.userId;
// Check if user is admin
if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) {
response.writeHead(403, "Forbidden");
response.end();
return;
}
// Prevent admin from deleting their own account
if(state.authenticatedUser.user_id === userId) {
response.writeHead(400, "Bad Request");
response.end("Cannot delete your own account");
return;
}
// Check if the user exists
var user = sqlTiddlerDatabase.getUser(userId);
if(!user) {
response.writeHead(404, "Not Found");
response.end("User not found");
return;
}
sqlTiddlerDatabase.deleteUserRolesByUserId(userId);
sqlTiddlerDatabase.deleteUserSessions(userId);
sqlTiddlerDatabase.deleteUser(userId);
// Redirect back to the users management page
response.writeHead(302, { "Location": "/admin/users" });
// Check if user is admin
if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/delete-user/error",
text: "You must be an administrator to delete user accounts"
}));
response.writeHead(302, { "Location": '/admin/users/'+userId });
response.end();
};
return;
}
// Prevent admin from deleting their own account
if(state.authenticatedUser.user_id === userId) {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/delete-user/error",
text: "Cannot delete your own account"
}));
response.writeHead(302, { "Location": '/admin/users/'+userId });
response.end();
return;
}
// Check if the user exists
var user = sqlTiddlerDatabase.getUser(userId);
if(!user) {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/delete-user/error",
text: "User not found"
}));
response.writeHead(302, { "Location": '/admin/users/'+userId });
response.end();
return;
}
// Check if this is the last admin account
var adminRole = sqlTiddlerDatabase.getRoleByName("ADMIN");
if(!adminRole) {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/delete-user/error",
text: "Admin role not found"
}));
response.writeHead(302, { "Location": '/admin/users/'+userId });
response.end();
return;
}
var adminUsers = sqlTiddlerDatabase.listUsersByRoleId(adminRole.role_id);
if(adminUsers.length <= 1 && adminUsers.some(admin => admin.user_id === parseInt(userId))) {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/delete-user/error",
text: "Cannot delete the last admin account"
}));
response.writeHead(302, { "Location": '/admin/users/'+userId });
response.end();
return;
}
sqlTiddlerDatabase.deleteUserRolesByUserId(userId);
sqlTiddlerDatabase.deleteUserSessions(userId);
sqlTiddlerDatabase.deleteUser(userId);
// Redirect back to the users management page
response.writeHead(302, { "Location": "/admin/users" });
response.end();
};
}());

View File

@ -7,91 +7,91 @@ GET /admin/acl
\*/
(function () {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "GET";
exports.method = "GET";
exports.path = /^\/admin\/acl\/(.+)$/;
exports.path = /^\/admin\/acl\/(.+)$/;
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var params = state.params[0].split("/")
var recipeName = params[0];
var bagName = params[params.length - 1];
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var params = state.params[0].split("/")
var recipeName = params[0];
var bagName = params[params.length - 1];
var recipes = sqlTiddlerDatabase.listRecipes()
var bags = sqlTiddlerDatabase.listBags()
var recipes = sqlTiddlerDatabase.listRecipes()
var bags = sqlTiddlerDatabase.listBags()
var recipe = recipes.find((entry) => entry.recipe_name === recipeName && entry.bag_names.includes(bagName))
var bag = bags.find((entry) => entry.bag_name === bagName);
var recipe = recipes.find((entry) => entry.recipe_name === recipeName && entry.bag_names.includes(bagName))
var bag = bags.find((entry) => entry.bag_name === bagName);
if (!recipe || !bag) {
response.writeHead(500, "Unable to handle request", { "Content-Type": "text/html" });
response.end();
return;
}
var recipeAclRecords = sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name);
var bagAclRecords = sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name);
var roles = state.server.sqlTiddlerDatabase.listRoles();
var permissions = state.server.sqlTiddlerDatabase.listPermissions();
// This ensures that the user attempting to view the ACL management page has permission to do so
if(!state.authenticatedUser || (recipeAclRecords.length > 0 && !sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, 'WRITE'))){
response.writeHead(403, "Forbidden");
response.end();
return
}
// Enhance ACL records with role and permission details
recipeAclRecords = recipeAclRecords.map(record => {
var role = roles.find(role => role.role_id === record.role_id);
var permission = permissions.find(perm => perm.permission_id === record.permission_id);
return ({
...record,
role,
permission,
role_name: role.role_name,
role_description: role.description,
permission_name: permission.permission_name,
permission_description: permission.description
})
});
bagAclRecords = bagAclRecords.map(record => {
var role = roles.find(role => role.role_id === record.role_id);
var permission = permissions.find(perm => perm.permission_id === record.permission_id);
return ({
...record,
role,
permission,
role_name: role.role_name,
role_description: role.description,
permission_name: permission.permission_name,
permission_description: permission.description
})
});
response.writeHead(200, "OK", { "Content-Type": "text/html" });
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
variables: {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl",
"roles-list": JSON.stringify(roles),
"permissions-list": JSON.stringify(permissions),
"bag": JSON.stringify(bag),
"recipe": JSON.stringify(recipe),
"recipe-acl-records": JSON.stringify(recipeAclRecords),
"bag-acl-records": JSON.stringify(bagAclRecords),
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
}
});
response.write(html);
if (!recipe || !bag) {
response.writeHead(500, "Unable to handle request", { "Content-Type": "text/html" });
response.end();
};
return;
}
var recipeAclRecords = sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name);
var bagAclRecords = sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name);
var roles = state.server.sqlTiddlerDatabase.listRoles();
var permissions = state.server.sqlTiddlerDatabase.listPermissions();
// This ensures that the user attempting to view the ACL management page has permission to do so
if(!state.authenticatedUser || (recipeAclRecords.length > 0 && !sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, 'WRITE'))){
response.writeHead(403, "Forbidden");
response.end();
return
}
// Enhance ACL records with role and permission details
recipeAclRecords = recipeAclRecords.map(record => {
var role = roles.find(role => role.role_id === record.role_id);
var permission = permissions.find(perm => perm.permission_id === record.permission_id);
return ({
...record,
role,
permission,
role_name: role.role_name,
role_description: role.description,
permission_name: permission.permission_name,
permission_description: permission.description
})
});
bagAclRecords = bagAclRecords.map(record => {
var role = roles.find(role => role.role_id === record.role_id);
var permission = permissions.find(perm => perm.permission_id === record.permission_id);
return ({
...record,
role,
permission,
role_name: role.role_name,
role_description: role.description,
permission_name: permission.permission_name,
permission_description: permission.description
})
});
response.writeHead(200, "OK", { "Content-Type": "text/html" });
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
variables: {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl",
"roles-list": JSON.stringify(roles),
"permissions-list": JSON.stringify(permissions),
"bag": JSON.stringify(bag),
"recipe": JSON.stringify(recipe),
"recipe-acl-records": JSON.stringify(recipeAclRecords),
"bag-acl-records": JSON.stringify(bagAclRecords),
"username": state.authenticatedUser ? state.authenticatedUser.username : state.firstGuestUser ? "Annonymous User" : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
}
});
response.write(html);
response.end();
};
}());

View File

@ -41,10 +41,10 @@ exports.handler = function(request,response,state) {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-index",
"bag-list": JSON.stringify(allowedBags),
"recipe-list": JSON.stringify(allowedRecipes),
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
}
});
"username": state.authenticatedUser ? state.authenticatedUser.username : state.firstGuestUser ? "Annonymous User" : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no",
"first-guest-user": state.firstGuestUser ? "yes" : "no"
}});
response.write(html);
response.end();
}

View File

@ -18,19 +18,19 @@ exports.path = /^\/admin\/users$/;
exports.handler = function(request,response,state) {
var userList = state.server.sqlTiddlerDatabase.listUsers();
// Ensure userList is an array
if (!Array.isArray(userList)) {
userList = [];
console.error("userList is not an array");
}
if(!state.authenticatedUser.isAdmin) {
if(!state.authenticatedUser.isAdmin && !state.firstGuestUser) {
response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" });
response.end("Forbidden");
return;
}
// Convert dates to strings and ensure all necessary fields are present
userList = userList.map(user => ({
user_id: user.user_id || '',
@ -49,12 +49,13 @@ exports.handler = function(request,response,state) {
variables: {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-users",
"user-list": JSON.stringify(userList),
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
"username": state.authenticatedUser ? state.authenticatedUser.username : state.firstGuestUser ? "Annonymous User" : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no",
"first-guest-user": state.firstGuestUser ? "yes" : "no"
}
});
response.write(html);
response.end();
};
}());

View File

@ -31,7 +31,7 @@ exports.handler = function(request, response, state) {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles",
"roles-list": JSON.stringify(roles),
"edit-role": editRole ? JSON.stringify(editRole) : "",
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
"username": state.authenticatedUser ? state.authenticatedUser.username : state.firstGuestUser ? "Annonymous User" : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
}
});

View File

@ -8,73 +8,92 @@ GET /admin/users/:user_id
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "GET";
exports.path = /^\/admin\/users\/([^\/]+)\/?$/;
exports.handler = function(request,response,state) {
var user_id = $tw.utils.decodeURIComponentSafe(state.params[0]);
var userData = state.server.sqlTiddlerDatabase.getUser(user_id);
if(!userData) {
response.writeHead(404, "Not Found", {"Content-Type": "text/html"});
var errorHtml = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/error", {
variables: {
"error-message": "User not found"
}
});
response.write(errorHtml);
response.end();
return;
}
// Check if the user is trying to access their own profile or is an admin
var hasPermission = ($tw.utils.parseInt(user_id, 10) === state.authenticatedUser.user_id) || state.authenticatedUser.isAdmin;
if(!hasPermission) {
response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" });
response.end("Forbidden");
return;
}
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
// Convert dates to strings and ensure all necessary fields are present
var user = {
user_id: userData.user_id || "",
username: userData.username || "",
email: userData.email || "",
created_at: userData.created_at ? new Date(userData.created_at).toISOString() : "",
last_login: userData.last_login ? new Date(userData.last_login).toISOString() : ""
};
// Get all roles which the user has been assigned
var userRole = state.server.sqlTiddlerDatabase.getUserRoles(user_id);
var allRoles = state.server.sqlTiddlerDatabase.listRoles();
exports.method = "GET";
// sort allRoles by placing the user's role at the top of the list
allRoles.sort(function(a, b){ (a.role_id === userRole.role_id ? -1 : 1) });
response.writeHead(200, "OK", {
"Content-Type": "text/html"
});
// Render the html
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
exports.path = /^\/admin\/users\/([^\/]+)\/?$/;
exports.handler = function(request,response,state) {
var user_id = $tw.utils.decodeURIComponentSafe(state.params[0]);
var userData = state.server.sqlTiddlerDatabase.getUser(user_id);
// Clean up any existing error/success messages if the user_id is different from the "$:/temp/mws/user-info/preview-user-id"
var lastPreviewedUser = $tw.wiki.getTiddlerText("$:/temp/mws/user-info/" + user_id + "/preview-user-id");
if(user_id !== lastPreviewedUser) {
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + user_id + "/ error");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + user_id + "/success");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/login/error");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/delete-user/" + user_id + "/error");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/delete-user/" + user_id + "/success");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/update-profile/" + user_id + "/error");
$tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/update-profile/" + user_id + "/success");
}
if(!userData) {
response.writeHead(404, "Not Found", {"Content-Type": "text/html"});
var errorHtml = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/error", {
variables: {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user",
"user": JSON.stringify(user),
"user-initials": user.username.split(" ").map(name => name[0]).join(""),
"user-role": JSON.stringify(userRole),
"all-roles": JSON.stringify(allRoles),
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id === $tw.utils.parseInt(user_id, 10) ? "yes" : "no",
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
"error-message": "User not found"
}
});
response.write(html);
response.write(errorHtml);
response.end();
return;
}
// Check if the user is trying to access their own profile or is an admin
var hasPermission = ($tw.utils.parseInt(user_id) === state.authenticatedUser.user_id) || state.authenticatedUser.isAdmin;
if(!hasPermission) {
response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" });
response.end("Forbidden");
return;
}
// Convert dates to strings and ensure all necessary fields are present
var user = {
user_id: userData.user_id || "",
username: userData.username || "",
email: userData.email || "",
created_at: userData.created_at ? new Date(userData.created_at).toISOString() : "",
last_login: userData.last_login ? new Date(userData.last_login).toISOString() : ""
};
}());
// Get all roles which the user has been assigned
var userRole = state.server.sqlTiddlerDatabase.getUserRoles(user_id);
var allRoles = state.server.sqlTiddlerDatabase.listRoles();
// sort allRoles by placing the user's role at the top of the list
allRoles.sort(function(a, b){ return (a.role_id === userRole?.role_id ? -1 : 1) });
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/user-info/" + user_id + "/preview-user-id",
text: user_id
}));
response.writeHead(200, "OK", {
"Content-Type": "text/html"
});
// Render the html
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
variables: {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user",
"user": JSON.stringify(user),
"user-initials": user.username.split(" ").map(name => name[0]).join(""),
"user-role": JSON.stringify(userRole),
"all-roles": JSON.stringify(allRoles),
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id === $tw.utils.parseInt(user_id, 10) ? "yes" : "no",
"username": state.authenticatedUser ? state.authenticatedUser.username : state.firstGuestUser ? "Annonymous User" : "Guest",
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no",
"user-id": user_id,
}
});
response.write(html);
response.end();
};
}());

View File

@ -8,57 +8,56 @@ POST /admin/post-acl
\*/
(function () {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.method = "POST";
exports.path = /^\/admin\/post-acl\/?$/;
exports.path = /^\/admin\/post-acl\/?$/;
exports.bodyFormat = "www-form-urlencoded";
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.csrfDisable = true;
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var entity_type = state.data.entity_type;
var recipe_name = state.data.recipe_name;
var bag_name = state.data.bag_name;
var role_id = state.data.role_id;
var permission_id = state.data.permission_id;
var isRecipe = entity_type === "recipe"
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var entity_type = state.data.entity_type;
var recipe_name = state.data.recipe_name;
var bag_name = state.data.bag_name;
var role_id = state.data.role_id;
var permission_id = state.data.permission_id;
var isRecipe = entity_type === "recipe"
var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true);
var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true);
var aclExists = entityAclRecords.some((record) => (
record.role_id == role_id && record.permission_id == permission_id
))
var aclExists = entityAclRecords.some((record) => (
record.role_id == role_id && record.permission_id == permission_id
))
// This ensures that the user attempting to modify the ACL has permission to do so
// if(!state.authenticatedUser || (entityAclRecords.length > 0 && !sqlTiddlerDatabase[isRecipe ? 'hasRecipePermission' : 'hasBagPermission'](state.authenticatedUser.user_id, isRecipe ? recipe_name : bag_name, 'WRITE'))){
// response.writeHead(403, "Forbidden");
// response.end();
// return
// }
if (aclExists) {
// do nothing, return the user back to the form
response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name });
response.end();
return
}
sqlTiddlerDatabase.createACL(
isRecipe ? recipe_name : bag_name,
entity_type,
role_id,
permission_id
)
// This ensures that the user attempting to modify the ACL has permission to do so
// if(!state.authenticatedUser || (entityAclRecords.length > 0 && !sqlTiddlerDatabase[isRecipe ? 'hasRecipePermission' : 'hasBagPermission'](state.authenticatedUser.user_id, isRecipe ? recipe_name : bag_name, 'WRITE'))){
// response.writeHead(403, "Forbidden");
// response.end();
// return
// }
if (aclExists) {
// do nothing, return the user back to the form
response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name });
response.end();
};
return
}
sqlTiddlerDatabase.createACL(
isRecipe ? recipe_name : bag_name,
entity_type,
role_id,
permission_id
)
response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name });
response.end();
};
}());

View File

@ -8,29 +8,29 @@ POST /admin/post-role
\*/
(function () {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.method = "POST";
exports.path = /^\/admin\/post-role\/?$/;
exports.path = /^\/admin\/post-role\/?$/;
exports.bodyFormat = "www-form-urlencoded";
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.csrfDisable = true;
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var role_name = state.data.role_name;
var role_description = state.data.role_description;
exports.handler = function (request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var role_name = state.data.role_name;
var role_description = state.data.role_description;
// Add your authentication check here if needed
// Add your authentication check here if needed
sqlTiddlerDatabase.createRole(role_name, role_description);
sqlTiddlerDatabase.createRole(role_name, role_description);
response.writeHead(302, { "Location": "/admin/roles" });
response.end();
};
response.writeHead(302, { "Location": "/admin/roles" });
response.end();
};
}());

View File

@ -8,56 +8,79 @@ POST /admin/post-user
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
if($tw.node) {
var crypto = require("crypto");
}
exports.method = "POST";
exports.method = "POST";
exports.path = /^\/admin\/post-user\/?$/;
exports.path = /^\/admin\/post-user\/?$/;
exports.bodyFormat = "www-form-urlencoded";
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.csrfDisable = true;
exports.handler = function(request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var username = state.data.username;
var email = state.data.email;
var password = state.data.password;
var confirmPassword = state.data.confirmPassword;
exports.handler = function(request, response, state) {
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
var username = state.data.username;
var email = state.data.email;
var password = state.data.password;
var confirmPassword = state.data.confirmPassword;
if(!state.authenticatedUser && !state.firstGuestUser) {
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
response.end("Unauthorized");
return;
}
if(!state.authenticatedUser) {
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
response.end("Unauthorized");
return;
}
if(!username || !email || !password || !confirmPassword) {
response.writeHead(400, {"Content-Type": "application/json"});
response.end(JSON.stringify({error: "All fields are required"}));
return;
}
if(!username || !email || !password || !confirmPassword) {
response.writeHead(400, {"Content-Type": "application/json"});
response.end(JSON.stringify({error: "All fields are required"}));
return;
}
if(password !== confirmPassword) {
response.writeHead(400, {"Content-Type": "application/json"});
response.end(JSON.stringify({error: "Passwords do not match"}));
return;
}
if(password !== confirmPassword) {
response.writeHead(400, {"Content-Type": "application/json"});
response.end(JSON.stringify({error: "Passwords do not match"}));
return;
}
// Check if user already exists
var existingUser = sqlTiddlerDatabase.getUser(username);
if(existingUser) {
response.writeHead(400, {"Content-Type": "application/json"});
response.end(JSON.stringify({error: "Username already exists"}));
return;
}
// Check if user already exists
var existingUser = sqlTiddlerDatabase.getUser(username);
if(existingUser) {
response.writeHead(400, {"Content-Type": "application/json"});
response.end(JSON.stringify({error: "Username already exists"}));
return;
}
var hasUsers = sqlTiddlerDatabase.listUsers().length > 0;
var hashedPassword = crypto.createHash("sha256").update(password).digest("hex");
// Create new user
var userId = sqlTiddlerDatabase.createUser(username, email, password);
// Create new user
var userId = sqlTiddlerDatabase.createUser(username, email, hashedPassword);
if(!hasUsers) {
// If this is the first guest user, assign admin privileges
sqlTiddlerDatabase.setUserAdmin(userId, true);
// Create a session for the new admin user
var auth = require('$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js').Authenticator;
var authenticator = auth(sqlTiddlerDatabase);
var sessionId = authenticator.createSession(userId);
// Set the session cookie and redirect
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
response.writeHead(302, {
'Location': '/'
});
response.end();
return;
} else {
response.writeHead(302, {"Location": "/admin/users/"+userId});
response.end();
};
}
};
}());

View File

@ -22,8 +22,12 @@ exports.csrfDisable = true;
exports.handler = function (request,response,state) {
if(!state.authenticatedUser) {
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
response.end("Unauthorized");
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: "You must be logged in to update profiles"
}));
response.writeHead(302, { "Location": "/login" });
response.end();
return;
}
@ -32,29 +36,39 @@ exports.handler = function (request,response,state) {
var email = state.data.email;
var roleId = state.data.role;
var currentUserId = state.authenticatedUser.user_id;
var hasPermission = ($tw.utils.parseInt(userId, 10) === currentUserId) || state.authenticatedUser.isAdmin;
var hasPermission = ($tw.utils.parseInt(userId) === currentUserId) || state.authenticatedUser.isAdmin;
if(!hasPermission) {
response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" });
response.end("Forbidden");
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/update-profile/" + userId + "/error",
text: "You don't have permission to update this profile"
}));
response.writeHead(302, { "Location": "/admin/users/" + userId });
response.end();
return;
}
if(!state.authenticatedUser.isAdmin) {
var userRole = state.server.sqlTiddlerDatabase.getUserRoles(userId);
var userRole = state.server.sqlTiddlerDatabase.getUserRoles(userId);
roleId = userRole.role_id;
}
var result = state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId);
if(result.success) {
response.setHeader("Set-Cookie", "flashMessage="+result.message+"; Path=/; HttpOnly; Max-Age=5");
response.writeHead(302, { "Location": "/admin/users/" + userId });
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/update-profile/" + userId + "/success",
text: result.message
}));
} else {
response.setHeader("Set-Cookie", "flashMessage="+result.message+"; Path=/; HttpOnly; Max-Age=5");
response.writeHead(302, { "Location": "/admin/users/" + userId });
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/update-profile/" + userId + "/error",
text: result.message
}));
}
response.writeHead(302, { "Location": "/admin/users/" + userId });
response.end();
};

View File

@ -42,7 +42,7 @@ exports.middleware = function (request, response, state, entityType, permissionN
entityName = state.data ? (state.data[entityType+"_name"] || state.params[0]) : state.params[0];
// First, replace '%3A' with ':' to handle TiddlyWiki's system tiddlers
var partiallyDecoded = entityName.replace(/%3A/g, ":");
var partiallyDecoded = entityName?.replace(/%3A/g, ":");
// Then use decodeURIComponent for the rest
var decodedEntityName = decodeURIComponent(partiallyDecoded);
var aclRecord = sqlTiddlerDatabase.getACLByName(entityType, decodedEntityName);

View File

@ -224,14 +224,6 @@ SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscon
$accesscontrol: accesscontrol,
$description: description
});
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);
}
return updateBags.lastInsertRowid;
};
@ -296,15 +288,6 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr
$bag_names: JSON.stringify(bag_names)
});
// update the permissions on ACL records
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);
}
return updateRecipes.lastInsertRowid;
};
@ -825,6 +808,18 @@ SqlTiddlerDatabase.prototype.getUserByUsername = function(username) {
});
};
SqlTiddlerDatabase.prototype.listUsersByRoleId = function(roleId) {
return this.engine.runStatementGetAll(`
SELECT u.*
FROM users u
JOIN user_roles ur ON u.user_id = ur.user_id
WHERE ur.role_id = $roleId
ORDER BY u.username
`, {
$roleId: roleId
});
};
SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, roleId) {
const existingUser = this.engine.runStatement(`
SELECT user_id FROM users
@ -1018,6 +1013,14 @@ SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) {
});
};
// Set the user as an admin
SqlTiddlerDatabase.prototype.setUserAdmin = function(userId) {
var admin = this.getRoleByName("ADMIN");
if(admin) {
this.addRoleToUser(userId, admin.role_id);
}
};
// Group CRUD operations
SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) {
const result = this.engine.runStatement(`

View File

@ -27,6 +27,20 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
</$set>
</$tiddler>
<$list filter="[<first-guest-user>match[yes]]">
<div class="mws-security-warning">
<div class="mws-security-warning-content">
<div class="mws-security-warning-icon">⚠️</div>
<div class="mws-security-warning-text">
<strong>Warning:</strong> TiddlyWiki is currently running in anonymous access mode which allows anyone with access to the server to read and modify data.
</div>
<div class="mws-security-warning-action">
<a href="/admin/users" class="mws-security-warning-button">Add Admin Account</a>
</div>
</div>
</div>
</$list>
<ul class="mws-vertical-list">
<$list filter="[<recipe-list>jsonindexes[]] :sort[<currentTiddler>jsonget[recipe_name]]" variable="recipe-index">
<li>
@ -226,4 +240,48 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
.mws-admin-dropdown:hover .mws-admin-dropdown-content {display: block;}
.mws-admin-dropdown:hover {background-color: #2980B9;}
.mws-security-warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
padding: 1rem;
margin-bottom: 1.5rem;
border-radius: 4px;
}
.mws-security-warning-content {
display: flex;
align-items: center;
gap: 1rem;
max-width: 1200px;
margin: 0 auto;
}
.mws-security-warning-icon {
font-size: 1.5rem;
}
.mws-security-warning-text {
flex-grow: 1;
color: #856404;
}
.mws-security-warning-button {
display: flex;
padding: 0.5rem 1rem;
background-color: #856404;
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
transition: background-color 0.2s;
flex-direction: row;
align-items: center;
text-align: center;
text-wrap: nowrap;
}
.mws-security-warning-button:hover {
background-color: #6d5204;
}
</style>

View File

@ -18,34 +18,36 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-users
</$tiddler>
<div class="users-container">
<div class="users-list">
<$list filter="[<user-list>jsonindexes[]]" variable="user-index">
<$let currentUser={{{ [<user-list>jsonextract<user-index>] }}}>
<$set name="user-id" value={{{ [<currentUser>jsonget[user_id]] }}}>
<a href={{{ [[/admin/users/]addsuffix<user-id>] }}} class="user-item">
<div class="user-info">
<span class="user-name">
<$text text={{{ [<currentUser>jsonget[username]] }}}/>
</span>
<span class="user-email">
<$text text={{{ [<currentUser>jsonget[email]] }}}/>
</span>
</div>
<div class="user-details">
<span class="user-created">
Created: <$text text={{{ [<currentUser>jsonget[created_at]] }}}/>
</span>
<span class="user-last-login">
Last Login: <$text text={{{ [<currentUser>jsonget[last_login]] }}}/>
</span>
</div>
</a>
</$set>
</$let>
</$list>
</div>
<$list filter="[<user-list>jsonindexes[]count[]!match[0]]">
<div class="users-list">
<$list filter="[<user-list>jsonindexes[]]" variable="user-index">
<$let currentUser={{{ [<user-list>jsonextract<user-index>] }}}>
<$set name="user-id" value={{{ [<currentUser>jsonget[user_id]] }}}>
<a href={{{ [[/admin/users/]addsuffix<user-id>] }}} class="user-item">
<div class="user-info">
<span class="user-name">
<$text text={{{ [<currentUser>jsonget[username]] }}}/>
</span>
<span class="user-email">
<$text text={{{ [<currentUser>jsonget[email]] }}}/>
</span>
</div>
<div class="user-details">
<span class="user-created">
Created: <$text text={{{ [<currentUser>jsonget[created_at]] }}}/>
</span>
<span class="user-last-login">
Last Login: <$text text={{{ [<currentUser>jsonget[last_login]] }}}/>
</span>
</div>
</a>
</$set>
</$let>
</$list>
</div>
</$list>
<$list filter="[<user-is-admin>match[yes]]">
<$list filter="[<user-is-admin>match[yes]][<first-guest-user>match[yes]]">
<div class="add-user-card">
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/add-user-form" mode="inline"/>
</div>
@ -73,6 +75,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-users
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 2rem;
margin: auto;
}
.user-item {
display: block;
@ -121,4 +124,9 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-users
.tc-btn-big-green:hover {
background-color: #45a049;
}
.no-users-message {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@ -28,6 +28,17 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
</div>
<% endif %>
<button type="submit" class="update-profile-btn">Update Profile</button>
<$list filter="[[$:/temp/mws/update-profile/]addsuffix<user-id>addsuffix[/error]!is[missing]]" variable="errorTiddler">
<div class="tc-error-message">
<$text text={{{[<errorTiddler>get[text]]}}}/>
</div>
</$list>
<$list filter="[[$:/temp/mws/update-profile/]addsuffix<user-id>addsuffix[/success]!is[missing]]" variable="successTiddler">
<div class="tc-success-message">
<$text text={{{[<successTiddler>get[text]]}}}/>
</div>
</$list>
</form>
</$set>
<% if [<user-is-admin>match[yes]] && [<is-current-user-profile>match[no]] %>
@ -35,6 +46,11 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
<form class="user-profile-form" action="/delete-user-account" method="POST" onsubmit="return confirm('Are you sure you want to delete this user account? This action cannot be undone.');">
<input type="hidden" name="userId" value={{{ [<user>jsonget[user_id]] }}}>
<button type="submit" class="delete-account-btn">Delete User Account</button>
<$list filter="[[$:/temp/mws/delete-user/]addsuffix<user-id>addsuffix[/error]!is[missing]]" variable="deleteErrorTiddler">
<div class="tc-error-message">
<$text text={{{[<deleteErrorTiddler>get[text]]}}}/>
</div>
</$list>
</form>
<% endif %>
<% if [<is-current-user-profile>match[yes]] %>
@ -51,6 +67,16 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
<input type="password" id="confirm-password" name="confirmPassword" required />
</div>
<button type="submit" class="update-password-btn">Change Password</button>
<$list filter="[[$:/temp/mws/change-password/]addsuffix<user-id>addsuffix[/error]!is[missing]]" variable="errorTiddler">
<div class="tc-error-message">
<$text text={{{[<errorTiddler>get[text]]}}}/>
</div>
</$list>
<$list filter="[[$:/temp/mws/change-password/]addsuffix<user-id>addsuffix[/success]!is[missing]]" variable="successTiddler">
<div class="tc-success-message">
<$text text={{{[<successTiddler>get[text]]}}}/>
</div>
</$list>
</form>
<% endif %>
</div>
@ -58,6 +84,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
<style>
.user-profile-management {
padding: 20px;
flex: 1;
}
.user-profile-management h2 {
@ -133,4 +160,14 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
.delete-account-btn:hover {
background: #c0392b;
}
.tc-error-message {
color: red;
font-weight: bold;
}
.tc-success-message {
color: green;
font-weight: bold;
}
</style>

View File

@ -14,7 +14,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
<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>
@ -39,7 +39,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
</div>
</div>
</div>
<% if [<user-is-admin>match[yes]] %>
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account">
<$transclude/>
@ -70,7 +70,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
}
.user-profile-container {
flex: 1;
flex: 4;
margin: 2rem auto;
background: #fff;
border-radius: 8px;

View File

@ -12,7 +12,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/mws-header
<a href="/admin/roles">Manage Roles</a>
</div>
</div>
<% elseif [<username>!match[Guest]] %>
<% elseif [<username>!match[Guest]]+[<first-guest-user>match[no]] %>
<a href={{{ [<user>jsonget[user_id]addprefix[/admin/users/]] }}}>
<button class="mws-profile-btn">Profile</button>
</a>