You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
4.2 KiB
JavaScript

const mongoose = require('mongoose');
const _ = require('lodash');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
require('dotenv').config();
// JWT Secret
const jwtSecret = process.env.JWT_SECRET;
const UserSchema = new mongoose.Schema({
email: {
type: String,
minlength: 1,
required: true,
trim: true,
unique: true
},
password: {
type: String,
required: true,
minlength: 8
},
sessions: [{
token: {
type: String,
required: true
},
expiresAt: {
type: Number,
required: true
}
}]
});
// Instance Methods
UserSchema.methods.toJSON = function() {
const user = this;
const userObject = user.toObject();
// return the document except the password and sessions (these shouldn't be made available)
return _.omit(userObject, ['password', 'sessions']);
}
UserSchema.methods.generateAccessAuthToken = function() {
const user = this;
return new Promise((resolve, reject) => {
// Create JWT (JSON Web Token)
jwt.sign({ _id: user._id.toHexString() }, jwtSecret, { expiresIn: "15m" }, (err, token) => {
if (!err) {
resolve(token);
} else {
reject();
}
});
});
}
UserSchema.methods.generateRefreshAuthToken = function() {
return new Promise((resolve) => {
crypto.randomBytes(64, (err, buffer) => {
if (!err) {
let token = buffer.toString('hex');
return resolve(token);
}
});
});
}
UserSchema.methods.createSession = function() {
let user = this;
return user.generateRefreshAuthToken().then((refreshToken) => {
return saveSessionToDatabase(user, refreshToken);
}).then((refreshToken) => {
return refreshToken;
}).catch((e) => {
return Promise.reject("Failed to Save Session To Database \n" + e);
});
}
/* MODEL METHODS (static methods) */
UserSchema.statics.getJWTSecret = () => {
return jwtSecret;
}
UserSchema.statics.findByIdAndToken = function (_id, token) {
// finds user by id and token
// used in auth middleware (verifySession)
const User = this;
return User.findOne({
_id,
'sessions.token': token
});
}
UserSchema.statics.findByCredentials = function (email, password) {
let User = this;
return User.findOne({ email }).then((user) => {
if (!user) return Promise.reject();
return new Promise((resolve, reject) => {
bcrypt.compare(password, user.password, (err, res) => {
if (res) {
resolve(user);
}
else {
reject();
}
});
});
});
}
UserSchema.statics.hasRefreshTokenExpired = (expiresAt) => {
let secondsSinceEpoch = Date.now() / 1000;
return expiresAt <= secondsSinceEpoch;
}
/* MIDDLEWARE */
// Before a user document is saved, this code runs
UserSchema.pre('save', function (next) {
let user = this;
let costFactor = 10;
if (user.isModified('password')) {
// if the password field has been edited/changed then run this code.
// Generate salt and hash password
bcrypt.genSalt(costFactor, (err, salt) => {
bcrypt.hash(user.password, salt, (err, hash) => {
user.password = hash;
next();
});
});
} else {
next();
}
});
/* HELPER METHODS */
let saveSessionToDatabase = (user, refreshToken) => {
return new Promise((resolve) => {
let expiresAt = generateRefreshTokenExpiryTime();
user.sessions.push({ 'token': refreshToken, expiresAt });
user.save().then(() => {
return resolve(refreshToken);
}).catch((e) => {
this.reject(e);
});
});
}
let generateRefreshTokenExpiryTime = () => {
let daysUntilExpire = "10";
let secondsUntilExpire = ((daysUntilExpire * 24) * 60) * 60;
return ((Date.now() / 1000) + secondsUntilExpire);
}
const User = mongoose.model('User', UserSchema);
module.exports = { User }