Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user profile management and account deletion functionality #8712

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9583fda
mws authentication
webplusai Sep 12, 2024
9b69959
add more tests and permission checkers
webplusai Sep 13, 2024
d331115
add logic to ensure that only authenticated users' requests are handled
webplusai Sep 16, 2024
4cb0379
add custom login page
webplusai Sep 17, 2024
c5bc0df
Implement user authentication as well as session handling
webplusai Sep 18, 2024
0f0d8be
work on user operations authorization
webplusai Sep 19, 2024
0885eda
add middleware to route handlers for bags & tiddlers routes
webplusai Sep 20, 2024
4e75360
add feature that only returns the tiddlers and bags which the user ha…
webplusai Sep 23, 2024
0932f2c
refactor auth routes & added user management page
webplusai Sep 27, 2024
887025b
fix Ci Test failure issue
webplusai Sep 30, 2024
316aa01
fix users list page, add manage roles page
webplusai Oct 1, 2024
c5053f5
add commands and scripts to create new user & assign roles and permis…
webplusai Oct 2, 2024
6a9dcac
resolved ci-test failure
webplusai Oct 2, 2024
13d7cd9
add ACL permissions to bags & tiddlers on creation
webplusai Oct 3, 2024
18f97ab
fix comments and access control list bug
webplusai Oct 4, 2024
1a1675a
fix indentation issues
webplusai Oct 7, 2024
b61789b
fix conflicts
webplusai Oct 7, 2024
f02c856
working on user profile edit
webplusai Oct 8, 2024
81f73de
remove list users command & added support for database in server options
webplusai Oct 10, 2024
851a7ab
implement user profile update and password change feature
webplusai Oct 11, 2024
c5492c1
update plugin readme
webplusai Oct 14, 2024
e6c2cb9
implement command which triggers protected mode on the server
webplusai Oct 15, 2024
5d11b23
revert server-wide auth flag. Implement selective authorization
webplusai Oct 17, 2024
13b49a5
ACL management feature
webplusai Oct 18, 2024
fc990ec
Complete Access control list implementation
webplusai Oct 22, 2024
cff8c29
Added support to manage users' assigned role by admin
webplusai Oct 23, 2024
c54766c
fix comments
webplusai Oct 24, 2024
9d6fe76
fix comment
webplusai Oct 25, 2024
6ed21c5
Merge branch 'multi-wiki-support' into feature/mws-authentication
webplusai Oct 30, 2024
3ecec30
Add user profile management and account deletion functionality
webplusai Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
};

}());
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
};

}());
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand Down Expand Up @@ -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(`
Expand Down
51 changes: 33 additions & 18 deletions plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading
Loading