/*Canopy - The next generation of stoner streaming software Copyright (C) 2024-2025 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 .*/ //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.js'); const loggerUtils = require('../../utils/loggerUtils.js') /** * Password reset token retention time */ const daysToExpire = 7; /** * DB Schema for documents containing a single expiring password reset token */ 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() } }); /** * Pre-save function, ensures IP's are hashed before saving */ 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); } //If the user was modified (usually only on document initialization) if(this.isModified('user')){ //Delete previous requests for the given user const requests = await this.model().deleteMany({user: this.user._id}); } next(); }); /** * Schedulable function for processing expired reset requests */ passwordResetSchema.statics.processExpiredRequests = async function(){ //Pull all requests from the DB //Tested finding request by date, but mongoose kept throwing casting errors. //This seems to be an intermittent issue online. Maybe it will work in a future version? 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 for(let requestIndex in requestDB){ //pull request from requestDB by index const request = requestDB[requestIndex]; //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 /** * Resets password and consumes token * @param {String} pass - New password to set * @param {String} confirmPass - Confirmation String to ensure new pass is correct */ passwordResetSchema.methods.consume = async function(pass, confirmPass){ //Check confirmation pass if(pass != confirmPass){ throw loggerUtils.exceptionSmith("Confirmation password does not match!", "validation"); } //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(); } /** * Generates password reset URL off of the token object * @returns {String} Reset URL */ passwordResetSchema.methods.getResetURL = function(){ //Check for default port based on protocol if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443) || config.proxied){ //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}`; } } /** * Returns number of days until token expiration * @returns {Number} Number of days until token expiration */ 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);