293 lines
8.7 KiB
JavaScript
293 lines
8.7 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("If you're seeing this, your 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); |