1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-04-08 19:56:39 +00:00

refactor auth routes & added user management page

This commit is contained in:
webplusai 2024-09-27 07:34:08 +00:00
parent 4e753603e8
commit 0932f2ce79
14 changed files with 310 additions and 126 deletions

View File

@ -4,8 +4,7 @@
"tiddlywiki/tiddlyweb",
"tiddlywiki/filesystem",
"tiddlywiki/multiwikiclient",
"tiddlywiki/multiwikiserver",
"tiddlywiki/authentication"
"tiddlywiki/multiwikiserver"
],
"themes": [
"tiddlywiki/vanilla",

View File

@ -1,9 +0,0 @@
{
"title": "$:/plugins/tiddlywiki/authentication",
"description": "Authentication plugin for TiddlyWiki",
"author": "Anon",
"version": "0.1.0",
"core-version": ">=5.0.0",
"plugin-type": "plugin",
"list": ["login"]
}

View File

@ -0,0 +1,19 @@
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login
tags: $:/tags/ServerRoute
route-method: GET
route-path: /login
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/styles"/>
<html>
<head>
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/head"/>
</head>
<body>
<div class="login-container">
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/header" mode="block"/>
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form" mode="block"/>
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/error-message" mode="block"/>
</div>
</body>
</html>

View File

@ -0,0 +1,7 @@
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/error-message
<$list filter="[[$:/temp/mws/login/error]!is[missing]]" variable="errorTiddler">
<div class="tc-error-message">
{{$:/temp/mws/login/error}}
</div>
</$list>

View File

@ -0,0 +1,10 @@
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form
<$macrocall $name="loginForm"/>
<form class="login-form" method="POST" action="/login">
<input type="hidden" name="returnUrl" value=<<returnUrl>>/>
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Log In"/>
</form>

View File

@ -0,0 +1,3 @@
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/head
<title>TiddlyWiki Login</title>

View File

@ -0,0 +1,3 @@
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/header
<h1>TiddlyWiki Login</h1>

View File

@ -1,19 +1,5 @@
title: $:/plugins/tiddlywiki/authentication/login
tags: $:/tags/ServerRoute
route-method: GET
route-path: /login
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/styles
\define loginForm()
<form class="login-form" method="POST" action="/login">
<input type="hidden" name="returnUrl" value=<<returnUrl>>/>
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Log In"/>
</form>
\end
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
@ -59,19 +45,4 @@ body {
text-align: center;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="login-container">
<h1>TiddlyWiki Login</h1>
<$set name="returnUrl" value={{{ [{$:/temp/mws/login/returnUrl}!is[blank]else{$:/info/url/query}split[returnUrl=]last[]else[/]] }}}>
<<loginForm>>
</$set>
<$list filter="[[$:/temp/mws/login/error]!is[missing]]" variable="errorTiddler">
<div class="tc-error-message">
{{$:/temp/mws/login/error}}
</div>
</$list>
</div>
</body>
</html>
</style>

View File

@ -77,8 +77,6 @@ function Server(options) {
$tw.modules.forEachModuleOfType("mws-route", function(title,routeDefinition) {
self.addRoute(routeDefinition);
});
// Load tiddler-based routes
self.loadAuthRoutes();
// Initialise the http vs https
this.listenOptions = null;
this.protocol = "http";
@ -304,88 +302,6 @@ Server.prototype.addRoute = function(route) {
this.routes.push(route);
};
Server.prototype.loadAuthRoutes = function () {
var self = this;
// add the login page route
self.addRoute({
method: "GET",
path: /^\/login$/,
handler: function (request, response, state) {
// Check if the user already has a valid session
const authenticatedUser = self.authenticateUser(request, response);
if (authenticatedUser) {
// User is already logged in, redirect to home page
response.writeHead(302, { 'Location': '/' });
response.end();
return;
}
var loginTiddler = self.wiki.getTiddler("$:/plugins/tiddlywiki/authentication/login");
if (loginTiddler) {
var text = self.wiki.renderTiddler("text/html", loginTiddler.fields.title);
response.writeHead(200, { "Content-Type": "text/html" });
response.end(text);
} else {
response.writeHead(404);
response.end("Login page not found");
}
}
});
// add the login submission handler route
self.addRoute({
method: "POST",
path: /^\/login$/,
csrfDisable: true,
handler: function(request, response, state) {
self.handleLogin(request, response, state);
}.bind(self)
});
self.addRoute({
method: "POST",
path: /^\/logout$/,
csrfDisable: true,
handler: function(request, response, state) {
self.handleLogout(request, response, state);
}.bind(self)
});
};
Server.prototype.handleLogout = function (request, response, state) {
var self = this;
if (state.authenticatedUser) {
self.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId);
}
response.setHeader('Set-Cookie', 'session=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
response.writeHead(302, { 'Location': '/login' });
response.end();
}
Server.prototype.handleLogin = function (request, response, state) {
var self = this;
const querystring = require('querystring');
const formData = querystring.parse(state.data);
const { username, password } = formData;
const user = self.sqlTiddlerDatabase.getUserByUsername(username);
const isPasswordValid = self.verifyPassword(password, user?.password)
if (user && isPasswordValid) {
const sessionId = self.createSession(user.user_id);
const {returnUrl} = this.parseCookieString(request.headers.cookie)
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
response.writeHead(302, {
'Location': returnUrl || '/'
});
} else {
this.wiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: "Invalid username or password"
}));
response.writeHead(302, {
'Location': '/login'
});
}
response.end();
};
Server.prototype.verifyPassword = function(inputPassword, storedHash) {
const hashedInput = this.hashPassword(inputPassword);
return hashedInput === storedHash;

View File

@ -0,0 +1,39 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-login.js
type: application/javascript
module-type: mws-route
GET /login
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "GET";
exports.path = /^\/login$/;
exports.handler = function(request,response,state) {
// Check if the user already has a valid session
var authenticatedUser = state.server.authenticateUser(request, response);
if(authenticatedUser) {
// User is already logged in, redirect to home page
response.writeHead(302, { "Location": "/" });
response.end();
return;
}
var loginTiddler = $tw.mws.store.adminWiki.getTiddler("$:/plugins/tiddlywiki/multiwikiserver/auth/form/login");
if(loginTiddler) {
var text = $tw.mws.store.adminWiki.renderTiddler("text/html", loginTiddler.fields.title);
response.writeHead(200, { "Content-Type": "text/html" });
response.end(text);
} else {
response.writeHead(404);
response.end("Login page not found");
}
};
}());

View File

@ -0,0 +1,54 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-users.js
type: application/javascript
module-type: mws-route
GET /admin/users
\*/
(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 userList = state.server.sqlTiddlerDatabase.listUsers();
// Ensure userList is an array
if (!Array.isArray(userList)) {
userList = [];
console.error("userList is not an array");
}
// Convert dates to strings and ensure all necessary fields are present
userList = userList.map(user => ({
user_id: user.user_id || '',
username: user.username || '',
email: user.email || '',
created_at: user.created_at ? new Date(user.created_at).toISOString() : '',
last_login: user.last_login ? new Date(user.last_login).toISOString() : ''
}));
console.log("Processed userList =>", userList);
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/get-users",
"user-list": JSON.stringify(userList),
}
});
response.write(html);
response.end();
};
}());

View File

@ -0,0 +1,53 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-login.js
type: application/javascript
module-type: mws-route
POST /login
Parameters:
username
password
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.path = /^\/login$/;
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
var username = state.data.username;
var password = state.data.password;
var user = state.server.sqlTiddlerDatabase.getUserByUsername(username);
var isPasswordValid = state.server.verifyPassword(password, user ? user.password : null)
if(user && isPasswordValid) {
var sessionId = state.server.createSession(user.user_id);
var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
response.writeHead(302, {
'Location': returnUrl || '/'
});
} else {
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: "Invalid username or password"
}));
response.writeHead(302, {
'Location': '/login'
});
}
response.end();
};
}());

View File

@ -0,0 +1,30 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-logout.js
type: application/javascript
module-type: mws-route
POST /logout
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.path = /^\/logout$/;
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
if(state.authenticatedUser) {
state.server.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId);
}
response.setHeader("Set-Cookie", "session=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
response.writeHead(302, { "Location": "/login" });
response.end();
};
}());

View File

@ -0,0 +1,89 @@
<!--
title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-users
-->
\define lingo-base() $:/language/ControlPanel/Tools/
<h1>User Management</h1>
<!-- Display raw user-list for debugging -->
<h2>Debug Info</h2>
<p><strong>Raw user list JSON:</strong> <$text text=<<user-list>>/></p>
<!-- Attempt to parse the user-list JSON -->
<$set name="userList" value=<<user-list>>>
<!-- Display a message if the userList is empty -->
<$list filter="[<userList>!is[blank]]" emptyMessage="The user list is empty or not provided.">
<p><strong>User list found:</strong> <$text text=<<userList>>/></p>
<!-- Attempt to parse the user list -->
<$set name="parsedUserList" value={{{ [<userList>jsonparse[]] }}}/>
<!-- Check if parsedUserList has any entries -->
<$list filter="[<parsedUserList>count[]compare:number:gt[0]]" emptyMessage="No users found or failed to parse user data">
<!-- Display parsed user list for debugging -->
<p><strong>Parsed User List (as JSON):</strong> <$text text={{{ [<parsedUserList>jsonstringify[]] }}}/></p>
<!-- Display total user count -->
<p><strong>Total users:</strong> <$text text={{{ [<parsedUserList>count[]] }}}/></p>
<!-- Render the user table if parsing was successful -->
<table class="tc-view-field-table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Created At</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<$list filter="[<userList>jsonindexes[]]" variable="userIndex">
<$let currentUser={{{ [<userList>jsonget<userIndex>] }}}>
<tr>
<td><$text text={{{ [<currentUser>jsonget[username]] }}}/></td>
<td><$text text={{{ [<currentUser>jsonget[email]] }}}/></td>
<td><$text text={{{ [<currentUser>jsonget[created_at]] }}}/></td>
<td><$text text={{{ [<currentUser>jsonget[last_login]] }}}/></td>
<td>
<$button message="tm-server-command" param="edit-user" user_id={{{ [<currentUser>jsonget[user_id]] }}} class="tc-btn-invisible">
{{$:/core/images/edit-button}} Edit
</$button>
<$button message="tm-server-command" param="delete-user" user_id={{{ [<currentUser>jsonget[user_id]] }}} class="tc-btn-invisible">
{{$:/core/images/delete-button}} Delete
</$button>
</td>
</tr>
</$let>
</$list>
</tbody>
</table>
</$list>
</$list>
</$set>
<$button message="tm-modal" param="$:/plugins/tiddlywiki/multiwikiserver/templates/add-user-modal" class="tc-btn-big-green">
Add New User
</$button>
<style>
.tc-view-field-table {
width: 100%;
border-collapse: collapse;
}
.tc-view-field-table th, .tc-view-field-table td {
border: 1px solid <<colour table-border>>;
padding: 0.5em;
}
.tc-view-field-table th {
background-color: <<colour table-header-background>>;
font-weight: bold;
}
.tc-view-field-table tr:nth-child(even) {
background-color: <<colour table-header-background>>;
}
</style>