diff --git a/src/controllers/api/account/registerController.js b/src/controllers/api/account/registerController.js
index 1ea0d2c..b7d1bf7 100644
--- a/src/controllers/api/account/registerController.js
+++ b/src/controllers/api/account/registerController.js
@@ -35,10 +35,10 @@ module.exports.post = async function(req, res){
//if we found any related nuked bans
if(nukedBans != null){
//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);
}else{
res.status(400);
diff --git a/src/controllers/tooltip/altListController.js b/src/controllers/tooltip/altListController.js
new file mode 100644
index 0000000..d192d48
--- /dev/null
+++ b/src/controllers/tooltip/altListController.js
@@ -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 .*/
+
+//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);
+ }
+}
\ No newline at end of file
diff --git a/src/routers/tooltipRouter.js b/src/routers/tooltipRouter.js
new file mode 100644
index 0000000..45080e2
--- /dev/null
+++ b/src/routers/tooltipRouter.js
@@ -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 .*/
+
+//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;
diff --git a/src/schemas/userSchema.js b/src/schemas/userSchema.js
index 6878414..3d2b8d3 100644
--- a/src/schemas/userSchema.js
+++ b/src/schemas/userSchema.js
@@ -31,6 +31,7 @@ const permissionModel = require('./permissionSchema');
const emoteModel = require('./emoteSchema');
//Utils
const hashUtil = require('../utils/hashUtils');
+const { profile } = require('console');
const userSchema = new mongoose.Schema({
@@ -135,6 +136,10 @@ const userSchema = new mongoose.Schema({
required: true,
default: new Date()
}
+ }],
+ alts:[{
+ type: mongoose.SchemaTypes.ObjectID,
+ ref: "user"
}]
});
@@ -183,18 +188,33 @@ userSchema.pre('save', async function (next){
});
//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;
+ //Check password confirmation matches
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(userDB || user.toLowerCase() == "tokebot"){
throw new Error("User name/email already taken!");
}else{
+ //Increment the user count, pulling the id to tattoo to the user
const id = await statModel.incrementUserCount();
+
+ //Create user document in the database
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{
throw new Error("Confirmation password doesn't match!");
@@ -397,6 +417,23 @@ userSchema.methods.getProfile = function(){
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){
//Find flair by name
const flairDB = await flairModel.findOne({name: flair});
@@ -471,6 +508,10 @@ userSchema.methods.tattooIPRecord = async function(ip){
//If there is no entry
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
const record = {
ipHash: ipHash,
@@ -478,6 +519,35 @@ userSchema.methods.tattooIPRecord = async function(ip){
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
this.recentIPs.push(record);
diff --git a/src/server.js b/src/server.js
index 1525b31..6e0ad85 100644
--- a/src/server.js
+++ b/src/server.js
@@ -38,6 +38,7 @@ const channelRouter = require('./routers/channelRouter');
const newChannelRouter = require('./routers/newChannelRouter');
const panelRouter = require('./routers/panelRouter');
const popupRouter = require('./routers/popupRouter');
+const tooltipRouter = require('./routers/tooltipRouter');
const apiRouter = require('./routers/apiRouter');
//Define Config
@@ -98,6 +99,8 @@ app.use('/newChannel', newChannelRouter);
app.use('/panel', panelRouter);
//Popup
app.use('/popup', popupRouter);
+//tooltip
+app.use('/tooltip', tooltipRouter);
//Bot-Ready
app.use('/api', apiRouter);
diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js
index d875abd..82bcdf7 100644
--- a/src/validators/accountValidator.js
+++ b/src/validators/accountValidator.js
@@ -21,7 +21,7 @@ const { check, body, checkSchema, checkExact} = require('express-validator');
const {isRank} = require('./permissionsValidator');
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
//that way we don't break old ones upon change
diff --git a/src/views/partial/adminPanel/userList.ejs b/src/views/partial/adminPanel/userList.ejs
index 6119dcd..fffe830 100644
--- a/src/views/partial/adminPanel/userList.ejs
+++ b/src/views/partial/adminPanel/userList.ejs
@@ -53,7 +53,7 @@ along with this program. If not, see .-->
-
+
<%- curUser.user %>
|
diff --git a/src/views/partial/tooltip/altList.ejs b/src/views/partial/tooltip/altList.ejs
new file mode 100644
index 0000000..3ad3358
--- /dev/null
+++ b/src/views/partial/tooltip/altList.ejs
@@ -0,0 +1,28 @@
+
+<% if(alts.length > 0){ %>
+
+ <% for(let alt in alts){%>
+
+ User: <%- alts[alt].user %>
+ ID: <%- alts[alt].id %>
+ Signature: <%- alts[alt].signature %>
+ Toke Count: <%- alts[alt].tokeCount %>
+ Joined: <%- alts[alt].date.toLocaleString() %>
+ <% } %>
+<% }else{ %>
+ No alts detected...
+<% } %>
\ No newline at end of file
diff --git a/www/css/global.css b/www/css/global.css
index 3f1a715..8d14b21 100644
--- a/www/css/global.css
+++ b/www/css/global.css
@@ -74,6 +74,13 @@ div.dynamic-container{
user-select: none;
}
+.seperator{
+ display: block;
+ width: 100%;
+ height: 1px;
+ margin: 0.5em 0;
+}
+
/* Navbar */
#navbar{
display: flex;
@@ -141,4 +148,17 @@ p.navbar-item, input.navbar-item{
.popup-title{
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;
}
\ No newline at end of file
diff --git a/www/css/panel/emote.css b/www/css/panel/emote.css
index 65cafe0..d809fca 100644
--- a/www/css/panel/emote.css
+++ b/www/css/panel/emote.css
@@ -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 .*/
.title-span{
display: flex;
flex-direction: row;
diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css
index b94b057..35967bf 100644
--- a/www/css/theme/movie-night.css
+++ b/www/css/theme/movie-night.css
@@ -188,6 +188,11 @@ input.control-prompt, input.control-prompt:focus{
outline: none;
}
+
+.seperator{
+ background-color: var(--accent0);
+}
+
/* navbar */
#navbar{
background-color: var(--bg1);
@@ -344,6 +349,15 @@ select.panel-head-element{
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 */
.title-filler-span{
background-color: var(--accent0);
diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js
index 3df0d25..94afd67 100644
--- a/www/js/adminPanel.js
+++ b/www/js/adminPanel.js
@@ -265,6 +265,7 @@ class canopyAdminUtils{
class adminUserList{
constructor(){
+ this.userNames = document.querySelectorAll(".admin-user-list-name");
this.rankSelectors = document.querySelectorAll(".admin-user-list-rank-select");
this.banIcons = document.querySelectorAll(".admin-user-list-ban-icon");
@@ -272,13 +273,33 @@ class adminUserList{
}
setupInput(){
- this.rankSelectors.forEach((rankSelector)=>{
- rankSelector.addEventListener("change", this.setRank.bind(this))
- });
+ for(let userName of this.userNames){
+ //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));
- })
+ }
}
async setRank(event){
diff --git a/www/js/profile.js b/www/js/profile.js
index ad128ca..2dcda67 100644
--- a/www/js/profile.js
+++ b/www/js/profile.js
@@ -160,7 +160,6 @@ class tokeList{
this.tokeList = document.querySelector('#profile-tokes');
this.tokeListLabel = document.querySelector('.profile-toke-count');
this.tokeListToggleIcon = document.querySelector('#toggle-toke-list');
- console.log(this.tokeList)
this.setupInput();
}
diff --git a/www/js/utils.js b/www/js/utils.js
index 4a64c27..a33920a 100644
--- a/www/js/utils.js
+++ b/www/js/utils.js
@@ -98,6 +98,80 @@ class canopyUXUtils{
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{
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){
var response = await fetch(`/api/channel/ban?chanName=${chanName}`,{
method: "GET",