canopy/src/schemas/userSchema.js

293 lines
8.6 KiB
JavaScript

/*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 {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);