mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-09-11 07:16:08 +00:00
Implement user authentication as well as session handling
This commit is contained in:
@@ -34,6 +34,7 @@ function Server(options) {
|
|||||||
this.authenticators = options.authenticators || [];
|
this.authenticators = options.authenticators || [];
|
||||||
this.wiki = options.wiki;
|
this.wiki = options.wiki;
|
||||||
this.boot = options.boot || $tw.boot;
|
this.boot = options.boot || $tw.boot;
|
||||||
|
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase;
|
||||||
// Initialise the variables
|
// Initialise the variables
|
||||||
this.variables = $tw.utils.extend({},this.defaultVariables);
|
this.variables = $tw.utils.extend({},this.defaultVariables);
|
||||||
if(options.variables) {
|
if(options.variables) {
|
||||||
@@ -310,6 +311,14 @@ Server.prototype.loadAuthRoutes = function () {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
path: /^\/login$/,
|
path: /^\/login$/,
|
||||||
handler: function (request, response, state) {
|
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");
|
var loginTiddler = self.wiki.getTiddler("$:/plugins/tiddlywiki/authentication/login");
|
||||||
if (loginTiddler) {
|
if (loginTiddler) {
|
||||||
var text = self.wiki.renderTiddler("text/html", loginTiddler.fields.title);
|
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;
|
var self = this;
|
||||||
const querystring = require('querystring');
|
const querystring = require('querystring');
|
||||||
const formData = querystring.parse(state.data);
|
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);
|
if (user && isPasswordValid) {
|
||||||
|
const sessionId = self.createSession(user.user_id);
|
||||||
// Use the SQL method to get the user
|
const {returnUrl} = this.parseCookieString(request.headers.cookie)
|
||||||
// const user = $tw.mws.sqlTiddlerDatabase.getUserByUsername(username);
|
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
|
||||||
// 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 ?? '/');
|
|
||||||
response.writeHead(302, {
|
response.writeHead(302, {
|
||||||
'Location': '/'//returnUrl ?? '/'
|
'Location': returnUrl || '/'
|
||||||
});
|
});
|
||||||
response.end();
|
} else {
|
||||||
// } else {
|
this.wiki.addTiddler(new $tw.Tiddler({
|
||||||
// // Authentication failed
|
title: "$:/temp/mws/login/error",
|
||||||
// self.wiki.addTiddler(new $tw.Tiddler({
|
text: errorMessage
|
||||||
// title: "$:/temp/mws/login/error",
|
}));
|
||||||
// text: "Invalid username or password"
|
response.writeHead(302, {
|
||||||
// }));
|
'Location': '/login'
|
||||||
// state.redirect(`/login?returnUrl=${encodeURIComponent(returnUrl)}`);
|
});
|
||||||
// }
|
}
|
||||||
|
response.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.verifyPassword = function(inputPassword, storedHash) {
|
Server.prototype.verifyPassword = function(inputPassword, storedHash) {
|
||||||
// Implement password verification logic here
|
const hashedInput = this.hashPassword(inputPassword);
|
||||||
// This depends on how you've stored the passwords (e.g., bcrypt, argon2)
|
return hashedInput === storedHash;
|
||||||
// For example, using bcrypt:
|
};
|
||||||
// return bcrypt.compareSync(inputPassword, storedHash);
|
|
||||||
|
Server.prototype.hashPassword = function(password) {
|
||||||
// Placeholder implementation (NOT SECURE, replace with proper verification):
|
return crypto.createHash('sha256').update(password).digest('hex');
|
||||||
return inputPassword === storedHash;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.createSession = function(userId) {
|
Server.prototype.createSession = function(userId) {
|
||||||
const sessionId = crypto.randomBytes(16).toString('hex');
|
const sessionId = crypto.randomBytes(16).toString('hex');
|
||||||
// Store the session in your database or in-memory store
|
// Store the session in your database or in-memory store
|
||||||
// For example:
|
this.sqlTiddlerDatabase.createOrUpdateUserSession(userId, sessionId);
|
||||||
// this.sqlTiddlerDatabase.createSession(sessionId, userId);
|
|
||||||
return 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));
|
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) {
|
Server.prototype.authenticateUser = function(request, response) {
|
||||||
const authHeader = request.headers.authorization;
|
const {session: session_id} = this.parseCookieString(request.headers.cookie)
|
||||||
if(!authHeader) {
|
if (!session_id) {
|
||||||
this.requestAuthentication(response);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// get user info
|
||||||
const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
|
const user = this.sqlTiddlerDatabase.findUserBySessionId(session_id);
|
||||||
const username = auth[0];
|
if (!user) {
|
||||||
const password = auth[1];
|
return false
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
delete user.password;
|
||||||
|
|
||||||
|
return user
|
||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.requestAuthentication = function(response) {
|
Server.prototype.requestAuthentication = function(response) {
|
||||||
if (!response.headersSent) {
|
if(!response.headersSent) {
|
||||||
response.writeHead(401, {
|
response.writeHead(401, {
|
||||||
'WWW-Authenticate': 'Basic realm="Secure Area"'
|
'WWW-Authenticate': 'Basic realm="Secure Area"'
|
||||||
});
|
});
|
||||||
@@ -468,11 +482,14 @@ Server.prototype.requestAuthentication = function(response) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.redirectToLogin = function(response, returnUrl) {
|
Server.prototype.redirectToLogin = function(response, returnUrl) {
|
||||||
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
|
if(!response.headersSent) {
|
||||||
response.writeHead(302, {
|
response.setHeader('Set-Cookie', `returnUrl=${returnUrl}; HttpOnly; Path=/`);
|
||||||
'Location': loginUrl
|
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
|
||||||
});
|
response.writeHead(302, {
|
||||||
response.end();
|
'Location': loginUrl
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.requestHandler = function(request,response,options) {
|
Server.prototype.requestHandler = function(request,response,options) {
|
||||||
@@ -480,7 +497,8 @@ Server.prototype.requestHandler = function(request,response,options) {
|
|||||||
const queryString = require("querystring");
|
const queryString = require("querystring");
|
||||||
|
|
||||||
// Authenticate the user
|
// Authenticate the user
|
||||||
const authenticatedUsername = this.authenticateUser(request, response);
|
const authenticatedUser = this.authenticateUser(request, response);
|
||||||
|
const authenticatedUsername = authenticatedUser?.username;
|
||||||
|
|
||||||
// Compose the state object
|
// Compose the state object
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -495,6 +513,7 @@ Server.prototype.requestHandler = function(request,response,options) {
|
|||||||
state.redirect = redirect.bind(self,request,response);
|
state.redirect = redirect.bind(self,request,response);
|
||||||
state.streamMultipartData = streamMultipartData.bind(self,request);
|
state.streamMultipartData = streamMultipartData.bind(self,request);
|
||||||
state.makeTiddlerEtag = makeTiddlerEtag.bind(self);
|
state.makeTiddlerEtag = makeTiddlerEtag.bind(self);
|
||||||
|
state.authenticatedUser = authenticatedUser;
|
||||||
state.authenticatedUsername = authenticatedUsername;
|
state.authenticatedUsername = authenticatedUsername;
|
||||||
|
|
||||||
// Get the principals authorized to access this resource
|
// Get the principals authorized to access this resource
|
||||||
|
@@ -43,9 +43,20 @@ SqlTiddlerDatabase.prototype.createTables = function() {
|
|||||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
email TEXT UNIQUE NOT NULL,
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
created_at TEXT DEFAULT (datetime('now')),
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
last_login TEXT
|
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
|
-- Groups table
|
||||||
CREATE TABLE IF NOT EXISTS groups (
|
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
|
// Group CRUD operations
|
||||||
SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) {
|
SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) {
|
||||||
const result = this.engine.runStatement(`
|
const result = this.engine.runStatement(`
|
||||||
|
Reference in New Issue
Block a user