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 loginController = require("../../controllers/api/account/loginController");
|
||||||
const logoutController = require("../../controllers/api/account/logoutController");
|
const logoutController = require("../../controllers/api/account/logoutController");
|
||||||
const registerController = require("../../controllers/api/account/registerController");
|
const registerController = require("../../controllers/api/account/registerController");
|
||||||
|
const migrationController = require("../../controllers/api/account/migrationController");
|
||||||
const updateController = require("../../controllers/api/account/updateController");
|
const updateController = require("../../controllers/api/account/updateController");
|
||||||
const rankEnumController = require("../../controllers/api/account/rankEnumController");
|
const rankEnumController = require("../../controllers/api/account/rankEnumController");
|
||||||
const passwordResetRequestController = require("../../controllers/api/account/passwordResetRequestController");
|
const passwordResetRequestController = require("../../controllers/api/account/passwordResetRequestController");
|
||||||
|
|
@ -38,18 +39,32 @@ router.post('/login', accountValidator.user(), accountValidator.pass(), loginCon
|
||||||
//logout
|
//logout
|
||||||
router.post('/logout', logoutController.post);
|
router.post('/logout', logoutController.post);
|
||||||
//register
|
//register
|
||||||
router.post('/register', accountValidator.user(),
|
router.post('/register',
|
||||||
|
accountValidator.user(),
|
||||||
accountValidator.securePass(),
|
accountValidator.securePass(),
|
||||||
accountValidator.pass('passConfirm'),
|
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
|
//update profile
|
||||||
router.post('/update', accountValidator.img(),
|
router.post('/update',
|
||||||
|
accountValidator.img(),
|
||||||
accountValidator.bio(),
|
accountValidator.bio(),
|
||||||
accountValidator.signature(),
|
accountValidator.signature(),
|
||||||
accountValidator.pronouns(),
|
accountValidator.pronouns(),
|
||||||
accountValidator.pass('passChange.oldPass'),
|
accountValidator.pass('passChange.oldPass'),
|
||||||
accountValidator.securePass('passChange.newPass'),
|
accountValidator.securePass('passChange.newPass'),
|
||||||
accountValidator.pass('passChange.confirmPass'), updateController.post);
|
accountValidator.pass('passChange.confirmPass'),
|
||||||
|
updateController.post);
|
||||||
|
|
||||||
//rankEnum
|
//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
|
//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);
|
router.get('/rankEnum', rankEnumController.get);
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,11 @@ const config = require('../../../config.json');
|
||||||
const {userModel} = require('../user/userSchema');
|
const {userModel} = require('../user/userSchema');
|
||||||
const permissionModel = require('../permissionSchema');
|
const permissionModel = require('../permissionSchema');
|
||||||
const tokeModel = require('../tokebot/tokeSchema');
|
const tokeModel = require('../tokebot/tokeSchema');
|
||||||
|
const statModel = require('../statSchema');
|
||||||
|
const emailChangeModel = require('../user/emailChangeSchema');
|
||||||
const loggerUtils = require('../../utils/loggerUtils');
|
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
|
* 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
|
//Methods
|
||||||
/**
|
/**
|
||||||
* Consumes a migration profile and creates a new, modern canopy profile from the original.
|
* 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} oldPass - Original password to authenticate migration against
|
||||||
* @param {String} newPass - New password to re-hash with modern hashing algo
|
* @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);
|
module.exports = mongoose.model("migration", migrationSchema);
|
||||||
|
|
@ -289,7 +289,7 @@ userSchema.statics.register = async function(userObj, ip){
|
||||||
if(email != null){
|
if(email != null){
|
||||||
const requestDB = await emailChangeModel.create({user: newUser._id, newEmail: email, ipHash: ip});
|
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{
|
}else{
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
//NPM Imports
|
//NPM Imports
|
||||||
const { csrfSync } = require('csrf-sync');
|
const { csrfSync } = require('csrf-sync');
|
||||||
|
|
||||||
//Local Imports
|
|
||||||
const {errorHandler} = require('./loggerUtils');
|
|
||||||
|
|
||||||
//Pull needed methods from csrfSync
|
//Pull needed methods from csrfSync
|
||||||
const {generateToken, revokeToken, csrfSynchronisedProtection, isRequestValid} = 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} pass - Plaintext Password
|
||||||
* @param {String} hash - Salty Hash
|
* @param {String} hash - Salty Hash
|
||||||
* @returns {Boolean} True if authentication success
|
* @returns {Boolean} True if authentication success
|
||||||
|
|
@ -43,6 +43,16 @@ module.exports.comparePassword = function(pass, hash){
|
||||||
return bcrypt.compareSync(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
|
* 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} 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 {Mongoose.Document} userDB - DB Document Object for the user we're verifying email against
|
||||||
* @param {String} newEmail - New email address to send to
|
* @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
|
//Send the reset url via email
|
||||||
await module.exports.mailem(
|
await module.exports.mailem(
|
||||||
newEmail,
|
newEmail,
|
||||||
`Email Change Request - ${userDB.user}`,
|
subject,
|
||||||
`<h1>Email Change Request</h1>
|
content,
|
||||||
<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>`,
|
|
||||||
true
|
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){
|
async login(user, pass, verification){
|
||||||
var response = await fetch(`/api/account/login`,{
|
var response = await fetch(`/api/account/login`,{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue