mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-04-03 09:16:55 +00:00
Add user profile management and account deletion functionality (#8712)
* 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
This commit is contained in:
parent
6a7612ddf8
commit
c7531e53ab
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
}());
|
@ -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 => ({
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -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);
|
||||
|
||||
|
@ -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(`
|
||||
|
@ -47,29 +47,44 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles
|
||||
<$text text={{{ [<role>jsonget[description]] }}}/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="role-actions">
|
||||
<$button class="tc-btn-invisible btn-edit">
|
||||
Edit
|
||||
<$action-sendmessage $message="tm-modal" $param="$:/plugins/tiddlywiki/multiwikiserver/templates/edit-role-modal" role-id={{{ [<role>jsonget[role_id]] }}}/>
|
||||
</$button>
|
||||
<form method="POST" action="/admin/delete-role">
|
||||
<input type="hidden" name="role_id" value={{{ [<role>jsonget[role_id]] }}}/>
|
||||
<button type="submit" class="tc-btn-invisible btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
<$list filter="[<role>jsonget[role_name]lowercase[]!match[admin]]" variable="ignore">
|
||||
<div class="role-actions">
|
||||
<a href={{{ [<role>jsonget[role_id]addprefix[/admin/roles/?edit=]] }}}>
|
||||
<$button class="tc-btn-invisible btn-edit">
|
||||
Edit
|
||||
</$button>
|
||||
</a>
|
||||
<form method="POST" action="/admin/delete-role">
|
||||
<input type="hidden" name="role_id" value={{{ [<role>jsonget[role_id]] }}}/>
|
||||
<button type="submit" class="tc-btn-invisible btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</$list>
|
||||
</div>
|
||||
</$let>
|
||||
</$list>
|
||||
</div>
|
||||
|
||||
<div class="add-role-card">
|
||||
<h2>Add New Role</h2>
|
||||
<form method="POST" action="/admin/post-role" class="add-role-form">
|
||||
<input name="role_name" type="text" placeholder="Role Name" required/>
|
||||
<input name="role_description" type="text" placeholder="Role Description" required/>
|
||||
<button type="submit" class="tc-btn-invisible btn-add">Add Role</button>
|
||||
</form>
|
||||
</div>
|
||||
<$let edit-role-id={{{ [<edit-role>jsonget[role_id]] }}}>
|
||||
<div class="add-role-card">
|
||||
<$list filter="[<edit-role-id>!is[blank]]" variable="ignore">
|
||||
<h2>Edit Role</h2>
|
||||
<form method="POST" action={{{ [<edit-role-id>addprefix[/admin/roles/]] }}} class="add-role-form">
|
||||
<input name="role_name" type="text" placeholder="Role Name" required value={{{ [<edit-role>jsonget[role_name]] }}}/>
|
||||
<input name="role_description" type="text" placeholder="Role Description" required value={{{ [<edit-role>jsonget[description]] }}}/>
|
||||
<button type="submit" class="tc-btn-invisible btn-add">Update Role</button>
|
||||
</form>
|
||||
</$list>
|
||||
<$list filter="[<edit-role-id>is[blank]]" variable="ignore">
|
||||
<h2>Add New Role</h2>
|
||||
<form method="POST" action="/admin/post-role" class="add-role-form">
|
||||
<input name="role_name" type="text" placeholder="Role Name" required/>
|
||||
<input name="role_description" type="text" placeholder="Role Description" required/>
|
||||
<button type="submit" class="tc-btn-invisible btn-add">Add Role</button>
|
||||
</form>
|
||||
</$list>
|
||||
</div>
|
||||
</$let>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -4,6 +4,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
|
||||
<h2>Manage Account</h2>
|
||||
<$set name="current-role-id" value={{{ [<user-role>jsonget[role_id]] }}}>
|
||||
<form class="user-profile-form" action="/update-user-profile" method="POST">
|
||||
<input type="hidden" name="userId" value={{{ [<user>jsonget[user_id]] }}}>
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" value={{{ [<user>jsonget[username]] }}} required />
|
||||
@ -16,10 +17,9 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
|
||||
<div class="form-group">
|
||||
<label for="role">Role:</label>
|
||||
<select id="role" name="role" required>
|
||||
<$list filter="[<all-roles>jsonindexes[]]" variable="role-index">
|
||||
<$set name="role-id" value={{{ [<all-roles>jsonextract<role-index>jsonget[role_id]] }}}>
|
||||
<option value=<<role-id>>
|
||||
<$list filter="[<current-role-id>match<role-id>]" variable="ignore">selected</$list>
|
||||
<$list filter="[<all-roles>jsonindexes[]]" variable="role-index">
|
||||
<$set name="role-id" value={{{ [<all-roles>jsonextract<role-index>jsonget[role_id]] }}}>
|
||||
<option value=<<role-id>>>
|
||||
<$text text={{{ [<all-roles>jsonextract<role-index>jsonget[role_name]] }}}/>
|
||||
</option>
|
||||
</$set>
|
||||
@ -30,19 +30,29 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
|
||||
<button type="submit" class="update-profile-btn">Update Profile</button>
|
||||
</form>
|
||||
</$set>
|
||||
<hr />
|
||||
<h2>Change Password</h2>
|
||||
<form class="user-profile-form" action="/change-user-password" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password:</label>
|
||||
<input type="password" id="new-password" name="newPassword" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm-password" name="confirmPassword" required />
|
||||
</div>
|
||||
<button type="submit" class="update-password-btn">Change Password</button>
|
||||
</form>
|
||||
<% if [<user-is-admin>match[yes]] && [<is-current-user-profile>match[no]] %>
|
||||
<hr />
|
||||
<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>
|
||||
</form>
|
||||
<% endif %>
|
||||
<% if [<is-current-user-profile>match[yes]] %>
|
||||
<hr />
|
||||
<h2>Change Password</h2>
|
||||
<form class="user-profile-form" action="/change-user-password" method="POST">
|
||||
<input type="hidden" name="userId" value={{{ [<user>jsonget[user_id]] }}}>
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password:</label>
|
||||
<input type="password" id="new-password" name="newPassword" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm-password" name="confirmPassword" required />
|
||||
</div>
|
||||
<button type="submit" class="update-password-btn">Change Password</button>
|
||||
</form>
|
||||
<% endif %>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@ -108,4 +118,19 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.delete-account-btn {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.delete-account-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
</style>
|
@ -9,7 +9,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
<div class="user-profile-container">
|
||||
<div class="user-profile-header">
|
||||
<div class="user-profile-avatar">
|
||||
<$text text={{{ [<user>jsonget[username]substr[0,1]uppercase[]] }}}/>
|
||||
<$text text={{{ [<user-initials>uppercase[]] }}}/>
|
||||
</div>
|
||||
<h1 class="user-profile-name"><$text text={{{ [<user>jsonget[username]] }}}/></h1>
|
||||
<p class="user-profile-email"><$text text={{{ [<user>jsonget[email]] }}}/></p>
|
||||
@ -96,6 +96,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.user-profile-avatar * {
|
||||
|
@ -4,7 +4,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/mws-header
|
||||
<h1><$text text=<<page-title>>/></h1>
|
||||
<div class="mws-user-info">
|
||||
<span>Hello, <$text text=<<username>>/></span>
|
||||
<$list filter="[<user-is-admin>match[yes]]">
|
||||
<% if [<user-is-admin>match[yes]] %>
|
||||
<div class="mws-admin-dropdown">
|
||||
<button class="mws-admin-dropbtn">⚙️</button>
|
||||
<div class="mws-admin-dropdown-content">
|
||||
@ -12,7 +12,11 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/mws-header
|
||||
<a href="/admin/roles">Manage Roles</a>
|
||||
</div>
|
||||
</div>
|
||||
</$list>
|
||||
<% elseif [<username>!match[Guest]] %>
|
||||
<a href={{{ [<user>jsonget[user_id]addprefix[/admin/users/]] }}}>
|
||||
<button class="mws-profile-btn">Profile</button>
|
||||
</a>
|
||||
<% endif %>
|
||||
<form action="/logout" method="post" class="mws-logout-form">
|
||||
<input type="submit" value="Logout" class="mws-logout-button"/>
|
||||
</form>
|
||||
@ -91,4 +95,13 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/mws-header
|
||||
.mws-admin-dropdown:hover .mws-admin-dropdown-content {display: block;}
|
||||
|
||||
.mws-admin-dropdown:hover {background-color: #2980B9;}
|
||||
</style>
|
||||
|
||||
.mws-profile-btn {
|
||||
background-color: #2980B9;
|
||||
margin-left: 10px;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user