diff --git a/config.example.json b/config.example.json index 6037e4f..00ad4a1 100644 --- a/config.example.json +++ b/config.example.json @@ -1,6 +1,8 @@ { "instanceName": "Canopy", "port": 8080, + "protocol": "http", + "domain": "localhost", "sessionSecret": "CHANGE_ME", "altchaSecret": "CHANGE_ME", "db":{ diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index 762e048..28fafb6 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -40,7 +40,6 @@ module.exports = class commandPreprocessor{ //If we don't pass sanatization/validation turn this car around if(!this.sanatizeCommand()){ - console.log('test'); return; } diff --git a/src/controllers/api/account/passwordResetController.js b/src/controllers/api/account/passwordResetController.js new file mode 100644 index 0000000..5bae3ed --- /dev/null +++ b/src/controllers/api/account/passwordResetController.js @@ -0,0 +1,67 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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'); + +//NPM Imports +const {validationResult, matchedData} = require('express-validator'); + +//local imports +const passwordResetModel = require('../../../schemas/passwordResetSchema'); +const altchaUtils = require('../../../utils/altchaUtils'); +const sessionUtils = require('../../../utils/sessionUtils'); +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 {token, pass, confirmPass} = 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'); + } + + //Kill users session since it *might* be the logged in user. + //Though realisitcally this shouldn't matter since most people wouldn't be logged in when resetting passwords + sessionUtils.killSession(req.session); + + //Consume the password reset token using given input + const requestDB = await passwordResetModel.findOne({token}); + + //If we have an invalid request + if(requestDB == null){ + return errorHandler(res, 'Invalid request token!', 'unauthorized'); + } + await requestDB.consume(pass, confirmPass); + + return res.sendStatus(200); + }else{ + res.status(400); + return res.send({errors: validResult.array()}); + } + }catch(err){ + return exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/controllers/api/account/updateController.js b/src/controllers/api/account/updateController.js index de7bb3e..9f65b21 100644 --- a/src/controllers/api/account/updateController.js +++ b/src/controllers/api/account/updateController.js @@ -67,7 +67,7 @@ module.exports.post = async function(req, res){ if(data.passChange){ //kill active session to prevent connect-mongo from freaking out accountUtils.killSession(req.session); - await userDB.passwordReset(data.passChange); + await userDB.changePassword(data.passChange); } await userDB.save(); diff --git a/src/controllers/api/admin/passwordResetController.js b/src/controllers/api/admin/passwordResetController.js new file mode 100644 index 0000000..2c3306c --- /dev/null +++ b/src/controllers/api/admin/passwordResetController.js @@ -0,0 +1,57 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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 {validationResult, matchedData} = require('express-validator'); + +//local imports +const {userModel} = require('../../../schemas/userSchema'); +const passwordResetModel = require("../../../schemas/passwordResetSchema"); +const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); + +module.exports.post = async function(req, res){ + try{ + //check for validation errors + const validResult = validationResult(req); + + //if none + if(validResult.isEmpty()){ + //grab validated/sanatized data + const {user} = matchedData(req); + //Find user from input + const userDB = await userModel.findOne({user}); + + //If there is no user + if(userDB == null){ + //Scream + return errorHandler(res, "User not found.", "Bad Query."); + } + + //Generate the password reset link + const requestDB = await passwordResetModel.generateResetToken(userDB); + + //send successful response + res.status(200); + return res.send({url: requestDB.getResetURL()}); + //otherwise scream + }else{ + res.status(400); + return res.send({errors: validResult.array()}) + } + }catch(err){ + return exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/controllers/passwordResetController.js b/src/controllers/passwordResetController.js new file mode 100644 index 0000000..4f4cf78 --- /dev/null +++ b/src/controllers/passwordResetController.js @@ -0,0 +1,58 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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'); + +//npm imports +const {validationResult, matchedData} = require('express-validator'); + +//Local Imports +const altchaUtils = require('../utils/altchaUtils'); + +//register page functions +module.exports.get = async function(req, res){ + try{ + //check for validation errors + const validResult = validationResult(req); + + //Generate captcha + const challenge = await altchaUtils.genCaptcha(); + + //if none + if(validResult.isEmpty()){ + //grab validated/sanatized data + const {token} = matchedData(req); + + /* + The decision to not check the token against the database here is a conscious security decision that should be kept. + This way, attackers would only be able to detect valid keys by requesting password resets against them. + A process which, unlike fetching this page, is checked against a captcha. + + Instead we should render this page, so long as the token fits the formatting rules for a token, regardless of DB presence. + */ + + //Render page + return res.render('passwordReset', {instance: config.instanceName, user: req.session.user, challenge, token}); + //If we didn't get a valid token + }else{ + //otherwise render generic page + return res.render('passwordReset', {instance: config.instanceName, user: req.session.user, challenge, token: null}); + } + }catch(err){ + return exceptionHandler(res, err); + } +} \ No newline at end of file diff --git a/src/routers/api/accountRouter.js b/src/routers/api/accountRouter.js index 65e997d..5192dad 100644 --- a/src/routers/api/accountRouter.js +++ b/src/routers/api/accountRouter.js @@ -24,6 +24,7 @@ const logoutController = require("../../controllers/api/account/logoutController const registerController = require("../../controllers/api/account/registerController"); const updateController = require("../../controllers/api/account/updateController"); const rankEnumController = require("../../controllers/api/account/rankEnumController"); +const passwordResetController = require("../../controllers/api/account/passwordResetController"); const deleteController = require("../../controllers/api/account/deleteController"); //globals @@ -50,6 +51,8 @@ router.post('/update', accountValidator.img(), //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.post('/passwordReset', accountValidator.securityToken(), accountValidator.securePass(), accountValidator.pass('confirmPass'), passwordResetController.post) + router.post('/delete', accountValidator.pass(), deleteController.post); module.exports = router; \ No newline at end of file diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js index b5a179a..27b6a2e 100644 --- a/src/routers/api/adminRouter.js +++ b/src/routers/api/adminRouter.js @@ -32,6 +32,7 @@ const permissionsController = require("../../controllers/api/admin/permissionsCo const banController = require("../../controllers/api/admin/banController"); const tokeCommandController = require('../../controllers/api/admin/tokeCommandController'); const emoteController = require('../../controllers/api/admin/emoteController'); +const passwordResetController = require('../../controllers/api/admin/passwordResetController'); //globals const router = Router(); @@ -59,5 +60,7 @@ router.delete('/tokeCommands', permissionSchema.reqPermCheck("editTokeCommands") router.get('/emote', permissionSchema.reqPermCheck('adminPanel'), emoteController.get); router.post('/emote', permissionSchema.reqPermCheck('editEmotes'), emoteValidator.name(), emoteValidator.link(), emoteController.post); router.delete('/emote', permissionSchema.reqPermCheck('editEmotes'), emoteValidator.name(), emoteController.delete); +//passwordReset +router.post('/genPasswordReset', permissionSchema.reqPermCheck('genPasswordReset'), accountValidator.user(), passwordResetController.post); module.exports = router; diff --git a/src/routers/passwordResetRouter.js b/src/routers/passwordResetRouter.js new file mode 100644 index 0000000..a65fdc6 --- /dev/null +++ b/src/routers/passwordResetRouter.js @@ -0,0 +1,31 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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 accountValidator = require('../validators/accountValidator'); +const passwordResetController = require("../controllers/passwordResetController"); + +//globals +const router = Router(); + +//routing functions +router.get('/', accountValidator.securityToken(), passwordResetController.get); + +module.exports = router; diff --git a/src/schemas/passwordResetSchema.js b/src/schemas/passwordResetSchema.js new file mode 100644 index 0000000..2522a3b --- /dev/null +++ b/src/schemas/passwordResetSchema.js @@ -0,0 +1,121 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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'); + +const daysToExpire = 7; + +const passwordResetSchema = new mongoose.Schema({ + user: { + type: mongoose.SchemaTypes.ObjectID, + ref: "user", + required: true + }, + token: { + type: mongoose.SchemaTypes.String, + required: true + }, + date: { + + type: mongoose.SchemaTypes.Date, + required: true, + default: new Date() + } +}); + +//statics +passwordResetSchema.statics.generateResetToken = async function(userDB){ + //Use a cryptographically secure algorythm to create a random hex string from 16 bytes as our reset token + const token = crypto.randomBytes(16).toString('hex'); + + //Create request object + const request = { + user: userDB._id, + token, + date: new Date() + } + + //Create the request entry in the DB and return the newly created record + return await this.create(request); +} + +passwordResetSchema.statics.processExpiredRequests = async function(){ + //Pull all requests from the DB + const requestDB = await this.find({}); + + requestDB.forEach(async (request) => { + //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 +passwordResetSchema.methods.consume = async function(pass, confirmPass){ + //Check confirmation pass + if(pass != confirmPass){ + throw new Error("Confirmation password does not match!"); + } + + //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(); +} + +passwordResetSchema.methods.getResetURL = function(){ + //Check for default port based on protocol + if((config.protocol == 'http' && config.port == 80) || (config.protocol == 'https' && config.port == 443)){ + //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}`; + } +} + +passwordResetSchema.methods.getDaysUntilExpiration = function(){ + //Get ban date + const expirationDate = new Date(this.date); + //Get expiration days and calculate expiration date + expirationDate.setDate(expirationDate.getDate() + daysToExpire); + //Calculate and return days until ban expiration + return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1); +} + +module.exports = mongoose.model("passwordReset", passwordResetSchema); diff --git a/src/schemas/userSchema.js b/src/schemas/userSchema.js index 730706c..7e33b5d 100644 --- a/src/schemas/userSchema.js +++ b/src/schemas/userSchema.js @@ -16,7 +16,7 @@ along with this program. If not, see .*/ //Built-In Imports -const crypto = require('crypto'); +const crypto = require('node:crypto'); //NPM Imports const {mongoose} = require('mongoose'); @@ -604,7 +604,7 @@ userSchema.methods.killAllSessions = async function(reason = "A full log-out fro server.channelManager.kickConnections(this.user, reason); } -userSchema.methods.passwordReset = async function(passChange){ +userSchema.methods.changePassword = async function(passChange){ if(this.checkPass(passChange.oldPass)){ if(passChange.newPass == passChange.confirmPass){ //Note: We don't have to worry about hashing here because the schema is written to do it auto-magically diff --git a/src/server.js b/src/server.js index 8b04a53..cb2adf9 100644 --- a/src/server.js +++ b/src/server.js @@ -37,6 +37,7 @@ const flairModel = require('./schemas/flairSchema'); const emoteModel = require('./schemas/emoteSchema'); const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema'); //Router +//Humie-Friendly const indexRouter = require('./routers/indexRouter'); const registerRouter = require('./routers/registerRouter'); const loginRouter = require('./routers/loginRouter'); @@ -44,17 +45,22 @@ const profileRouter = require('./routers/profileRouter'); const adminPanelRouter = require('./routers/adminPanelRouter'); const channelRouter = require('./routers/channelRouter'); const newChannelRouter = require('./routers/newChannelRouter'); +const passwordResetRouter = require('./routers/passwordResetRouter'); +//Panel const panelRouter = require('./routers/panelRouter'); +//Popup const popupRouter = require('./routers/popupRouter'); +//Tooltip const tooltipRouter = require('./routers/tooltipRouter'); +//Api const apiRouter = require('./routers/apiRouter'); -//Define Config +//Define Config variables const config = require('../config.json'); const port = config.port; const dbUrl = `mongodb://${config.db.user}:${config.db.pass}@${config.db.address}:${config.db.port}/${config.db.database}`; -//Define Node JS +//Define express const app = express(); //Define session-store (exported so we can kill sessions from user schema) @@ -72,6 +78,10 @@ const sessionMiddleware = session({ const httpServer = createServer(app); const io = new Server(httpServer, {}); +if(config.protocol == 'http'){ + console.warn("Starting in HTTP mode. This server should be used for development purposes only!"); +} + //Connect mongoose to the database mongoose.set("sanitizeFilter", true).connect(dbUrl).then(() => { console.log("Connected to DB"); @@ -105,6 +115,7 @@ app.use('/profile', profileRouter); app.use('/adminPanel', adminPanelRouter); app.use('/c', channelRouter); app.use('/newChannel', newChannelRouter); +app.use('/passwordReset', passwordResetRouter); //Panel app.use('/panel', panelRouter); //Popup @@ -127,7 +138,7 @@ statModel.incrementLaunchCount(); //Load default flairs flairModel.loadDefaults(); -//Load default emots +//Load default emotes emoteModel.loadDefaults(); //Load default toke commands diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js index 6644fa9..9e9ee6a 100644 --- a/src/utils/scheduler.js +++ b/src/utils/scheduler.js @@ -20,6 +20,7 @@ const cron = require('node-cron'); //Local Imports const {userModel} = require('../schemas/userSchema'); const userBanModel = require('../schemas/userBanSchema'); +const passwordResetModel = require('../schemas/passwordResetSchema'); const channelModel = require('../schemas/channel/channelSchema'); const sessionUtils = require('./sessionUtils'); @@ -32,6 +33,8 @@ module.exports.schedule = function(){ cron.schedule('0 0 * * *', ()=>{channelModel.processExpiredBans()},{scheduled: true, timezone: "UTC"}); //Process expired failed login attempts every night at midnight cron.schedule('0 0 * * *', ()=>{sessionUtils.processExpiredAttempts()},{scheduled: true, timezone: "UTC"}); + //Process expired password reset requests every night at midnight + cron.schedule('0 0 * * *', ()=>{passwordResetModel.processExpiredRequests()},{scheduled: true, timezone: "UTC"}); } module.exports.kickoff = function(){ @@ -39,8 +42,11 @@ module.exports.kickoff = function(){ userModel.processAgedIPRecords(); //Process expired global bans that may have expired since last restart userBanModel.processExpiredBans() - //Process expired channel bans that may haven ot expired since last restart + //Process expired channel bans that may have expired since last restart channelModel.processExpiredBans(); + //Process expired password reset requests that may have expired since last restart + passwordResetModel.processExpiredRequests(); + //Schedule jobs module.exports.schedule(); diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 1dfa124..6e9ff8a 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -36,5 +36,7 @@ module.exports = { bio: (field = 'bio') => body(field).optional().escape().trim().isLength({min: 1, max: 1000}), - rank: (field = 'rank') => body(field).escape().trim().custom(isRank) + rank: (field = 'rank') => body(field).escape().trim().custom(isRank), + + securityToken: (field = 'token') => check(field).escape().trim().isHexadecimal().isLength({min:32, max:32}) } \ No newline at end of file diff --git a/src/views/passwordReset.ejs b/src/views/passwordReset.ejs new file mode 100644 index 0000000..d69ed4b --- /dev/null +++ b/src/views/passwordReset.ejs @@ -0,0 +1,54 @@ + + + + + <%- include('partial/styles', {instance, user}); %> + + + <%= instance %> + + + <%- include('partial/navbar', {user}); %> +

Password Reset

+ + <% if(token != null){ %> +

Enter new password below.

+
+ + + + + + +
+ + <% }else{ %> +

Enter username to initiate password reset.

+
+ + + + +
+ <% } %> + +
+ <%- include('partial/scripts', {user}); %> + + +
+ diff --git a/www/css/panel/emote.css b/www/css/panel/emote.css index d809fca..32b22bd 100644 --- a/www/css/panel/emote.css +++ b/www/css/panel/emote.css @@ -96,10 +96,10 @@ span.emote-list-trash-icon{ top: -0.5em; right: -0.5em; z-index: 1; + align-items: center; + justify-content: center; } i.emote-list-trash-icon{ - flex: 1; text-align: center; - margin: auto; } \ No newline at end of file diff --git a/www/css/passwordReset.css b/www/css/passwordReset.css new file mode 100644 index 0000000..ab130e0 --- /dev/null +++ b/www/css/passwordReset.css @@ -0,0 +1,35 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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 .*/ +h3, p{ + text-align: center; +} + +form{ + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5em; + margin: 0 17%; +} + +.reset-pass-prompt{ + width: 100% +} + +#reset-pass-button{ + width: 6em; + height: 3em; +} \ No newline at end of file diff --git a/www/css/profile.css b/www/css/profile.css index 0f3e2fc..f18b105 100644 --- a/www/css/profile.css +++ b/www/css/profile.css @@ -59,6 +59,7 @@ p.profile-toke-count{ min-height: 1.5em; max-height: 5.8em; display: none; + border-bottom-right-radius: 0; } /*Little hacky but this keeps initial max-height from fucking up resizing*/ diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js index 94afd67..9592847 100644 --- a/www/js/adminPanel.js +++ b/www/js/adminPanel.js @@ -90,6 +90,24 @@ class canopyAdminUtils{ } } + async genPasswordResetLink(user){ + var response = await fetch(`/api/admin/genPasswordReset`,{ + method: "POST", + headers: { + "Content-Type": "application/json" + }, + //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... + body: JSON.stringify({user}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + + async setPermission(permMap){ var response = await fetch(`/api/admin/permissions`,{ method: "POST", diff --git a/www/js/passwordReset.js b/www/js/passwordReset.js new file mode 100644 index 0000000..7d67c27 --- /dev/null +++ b/www/js/passwordReset.js @@ -0,0 +1,82 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 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 registerPrompt{ + constructor(){ + //Grab user prompt + this.user = document.querySelector("#reset-pass-username-prompt"); + //Detect if we're initiating or completing a password request + this.initiating = this.user != null + + //If we're working with an existing request + if(!this.initiating){ + //Grab pass prompts + this.pass = document.querySelector("#reset-pass-prompt"); + this.passConfirm = document.querySelector("#reset-pass-confirm-prompt"); + //Strip reset token from query string + this.token = window.location.search.replace('?token=',''); + } + + //Grab register button + this.button = document.querySelector("#reset-pass-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 register event listener to register button + this.button.addEventListener("click", this.register.bind(this)); + } + + verify(event){ + //pull verification payload from event + this.verification = event.detail.payload; + } + + register(){ + //If altcha verification isn't complete + if(this.verification == null){ + //don't bother + return; + } + + //If we're initiating a password change request + if(this.initiating){ + + //If we're completing a password change + }else{ + //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.resetPassword(this.token , this.pass.value , this.passConfirm.value , this.verification); + } + } +} + +const registerForm = new registerPrompt(); \ No newline at end of file diff --git a/www/js/register.js b/www/js/register.js index fa56868..1ed53dd 100644 --- a/www/js/register.js +++ b/www/js/register.js @@ -66,4 +66,4 @@ class registerPrompt{ } } -const registerForm = new registerPrompt(); \ No newline at end of file +const registerForm = new resetPrompt(); \ No newline at end of file diff --git a/www/js/utils.js b/www/js/utils.js index ac37b0b..2f14395 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -215,8 +215,8 @@ class canopyUXUtils{ //Bit hacky but the only way to remove an event listener while keeping the function bound to this //Isn't javascript precious? this.keyClose = ((event)=>{ - //If we hit enter - if(event.key == "Enter"){ + //If we hit enter or escape + if(event.key == "Enter" || event.key == "Escape"){ //Close the pop-up this.closePopup(); //Remove this event listener @@ -434,7 +434,6 @@ class canopyAjaxUtils{ } } - //We need to fix this one to use displayResponseError function async updateProfile(update){ const response = await fetch(`/api/account/update`,{ method: "POST", @@ -479,6 +478,22 @@ class canopyAjaxUtils{ } } + async resetPassword(token, pass, confirmPass, verification){ + const response = await fetch(`/api/account/passwordReset`,{ + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({token, pass, confirmPass, verification}) + }); + + if(response.status == 200){ + return await response.json(); + }else{ + utils.ux.displayResponseError(await response.json()); + } + } + async newChannel(name, description, thumbnail, verification){ var response = await fetch(`/api/channel/register`,{ method: "POST", @@ -547,7 +562,6 @@ class canopyAjaxUtils{ headers: { "Content-Type": "application/json" }, - //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible... body: JSON.stringify({chanName, user, rank}) });