Migrated user related sechamas to src/schemas/user/

This commit is contained in:
rainbow napkin 2024-12-28 16:21:33 -05:00
parent 3eddd0ea5b
commit 3de4bff68d
26 changed files with 41 additions and 39 deletions

View file

@ -0,0 +1,127 @@
/*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/>.*/
//You could make an argument for making this part of the userModel
//However, this is so rarely used the preformance benefits aren't worth the extra clutter
//Config
const config = require('../../../config.json');
//Node Imports
const crypto = require("node:crypto");
//NPM Imports
const {mongoose} = require('mongoose');
//Local Imports
const hashUtil = require('../../utils/hashUtils');
const daysToExpire = 7;
const passwordResetSchema = new mongoose.Schema({
user: {
type: mongoose.SchemaTypes.ObjectID,
ref: "user",
required: true
},
token: {
type: mongoose.SchemaTypes.String,
required: true,
//Use a cryptographically secure algorythm to create a random hex string from 16 bytes as our reset token
default: ()=>{return crypto.randomBytes(16).toString('hex')}
},
ipHash: {
type: mongoose.SchemaTypes.String,
required: true
},
date: {
type: mongoose.SchemaTypes.Date,
required: true,
default: new Date()
}
});
//Presave function
passwordResetSchema.pre('save', async function (next){
//If we're saving an ip
if(this.isModified('ipHash')){
//Hash that sunnuvabitch
this.ipHash = hashUtil.hashIP(this.ipHash);
}
next();
});
//statics
passwordResetSchema.statics.processExpiredRequests = async function(){
//Pull all requests from the DB
const requestDB = await this.find({});
//Fire em all off at once without waiting for the last one to complete since we don't fuckin' need to
requestDB.forEach(async (request) => {
//If the request hasn't been processed and it's been expired
if(request.getDaysUntilExpiration() <= 0){
//Delete the request
await this.deleteOne({_id: request._id});
}
});
}
//methods
passwordResetSchema.methods.consume = async function(pass, confirmPass){
//Check confirmation pass
if(pass != confirmPass){
throw new Error("Confirmation password does not match!");
}
//Populate the user reference
await this.populate('user');
//Set the users password
this.user.pass = pass;
//Save the user
await this.user.save();
//Kill all authed sessions for security purposes
await this.user.killAllSessions("Your password has been reset.");
//Delete the request token now that it has been consumed
await this.deleteOne();
}
passwordResetSchema.methods.getResetURL = function(){
//Check for default port based on protocol
if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443)){
//Return path
return `${config.protocol}://${config.domain}/passwordReset?token=${this.token}`;
}else{
//Return path
return `${config.protocol}://${config.domain}:${config.port}/passwordReset?token=${this.token}`;
}
}
passwordResetSchema.methods.getDaysUntilExpiration = function(){
//Get request date
const expirationDate = new Date(this.date);
//Get expiration days and calculate expiration date
expirationDate.setDate(expirationDate.getDate() + daysToExpire);
//Calculate and return days until request expiration
return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1);
}
module.exports = mongoose.model("passwordReset", passwordResetSchema);

View file

@ -0,0 +1,258 @@
/*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 {userModel} = require('./userSchema');
const userBanSchema = new mongoose.Schema({
user: {
type: mongoose.SchemaTypes.ObjectID,
ref: "user"
},
//To be used in future when ip-hashing/better session tracking is implemented
ips: {
type: [mongoose.SchemaTypes.String],
required: false
},
//To be used in future when alt-detection has been implemented
alts: {
type: mongoose.SchemaTypes.ObjectID,
ref: "user"
},
deletedNames: {
type: [mongoose.SchemaTypes.String],
required: false
},
banDate: {
type: mongoose.SchemaTypes.Date,
required: true,
default: new Date()
},
expirationDays: {
type: mongoose.SchemaTypes.Number,
required: true,
default: 30
},
//If true, then expiration date deletes associated accounts instead of deleting the ban record
permanent: {
type: mongoose.SchemaTypes.Boolean,
required: true,
default: false
}
});
userBanSchema.statics.checkBanByUserDoc = async function(userDB){
const banDB = await this.find({});
var foundBan = null;
banDB.forEach((ban) => {
if(ban.user != null){
if(ban.user.toString() == userDB._id.toString()){
foundBan = ban;
}
}
});
return foundBan;
}
userBanSchema.statics.checkBan = async function(user){
const userDB = await userModel.findOne({user: user.user});
return this.checkBanByUserDoc(userDB);
}
userBanSchema.statics.checkProcessedBans = async function(user){
//Pull banlist and create empty variable to hold any found ban
const banDB = await this.find({});
var foundBan = null;
//For each ban in list
banDB.forEach((ban)=>{
//For each deleted account associated with the ban
ban.deletedNames.forEach((name)=>{
//If the banned name equals the name we're checking against
if(name == user){
//We've found our ban
foundBan = ban;
}
})
});
//Return any found associated ban
return foundBan;
}
userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays){
//Prevent missing users
if(userDB == null){
throw new Error("User not found")
}
//Ensure the user isn't already banned
if(await this.checkBanByUserDoc(userDB) != null){
throw new Error("User already banned");
}
if(expirationDays < 0){
throw new Error("Expiration Days must be a positive integer!");
}else if(expirationDays < 30 && permanent){
throw new Error("Permanent bans must be given at least 30 days before automatic account deletion!");
}else if(expirationDays > 185){
throw new Error("Expiration/Deletion date cannot be longer than half a year out from the original ban date.");
}
//Log the user out
if(permanent){
await userDB.killAllSessions(`Your account has been permanently banned, and will be nuked from the database in ${expirationDays} day(s).`);
}else{
await userDB.killAllSessions(`Your account has been temporarily banned, and will be reinstated in: ${expirationDays} day(s).`);
}
//Add the ban to the database
return await this.create({user: userDB._id, permanent, expirationDays});
}
userBanSchema.statics.ban = async function(user, permanent, expirationDays){
const userDB = await userModel.findOne({user: user.user});
return this.banByUserDoc(userDB, permanent, expirationDays);
}
userBanSchema.statics.unbanByUserDoc = async function(userDB){
//Prevent missing users
if(userDB == null){
throw new Error("User not found")
}
const banDB = await this.checkBanByUserDoc(userDB);
if(!banDB){
throw new Error("User already un-banned");
}
//Use _id in-case mongoose wants to be a cunt
var oldBan = await this.deleteOne({_id: banDB._id});
return oldBan;
}
userBanSchema.statics.unbanDeleted = async function(user){
const banDB = await this.checkProcessedBans(user);
if(!banDB){
throw new Error("User already un-banned");
}
const oldBan = await this.deleteOne({_id: banDB._id});
return oldBan;
}
userBanSchema.statics.unban = async function(user){
//Find user in DB
const userDB = await userModel.findOne({user: user.user});
//If user was deleted
if(userDB == null){
//unban deleted user
return await this.unbanDeleted(user.user);
}else{
//unban by user doc
return await this.unbanByUserDoc(userDB);
}
}
userBanSchema.statics.getBans = async function(){
const banDB = await this.find({}).populate('user');
var bans = [];
banDB.forEach((ban) => {
//Calcualte expiration date
var expirationDate = new Date(ban.banDate);
expirationDate.setDate(expirationDate.getDate() + ban.expirationDays);
//Make sure we're not about to read the properties of a null object
if(ban.user != null){
var userObj = {
id: ban.user.id,
user: ban.user.user,
img: ban.user.img,
date: ban.user.date
}
}
const banObj = {
banDate: ban.banDate,
expirationDays: ban.expirationDays,
expirationDate: expirationDate,
daysUntilExpiration: ban.getDaysUntilExpiration(),
user: userObj,
ips: ban.ips,
alts: ban.alts,
deletedNames: ban.deletedNames,
permanent: ban.permanent
}
bans.push(banObj);
});
return bans;
}
userBanSchema.statics.processExpiredBans = async function(){
const banDB = await this.find({});
banDB.forEach(async (ban) => {
//This ban was already processed, and it's user has been deleted. There is no more to be done...
if(ban.user == null){
return;
}
//If the ban hasn't been processed and it's got 0 or less days to go
if(ban.getDaysUntilExpiration() <= 0){
//If the ban is permanent
if(ban.permanent){
//Populate the user field
await ban.populate('user');
//Add the name to our deleted names list
ban.deletedNames.push(ban.user.user);
//Hey hey hey, goodbye!
await userModel.deleteOne({_id: ban.user._id});
//Empty out the reference
ban.user = null;
//Save the ban
await ban.save();
}else{
//Otherwise, delete the ban and let our user back in :P
await this.deleteOne({_id: ban._id});
}
}
})
}
//methods
userBanSchema.methods.getDaysUntilExpiration = function(){
//Get ban date
const expirationDate = new Date(this.banDate);
//Get expiration days and calculate expiration date
expirationDate.setDate(expirationDate.getDate() + this.expirationDays);
//Calculate and return days until ban expiration
return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1);
}
module.exports = mongoose.model("userBan", userBanSchema);

View file

@ -0,0 +1,638 @@
/*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/>.*/
//Node Imports
const { profile } = require('console');
//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');
//Utils
const hashUtil = require('../../utils/hashUtils');
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,
maxLength: 1000,
default: "Bio not set!"
},
pronouns:{
type: mongoose.SchemaTypes.String,
optional: true,
maxLength: 20,
default: ""
},
signature: {
type: mongoose.SchemaTypes.String,
required: true,
maxLength: 150,
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"
}]
});
//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")){
//Get flair properties
await this.populate('flair');
if(permissionModel.rankToNum(this.rank) < permissionModel.rankToNum(this.flair.rank)){
throw new Error(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`);
}
}
//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();
});
//post-delete function (document not query)
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
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 we didn't find a user and we submitted an email
if(userDB == null && email != null){
//Check to make sure no one's used this email address
userDB = await this.findOne({email});
}
//If the user is found or someones trying to impersonate tokeboi
if(userDB || user.toLowerCase() == "tokebot"){
throw new Error("User name/email already taken!");
}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, email});
//Tattoo the hashed IP used to register to the new user
await newUser.tattooIPRecord(ip);
}
}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.");
}
}
userSchema.statics.findProfile = async function(user){
//If someone's looking for tokebot
if(user.user.toLowerCase() == "tokebot"){
//fake a profile hashtable for tokebot
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();
}
}
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();
}
});
}
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;
}
userSchema.statics.processAgedIPRecords = async function(){
//Pull full userlist
const users = await this.find({});
//for every user
users.forEach((userDB) => {
//For every recent ip within the user
userDB.recentIPs.forEach((record, recordI) => {
//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(recordI, 1);
//No reason to wait on this since we're done with this user
userDB.save();
}
});
});
}
//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.methods.getProfile = function(){
//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,
bio: this.bio
};
//return profile hashtable
return profile;
}
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;
}
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;
}
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;
}
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;
}
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;
}
}
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!
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.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 new Error("Mismatched confirmation password!");
}
}else{
//Old password wrong
throw new Error("Incorrect Password!");
}
}
userSchema.methods.nuke = async function(pass){
//Check we have a confirmation password
if(pass == "" || pass == null){
//scream and shout
throw new Error("No confirmation password!");
}
//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 new Error("Bad pass.");
}
}
module.exports.userModel = mongoose.model("user", userSchema);