/*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 {mongoose} = require('mongoose'); //local imports const server = require('../server'); const statModel = require('./statSchema'); const flairModel = require('./flairSchema'); const permissionModel = require('./permissionSchema'); const hashUtil = require('../utils/hashUtils'); const userSchema = new mongoose.Schema({ id: { type: mongoose.SchemaTypes.Number, required: true }, user: { type: mongoose.SchemaTypes.String, required: true, }, pass: { type: mongoose.SchemaTypes.String, required: true }, email: { type: mongoose.SchemaTypes.String }, 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, default: "Bio not set!" }, pronouns:{ type: mongoose.SchemaTypes.String, optional: true, default: "" }, signature: { type: mongoose.SchemaTypes.String, required: true, default: "Signature not set!" }, flair: { type: mongoose.SchemaTypes.String, required: false, default: "" } }); //This is one of those places where you really DON'T want to use an arrow function over an anonymous one! 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")){ //If we're not disabling flair if(this.flair != ""){ //Look for the flair that was set const foundFlair = await flairModel.findOne({name: this.flair}); //If new flair value doesn't corrispond to an existing flair if(!foundFlair){ //Throw a shit fit. Do not pass go. Do not collect $200. throw new Error("Invalid flair!"); } if(permissionModel.rankToNum(this.rank) < permissionModel.rankToNum(foundFlair.rank)){ throw new Error(`User '${this.user}' does not have a high enough rank for flair '${foundFlair.displayName}'!`); } } } if(this.isModified("rank")){ await this.killAllSessions("Your site-wide rank has changed. Sign-in required."); } //All is good, continue on saving. next(); }); //statics userSchema.statics.register = async function(userObj){ const {user, pass, passConfirm, email} = userObj; if(pass == passConfirm){ const userDB = await this.findOne({$or: email ? [{user}, {email}] : [{user}]}); if(userDB){ throw new Error("User name/email already taken!"); }else{ const id = await statModel.incrementUserCount(); const newUser = await this.create({id, user, pass, email}); } }else{ throw new Error("Confirmation password doesn't match!"); } } userSchema.statics.authenticate = async function(user, pass){ //check for missing pass if(!user || !pass){ throw new Error("Missing user/pass."); } //get the user if it exists const userDB = await this.findOne({ user }); //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 unknowin which is bad for security reasons. function badLogin(){ throw new Error("Bad Username or Password."); } } //methods userSchema.methods.checkPass = function(pass){ return hashUtil.comparePassword(pass, this.pass); } 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) => { //if a session matches the current user if(session.user.id == this.id){ //we return it returnArr.push(session); } }); resolve(returnArr); }); }); } 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; } //note: if you gotta call this from a request authenticated by it's user, make sure to kill that session first! 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); } userSchema.methods.passwordReset = 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 new Error("Mismatched confirmation password!"); } }else{ //Old password wrong throw new Error("Incorrect Password!"); } } userSchema.methods.nuke = async function(pass){ if(pass == "" || pass == null){ throw new Error("No confirmation password!"); } if(this.checkPass(pass)){ //Annoyingly there isnt a good way to do this from 'this' var oldUser = await module.exports.userModel.deleteOne(this); if(oldUser){ await this.killAllSessions("This account has been deleted. So long, and thanks for all the fish! <3"); }else{ throw new Error("Server Error: Unable to delete account! Please report this error to your server administrator, and with timestamp."); } }else{ throw new Error("Bad pass."); } } module.exports.userSchema = userSchema; module.exports.userModel = mongoose.model("user", userSchema);