Implemented Profile Migration Backend
This commit is contained in:
parent
da9428205f
commit
6cbb726764
96
src/controllers/api/account/migrationController.js
Normal file
96
src/controllers/api/account/migrationController.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*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/>.*/
|
||||
|
||||
//Config
|
||||
const config = require('../../../../config.json');
|
||||
|
||||
//NPM Imports
|
||||
const {validationResult, matchedData} = require('express-validator');
|
||||
|
||||
//local imports
|
||||
const userBanModel = require('../../../schemas/user/userBanSchema');
|
||||
const altchaUtils = require('../../../utils/altchaUtils');
|
||||
const migrationModel = require('../../../schemas/user/migrationSchema');
|
||||
const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
|
||||
|
||||
module.exports.post = async function(req, res){
|
||||
try{
|
||||
//Check for validation errors
|
||||
const validResult = validationResult(req);
|
||||
|
||||
//If there are none
|
||||
if(validResult.isEmpty()){
|
||||
//Get sanatized/validated data
|
||||
const migration = matchedData(req);
|
||||
//Verify Altcha Payload
|
||||
const verified = await altchaUtils.verify(req.body.verification);
|
||||
|
||||
//If altcha verification failed
|
||||
if(!verified){
|
||||
return errorHandler(res, 'Altcha verification failed, Please refresh the page!', 'unauthorized');
|
||||
}
|
||||
|
||||
//If we're proxied use passthrough IP
|
||||
const ip = config.proxied ? req.headers['x-forwarded-for'] : req.ip;
|
||||
|
||||
//Look for ban by IP
|
||||
const ipBanDB = await userBanModel.checkBanByIP(ip);
|
||||
|
||||
//If this ip is randy bobandy
|
||||
if(ipBanDB != null){
|
||||
//Make the number a little prettier despite the lack of precision since we're not doing calculations here :P
|
||||
const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration();
|
||||
let banMsg = [];
|
||||
|
||||
//If the ban is permanent
|
||||
if(ipBanDB.permanent){
|
||||
//tell it to fuck off
|
||||
//Make the code and message look pretty (kinda) at the same time
|
||||
banMsg = [
|
||||
'The IP address you are trying to migrate an account from has been permanently banned.',
|
||||
'Your cleartext IP has been saved to the database.',
|
||||
`Any accounts associated will be nuked in ${expiration} day(s).`,
|
||||
'If you beleive this to be an error feel free to reach out to your server administrator.',
|
||||
'Otherwise, fuck off :)'
|
||||
];
|
||||
}else{
|
||||
//tell it to fuck off
|
||||
//Make the code and message look pretty (kinda) at the same time
|
||||
banMsg = [
|
||||
'The IP address you are trying to migrate an account from has been temporarily banned.',
|
||||
`Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`,
|
||||
'If you beleive this to be an error feel free to reach out to your server administrator.',
|
||||
'Otherwise, fuck off :)'
|
||||
];
|
||||
}
|
||||
|
||||
//tell it to fuck off
|
||||
return errorHandler(res, banMsg.join('<br>'), 'unauthorized');
|
||||
}
|
||||
|
||||
//Find and consume migration document
|
||||
await migrationModel.consumeByUsername(ip, migration);
|
||||
|
||||
//tell of our success
|
||||
return res.sendStatus(200);
|
||||
}else{
|
||||
res.status(400);
|
||||
return res.send({errors: validResult.array()});
|
||||
}
|
||||
}catch(err){
|
||||
return exceptionHandler(res, err);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ const accountValidator = require("../../validators/accountValidator");
|
|||
const loginController = require("../../controllers/api/account/loginController");
|
||||
const logoutController = require("../../controllers/api/account/logoutController");
|
||||
const registerController = require("../../controllers/api/account/registerController");
|
||||
const migrationController = require("../../controllers/api/account/migrationController");
|
||||
const updateController = require("../../controllers/api/account/updateController");
|
||||
const rankEnumController = require("../../controllers/api/account/rankEnumController");
|
||||
const passwordResetRequestController = require("../../controllers/api/account/passwordResetRequestController");
|
||||
|
|
@ -38,18 +39,32 @@ router.post('/login', accountValidator.user(), accountValidator.pass(), loginCon
|
|||
//logout
|
||||
router.post('/logout', logoutController.post);
|
||||
//register
|
||||
router.post('/register', accountValidator.user(),
|
||||
router.post('/register',
|
||||
accountValidator.user(),
|
||||
accountValidator.securePass(),
|
||||
accountValidator.pass('passConfirm'),
|
||||
accountValidator.email(), registerController.post);
|
||||
accountValidator.email(),
|
||||
registerController.post);
|
||||
|
||||
//migrate legacy profile
|
||||
router.post('/migrate',
|
||||
accountValidator.user(),
|
||||
accountValidator.pass('oldPass'),
|
||||
accountValidator.securePass('newPass'),
|
||||
accountValidator.pass('passConfirm'),
|
||||
migrationController.post);
|
||||
|
||||
//update profile
|
||||
router.post('/update', accountValidator.img(),
|
||||
router.post('/update',
|
||||
accountValidator.img(),
|
||||
accountValidator.bio(),
|
||||
accountValidator.signature(),
|
||||
accountValidator.pronouns(),
|
||||
accountValidator.pass('passChange.oldPass'),
|
||||
accountValidator.securePass('passChange.newPass'),
|
||||
accountValidator.pass('passChange.confirmPass'), updateController.post);
|
||||
accountValidator.pass('passChange.confirmPass'),
|
||||
updateController.post);
|
||||
|
||||
//rankEnum
|
||||
//This might seem silly, but it allows us to cleanly get the current rank list to compare against, without storing it in multiple places
|
||||
router.get('/rankEnum', rankEnumController.get);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ const config = require('../../../config.json');
|
|||
const {userModel} = require('../user/userSchema');
|
||||
const permissionModel = require('../permissionSchema');
|
||||
const tokeModel = require('../tokebot/tokeSchema');
|
||||
const statModel = require('../statSchema');
|
||||
const emailChangeModel = require('../user/emailChangeSchema');
|
||||
const loggerUtils = require('../../utils/loggerUtils');
|
||||
const hashUtils = require('../../utils/hashUtils');
|
||||
const mailUtils = require('../../utils/mailUtils');
|
||||
|
||||
/**
|
||||
* DB Schema for documents representing legacy fore.st migration data for a single user account
|
||||
|
|
@ -307,15 +311,62 @@ migrationSchema.statics.buildMigrationCache = async function(){
|
|||
}
|
||||
}
|
||||
|
||||
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')});
|
||||
|
||||
//Wait on the miration DB token to be consumed
|
||||
await migrationDB.consume(ip, migration);
|
||||
}
|
||||
|
||||
//Methods
|
||||
/**
|
||||
* Consumes a migration profile and creates a new, modern canopy profile from the original.
|
||||
* @param {String} oldPass - Original password to authenticate migration against
|
||||
* @param {String} newPass - New password to re-hash with modern hashing algo
|
||||
* @param {String} confirmPass - Confirmation for the new pass
|
||||
* @param {String} passConfirm - Confirmation for the new pass
|
||||
*/
|
||||
migrationSchema.methods.consume = async function(oldPass, newPass, confirmPass){
|
||||
migrationSchema.methods.consume = async function(ip, migration){
|
||||
//If we where handed a bad password
|
||||
if(!hashUtils.compareLegacyPassword(migration.oldPass, this.pass)){
|
||||
//Complain
|
||||
throw loggerUtils.exceptionSmith("Incorrect username/password.", "migration");
|
||||
}
|
||||
|
||||
//If we where handed a mismatched confirmation password
|
||||
if(migration.newPass != migration.passConfirm){
|
||||
//Complain
|
||||
throw loggerUtils.exceptionSmith("New password does not match confirmation password.", "migration");
|
||||
}
|
||||
|
||||
//Increment user count
|
||||
const id = await statModel.incrementUserCount();
|
||||
|
||||
//Create new user from profile info
|
||||
const newUser = await userModel.create({
|
||||
id,
|
||||
user: this.user,
|
||||
pass: migration.newPass,
|
||||
rank: permissionModel.rankEnum[this.rank],
|
||||
bio: this.bio,
|
||||
img: this.image,
|
||||
date: this.date,
|
||||
tokes: new Map([["Legacy Tokes", this.tokes]])
|
||||
});
|
||||
|
||||
//Tattoo hashed IP use to migrate to the new user account
|
||||
await newUser.tattooIPRecord(ip);
|
||||
|
||||
//if we submitted an email
|
||||
if(this.email != null && this.email != ''){
|
||||
//Generate new request
|
||||
const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: this.email, ipHash: ip});
|
||||
|
||||
//Send confirmation email
|
||||
mailUtils.sendAddressVerification(requestDB, newUser, this.email, false, true);
|
||||
}
|
||||
|
||||
await this.deleteOne();
|
||||
}
|
||||
|
||||
module.exports = mongoose.model("migration", migrationSchema);
|
||||
|
|
@ -289,7 +289,7 @@ userSchema.statics.register = async function(userObj, ip){
|
|||
if(email != null){
|
||||
const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: email, ipHash: ip});
|
||||
|
||||
await mailUtil.sendAddressVerification(requestDB, newUser, email)
|
||||
await mailUtil.sendAddressVerification(requestDB, newUser, email, true);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
//NPM Imports
|
||||
const { csrfSync } = require('csrf-sync');
|
||||
|
||||
//Local Imports
|
||||
const {errorHandler} = require('./loggerUtils');
|
||||
|
||||
//Pull needed methods from csrfSync
|
||||
const {generateToken, revokeToken, csrfSynchronisedProtection, isRequestValid} = csrfSync();
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ module.exports.hashPassword = function(pass){
|
|||
}
|
||||
|
||||
/**
|
||||
* Sitewide password for authenticating/comparing passwords agianst hashes
|
||||
* Sitewide method for authenticating/comparing passwords agianst hashes
|
||||
* @param {String} pass - Plaintext Password
|
||||
* @param {String} hash - Salty Hash
|
||||
* @returns {Boolean} True if authentication success
|
||||
|
|
@ -43,6 +43,16 @@ module.exports.comparePassword = function(pass, hash){
|
|||
return bcrypt.compareSync(pass, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sitewide method for authenticating/comparing passwords agianst hashes for legacy profiles
|
||||
* @param {String} pass - Plaintext Password
|
||||
* @param {String} hash - Salty Hash
|
||||
* @returns {Boolean} True if authentication success
|
||||
*/
|
||||
module.exports.compareLegacyPassword = function(pass, hash){
|
||||
return bcrypt.compareSync(pass, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide IP hashing/salting function
|
||||
*
|
||||
|
|
|
|||
|
|
@ -72,16 +72,40 @@ module.exports.mailem = async function(to, subject, body, htmlBody = false){
|
|||
* @param {Mongoose.Document} requestDB - DB Document Object for the current email change request token
|
||||
* @param {Mongoose.Document} userDB - DB Document Object for the user we're verifying email against
|
||||
* @param {String} newEmail - New email address to send to
|
||||
* @param {Boolean} newUser - Denotes an email going out to a new account
|
||||
* @param {Boolean} migration - Denotes an email going out to an account which was just mirated
|
||||
*/
|
||||
module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail){
|
||||
module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail, newUser = false, migration = false,){
|
||||
let subject = `Email Change Request - ${userDB.user}`;
|
||||
let content = `<h1>email change request</h1>
|
||||
<p>a request to change the email associated with the ${config.instanceName} account '${userDB.user}' to this address has been requested.<br>
|
||||
<a href="${requestDB.getChangeURL()}">click here</a> to confirm this change.</p>
|
||||
<sup>if you received this email without request, feel free to ignore and delete it! -tokebot</sup>`;
|
||||
|
||||
if(newUser){
|
||||
subject = `New User Email Confirmation - ${userDB.user}`;
|
||||
|
||||
content = `<h1>New user email confirmation</h1>
|
||||
<p>a new ${config.instanceName} account '${userDB.user}' was created with this email address.<br>
|
||||
<a href="${requestDB.getChangeURL()}">click here</a> to confirm this change.</p>
|
||||
<sup>if you received this email without request, feel free to ignore and delete it! -tokebot</sup>`;
|
||||
}
|
||||
|
||||
if(migration){
|
||||
subject = `User Migration Email Confirmation - ${userDB.user}`;
|
||||
|
||||
content = `<h1>User migration email confirmation</h1>
|
||||
<p>The ${config.instanceName} account '${userDB.user}' was successfully migrated to our <a href="https://git.ourfore.st/rainbownapkin/canopy">fancy new codebase</a>.<br>
|
||||
<a href="${requestDB.getChangeURL()}">click here</a> to confirm this change.</p>
|
||||
<sup>if you received this email without request, reach out to an admin, as your old account might be getting jacked! -tokebot</sup>`;
|
||||
}
|
||||
|
||||
|
||||
//Send the reset url via email
|
||||
await module.exports.mailem(
|
||||
newEmail,
|
||||
`Email Change Request - ${userDB.user}`,
|
||||
`<h1>Email Change Request</h1>
|
||||
<p>A request to change the email associated with the ${config.instanceName} account '${userDB.user}' to this address has been requested.<br>
|
||||
<a href="${requestDB.getChangeURL()}">Click here</a> to confirm this change.</p>
|
||||
<sup>If you received this email without request, feel free to ignore and delete it! -Tokebot</sup>`,
|
||||
subject,
|
||||
content,
|
||||
true
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -737,6 +737,24 @@ class canopyAjaxUtils{
|
|||
}
|
||||
}
|
||||
|
||||
async migrate(user, oldPass, newPass, passConfirm, verification){
|
||||
var response = await fetch(`/api/account/migrate`,{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
//It's either this or find and bind all event listeners :P
|
||||
"x-csrf-token": utils.ajax.getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({user, oldPass, newPass, passConfirm, verification})
|
||||
});
|
||||
|
||||
if(response.ok){
|
||||
location = "/";
|
||||
}else{
|
||||
utils.ux.displayResponseError(await response.json());
|
||||
}
|
||||
}
|
||||
|
||||
async login(user, pass, verification){
|
||||
var response = await fetch(`/api/account/login`,{
|
||||
method: "POST",
|
||||
|
|
|
|||
Loading…
Reference in a new issue