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 @@
+
+
+
+
+
+ <% } %>
+
+
+
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})
});