From eb48b925512ec54075a5c4d119f1247023a11e2a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 16 Oct 2025 08:25:13 -0400 Subject: [PATCH] Started work on migration UI. Improved email handling. --- src/controllers/migrateController.js | 31 +++++++++++++ src/routers/migrateRouter.js | 30 ++++++++++++ src/schemas/user/migrationSchema.js | 8 +++- src/server.js | 2 + src/utils/loggerUtils.js | 4 +- src/utils/mailUtils.js | 55 ++++++++++++++-------- src/views/migrate.ejs | 46 +++++++++++++++++++ www/css/migrate.css | 31 +++++++++++++ www/js/migrate.js | 68 ++++++++++++++++++++++++++++ 9 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 src/controllers/migrateController.js create mode 100644 src/routers/migrateRouter.js create mode 100644 src/views/migrate.ejs create mode 100644 www/css/migrate.css create mode 100644 www/js/migrate.js diff --git a/src/controllers/migrateController.js b/src/controllers/migrateController.js new file mode 100644 index 0000000..ea82e5e --- /dev/null +++ b/src/controllers/migrateController.js @@ -0,0 +1,31 @@ +/*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 .*/ + +//Config +const config = require('../../config.json'); + +//Local Imports +const altchaUtils = require('../utils/altchaUtils'); +const csrfUtils = require('../utils/csrfUtils'); + +//register page functions +module.exports.get = async function(req, res){ + //Generate captcha + const challenge = await altchaUtils.genCaptcha(); + + //Render page + return res.render('migrate', {instance: config.instanceName, user: req.session.user, challenge, csrfToken: csrfUtils.generateToken(req)}); +} \ No newline at end of file diff --git a/src/routers/migrateRouter.js b/src/routers/migrateRouter.js new file mode 100644 index 0000000..8ee67be --- /dev/null +++ b/src/routers/migrateRouter.js @@ -0,0 +1,30 @@ +/*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 .*/ + +//npm imports +const { Router } = require('express'); + + +//local imports +const migrateController = require("../controllers/migrateController"); + +//globals +const router = Router(); + +//routing functions +router.get('/', migrateController.get); + +module.exports = router; diff --git a/src/schemas/user/migrationSchema.js b/src/schemas/user/migrationSchema.js index 3cdad49..c17ed51 100644 --- a/src/schemas/user/migrationSchema.js +++ b/src/schemas/user/migrationSchema.js @@ -316,6 +316,12 @@ migrationSchema.statics.consumeByUsername = async function(ip, migration){ //Pull migration doc by case-insensitive username const migrationDB = await this.findOne({user: new RegExp(migration.user, 'i')}); + //If we have no migration document + if(migrationDB == null){ + //Bitch and moan + throw loggerUtils.exceptionSmith("Incorrect username/password.", "migration"); + } + //Wait on the miration DB token to be consumed await migrationDB.consume(ip, migration); } @@ -359,7 +365,7 @@ migrationSchema.methods.consume = async function(ip, migration){ await newUser.tattooIPRecord(ip); //if we submitted an email - if(this.email != null && this.email != ''){ + if(this.email != null && validator.isEmail(this.email)){ //Generate new email change request const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: this.email, ipHash: ip}); diff --git a/src/server.js b/src/server.js index 43a1f57..336071c 100644 --- a/src/server.js +++ b/src/server.js @@ -58,6 +58,7 @@ const channelRouter = require('./routers/channelRouter'); const newChannelRouter = require('./routers/newChannelRouter'); const passwordResetRouter = require('./routers/passwordResetRouter'); const emailChangeRouter = require('./routers/emailChangeController'); +const migrateRouter = require('./routers/migrateRouter'); //Panel const panelRouter = require('./routers/panelRouter'); //Popup @@ -161,6 +162,7 @@ app.use('/c', channelRouter); app.use('/newChannel', newChannelRouter); app.use('/passwordReset', passwordResetRouter); app.use('/emailChange', emailChangeRouter); +app.use('/migrate', migrateRouter); //Panel app.use('/panel', panelRouter); //tooltip diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index eb51977..12e5ad8 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -173,10 +173,10 @@ module.exports.errorMiddleware = function(err, req, res, next){ * @param {Error} err - error to dump to file * @param {Date} date - Date of error, defaults to now */ -module.exports.dumpError = async function(err, date = new Date()){ +module.exports.dumpError = async function(err, date = new Date(), subDir){ try{ //Crash directory - const dir = "./log/crash/" + const dir = `./log/crash/${subDir}` //Double check crash folder exists try{ diff --git a/src/utils/mailUtils.js b/src/utils/mailUtils.js index ec825a1..fce92ca 100644 --- a/src/utils/mailUtils.js +++ b/src/utils/mailUtils.js @@ -19,6 +19,11 @@ const config = require('../../config.json'); //NPM imports const nodeMailer = require("nodemailer"); +const validator = require('validator'); + +//local imports +const loggerUtils = require('./loggerUtils'); + //Setup mail transport /** @@ -43,28 +48,38 @@ const transporter = nodeMailer.createTransport({ * @returns {Object} Sent mail info */ module.exports.mailem = async function(to, subject, body, htmlBody = false){ - //Create mail object - const mailObj = { - from: `"Tokebot🤖💨"<${config.mail.address}>`, - to, - subject - }; + try{ + //If we have a bad email address + if(!validator.isEmail(to)){ + //fuck off + return; + } - //If we're sending HTML - if(htmlBody){ - //set body as html - mailObj.html = body; - //If we're sending plaintext - }else{ - //Set body as plaintext - mailObj.text = body + //Create mail object + const mailObj = { + from: `"Tokebot🤖💨"<${config.mail.address}>`, + to, + subject + }; + + //If we're sending HTML + if(htmlBody){ + //set body as html + mailObj.html = body; + //If we're sending plaintext + }else{ + //Set body as plaintext + mailObj.text = body + } + + //Send mail based on mail object + const sentMail = await transporter.sendMail(mailObj); + + //return the mail info + return sentMail; + }catch(err){ + loggerUtils.dumpError(err, new Date(), 'mail/'); } - - //Send mail based on mail object - const sentMail = await transporter.sendMail(mailObj); - - //return the mail info - return sentMail; } /** diff --git a/src/views/migrate.ejs b/src/views/migrate.ejs new file mode 100644 index 0000000..3744efb --- /dev/null +++ b/src/views/migrate.ejs @@ -0,0 +1,46 @@ +<%# 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 . %> + + + + + <%- include('partial/styles', {instance, user}); %> + <%- include('partial/csrfToken', {csrfToken}); %> + + + <%= instance %> - Account Migration + + + <%- include('partial/navbar', {user}); %> +
+ + + + + + + + + + +
+ +
+ <%- include('partial/scripts', {user}); %> + + +
+ diff --git a/www/css/migrate.css b/www/css/migrate.css new file mode 100644 index 0000000..02ef485 --- /dev/null +++ b/www/css/migrate.css @@ -0,0 +1,31 @@ +/*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 .*/ +form{ + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5em; + margin: 5% 17%; +} + +.migrate-prompt{ + width: 100% +} + +#migrate-button{ + width: 6em; + height: 2em; +} \ No newline at end of file diff --git a/www/js/migrate.js b/www/js/migrate.js new file mode 100644 index 0000000..ed4a1ee --- /dev/null +++ b/www/js/migrate.js @@ -0,0 +1,68 @@ +/*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 .*/ + +class migratePrompt{ + constructor(){ + //Grab user prompt + this.user = document.querySelector("#migrate-username"); + //Grab pass prompts + this.oldPass = document.querySelector("#migrate-password-old"); + this.pass = document.querySelector("#migrate-password"); + this.passConfirm = document.querySelector("#migrate-password-confirm"); + //Grab migrate button + this.button = document.querySelector("#migrate-button"); + //Grab altcha widget + this.altcha = document.querySelector("altcha-widget"); + //Setup null property to hold verification payload from altcha widget + this.verification = null + + //Run input setup after DOM content has completely loaded to ensure altcha event listeners work + document.addEventListener('DOMContentLoaded', this.setupInput.bind(this)); + } + + setupInput(){ + //Add verification event listener to altcha widget + this.altcha.addEventListener("verified", this.verify.bind(this)); + + //Add migrate event listener to migrate button + this.button.addEventListener("click", this.migrate.bind(this)); + } + + verify(event){ + //pull verification payload from event + this.verification = event.detail.payload; + } + + migrate(){ + //If altcha verification isn't complete + if(this.verification == null){ + //don't bother + return; + } + + //if the confirmation password doesn't match + if(this.pass.value != this.passConfirm.value){ + //Scream and shout + new canopyUXUtils.popup(`

Confirmation password does not match!

`); + return; + } + + //Send the registration informaiton off to the server + utils.ajax.migrate(this.user.value , this.oldPass.value, this.pass.value , this.passConfirm.value , this.verification); + } +} + +const migrateForm = new migratePrompt(); \ No newline at end of file