/*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'); const mailUtils = require('../../utils/mailUtils'); /** * Email change token retention time */ const daysToExpire = 7; /** * DB Schema for Document representing a single email change request */ const emailChangeSchema = new mongoose.Schema({ user: { type: mongoose.SchemaTypes.ObjectID, ref: "user", required: true }, newEmail: { type: mongoose.SchemaTypes.String, 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 change/cancel token default: ()=>{return crypto.randomBytes(32).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 and previous requests are deleted upon request creation */ emailChangeSchema.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(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 email change requests */ emailChangeSchema.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}); } } } /** * Consumes email change token, changing email address on a given user */ emailChangeSchema.methods.consume = async function(){ //Populate the user reference await this.populate('user'); const oldMail = this.user.email; //Set the new email this.user.email = this.newEmail; //Save the user await this.user.save(); //Delete the request token now that it has been consumed await this.deleteOne(); //If we had a previous email address if(oldMail != null && oldMail != ''){ //Notify it of the change await mailUtils.mailem( oldMail, `Email Change Notification - ${this.user.user}`, `

Email Change Notification

The ${config.instanceName} account '${this.user.user}' is no longer associated with this email address.
If you received this email without request, you should immediately change your password and contact the server adminsitrator! -Tokebot`, true ); } //Notify the new inbox of the change await mailUtils.mailem( this.newEmail, `Email Change Notification - ${this.user.user}`, `

Email Change Notification

The ${config.instanceName} account '${this.user.user}' is now associated with this email address.
If you received this email without request, you should immediately check who's been inside your inbox! -Tokebot`, true ); } /** * Generates email change URL from a given token * @returns {String} Email change URL generated from token */ emailChangeSchema.methods.getChangeURL = 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}/emailChange?token=${this.token}`; }else{ //Return path return `${config.protocol}://${config.domain}:${config.port}/emailChange?token=${this.token}`; } } /** * Calculates days until token expiration * @returns {Number} Days until token expiration */ emailChangeSchema.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("emailChange", emailChangeSchema);