Source: schemas/user/passwordResetSchema.js

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