diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js index f44aafd39..2633ce10f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js @@ -29,9 +29,18 @@ exports.handler = function (request, response, state) { } var auth = authenticator(state.server.sqlTiddlerDatabase); - var userId = state.authenticatedUser.user_id; + var userId = state.data.userId; 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; + + if(!hasPermission) { + response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); + response.end("Forbidden"); + return; + } if(newPassword !== confirmPassword) { response.setHeader("Set-Cookie", "flashMessage=New passwords do not match; Path=/; HttpOnly; Max-Age=5"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js index 3cc81e593..43fa1aa6f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js @@ -38,17 +38,15 @@ POST /admin/delete-role return; } + // Delete the role + sqlTiddlerDatabase.deleteRole(role_id); + // Check if the role is in use var isRoleInUse = sqlTiddlerDatabase.isRoleInUse(role_id); if(isRoleInUse) { - response.writeHead(400, "Bad Request"); - response.end("Cannot delete role as it is still in use"); - return; + sqlTiddlerDatabase.deleteUserRolesByRoleId(role_id); } - // Delete the role - sqlTiddlerDatabase.deleteRole(role_id); - // Redirect back to the roles management page response.writeHead(302, { "Location": "/admin/roles" }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js new file mode 100644 index 000000000..426683a29 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js @@ -0,0 +1,58 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/delete-user-account.js +type: application/javascript +module-type: mws-route + +POST /delete-user-account + +\*/ +(function () { + + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; + + exports.method = "POST"; + + exports.path = /^\/delete-user-account\/?$/; + + exports.bodyFormat = "www-form-urlencoded"; + + exports.csrfDisable = true; + + 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" }); + response.end(); + }; + +}()); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js index a0df877bd..380c04b04 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js @@ -24,6 +24,12 @@ exports.handler = function(request,response,state) { userList = []; console.error("userList is not an array"); } + + if(!state.authenticatedUser.isAdmin) { + 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 => ({ diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js index 00a7a31be..c76aa5a00 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js @@ -18,13 +18,19 @@ exports.path = /^\/admin\/roles\/?$/; exports.handler = function(request, response, state) { var roles = state.server.sqlTiddlerDatabase.listRoles(); + var editRoleId = request.url.includes("?") ? request.url.split("?")[1]?.split("=")[1] : null; + var editRole = editRoleId ? roles.find(role => role.role_id === $tw.utils.parseInt(editRoleId, 10)) : null; - response.writeHead(200, "OK", {"Content-Type": "text/html"}); + if(editRole && editRole.role_name.toLowerCase().includes("admin")) { + editRole = null; + editRoleId = null; + } var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { variables: { "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", "user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no" } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js index 8bd40ba9c..771e0233e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js @@ -32,18 +32,29 @@ GET /admin/users/:user_id 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; + } + // Convert dates to strings and ensure all necessary fields are present - const 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() : '' + 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){ (a.role_id === userRole.role_id ? -1 : 1) }); response.writeHead(200, "OK", { "Content-Type": "text/html" @@ -54,6 +65,7 @@ GET /admin/users/:user_id 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", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js new file mode 100644 index 000000000..081ba9b73 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js @@ -0,0 +1,66 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/update-role.js +type: application/javascript +module-type: mws-route + +POST /admin/roles/:id + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "POST"; + +exports.path = /^\/admin\/roles\/([^\/]+)\/?$/; + +exports.bodyFormat = "www-form-urlencoded"; + +exports.csrfDisable = true; + +exports.handler = function(request, response, state) { + var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; + var role_id = state.params[0]; + var role_name = state.data.role_name; + var role_description = state.data.role_description; + + if(!state.authenticatedUser.isAdmin) { + response.writeHead(403, "Forbidden"); + response.end(); + return; + } + + // get the role + var role = sqlTiddlerDatabase.getRoleById(role_id); + + if(!role) { + response.writeHead(404, "Role not found"); + response.end(); + return; + } + + if(role.role_name.toLowerCase().includes("admin")) { + response.writeHead(400, "Admin role cannot be updated"); + response.end(); + return; + } + + try { + sqlTiddlerDatabase.updateRole( + role_id, + role_name, + role_description + ); + + response.writeHead(302, { "Location": "/admin/roles" }); + response.end(); + } catch(error) { + console.error("Error updating role:", error); + response.writeHead(500, "Internal Server Error"); + response.end(); + } +}; + +}()); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js index 112ed712e..95223db4a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js @@ -27,10 +27,24 @@ exports.handler = function (request,response,state) { return; } - var userId = state.authenticatedUser.user_id; + var userId = state.data.userId; var username = state.data.username; 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; + + if(!hasPermission) { + response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); + response.end("Forbidden"); + return; + } + + if(!state.authenticatedUser.isAdmin) { + var userRole = state.server.sqlTiddlerDatabase.getUserRoles(userId); + roleId = userRole.role_id; + } var result = state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 75f4e49ed..09adb3ebf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -1009,6 +1009,15 @@ SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) { }); }; +SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) { + this.engine.runStatement(` + DELETE FROM sessions + WHERE user_id = $userId + `, { + $userId: userId + }); +}; + // Group CRUD operations SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) { const result = this.engine.runStatement(` @@ -1321,6 +1330,24 @@ SqlTiddlerDatabase.prototype.getUserRoles = function(userId) { return this.engine.runStatementGet(query, { $userId: userId }); }; +SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = function(roleId) { + this.engine.runStatement(` + DELETE FROM user_roles + WHERE role_id = $roleId + `, { + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = function(userId) { + this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId + `, { + $userId: userId + }); +}; + SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { // Check if the role is assigned to any users const userRoleCheck = this.engine.runStatementGet(` diff --git a/plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid b/plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid index a7ad9d284..032c0b6a3 100644 --- a/plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid +++ b/plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid @@ -47,29 +47,44 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles <$text text={{{ [jsonget[description]] }}}/> -
- <$button class="tc-btn-invisible btn-edit"> - Edit - <$action-sendmessage $message="tm-modal" $param="$:/plugins/tiddlywiki/multiwikiserver/templates/edit-role-modal" role-id={{{ [jsonget[role_id]] }}}/> - -
- jsonget[role_id]] }}}/> - -
-
+ <$list filter="[jsonget[role_name]lowercase[]!match[admin]]" variable="ignore"> + + -
-

Add New Role

-
- - - -
-
+ <$let edit-role-id={{{ [jsonget[role_id]] }}}> +
+ <$list filter="[!is[blank]]" variable="ignore"> +

Edit Role

+
addprefix[/admin/roles/]] }}} class="add-role-form"> + jsonget[role_name]] }}}/> + jsonget[description]] }}}/> + +
+ + <$list filter="[is[blank]]" variable="ignore"> +

Add New Role

+
+ + + +
+ +
+ \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid b/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid index 1c632a062..c2cceb1df 100644 --- a/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid +++ b/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid @@ -9,7 +9,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user