Started work on URL-Token based password reset system. Email not yet implemented.
This commit is contained in:
parent
8ee92541de
commit
ed698f40c7
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"instanceName": "Canopy",
|
||||
"port": 8080,
|
||||
"protocol": "http",
|
||||
"domain": "localhost",
|
||||
"sessionSecret": "CHANGE_ME",
|
||||
"altchaSecret": "CHANGE_ME",
|
||||
"db":{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
67
src/controllers/api/account/passwordResetController.js
Normal file
67
src/controllers/api/account/passwordResetController.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
57
src/controllers/api/admin/passwordResetController.js
Normal file
57
src/controllers/api/admin/passwordResetController.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
58
src/controllers/passwordResetController.js
Normal file
58
src/controllers/passwordResetController.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
31
src/routers/passwordResetRouter.js
Normal file
31
src/routers/passwordResetRouter.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//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;
|
||||
121
src/schemas/passwordResetSchema.js
Normal file
121
src/schemas/passwordResetSchema.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
//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);
|
||||
|
|
@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
|
||||
|
||||
//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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
54
src/views/passwordReset.ejs
Normal file
54
src/views/passwordReset.ejs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!--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 <https://www.gnu.org/licenses/>.-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<%- include('partial/styles', {instance, user}); %>
|
||||
<link rel="stylesheet" type="text/css" href="/css/passwordReset.css">
|
||||
<link rel="stylesheet" type="text/css" href="/lib/altcha/altcha.css">
|
||||
<title><%= instance %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%- include('partial/navbar', {user}); %>
|
||||
<h3>Password Reset</h3>
|
||||
<!-- if we received a valid reset token -->
|
||||
<% if(token != null){ %>
|
||||
<p>Enter new password below.</p>
|
||||
<form action="javascript:">
|
||||
<label>New Password:</label>
|
||||
<input class="reset-pass-prompt" id="reset-pass-prompt" type="password">
|
||||
<label>Confirm New Password:</label>
|
||||
<input class="reset-pass-prompt" id="reset-pass-confirm-prompt" type="password">
|
||||
<altcha-widget floating challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget>
|
||||
<button id="reset-pass-button" class='positive-button'>Change Password</button>
|
||||
</form>
|
||||
<!-- Otherwise -->
|
||||
<% }else{ %>
|
||||
<p>Enter username to initiate password reset.</p>
|
||||
<form action="javascript:">
|
||||
<label>Username:</label>
|
||||
<input class="reset-pass-prompt" id="reset-pass-username-prompt" placeholder="username">
|
||||
<altcha-widget floating challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget>
|
||||
<button id="reset-pass-button" class='positive-button'>Send Email</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</body>
|
||||
<footer>
|
||||
<%- include('partial/scripts', {user}); %>
|
||||
<script src="/lib/altcha/altcha.js" type="module"></script>
|
||||
<script src="/js/passwordReset.js"></script>
|
||||
</footer>
|
||||
</html>
|
||||
|
|
@ -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;
|
||||
}
|
||||
35
www/css/passwordReset.css
Normal file
35
www/css/passwordReset.css
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
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;
|
||||
}
|
||||
|
|
@ -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*/
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
82
www/js/passwordReset.js
Normal file
82
www/js/passwordReset.js
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
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(`<p>Confirmation password does not match!</p>`);
|
||||
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();
|
||||
|
|
@ -66,4 +66,4 @@ class registerPrompt{
|
|||
}
|
||||
}
|
||||
|
||||
const registerForm = new registerPrompt();
|
||||
const registerForm = new resetPrompt();
|
||||
|
|
@ -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})
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue