diff --git a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js index be2a15d84..1af7417b0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js @@ -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 diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 19b45f4a0..8685c69e6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -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(`