Added alt-detection. Just need to set up pre-delete for userModel function to remove refrences to itself on alt accounts

This commit is contained in:
rainbow napkin 2024-12-24 10:57:55 -05:00
parent 6e785dc211
commit 980c84afff
14 changed files with 346 additions and 12 deletions

View file

@ -35,10 +35,10 @@ module.exports.post = async function(req, res){
//if we found any related nuked bans //if we found any related nuked bans
if(nukedBans != null){ if(nukedBans != null){
//Shit our pants! //Shit our pants!
return errorHandler(res, 'Cannon re-create banned account!', 'unauthorized'); return errorHandler(res, 'Cannot get alts for non-existant user!');
} }
await userModel.register(user) await userModel.register(user, req.ip);
return res.sendStatus(200); return res.sendStatus(200);
}else{ }else{
res.status(400); res.status(400);

View file

@ -0,0 +1,46 @@
/*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 <https://www.gnu.org/licenses/>.*/
//NPM Imports
const {validationResult, matchedData} = require('express-validator');
//local imports
const {userModel} = require('../../schemas/userSchema');
const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils');
//root index functions
module.exports.get = async function(req, res){
try{
const validResult = validationResult(req);
if(validResult.isEmpty()){
const data = matchedData(req);
const userDB = await userModel.findOne({user: data.user});
if(userDB == null){
return errorHandler(res, 'Cannot re-create banned account!', 'unauthorized');
}
return res.render('partial/tooltip/altList', {alts: await userDB.getAltProfiles()});
}else{
res.status(400);
return res.send({errors: validResult.array()})
}
}catch(err){
return exceptionHandler(res, err);
}
}

View file

@ -0,0 +1,32 @@
/*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 <https://www.gnu.org/licenses/>.*/
//npm imports
const { Router } = require('express');
//local imports
const altListController = require("../controllers/tooltip/altListController");
const permissionSchema = require("../schemas/permissionSchema");
const accountValidator = require("../validators/accountValidator");
//globals
const router = Router();
//routing functions
router.get('/altList', accountValidator.user(), permissionSchema.reqPermCheck("adminPanel"), altListController.get);
module.exports = router;

View file

@ -31,6 +31,7 @@ const permissionModel = require('./permissionSchema');
const emoteModel = require('./emoteSchema'); const emoteModel = require('./emoteSchema');
//Utils //Utils
const hashUtil = require('../utils/hashUtils'); const hashUtil = require('../utils/hashUtils');
const { profile } = require('console');
const userSchema = new mongoose.Schema({ const userSchema = new mongoose.Schema({
@ -135,6 +136,10 @@ const userSchema = new mongoose.Schema({
required: true, required: true,
default: new Date() default: new Date()
} }
}],
alts:[{
type: mongoose.SchemaTypes.ObjectID,
ref: "user"
}] }]
}); });
@ -183,18 +188,33 @@ userSchema.pre('save', async function (next){
}); });
//statics //statics
userSchema.statics.register = async function(userObj){ userSchema.statics.register = async function(userObj, ip){
//Pull values from user object
const {user, pass, passConfirm, email} = userObj; const {user, pass, passConfirm, email} = userObj;
//Check password confirmation matches
if(pass == passConfirm){ if(pass == passConfirm){
const userDB = await this.findOne({$or: email ? [{user}, {email}] : [{user}]}); //Look for a user (case insensitive)
var userDB = await this.findOne({user: new RegExp(user, 'i')});
//If we didn't find a user and we submitted an email
if(userDB == null && email != null){
//Check to make sure no one's used this email address
userDB = await this.findOne({email});
}
//If the user is found or someones trying to impersonate tokeboi //If the user is found or someones trying to impersonate tokeboi
if(userDB || user.toLowerCase() == "tokebot"){ if(userDB || user.toLowerCase() == "tokebot"){
throw new Error("User name/email already taken!"); throw new Error("User name/email already taken!");
}else{ }else{
//Increment the user count, pulling the id to tattoo to the user
const id = await statModel.incrementUserCount(); const id = await statModel.incrementUserCount();
//Create user document in the database
const newUser = await this.create({id, user, pass, email}); const newUser = await this.create({id, user, pass, email});
//Tattoo the hashed IP used to register to the new user
await newUser.tattooIPRecord(ip);
} }
}else{ }else{
throw new Error("Confirmation password doesn't match!"); throw new Error("Confirmation password doesn't match!");
@ -397,6 +417,23 @@ userSchema.methods.getProfile = function(){
return profile; return profile;
} }
userSchema.methods.getAltProfiles = async function(){
//Create an empty list to hold alt profiles
const profileList = [];
//populate the users alt list
await this.populate('alts');
//For every alt for the current user
for(let alt of this.alts){
//get the alts profile and push it to the profile list
profileList.push(alt.getProfile());
}
//return our generated profile list
return profileList;
}
userSchema.methods.setFlair = async function(flair){ userSchema.methods.setFlair = async function(flair){
//Find flair by name //Find flair by name
const flairDB = await flairModel.findOne({name: flair}); const flairDB = await flairModel.findOne({name: flair});
@ -471,6 +508,10 @@ userSchema.methods.tattooIPRecord = async function(ip){
//If there is no entry //If there is no entry
if(foundIndex == -1){ if(foundIndex == -1){
//Pull the entire userlist
//TODO: update query to only pull users with recentIPs, so we aren't looping through inactive users
const users = await this.model().find({});
//create record object //create record object
const record = { const record = {
ipHash: ipHash, ipHash: ipHash,
@ -478,6 +519,35 @@ userSchema.methods.tattooIPRecord = async function(ip){
lastLog: new Date() lastLog: new Date()
}; };
//We should really start using for loops and stop acting like its 2008
//Though to be quite honest this bit would be particularly brutal without them
//For every user in the userlist
for(let curUser of users){
//Ensure we're not checking the user against itself
if(curUser._id != this._id){
//For every IP record in the current user
for(let curRecord of curUser.recentIPs){
//If it matches the current ipHash
if(curRecord.ipHash == ipHash){
//Check if we've already marked the user as an alt
const foundAlt = this.alts.indexOf(curUser._id);
//If these accounts aren't already marked as alts
if(foundAlt == -1){
//Add found user to this users alt list
this.alts.push(curUser._id);
//add this user to found users alt list
curUser.alts.push(this._id);
//Save changes to the found user, this user will save at the end of the function
await curUser.save();
}
}
}
}
}
//Pop it into place //Pop it into place
this.recentIPs.push(record); this.recentIPs.push(record);

View file

@ -38,6 +38,7 @@ const channelRouter = require('./routers/channelRouter');
const newChannelRouter = require('./routers/newChannelRouter'); const newChannelRouter = require('./routers/newChannelRouter');
const panelRouter = require('./routers/panelRouter'); const panelRouter = require('./routers/panelRouter');
const popupRouter = require('./routers/popupRouter'); const popupRouter = require('./routers/popupRouter');
const tooltipRouter = require('./routers/tooltipRouter');
const apiRouter = require('./routers/apiRouter'); const apiRouter = require('./routers/apiRouter');
//Define Config //Define Config
@ -98,6 +99,8 @@ app.use('/newChannel', newChannelRouter);
app.use('/panel', panelRouter); app.use('/panel', panelRouter);
//Popup //Popup
app.use('/popup', popupRouter); app.use('/popup', popupRouter);
//tooltip
app.use('/tooltip', tooltipRouter);
//Bot-Ready //Bot-Ready
app.use('/api', apiRouter); app.use('/api', apiRouter);

View file

@ -21,7 +21,7 @@ const { check, body, checkSchema, checkExact} = require('express-validator');
const {isRank} = require('./permissionsValidator'); const {isRank} = require('./permissionsValidator');
module.exports = { module.exports = {
user: (field = 'user') => body(field).escape().trim().isLength({min: 1, max: 22}), user: (field = 'user') => check(field).escape().trim().isLength({min: 1, max: 22}),
//Password security requirements may change over time, therefore we should only validate against strongPassword() when creating new accounts //Password security requirements may change over time, therefore we should only validate against strongPassword() when creating new accounts
//that way we don't break old ones upon change //that way we don't break old ones upon change

View file

@ -53,7 +53,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.-->
</a> </a>
</td> </td>
<td id="admin-user-list-entry-name-<%- curUser.user %>" class="admin-list-entry-item not-first-col"> <td id="admin-user-list-entry-name-<%- curUser.user %>" class="admin-list-entry-item not-first-col">
<a href="/profile/<%- curUser.user %>" class="admin-list-entry-item"> <a href="/profile/<%- curUser.user %>" class="admin-list-entry-item admin-user-list-name" id="admin-user-list-name-<%- curUser.user %>">
<%- curUser.user %> <%- curUser.user %>
</a> </a>
</td> </td>

View file

@ -0,0 +1,28 @@
<!--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 <https://www.gnu.org/licenses/>.-->
<% if(alts.length > 0){ %>
<h3 class="tooltip">Known Alts:</h3>
<% for(let alt in alts){%>
<div class="seperator"></div>
<p class="tooltip">User: <%- alts[alt].user %></p>
<p class="tooltip">ID: <%- alts[alt].id %></p>
<p class="tooltip">Signature: <%- alts[alt].signature %></p>
<p class="tooltip">Toke Count: <%- alts[alt].tokeCount %></p>
<p class="tooltip">Joined: <%- alts[alt].date.toLocaleString() %></p>
<% } %>
<% }else{ %>
<p class="tooltip">No alts detected...</p>
<% } %>

View file

@ -74,6 +74,13 @@ div.dynamic-container{
user-select: none; user-select: none;
} }
.seperator{
display: block;
width: 100%;
height: 1px;
margin: 0.5em 0;
}
/* Navbar */ /* Navbar */
#navbar{ #navbar{
display: flex; display: flex;
@ -141,4 +148,17 @@ p.navbar-item, input.navbar-item{
.popup-title{ .popup-title{
margin-top: 0; margin-top: 0;
}
/* tooltip */
div.tooltip{
position: fixed;
pointer-events: all;
min-width: 1em;
min-height: 1em;
padding: 0.5em;
}
p.tooltip, h3.tooltip{
margin: 0 auto;
} }

View file

@ -1,3 +1,18 @@
/*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 <https://www.gnu.org/licenses/>.*/
.title-span{ .title-span{
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -188,6 +188,11 @@ input.control-prompt, input.control-prompt:focus{
outline: none; outline: none;
} }
.seperator{
background-color: var(--accent0);
}
/* navbar */ /* navbar */
#navbar{ #navbar{
background-color: var(--bg1); background-color: var(--bg1);
@ -344,6 +349,15 @@ select.panel-head-element{
color: var(--danger0); color: var(--danger0);
} }
/* tooltip */
div.tooltip{
background-color: var(--bg1);
color: var(--accent1);
border: 1px solid var(--accent1);
box-shadow: 4px 4px 1px var(--bg1-alt0) inset;
border-radius: 1em;
}
/* panel */ /* panel */
.title-filler-span{ .title-filler-span{
background-color: var(--accent0); background-color: var(--accent0);

View file

@ -265,6 +265,7 @@ class canopyAdminUtils{
class adminUserList{ class adminUserList{
constructor(){ constructor(){
this.userNames = document.querySelectorAll(".admin-user-list-name");
this.rankSelectors = document.querySelectorAll(".admin-user-list-rank-select"); this.rankSelectors = document.querySelectorAll(".admin-user-list-rank-select");
this.banIcons = document.querySelectorAll(".admin-user-list-ban-icon"); this.banIcons = document.querySelectorAll(".admin-user-list-ban-icon");
@ -272,13 +273,33 @@ class adminUserList{
} }
setupInput(){ setupInput(){
this.rankSelectors.forEach((rankSelector)=>{ for(let userName of this.userNames){
rankSelector.addEventListener("change", this.setRank.bind(this)) //Splice username out of class name
}); const name = userName.id.replace('admin-user-list-name-','');
this.banIcons.forEach((banIcon) => { //When the mouse starts to hover
userName.addEventListener('mouseenter',(event)=>{
//Create the tooltip
const tooltip = new canopyUXUtils.tooltip(`altList?user=${name}`, true);
//Do intial mouse move
tooltip.moveToMouse(event);
//Move the tooltip with the mouse
userName.addEventListener('mousemove', tooltip.moveToMouse.bind(tooltip));
//remove the tooltip on mouseleave
userName.addEventListener('mouseleave', tooltip.remove.bind(tooltip));
});
}
for(let rankSelector of this.rankSelectors){
rankSelector.addEventListener("change", this.setRank.bind(this))
}
for(let banIcon of this.banIcons){
banIcon.addEventListener("click", this.banPopup.bind(this)); banIcon.addEventListener("click", this.banPopup.bind(this));
}) }
} }
async setRank(event){ async setRank(event){

View file

@ -160,7 +160,6 @@ class tokeList{
this.tokeList = document.querySelector('#profile-tokes'); this.tokeList = document.querySelector('#profile-tokes');
this.tokeListLabel = document.querySelector('.profile-toke-count'); this.tokeListLabel = document.querySelector('.profile-toke-count');
this.tokeListToggleIcon = document.querySelector('#toggle-toke-list'); this.tokeListToggleIcon = document.querySelector('#toggle-toke-list');
console.log(this.tokeList)
this.setupInput(); this.setupInput();
} }

View file

@ -98,6 +98,80 @@ class canopyUXUtils{
return entryRow; return entryRow;
} }
static tooltip = class{
constructor(content, ajaxTooltip = false, cb){
//Define non-tooltip node values
this.content = content;
this.ajaxPopup = ajaxTooltip;
this.cb = cb;
this.id = Math.random();
//create and append tooltip
this.tooltip = document.createElement('div');
this.tooltip.classList.add('tooltip');
//Display tooltip even if it's not loaded to prevent removal of unloaded tooltips
this.displayTooltip();
//Fill the tooltip
this.fillTooltipContent();
}
async fillTooltipContent(){
if(this.ajaxPopup){
this.tooltip.textContent = "Loading tooltip..."
this.tooltip.innerHTML = await utils.ajax.getTooltip(this.content);
}else{
this.tooltip.innerHTML = this.content;
}
if(this.cb){
//Callbacks are kinda out of vogue, but there really isn't a good way to handle asynchronously constructed objects/classes
this.cb();
}
}
displayTooltip(){
document.body.appendChild(this.tooltip);
}
moveToPos(x,y){
//If the distance between the left edge of the window - the window width is more than the width of our tooltip
if((window.innerWidth - (window.innerWidth - x)) > this.tooltip.getBoundingClientRect().width){
//Push it to the right edge of the cursor, where the hard edge typically is
this.tooltip.style.right = `${window.innerWidth - x}px`;
this.tooltip.style.left = '';
//otherwise, if we're close to the edge
}else{
//push it away from the edge of the screen
this.tooltip.style.right = ''
this.tooltip.style.left = `${x}px`
}
//If the distance between the top edge of the window - the window height is more than the heigt of our tooltip
if((window.innerHeight - (window.innerHeight - y)) > this.tooltip.getBoundingClientRect().height){
//Push it above the mouse
this.tooltip.style.bottom = `${window.innerHeight - y}px`;
this.tooltip.style.top = '';
//otherwise if we're close to the edge
}else{
//Push it below the mouse to avoid the top edge of the screen
//-50px to account for a normal sized cursor
this.tooltip.style.bottom = '';
this.tooltip.style.top = `${y+15}px`;
}
}
moveToMouse(event){
this.moveToPos(event.clientX, event.clientY)
}
remove(){
this.tooltip.remove();
}
}
static popup = class{ static popup = class{
constructor(content, ajaxPopup = false, cb){ constructor(content, ajaxPopup = false, cb){
@ -468,6 +542,18 @@ class canopyAjaxUtils{
} }
} }
async getTooltip(tooltip){
var response = await fetch(`/tooltip/${tooltip}`,{
method: "GET"
});
if(response.status == 200){
return (await response.text())
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getChanBans(chanName){ async getChanBans(chanName){
var response = await fetch(`/api/channel/ban?chanName=${chanName}`,{ var response = await fetch(`/api/channel/ban?chanName=${chanName}`,{
method: "GET", method: "GET",