898 lines
32 KiB
HTML
898 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>JSDoc: Source: schemas/user/userSchema.js</title>
|
|
|
|
<script src="scripts/prettify/prettify.js"> </script>
|
|
<script src="scripts/prettify/lang-css.js"> </script>
|
|
<!--[if lt IE 9]>
|
|
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
<![endif]-->
|
|
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
|
|
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="main">
|
|
|
|
<h1 class="page-title">Source: schemas/user/userSchema.js</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
<article>
|
|
<pre class="prettyprint source linenums"><code>/*Canopy - The next generation of stoner streaming software
|
|
Copyright (C) 2024-2025 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 {mongoose} = require('mongoose');
|
|
|
|
//local imports
|
|
//server
|
|
const server = require('../../server');
|
|
//DB Models
|
|
const statModel = require('../statSchema');
|
|
const flairModel = require('../flairSchema');
|
|
const permissionModel = require('../permissionSchema');
|
|
const emoteModel = require('../emoteSchema');
|
|
const emailChangeModel = require('./emailChangeSchema');
|
|
const playlistSchema = require('../channel/media/playlistSchema');
|
|
//Utils
|
|
const hashUtil = require('../../utils/hashUtils');
|
|
const mailUtil = require('../../utils/mailUtils');
|
|
const loggerUtils = require('../../utils/loggerUtils')
|
|
|
|
|
|
/**
|
|
* Mongoose Schema for a document representing a single canopy user
|
|
*/
|
|
const userSchema = new mongoose.Schema({
|
|
id: {
|
|
type: mongoose.SchemaTypes.Number,
|
|
required: true
|
|
},
|
|
user: {
|
|
type: mongoose.SchemaTypes.String,
|
|
maxLength: 22,
|
|
required: true,
|
|
},
|
|
pass: {
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true
|
|
},
|
|
email: {
|
|
type: mongoose.SchemaTypes.String,
|
|
optional: true,
|
|
default: ""
|
|
},
|
|
date: {
|
|
type: mongoose.SchemaTypes.Date,
|
|
required: true,
|
|
default: new Date()
|
|
},
|
|
rank: {
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true,
|
|
enum: permissionModel.rankEnum,
|
|
default: "user"
|
|
},
|
|
tokes: {
|
|
type: mongoose.SchemaTypes.Map,
|
|
required: true,
|
|
default: new Map()
|
|
},
|
|
img: {
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true,
|
|
default: "/img/johnny.png"
|
|
},
|
|
bio: {
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true,
|
|
//Calculate max length by the validator max length and the size of an escaped character
|
|
maxLength: 1000 * 6,
|
|
default: "Bio not set!"
|
|
},
|
|
pronouns:{
|
|
type: mongoose.SchemaTypes.String,
|
|
optional: true,
|
|
//Calculate max length by the validator max length and the size of an escaped character
|
|
maxLength: 15 * 6,
|
|
default: ""
|
|
},
|
|
signature: {
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true,
|
|
//Calculate max length by the validator max length and the size of an escaped character
|
|
maxLength: 25 * 6,
|
|
default: "Signature not set!"
|
|
},
|
|
highLevel: {
|
|
type: mongoose.SchemaTypes.Number,
|
|
required: true,
|
|
min: 0,
|
|
max: 10,
|
|
default: 0
|
|
},
|
|
flair: {
|
|
type: mongoose.SchemaTypes.ObjectID,
|
|
default: null,
|
|
ref: "flair"
|
|
},
|
|
//Not re-using the site-wide schema because post/pre save should call different functions
|
|
emotes: [{
|
|
name:{
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true
|
|
},
|
|
link:{
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true
|
|
},
|
|
type:{
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true,
|
|
enum: emoteModel.typeEnum,
|
|
default: emoteModel.typeEnum[0]
|
|
}
|
|
}],
|
|
recentIPs: [{
|
|
ipHash: {
|
|
type: mongoose.SchemaTypes.String,
|
|
required: true
|
|
},
|
|
firstLog: {
|
|
type: mongoose.SchemaTypes.Date,
|
|
required: true,
|
|
default: new Date()
|
|
},
|
|
lastLog: {
|
|
type: mongoose.SchemaTypes.Date,
|
|
required: true,
|
|
default: new Date()
|
|
}
|
|
}],
|
|
alts:[{
|
|
type: mongoose.SchemaTypes.ObjectID,
|
|
ref: "user"
|
|
}],
|
|
playlists: [playlistSchema]
|
|
});
|
|
|
|
//This is one of those places where you really DON'T want to use an arrow function over an anonymous one!
|
|
/**
|
|
* Pre-Save function for user document, handles password hashing, flair updates, emote refreshes, and kills sessions upon rank change
|
|
*/
|
|
userSchema.pre('save', async function (next){
|
|
|
|
//If the password was changed
|
|
if(this.isModified("pass")){
|
|
//Hash that sunnovabitch, no questions asked.
|
|
this.pass = hashUtil.hashPassword(this.pass);
|
|
}
|
|
|
|
//If the flair was changed
|
|
if(this.isModified("flair")){
|
|
//Get flair properties
|
|
await this.populate('flair');
|
|
|
|
if(permissionModel.rankToNum(this.rank) < permissionModel.rankToNum(this.flair.rank)){
|
|
throw loggerUtils.exceptionSmith(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`, "unauthorized");
|
|
}
|
|
}
|
|
|
|
//Ensure we don't have empty flair
|
|
if(this.flair == null){
|
|
const flairDB = await flairModel.findOne({});
|
|
this.flair = flairDB._id;
|
|
}
|
|
|
|
//If rank was changed
|
|
if(this.isModified("rank")){
|
|
//force a full log-out
|
|
await this.killAllSessions("Your site-wide rank has changed. Sign-in required.");
|
|
}
|
|
|
|
//if emotes where modified
|
|
if(this.isModified('emotes')){
|
|
//Get the active Channel object from the application side of the house
|
|
server.channelManager.crawlConnections(this.user, (conn)=>{
|
|
//Send out emotes to each one
|
|
conn.sendPersonalEmotes(this);
|
|
});
|
|
}
|
|
|
|
//All is good, continue on saving.
|
|
next();
|
|
});
|
|
|
|
/**
|
|
* Pre-Delete function for user accounts, drops connections and rips it self out from alt accounts
|
|
*/
|
|
userSchema.post('deleteOne', {document: true}, async function (){
|
|
//Kill any active sessions
|
|
await this.killAllSessions("If you're seeing this, your account has been deleted. So long, and thanks for all the fish! <3");
|
|
|
|
//Populate alts
|
|
await this.populate('alts');
|
|
|
|
//iterate through alts
|
|
for(alt in this.alts){
|
|
//Find the index of the alt entry for this user inside of the alt users array of alt users
|
|
const altIndex = this.alts[alt].alts.indexOf(this._id);
|
|
|
|
//splice the entry for this user out of the alt users array of alt users
|
|
this.alts[alt].alts.splice(altIndex,1);
|
|
|
|
//Save the alt user
|
|
await this.alts[alt].save();
|
|
}
|
|
});
|
|
|
|
//statics
|
|
/**
|
|
* Registers a new user account with given information
|
|
* @param {Object} userObj - Object representing user to register, generated by the client
|
|
* @param {String} ip - IP Address of connection registering the account
|
|
*/
|
|
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){
|
|
//Look for a user (case insensitive)
|
|
var userDB = await this.findOne({user: new RegExp(user, 'i')});
|
|
|
|
//If the user is found or someones trying to impersonate tokeboi
|
|
if(userDB || user.toLowerCase() == "tokebot"){
|
|
throw loggerUtils.exceptionSmith("User name/email already taken!", "validation");
|
|
}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});
|
|
|
|
//Tattoo the hashed IP used to register to the new user
|
|
await newUser.tattooIPRecord(ip);
|
|
|
|
//if we submitted an email
|
|
if(email != null){
|
|
const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: email, ipHash: ip});
|
|
|
|
await mailUtil.sendAddressVerification(requestDB, newUser, email)
|
|
}
|
|
}
|
|
}else{
|
|
throw loggerUtils.exceptionSmith("Confirmation password doesn't match!", "validation");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authenticates a username and password pair
|
|
* @param {String} user - Username of account to login as
|
|
* @param {String} pass - Password to authenticat account
|
|
* @param {String} failLine - Line to paste into custom error upon login failure
|
|
* @returns {Mongoose.Document} - User DB Document upon success
|
|
*/
|
|
userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Username or Password."){
|
|
//check for missing pass
|
|
if(!user || !pass){
|
|
throw loggerUtils.exceptionSmith("Missing user/pass.", "validation");
|
|
}
|
|
|
|
//get the user if it exists
|
|
const userDB = await this.findOne({ user: new RegExp(user, 'i')});
|
|
|
|
//if not scream and shout
|
|
if(!userDB){
|
|
badLogin();
|
|
}
|
|
|
|
//Check our password is correct
|
|
if(userDB.checkPass(pass)){
|
|
return userDB;
|
|
}else{
|
|
//if not scream and shout
|
|
badLogin();
|
|
}
|
|
|
|
//standardize bad login response so it's unknown which is bad for security reasons.
|
|
function badLogin(){
|
|
throw loggerUtils.exceptionSmith(failLine, "unauthorized");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets profile by username
|
|
* @param {String} user - name of user to find
|
|
* @param {Boolean} includeEmail - Whether or not to include email in the final result
|
|
* @returns {Object} A network-friendly browser-digestable Object representing a single user profile
|
|
*/
|
|
userSchema.statics.findProfile = async function(user, includeEmail = false){
|
|
//Catch null users
|
|
if(user == null || user.user == null){
|
|
return null;
|
|
//If someone's looking for tokebot
|
|
}else if(user.user.toLowerCase() == "tokebot"){
|
|
//fake a profile hashtable for tokebot
|
|
const profile = {
|
|
id: -420,
|
|
user: "Tokebot",
|
|
date: (await statModel.getStats()).firstLaunch,
|
|
tokes: await statModel.getTokeCommandCounts(),
|
|
tokeCount: await statModel.getTokeCount(),
|
|
img: "/img/johnny.png",
|
|
signature: "!TOKE",
|
|
bio: "!TOKE OR DIE!"
|
|
};
|
|
|
|
//return the faked profile
|
|
return profile;
|
|
}else{
|
|
//find user
|
|
const userDB = await this.findOne({user: user.user});
|
|
|
|
//If we don't find a user just return a null profile
|
|
if(userDB == null){
|
|
return null
|
|
}
|
|
|
|
//return the profile
|
|
return userDB.getProfile(includeEmail);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Tattoos a single toke callout to all the users within it
|
|
* @param {Map} tokemap - Map containing list of users and the toke command they used to join the toke
|
|
*/
|
|
userSchema.statics.tattooToke = function(tokemap){
|
|
//For each toke, asynchronously:
|
|
tokemap.forEach(async (toke, user) => {
|
|
//get user
|
|
const userDB = await this.findOne({user});
|
|
|
|
//Check that the user exists (might come in handy for future treez.one integration?)
|
|
if(userDB != null){
|
|
var tokeCount = userDB.tokes.get(toke);
|
|
|
|
//if this is the first time using this toke command
|
|
if(tokeCount == null){
|
|
//set toke count to one
|
|
tokeCount = 1;
|
|
//otherwise
|
|
}else{
|
|
//increment tokecount
|
|
tokeCount++;
|
|
}
|
|
|
|
//Set the toke count for the specific command
|
|
userDB.tokes.set(toke, tokeCount);
|
|
|
|
//Save the user doc to the database
|
|
await userDB.save();
|
|
|
|
//Would rather do this inside of tokebot but then our boi would have to wait for DB or pass a nasty-looking callback function
|
|
//Crawl through active connections
|
|
server.channelManager.crawlConnections(userDB.user, (conn)=>{
|
|
//Update used toke list
|
|
conn.sendUsedTokes(userDB);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Acquires a list of the entire userbase
|
|
* @param {Boolean} fullList - Determines whether or not we're listing rank and email
|
|
* @returns {Array} Array of objects containing user metadata
|
|
*/
|
|
userSchema.statics.getUserList = async function(fullList = false){
|
|
var userList = [];
|
|
//Get all of our users
|
|
const users = await this.find({});
|
|
|
|
//Return empty if we don't find nuthin'
|
|
if(users == null){
|
|
return [];
|
|
}
|
|
|
|
//For each user
|
|
users.forEach((user)=>{
|
|
//create a user object with limited properties (safe for public consumption)
|
|
var userObj = {
|
|
id: user.id,
|
|
user: user.user,
|
|
img: user.img,
|
|
date: user.date
|
|
}
|
|
|
|
//Put together a spicier version for admins when told so (permission checks should happen before this is called)
|
|
if(fullList){
|
|
userObj.rank = user.rank,
|
|
userObj.email = user.email
|
|
}
|
|
|
|
//Add user object to list
|
|
userList.push(userObj);
|
|
});
|
|
|
|
//return the userlist
|
|
return userList;
|
|
}
|
|
|
|
/**
|
|
* Process and Deletes Aged IP Records
|
|
*/
|
|
userSchema.statics.processAgedIPRecords = async function(){
|
|
//Pull full userlist
|
|
const users = await this.find({});
|
|
|
|
//Not a fan of iterating through the DB but there doesn't seem to be a way to search for a doc based on the properties of it's subdoc
|
|
for(let userIndex in users){
|
|
//Pull user record from users by index
|
|
const userDB = users[userIndex];
|
|
//For every recent ip within the user
|
|
for(let recordIndex in userDB.recentIPs){
|
|
//Pull record from recent IPs by index
|
|
const record = userDB.recentIPs[recordIndex];
|
|
//Check how long it's been since we've last seen the IP
|
|
const daysSinceLastUse = ((new Date() - record.lastLog) / (1000 * 60 * 60 * 24)).toFixed(1);
|
|
|
|
//If it's been more than a week
|
|
if(daysSinceLastUse >= 7){
|
|
//Splice out the IP record
|
|
userDB.recentIPs.splice(recordIndex, 1);
|
|
//No reason to wait on this since we're done with this user
|
|
await userDB.save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//methods
|
|
/**
|
|
* Checks password against a user document
|
|
* @param {String} pass - Password to authenticate
|
|
* @returns {Boolean} True if authenticated
|
|
*/
|
|
userSchema.methods.checkPass = function(pass){
|
|
return hashUtil.comparePassword(pass, this.pass)
|
|
}
|
|
|
|
/**
|
|
* Lists authenticated sessions for a given user document
|
|
* @returns {Promise} Promise containing an array of active user sessions for a given user
|
|
*/
|
|
userSchema.methods.getAuthenticatedSessions = async function(){
|
|
var returnArr = [];
|
|
|
|
//retrieve active sessions (they really need to implement this shit async already)
|
|
return new Promise((resolve) => {
|
|
server.store.all((err, sessions) => {
|
|
//You guys ever hear of a 'not my' problem? Fucking y33tskies lmao, better use a try/catch
|
|
if(err){
|
|
throw err;
|
|
|
|
}
|
|
|
|
//crawl through active sessions
|
|
sessions.forEach((session) => {
|
|
//Skip un-authed sessions
|
|
if(session.user != null){
|
|
//if a session matches the current user
|
|
if(session.user.id == this.id){
|
|
//we return it
|
|
returnArr.push(session);
|
|
}
|
|
}
|
|
});
|
|
|
|
resolve(returnArr);
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* Generates a network-friendly browser-digestable profile object for a sepcific user document
|
|
* @param {Boolean} includeEmail - Whether or not to include the email address in the complete profile object
|
|
* @returns {Object} Network-Friendly Browser-Digestable object containing profile metadata
|
|
*/
|
|
userSchema.methods.getProfile = function(includeEmail = false){
|
|
//Create profile hashtable
|
|
const profile = {
|
|
id: this.id,
|
|
user: this.user,
|
|
date: this.date,
|
|
tokes: this.tokes,
|
|
tokeCount: this.getTokeCount(),
|
|
img: this.img,
|
|
signature: this.signature,
|
|
pronouns: this.pronouns,
|
|
bio: this.bio
|
|
};
|
|
|
|
//Include the email if we need to
|
|
if(includeEmail){
|
|
profile.email = this.email;
|
|
}
|
|
|
|
//return profile hashtable
|
|
return profile;
|
|
}
|
|
|
|
/**
|
|
* Gets list of profiles of alt accounts related to the given user document
|
|
* @returns {Array} List of alternate profiles
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Sets flair for current user documetn
|
|
* @param {String} flair - flair to set
|
|
* @returns {Mongoose.Document} returns found flair document from DB
|
|
*/
|
|
userSchema.methods.setFlair = async function(flair){
|
|
//Find flair by name
|
|
const flairDB = await flairModel.findOne({name: flair});
|
|
//Set the users flair ref to the found flairs _id
|
|
this.flair = flairDB._id;
|
|
//Save the user
|
|
await this.save();
|
|
//return the found flair
|
|
return flairDB;
|
|
}
|
|
|
|
/**
|
|
* Gets number of tokes for a given user document
|
|
* @returns {Number} Number of tokes recorded for the given user document
|
|
*/
|
|
userSchema.methods.getTokeCount = function(){
|
|
//Set tokeCount to 0
|
|
var tokeCount = 0;
|
|
|
|
//For each toke command the user has used
|
|
this.tokes.forEach((commandCount) => {
|
|
//Add the count for that specific command to the total
|
|
tokeCount += commandCount;
|
|
});
|
|
|
|
//Return the amount of tokes recorded
|
|
return tokeCount;
|
|
}
|
|
|
|
/**
|
|
* Gets number of emotes for a given user document
|
|
* @returns {Array} Array of objects representing emotes for the given user document
|
|
*/
|
|
userSchema.methods.getEmotes = function(){
|
|
//Create an empty array to hold our emote list
|
|
const emoteList = [];
|
|
|
|
//For each channel emote
|
|
this.emotes.forEach((emote) => {
|
|
//Push an object with select information from the emote to the emote list
|
|
emoteList.push({
|
|
name: emote.name,
|
|
link: emote.link,
|
|
type: emote.type
|
|
});
|
|
});
|
|
|
|
//return the emote list
|
|
return emoteList;
|
|
}
|
|
|
|
/**
|
|
* Deletes an emote from the user Document
|
|
* @param {String} name - Name of emote to delete
|
|
*/
|
|
userSchema.methods.deleteEmote = async function(name){
|
|
//Get index by emote name
|
|
const emoteIndex = this.emotes.findIndex(checkName);
|
|
|
|
//Splice out found emote
|
|
this.emotes.splice(emoteIndex, 1);
|
|
|
|
//Save the user doc
|
|
await this.save();
|
|
|
|
function checkName(emote){
|
|
//return emotes
|
|
return emote.name == name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets list of user playlists
|
|
* @returns {Array} Array of objects represnting a playlist containing media objects
|
|
*/
|
|
userSchema.methods.getPlaylists = function(){
|
|
//Create an empty array to hold our emote list
|
|
const playlists = [];
|
|
|
|
//For each channel emote
|
|
for(let playlist of this.playlists){
|
|
//Push an object with select information from the emote to the emote list
|
|
playlists.push(playlist.dehydrate());
|
|
}
|
|
|
|
//return the emote list
|
|
return playlists;
|
|
}
|
|
|
|
/**
|
|
* Iterates through user playlists and calls a given callback function against them
|
|
* @param {Function} cb - Callback function to call against user playlists
|
|
*/
|
|
userSchema.methods.playlistCrawl = function(cb){
|
|
for(let listIndex in this.playlists){
|
|
//Grab the associated playlist
|
|
playlist = this.playlists[listIndex];
|
|
|
|
//Call the callback with the playlist and list index as arguments
|
|
cb(playlist, listIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns playlist by name
|
|
* @param {String} name - Playlist to get by name
|
|
* @returns {Object} Object representing the requested playlist
|
|
*/
|
|
userSchema.methods.getPlaylistByName = function(name){
|
|
//Create null value to hold our found playlist
|
|
let foundPlaylist = null;
|
|
|
|
//Crawl through active playlists
|
|
this.playlistCrawl((playlist, listIndex) => {
|
|
//If we found a match based on name
|
|
if(playlist.name == name){
|
|
//Keep it
|
|
foundPlaylist = playlist;
|
|
//Pass down the list index
|
|
foundPlaylist.listIndex = listIndex;
|
|
}
|
|
});
|
|
|
|
//return the given playlist
|
|
return foundPlaylist;
|
|
}
|
|
|
|
/**
|
|
* Deletes a playlist from the user document by name
|
|
* @param {String} name - Name of playlist to delete
|
|
*/
|
|
userSchema.methods.deletePlaylistByName = async function(name){
|
|
//Find the playlist
|
|
const playlist = this.getPlaylistByName(name);
|
|
|
|
//splice out the given playlist
|
|
this.playlists.splice(playlist.listIndex, 1);
|
|
|
|
//save the channel document
|
|
await this.save();
|
|
}
|
|
|
|
/**
|
|
* Salts, Hashes and Tattoo's IP address to user document in a privacy respecting manner
|
|
* @param {String} ip - Plaintext IP Address to Salt, Hash and Tattoo
|
|
*/
|
|
userSchema.methods.tattooIPRecord = async function(ip){
|
|
//Hash the users ip
|
|
const ipHash = hashUtil.hashIP(ip);
|
|
|
|
//Look for a pre-existing entry for this ipHash
|
|
const foundIndex = this.recentIPs.findIndex(checkHash);
|
|
|
|
//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,
|
|
firstLog: 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
|
|
this.recentIPs.push(record);
|
|
|
|
//Save the user doc
|
|
await this.save();
|
|
//Otherwise, if we already have a record for this IP
|
|
}else{
|
|
//Update the last logged date for the found record
|
|
this.recentIPs[foundIndex].lastLog = new Date();
|
|
|
|
//Save the user doc
|
|
await this.save();
|
|
}
|
|
|
|
//Look for matching ip record
|
|
function checkHash(ipRecord){
|
|
//return matching records
|
|
return ipRecord.ipHash == ipHash;
|
|
}
|
|
}
|
|
|
|
//note: if you gotta call this from a request authenticated by it's user, make sure to kill that session first!
|
|
/**
|
|
* Kills all sessions for a given user
|
|
* @param {String} reason - Reason to kill user sessions
|
|
*/
|
|
userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){
|
|
//get authenticated sessions
|
|
var sessions = await this.getAuthenticatedSessions();
|
|
|
|
//crawl through and kill all sessions
|
|
sessions.forEach((session) => {
|
|
server.store.destroy(session.seshid);
|
|
});
|
|
|
|
//Tell the application side of the house to kick the user out as well
|
|
server.channelManager.kickConnections(this.user, reason);
|
|
}
|
|
|
|
/**
|
|
* Changes password for a given user document
|
|
* @param {Object} passChange - passChange object handed down from Browser
|
|
*/
|
|
userSchema.methods.changePassword = async function(passChange){
|
|
if(this.checkPass(passChange.oldPass)){
|
|
if(passChange.newPass == passChange.confirmPass){
|
|
//Note: We don't have to worry about hashing here because the schema is written to do it auto-magically
|
|
this.pass = passChange.newPass;
|
|
|
|
//Save our password
|
|
await this.save();
|
|
|
|
//Kill all authed sessions for security purposes
|
|
await this.killAllSessions("Your password has been reset.");
|
|
}else{
|
|
//confirmation pass doesn't match
|
|
throw loggerUtils.exceptionSmith("Mismatched confirmation password!", "validation");
|
|
}
|
|
}else{
|
|
//Old password wrong
|
|
throw loggerUtils.exceptionSmith("Incorrect Password!", "validation");
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Checks another user document against this one to see if they're alts
|
|
* @param {Mongoose.Document} userDB - User document to check for alts against
|
|
* @returns {Boolean} True if alt
|
|
*/
|
|
userSchema.methods.altCheck = async function(userDB){
|
|
//Found alt flag
|
|
let foundAlt = false;
|
|
|
|
for(alt in this.alts){
|
|
//If we found a match
|
|
if(this.alts[alt]._id.toString() == userDB._id.toString()){
|
|
foundAlt = true;
|
|
}
|
|
}
|
|
|
|
//return the results
|
|
return foundAlt;
|
|
}
|
|
|
|
/**
|
|
* Nukes user account from the face of the planet upon said user's request
|
|
* @param {String} pass - Password to authenticate against before deleting
|
|
*/
|
|
userSchema.methods.nuke = async function(pass){
|
|
//Check we have a confirmation password
|
|
if(pass == "" || pass == null){
|
|
//scream and shout
|
|
throw loggerUtils.exceptionSmith("No confirmation password!", "validation");
|
|
}
|
|
|
|
//Check that the password is correct
|
|
if(this.checkPass(pass)){
|
|
//delete the user
|
|
var oldUser = await this.deleteOne();
|
|
}else{
|
|
//complain about a bad pass
|
|
throw loggerUtils.exceptionSmith("Bad pass.", "unauthorized");
|
|
}
|
|
}
|
|
|
|
module.exports.userModel = mongoose.model("user", userSchema);</code></pre>
|
|
</article>
|
|
</section>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<nav>
|
|
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="activeChannel.html">activeChannel</a></li><li><a href="channelManager.html">channelManager</a></li><li><a href="chat.html">chat</a></li><li><a href="chatBuffer.html">chatBuffer</a></li><li><a href="chatHandler.html">chatHandler</a></li><li><a href="commandPreprocessor.html">commandPreprocessor</a></li><li><a href="commandProcessor.html">commandProcessor</a></li><li><a href="connectedUser.html">connectedUser</a></li><li><a href="media.html">media</a></li><li><a href="playlistHandler.html">playlistHandler</a></li><li><a href="queue.html">queue</a></li><li><a href="queuedMedia.html">queuedMedia</a></li><li><a href="tokebot.html">tokebot</a></li></ul><h3>Global</h3><ul><li><a href="global.html#authenticateSession">authenticateSession</a></li><li><a href="global.html#cache">cache</a></li><li><a href="global.html#channelBanSchema">channelBanSchema</a></li><li><a href="global.html#channelPermissionSchema">channelPermissionSchema</a></li><li><a href="global.html#channelSchema">channelSchema</a></li><li><a href="global.html#chatSchema">chatSchema</a></li><li><a href="global.html#comparePassword">comparePassword</a></li><li><a href="global.html#consoleWarn">consoleWarn</a></li><li><a href="global.html#daysToExpire">daysToExpire</a></li><li><a href="global.html#emailChangeSchema">emailChangeSchema</a></li><li><a href="global.html#emoteSchema">emoteSchema</a></li><li><a href="global.html#errorHandler">errorHandler</a></li><li><a href="global.html#errorMiddleware">errorMiddleware</a></li><li><a href="global.html#escapeRegex">escapeRegex</a></li><li><a href="global.html#exceptionHandler">exceptionHandler</a></li><li><a href="global.html#exceptionSmith">exceptionSmith</a></li><li><a href="global.html#failedAttempts">failedAttempts</a></li><li><a href="global.html#fetchMetadata">fetchMetadata</a></li><li><a href="global.html#fetchVideoMetadata">fetchVideoMetadata</a></li><li><a href="global.html#fetchYoutubeMetadata">fetchYoutubeMetadata</a></li><li><a href="global.html#fetchYoutubePlaylistMetadata">fetchYoutubePlaylistMetadata</a></li><li><a href="global.html#flairSchema">flairSchema</a></li><li><a href="global.html#genCaptcha">genCaptcha</a></li><li><a href="global.html#getLoginAttempts">getLoginAttempts</a></li><li><a href="global.html#getMediaType">getMediaType</a></li><li><a href="global.html#hashIP">hashIP</a></li><li><a href="global.html#hashPassword">hashPassword</a></li><li><a href="global.html#kickoff">kickoff</a></li><li><a href="global.html#killSession">killSession</a></li><li><a href="global.html#lifetime">lifetime</a></li><li><a href="global.html#localExceptionHandler">localExceptionHandler</a></li><li><a href="global.html#mailem">mailem</a></li><li><a href="global.html#markLink">markLink</a></li><li><a href="global.html#maxAttempts">maxAttempts</a></li><li><a href="global.html#mediaSchema">mediaSchema</a></li><li><a href="global.html#passwordResetSchema">passwordResetSchema</a></li><li><a href="global.html#permissionSchema">permissionSchema</a></li><li><a href="global.html#playlistMediaProperties">playlistMediaProperties</a></li><li><a href="global.html#playlistSchema">playlistSchema</a></li><li><a href="global.html#processExpiredAttempts">processExpiredAttempts</a></li><li><a href="global.html#queuedProperties">queuedProperties</a></li><li><a href="global.html#rankEnum">rankEnum</a></li><li><a href="global.html#refreshRawLink">refreshRawLink</a></li><li><a href="global.html#schedule">schedule</a></li><li><a href="global.html#securityCheck">securityCheck</a></li><li><a href="global.html#sendAddressVerification">sendAddressVerification</a></li><li><a href="global.html#socketCriticalExceptionHandler">socketCriticalExceptionHandler</a></li><li><a href="global.html#socketErrorHandler">socketErrorHandler</a></li><li><a href="global.html#socketExceptionHandler">socketExceptionHandler</a></li><li><a href="global.html#spent">spent</a></li><li><a href="global.html#statSchema">statSchema</a></li><li><a href="global.html#throttleAttempts">throttleAttempts</a></li><li><a href="global.html#tokeCommandSchema">tokeCommandSchema</a></li><li><a href="global.html#transporter">transporter</a></li><li><a href="global.html#typeEnum">typeEnum</a></li><li><a href="global.html#userBanSchema">userBanSchema</a></li><li><a href="global.html#userSchema">userSchema</a></li><li><a href="global.html#verify">verify</a></li><li><a href="global.html#yankMedia">yankMedia</a></li><li><a href="global.html#ytdlpFetch">ytdlpFetch</a></li></ul>
|
|
</nav>
|
|
|
|
<br class="clear">
|
|
|
|
<footer>
|
|
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.4</a> on Wed Sep 03 2025 07:51:51 GMT-0400 (Eastern Daylight Time)
|
|
</footer>
|
|
|
|
<script> prettyPrint(); </script>
|
|
<script src="scripts/linenumber.js"> </script>
|
|
</body>
|
|
</html>
|