1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-09-03 03:18:02 +00:00

Implement user authentication as well as session handling

This commit is contained in:
webplusai
2024-09-18 19:07:05 +00:00
parent 4cb0379219
commit c5bc0df47d
2 changed files with 169 additions and 53 deletions

View File

@@ -34,6 +34,7 @@ function Server(options) {
this.authenticators = options.authenticators || [];
this.wiki = options.wiki;
this.boot = options.boot || $tw.boot;
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase;
// Initialise the variables
this.variables = $tw.utils.extend({},this.defaultVariables);
if(options.variables) {
@@ -310,6 +311,14 @@ Server.prototype.loadAuthRoutes = function () {
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);
@@ -332,52 +341,46 @@ Server.prototype.loadAuthRoutes = function () {
});
};
Server.prototype.handleLogin = function(request, response, state) {
Server.prototype.handleLogin = function (request, response, state) {
var self = this;
const querystring = require('querystring');
const formData = querystring.parse(state.data);
const { username, password, returnUrl } = formData;
const { username, password } = formData;
const user = self.sqlTiddlerDatabase.getUserByUsername(username);
const isPasswordValid = self.verifyPassword(password, user?.password)
console.log("Parsed form data:", formData);
// Use the SQL method to get the user
// const user = $tw.mws.sqlTiddlerDatabase.getUserByUsername(username);
// console.log("USER =>", username, user);
// if(user && self.verifyPassword(password, user.password_hash)) {
// // Authentication successful
// const sessionId = self.createSession(user.user_id);
// response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
// state.redirect(returnUrl ?? '/');
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 ?? '/'
'Location': returnUrl || '/'
});
response.end();
// } else {
// // Authentication failed
// self.wiki.addTiddler(new $tw.Tiddler({
// title: "$:/temp/mws/login/error",
// text: "Invalid username or password"
// }));
// state.redirect(`/login?returnUrl=${encodeURIComponent(returnUrl)}`);
// }
} else {
this.wiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: errorMessage
}));
response.writeHead(302, {
'Location': '/login'
});
}
response.end();
};
Server.prototype.verifyPassword = function(inputPassword, storedHash) {
// Implement password verification logic here
// This depends on how you've stored the passwords (e.g., bcrypt, argon2)
// For example, using bcrypt:
// return bcrypt.compareSync(inputPassword, storedHash);
// Placeholder implementation (NOT SECURE, replace with proper verification):
return inputPassword === storedHash;
const hashedInput = this.hashPassword(inputPassword);
return hashedInput === storedHash;
};
Server.prototype.hashPassword = function(password) {
return crypto.createHash('sha256').update(password).digest('hex');
};
Server.prototype.createSession = function(userId) {
const sessionId = crypto.randomBytes(16).toString('hex');
// Store the session in your database or in-memory store
// For example:
// this.sqlTiddlerDatabase.createSession(sessionId, userId);
this.sqlTiddlerDatabase.createOrUpdateUserSession(userId, sessionId);
return sessionId;
};
@@ -438,28 +441,39 @@ Server.prototype.isAuthorized = function(authorizationType,username) {
return principals.indexOf("(anon)") !== -1 || (username && (principals.indexOf("(authenticated)") !== -1 || principals.indexOf(username) !== -1));
}
Server.prototype.parseCookieString = function(cookieString) {
const cookies = {};
if (typeof cookieString !== 'string') return cookies;
cookieString.split(';').forEach(cookie => {
const parts = cookie.split('=');
if (parts.length >= 2) {
const key = parts[0].trim();
const value = parts.slice(1).join('=').trim();
cookies[key] = decodeURIComponent(value);
}
});
return cookies;
}
Server.prototype.authenticateUser = function(request, response) {
const authHeader = request.headers.authorization;
if(!authHeader) {
this.requestAuthentication(response);
const {session: session_id} = this.parseCookieString(request.headers.cookie)
if (!session_id) {
return false;
}
const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
const username = auth[0];
const password = auth[1];
// console.log({authHeader, auth, username, password, setUsername: this.get("username"), setPassword: this.get("password")})
// Check if the username and password match the configured credentials
if(username === this.get("username") && password === this.get("password")) {
return username;
}else{
return false;
// get user info
const user = this.sqlTiddlerDatabase.findUserBySessionId(session_id);
if (!user) {
return false
}
delete user.password;
return user
};
Server.prototype.requestAuthentication = function(response) {
if (!response.headersSent) {
if(!response.headersSent) {
response.writeHead(401, {
'WWW-Authenticate': 'Basic realm="Secure Area"'
});
@@ -468,11 +482,14 @@ Server.prototype.requestAuthentication = function(response) {
};
Server.prototype.redirectToLogin = function(response, returnUrl) {
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
response.writeHead(302, {
'Location': loginUrl
});
response.end();
if(!response.headersSent) {
response.setHeader('Set-Cookie', `returnUrl=${returnUrl}; HttpOnly; Path=/`);
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
response.writeHead(302, {
'Location': loginUrl
});
response.end();
}
};
Server.prototype.requestHandler = function(request,response,options) {
@@ -480,7 +497,8 @@ Server.prototype.requestHandler = function(request,response,options) {
const queryString = require("querystring");
// Authenticate the user
const authenticatedUsername = this.authenticateUser(request, response);
const authenticatedUser = this.authenticateUser(request, response);
const authenticatedUsername = authenticatedUser?.username;
// Compose the state object
var self = this;
@@ -495,6 +513,7 @@ Server.prototype.requestHandler = function(request,response,options) {
state.redirect = redirect.bind(self,request,response);
state.streamMultipartData = streamMultipartData.bind(self,request);
state.makeTiddlerEtag = makeTiddlerEtag.bind(self);
state.authenticatedUser = authenticatedUser;
state.authenticatedUsername = authenticatedUsername;
// Get the principals authorized to access this resource

View File

@@ -43,9 +43,20 @@ SqlTiddlerDatabase.prototype.createTables = function() {
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
last_login TEXT
)
`,`
-- User Session table
CREATE TABLE IF NOT EXISTS sessions (
user_id INTEGER NOT NULL,
session_id TEXT NOT NULL,
created_at TEXT NOT NULL,
last_accessed TEXT NOT NULL,
PRIMARY KEY (user_id),
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
`,`
-- Groups table
CREATE TABLE IF NOT EXISTS groups (
@@ -765,6 +776,92 @@ SqlTiddlerDatabase.prototype.listUsers = function() {
`);
};
SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) {
const currentTimestamp = new Date().toISOString();
// First, try to update an existing session
const updateResult = this.engine.runStatement(`
UPDATE sessions
SET session_id = $sessionId, last_accessed = $timestamp
WHERE user_id = $userId
`, {
$userId: userId,
$sessionId: sessionId,
$timestamp: currentTimestamp
});
// If no existing session was updated, create a new one
if (updateResult.changes === 0) {
this.engine.runStatement(`
INSERT INTO sessions (user_id, session_id, created_at, last_accessed)
VALUES ($userId, $sessionId, $timestamp, $timestamp)
`, {
$userId: userId,
$sessionId: sessionId,
$timestamp: currentTimestamp
});
}
return sessionId;
};
SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) {
// First, get the user_id from the sessions table
const sessionResult = this.engine.runStatementGet(`
SELECT user_id, last_accessed
FROM sessions
WHERE session_id = $sessionId
`, {
$sessionId: sessionId
});
if (!sessionResult) {
return null; // Session not found
}
const lastAccessed = new Date(sessionResult.last_accessed);
const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
if (new Date() - lastAccessed > expirationTime) {
// Session has expired
this.deleteSession(sessionId);
return null;
}
// Update the last_accessed timestamp
const currentTimestamp = new Date().toISOString();
this.engine.runStatement(`
UPDATE sessions
SET last_accessed = $timestamp
WHERE session_id = $sessionId
`, {
$sessionId: sessionId,
$timestamp: currentTimestamp
});
const userResult = this.engine.runStatementGet(`
SELECT *
FROM users
WHERE user_id = $userId
`, {
$userId: sessionResult.user_id
});
if (!userResult) {
return null;
}
return userResult;
};
SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) {
this.engine.runStatement(`
DELETE FROM sessions
WHERE session_id = $sessionId
`, {
$sessionId: sessionId
});
};
// Group CRUD operations
SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) {
const result = this.engine.runStatement(`