/*Canopy - The next generation of stoner streaming software Copyright (C) 2024 Rainbownapkin and the TTN Community This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ //local imports const {userModel} = require('../schemas/user/userSchema'); const userBanModel = require('../schemas/user/userBanSchema') const altchaUtils = require('../utils/altchaUtils'); //Create failed sign-in cache since it's easier and more preformant to implement it this way than adding extra burdon to the database //Server restarts are far and few between. It would take multiple during a single bruteforce attempt for this to become an issue. const failedAttempts = new Map(); const throttleAttempts = 5; const maxAttempts = 200; //this module is good for keeping wrappers for userModel and other shit in that does more session handling than database access/modification. module.exports.authenticateSession = async function(user, pass, req){ //Fuck you yoda try{ //Grab previous attempts const attempt = failedAttempts.get(user); //If we have failed attempts if(attempt != null){ //If we have more failed attempts than allowed if(attempt.count > maxAttempts){ throw new Error("This account has been locked for at 24 hours due to a large amount of failed log-in attempts"); } //If we're throttling logins if(attempt.count > throttleAttempts){ //Verification doesnt get sanatized or checked since that would most likely break the cryptography //Since we've already got access to the request and dont need to import anything, why bother getting it from a parameter? if(req.body.verification == null){ throw new Error("Verification failed!"); }else if(!altchaUtils.verify(req.body.verification, user)){ throw new Error("Verification failed!"); } } } //Authenticate the session const userDB = await userModel.authenticate(user, pass); const banDB = await userBanModel.checkBanByUserDoc(userDB); //If the user is banned if(banDB){ //Make the number a little prettier despite the lack of precision since we're not doing calculations here :P const expiration = banDB.getDaysUntilExpiration() < 1 ? 0 : banDB.getDaysUntilExpiration(); if(banDB.permanent){ throw new Error(`Your account has been permanently banned, and will be nuked from the database in: ${expiration} day(s)`); }else{ throw new Error(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`); } } //Tattoo the session with user and metadata //unfortunately store.all() does not return sessions w/ their ID so we had to improvise... //Not sure if this is just how connect-mongo is implemented or if it's an express issue, but connect-mongodb-session seems to not implement the all() function what so ever... req.session.seshid = req.session.id; req.session.authdate = new Date(); req.session.authip = req.ip; req.session.user = { user: userDB.user, id: userDB.id, rank: userDB.rank } //Tattoo hashed IP address to user account for seven days userDB.tattooIPRecord(req.ip); //If we got to here then the log-in was successful. We should clear-out any failed attempts. failedAttempts.delete(user); //return user return userDB.user; }catch(err){ //Look for previous failed attempts var attempt = failedAttempts.get(user); //If this is the first attempt if(attempt == null){ //Create new attempt object attempt = { count: 1, lastAttempt: new Date() } }else{ //Create updated attempt object attempt = { count: attempt.count + 1, lastAttempt: new Date() } } //Commit the failed attempt to the failed sign-in cache failedAttempts.set(user, attempt); //y33t throw err; } } module.exports.killSession = async function(session){ session.destroy(); } module.exports.getLoginAttempts = function(user){ //Read the code, i'm not explaining this return failedAttempts.get(user); } module.exports.processExpiredAttempts = function(){ for(user of failedAttempts.keys()){ //Get attempt by user const attempt = failedAttempts.get(user); //Check how long its been const daysSinceLastAttempt = ((new Date() - attempt.lastAttempt) / (1000 * 60 * 60 * 24)).toFixed(1); //If it's been more than a day since anyones tried to log in as this user if(daysSinceLastAttempt >= 1){ //Clear out the attempts so that they don't need to fuck with a captcha anymore failedAttempts.delete(user); } } } module.exports.throttleAttempts = throttleAttempts; module.exports.maxAttempts = maxAttempts;