From 25a61b580970cdabb76da9558f7b0faf5e0d6f49 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 8 Nov 2026 21:49:36 -0500 Subject: [PATCH 1/2] Created http api endpoints for managing ignored users list --- .../api/account/ignoredController.js | 125 ++++++++++++++++++ src/controllers/profileController.js | 4 +- src/routers/api/accountRouter.js | 5 + src/schemas/user/userSchema.js | 4 - 4 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 src/controllers/api/account/ignoredController.js diff --git a/src/controllers/api/account/ignoredController.js b/src/controllers/api/account/ignoredController.js new file mode 100644 index 0000000..04c6c4c --- /dev/null +++ b/src/controllers/api/account/ignoredController.js @@ -0,0 +1,125 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024-2026 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/user/userSchema'); +const accountUtils = require('../../../utils/sessionUtils'); +const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); + +//api account functions +module.exports.get = async function(req, res){ + try{ + //syntactic sugar :P + const user = req.session.user; + + //make sure we're not bullshitting ourselves here. + if(user == null || user.user == null){ + return errorHandler(res, 'You must be logged in to retrieve your ignored users!', 'unauthorized'); + } + + //Grab user + const userDB = await userModel.findOne({user: user.user}); + + //Bitch about non-existant users + if(!userDB){ + return errorHandler(res, 'User not found!', 'unauthorized'); + } + + //Set status and send ignored user list + res.status(200); + return res.send((await userDB.getIgnoredUsers(false)).user); + + }catch(err){ + exceptionHandler(res, err); + } +} + +module.exports.post = async function(req, res){ + try{ + const validResult = validationResult(req); + + if(validResult.isEmpty()){ + //syntactic sugar :P + const user = req.session.user; + const data = matchedData(req); + + //make sure we're not bullshitting ourselves here. + if(user == null || user.user == null){ + return errorHandler(res, 'You must be logged in to ignore a user!', 'unauthorized'); + } + + //Grab user + const userDB = await userModel.findOne({user: user.user}); + + //Bitch about non-existant users + if(!userDB){ + return errorHandler(res, 'User not found!', 'unauthorized'); + } + + //Ignore given user + await userDB.ignoreByUser(data.user); + + //Set status and send ignored user list + res.status(200); + return res.send((await userDB.getIgnoredUsers(false)).user); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } +} + +module.exports.delete = async function(req, res){ + try{ + const validResult = validationResult(req); + + if(validResult.isEmpty()){ + //syntactic sugar :P + const user = req.session.user; + const data = matchedData(req); + + //make sure we're not bullshitting ourselves here. + if(user == null || user.user == null){ + return errorHandler(res, 'You must be logged in to ignore a user!', 'unauthorized'); + } + + //Grab user + const userDB = await userModel.findOne({user: user.user}); + + //Bitch about non-existant users + if(!userDB){ + return errorHandler(res, 'User not found!', 'unauthorized'); + } + + //Unignore given user + await userDB.unignoreByUser(data.user); + + //Set status and send ignored user list + res.status(200); + return res.send((await userDB.getIgnoredUsers(false)).user); + }else{ + res.status(400); + res.send({errors: validResult.array()}) + } + }catch(err){ + exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/controllers/profileController.js b/src/controllers/profileController.js index cb7c846..4b382cc 100644 --- a/src/controllers/profileController.js +++ b/src/controllers/profileController.js @@ -27,9 +27,11 @@ const validator = require('validator');//No express here, so regular validator i //profile functions module.exports.get = async function(req, res){ try{ + //Get profile name from URL var profileName = req.url.slice(1) == '' ? (req.session.user ? req.session.user.user : null) : req.url.slice(1); - const profile = await userModel.findProfile({user: profileName}, true); + //Pull profile including semi-sensitive information (this object will only be parsed server side) + const profile = await userModel.findProfile({user: profileName}, true, true); if(profile){ //If we have a user, check if the is looking at their own profile diff --git a/src/routers/api/accountRouter.js b/src/routers/api/accountRouter.js index a846d6e..fceeba5 100644 --- a/src/routers/api/accountRouter.js +++ b/src/routers/api/accountRouter.js @@ -28,6 +28,7 @@ const rankEnumController = require("../../controllers/api/account/rankEnumContro const passwordResetRequestController = require("../../controllers/api/account/passwordResetRequestController"); const passwordResetController = require("../../controllers/api/account/passwordResetController"); const emailChangeRequestController = require('../../controllers/api/account/emailChangeRequestController'); +const ignoredController = require("../../controllers/api/account/ignoredController"); const deleteController = require("../../controllers/api/account/deleteController"); //globals @@ -74,6 +75,10 @@ router.post('/passwordResetRequest', accountValidator.user(), passwordResetReque router.post('/passwordReset', accountValidator.securityToken(), accountValidator.securePass(), accountValidator.pass('confirmPass'), passwordResetController.post); //email change request router.post('/emailChangeRequest', accountValidator.email(), accountValidator.pass(), emailChangeRequestController.post); +//ignored users list management +router.get('/ignore', ignoredController.get); +router.post('/ignore', accountValidator.user(), ignoredController.post); +router.post('/unignore', accountValidator.user(), ignoredController.delete); //account deletion router.post('/delete', accountValidator.pass(), deleteController.post); diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 5139e99..c7926e3 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -702,8 +702,6 @@ userSchema.methods.ignoreByUserDoc = async function(userDB){ //Save the DB document await this.save(); - - console.log(await this.getIgnoredUsers()); } /** @@ -737,8 +735,6 @@ userSchema.methods.unignoreByUserDoc = async function(userDB){ //Save the DB document await this.save(); - - console.log(await this.getIgnoredUsers()); } userSchema.methods.getIgnoredUsers = async function(fetchAlts = true){ From c2bbf0b6f8283b4384f28d37fa38c7fad6021bcf Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 8 Nov 2026 23:01:02 -0500 Subject: [PATCH 2/2] Finished up work on ignored users management popup in profile page --- src/views/partial/profile/settings.ejs | 1 + www/css/global.css | 3 +- www/css/popup/ignoredUsers.css | 38 ++++++++ www/css/theme/movie-night.css | 13 +++ www/css/theme/the-network-classic.css | 12 +++ www/js/profile.js | 130 +++++++++++++++++-------- www/js/utils.js | 46 +++++++++ www/popup/ignoredUsers.html | 24 +++++ 8 files changed, 225 insertions(+), 42 deletions(-) create mode 100644 www/css/popup/ignoredUsers.css create mode 100644 www/popup/ignoredUsers.html diff --git a/src/views/partial/profile/settings.ejs b/src/views/partial/profile/settings.ejs index 8f5a9f8..aabe7a8 100644 --- a/src/views/partial/profile/settings.ejs +++ b/src/views/partial/profile/settings.ejs @@ -22,6 +22,7 @@ along with this program. If not, see . %> \ No newline at end of file diff --git a/www/css/global.css b/www/css/global.css index d3a2919..9707e01 100644 --- a/www/css/global.css +++ b/www/css/global.css @@ -153,7 +153,8 @@ p.navbar-item, input.navbar-item{ bottom: 0; right: 0; left: 0; - z-index: 9000; + /*Popup backer has to exist behind popups that are behind default ones for error reasons*/ + z-index: 8998; } .popup-div{ diff --git a/www/css/popup/ignoredUsers.css b/www/css/popup/ignoredUsers.css new file mode 100644 index 0000000..18ee7ce --- /dev/null +++ b/www/css/popup/ignoredUsers.css @@ -0,0 +1,38 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024-2026 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 .*/ + +/*Gotta keep er' behind the errors*/ +.popup-div:has(#ignored-users-popup){ + z-index: 8999; +} + +#ignored-users-popup-list-div{ + padding: 0; + margin-bottom: 1em; +} + +.ignored-users-popup-ignored-user { + display: flex; +} + +.ignored-users-popup-ignored-user-label { + margin: 0; +} + +.ignored-users-popup-ignored-user { + justify-content: space-between; + padding: 0 0.5em; +} diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 053356f..2f7b2df 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -23,6 +23,7 @@ along with this program. If not, see .*/ --bg1: rgb(70, 70, 70); --bg2: rgb(220, 220, 220); --bg1-alt0: rgb(30, 30, 30); + --bg1-alt1: rgb(10, 10, 10); --bg2-alt0: rgb(180, 180, 180); --accent0: rgb(47, 47, 47); @@ -588,6 +589,18 @@ select.panel-head-element{ color: var(--accent1); } +/*Ignored Users Popup*/ +#ignored-users-popup-list-div{ + border-radius: 0; + border: 1px solid var(--accent1); +} + + +.ignored-users-popup-ignored-user:nth-child(even):not(.chat-entry-toke){ + box-shadow: 3px 0px 1px var(--bg1-alt1) inset; + background-color: var(--bg1-alt0); +} + /* psa */ #psa-container{ border: 3px solid var(--danger0); diff --git a/www/css/theme/the-network-classic.css b/www/css/theme/the-network-classic.css index b0edd61..6a16288 100644 --- a/www/css/theme/the-network-classic.css +++ b/www/css/theme/the-network-classic.css @@ -778,6 +778,18 @@ select.panel-head-element{ color: var(--accent1); } +/*Ignored Users Popup*/ +#ignored-users-popup-list-div{ + border-radius: 0; + border: 1px solid var(--accent1); +} + + +.ignored-users-popup-ignored-user:nth-child(even):not(.chat-entry-toke){ + box-shadow: 3px 0px 1px var(--bg1) inset; + background-color: var(--bg2-alt0); +} + /* psa */ #psa-container{ border: 3px solid var(--danger0); diff --git a/www/js/profile.js b/www/js/profile.js index ed823fe..91847ae 100644 --- a/www/js/profile.js +++ b/www/js/profile.js @@ -142,46 +142,6 @@ class profileUpdateImagePrompt extends profileUpdatePrompt{ } } -class passwordResetPrompt{ - constructor(){ - this.oldPassNode = document.querySelector('#account-settings-password-reset-old'); - this.newPassNode = document.querySelector('#account-settings-password-reset-new'); - this.confirmPassNode = document.querySelector('#account-settings-password-reset-confirm'); - - this.setupInput(this.oldPassNode); - this.setupInput(this.newPassNode); - this.setupInput(this.confirmPassNode); - } - - setupInput(node){ - if(node != null){ - node.addEventListener("keydown", this.update.bind(this)); - } - } - - async update(event){ - var hasVal = (this.oldPassNode.value && this.newPassNode.value && this.confirmPassNode.value); - if((!event || event.key == "Enter") && hasVal){ - if(this.newPassNode.value == this.confirmPassNode.value){ - const updateObj = {}; - - updateObj.passChange = { - oldPass: this.oldPassNode.value, - newPass: this.newPassNode.value, - confirmPass: this.confirmPassNode.value - }; - - const response = await utils.ajax.updateProfile(updateObj); - - if(response != null){ - //Return user homepage after good pass change, as we've probably been logged out by the server for security. - window.location.pathname = '/'; - } - } - } - } -} - class tokeList{ constructor(){ this.tokeList = document.querySelector('#profile-tokes'); @@ -288,6 +248,93 @@ class changePasswordPopup{ } } +class ignoredUsersPopup{ + constructor(){ + this.popup = new canopyUXUtils.popup("ignoredUsers", true, this.asyncConstructor.bind(this), document, false); + } + + asyncConstructor(){ + this.ignoredUsersListDiv = document.querySelector('#ignored-users-popup-list-div') + this.ignorePrompt = document.querySelector('#ignored-users-popup-prompt'); + this.ignoreButton = document.querySelector('#ignored-users-popup-button'); + + //Refresh ignored list + this.refreshList(); + + this.setupInput(); + } + + //grabs and re-renders list, good for asynchronous fire-and-forget usage + async refreshList(){ + this.renderUsers(await utils.ajax.getIgnoredUsers()); + } + + renderUsers(users){ + //Clear out list div + this.ignoredUsersListDiv.innerHTML = ''; + + //run through our ignored users + for(const user of users){ + //Create a div about it + const userDiv = document.createElement('div'); + userDiv.classList.add('ignored-users-popup-ignored-user'); + + //Create userlabel + const userLabel = document.createElement('p'); + userLabel.classList.add('ignored-users-popup-ignored-user-label'); + userLabel.textContent = user.user; + + //Create unignore icon + const unignoreIcon = document.createElement('i'); + unignoreIcon.classList.add('bi-emoji-smile-fill', 'ignored-users-popup-unignore-user-icon'); + unignoreIcon.title = `Unignore ${user.user}`; + + //Make the unignore button actually fuckin' do something for once in its no-good, lazy, yellowbellied, chicken-fucking life + unignoreIcon.addEventListener('click', (event)=>{this.unignore(user.user)}); + + //ASSEMBLE + userDiv.append(userLabel, unignoreIcon); + + //Render out to popup + this.ignoredUsersListDiv.appendChild(userDiv); + + } + } + + setupInput(){ + this.ignorePrompt.addEventListener('keydown', this.ignore.bind(this)); + this.ignoreButton.addEventListener('click', this.ignore.bind(this)); + } + + async ignore(event){ + //If we clicked or hit enter + if(event.key == null || event.key == "Enter"){ + //Tell server who you wanna ignore and get the new list back + const ignoredList = await utils.ajax.ignoreUser(this.ignorePrompt.value); + + //If we received a good list + if(ignoredList != null){ + //Render out received ignored lsit + this.renderUsers(ignoredList); + } + + //Clear prompt + this.ignorePrompt.value = ""; + } + } + + async unignore(user){ + //Tell server who you wanna unignore and get the new list back + const ignoredList = await utils.ajax.unignoreUser(user); + + //If we received a good list + if(ignoredList != null){ + //Render out received ignored lsit + this.renderUsers(ignoredList); + } + } +} + class deleteAccountPopup{ constructor(){ this.popup = new canopyUXUtils.popup("nukeUser", true, this.asyncConstructor.bind(this)); @@ -318,4 +365,5 @@ const bioPrompt = new profileUpdateTextPrompt('bio', true); const profileTokeList = new tokeList(); const accountEmailChange = new accountSettingsButton('update-email', changeEmailPopup); const accountPasswordChange = new accountSettingsButton('change-password', changePasswordPopup); -const accountDeleteButton = new accountSettingsButton('delete', deleteAccountPopup); \ No newline at end of file +const accountIgnoredUsers = new accountSettingsButton('change-password', changePasswordPopup); +const accountDeleteButton = new accountSettingsButton('ignored-users', ignoredUsersPopup); \ No newline at end of file diff --git a/www/js/utils.js b/www/js/utils.js index bdfa514..4661e3a 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -885,6 +885,52 @@ class canopyAjaxUtils{ } } + async getIgnoredUsers(){ + var response = await fetch(`/api/account/ignore`,{ + method: "GET" + }); + + if(response.ok){ + return (await response.json()) + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + + async ignoreUser(user){ + const response = await fetch(`/api/account/ignore`,{ + method: "POST", + headers: { + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() + }, + body: JSON.stringify({user}) + }); + + if(response.ok){ + return (await response.json()) + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + + async unignoreUser(user){ + const response = await fetch(`/api/account/unignore`,{ + method: "POST", + headers: { + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() + }, + body: JSON.stringify({user}) + }); + + if(response.ok){ + return (await response.json()) + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + async deleteAccount(pass){ const response = await fetch(`/api/account/delete`,{ method: "POST", diff --git a/www/popup/ignoredUsers.html b/www/popup/ignoredUsers.html new file mode 100644 index 0000000..5eae53a --- /dev/null +++ b/www/popup/ignoredUsers.html @@ -0,0 +1,24 @@ + + + + +
+
+
+ + +
\ No newline at end of file