Basic brute force detection added. Accounts throttle by captcha after 5 failed attempts, and locked out for 24 hours after 200 attempts.
This commit is contained in:
parent
e0f53df176
commit
9c18c23ad5
|
|
@ -14,32 +14,58 @@ GNU Affero General Public License for more details.
|
||||||
You should have received a copy of the GNU Affero General Public License
|
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/>.*/
|
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
|
|
||||||
|
//Config
|
||||||
|
const config = require('../../../../config.json');
|
||||||
|
|
||||||
//npm imports
|
//npm imports
|
||||||
const {validationResult, matchedData} = require('express-validator');
|
const {validationResult, matchedData} = require('express-validator');
|
||||||
|
|
||||||
//local imports
|
//local imports
|
||||||
const accountUtils = require('../../../utils/sessionUtils');
|
const sessionUtils = require('../../../utils/sessionUtils');
|
||||||
const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
|
const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
|
||||||
|
const altchaUtils = require('../../../utils/altchaUtils');
|
||||||
|
const session = require('express-session');
|
||||||
|
|
||||||
//api account functions
|
//api account functions
|
||||||
module.exports.post = async function(req, res){
|
module.exports.post = async function(req, res){
|
||||||
try{
|
try{
|
||||||
|
//Check validation results
|
||||||
const validResult = validationResult(req);
|
const validResult = validationResult(req);
|
||||||
|
|
||||||
|
//if we don't have errors
|
||||||
if(validResult.isEmpty()){
|
if(validResult.isEmpty()){
|
||||||
const data = matchedData(req);
|
//Pull sanatzied/validated data
|
||||||
const {user, pass} = data;
|
const {user, pass} = matchedData(req);
|
||||||
|
|
||||||
//try to authenticate the session, and return a successful code if it works
|
//try to authenticate the session, and return a successful code if it works
|
||||||
await accountUtils.authenticateSession(user, pass, req);
|
await sessionUtils.authenticateSession(user, pass, req);
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}else{
|
}else{
|
||||||
res.status(400);
|
res.status(400);
|
||||||
res.send({errors: validResult.array()})
|
return res.send({errors: validResult.array()})
|
||||||
}
|
}
|
||||||
}catch(err){
|
}catch(err){
|
||||||
exceptionHandler(res, err);
|
//Check validation results
|
||||||
|
const validResult = validationResult(req);
|
||||||
|
|
||||||
|
//if we don't have errors
|
||||||
|
if(validResult.isEmpty()){
|
||||||
|
//Get login attempts for current user
|
||||||
|
const {user} = matchedData(req);
|
||||||
|
const attempts = sessionUtils.getLoginAttempts(user)
|
||||||
|
|
||||||
|
//if we've gone over max attempts and
|
||||||
|
if(attempts.count > sessionUtils.throttleAttempts){
|
||||||
|
//tell client it needs a captcha
|
||||||
|
return res.sendStatus(429);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
res.status(400);
|
||||||
|
return res.send({errors: validResult.array()})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
return exceptionHandler(res, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
64
src/controllers/loginController.js
Normal file
64
src/controllers/loginController.js
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*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 sessionUtils = require('../utils/sessionUtils');
|
||||||
|
const altchaUtils = require('../utils/altchaUtils');
|
||||||
|
|
||||||
|
//register page functions
|
||||||
|
module.exports.get = async function(req, res){
|
||||||
|
//Check for validation errors
|
||||||
|
const validResult = validationResult(req);
|
||||||
|
|
||||||
|
//If there are none
|
||||||
|
if(validResult.isEmpty()){
|
||||||
|
//Get username from sanatized/validated data
|
||||||
|
const {user} = matchedData(req);
|
||||||
|
const attempts = sessionUtils.getLoginAttempts(user);
|
||||||
|
|
||||||
|
//if we have previous attempts for this user
|
||||||
|
if(attempts != null){
|
||||||
|
if(attempts.count > sessionUtils.maxAttempts){
|
||||||
|
return res.render('lockedAccount', {instance: config.instanceName, user: req.session.user});
|
||||||
|
}
|
||||||
|
|
||||||
|
//If the users login's are being throttled
|
||||||
|
if(attempts.count > sessionUtils.throttleAttempts){
|
||||||
|
//Get diffuculty based on amount of attempts past the max amount
|
||||||
|
const difficulty = attempts.count - sessionUtils.throttleAttempts;
|
||||||
|
//Generate challenge unique to specific user, with difficulty set based on failed login attempts
|
||||||
|
const challenge = await altchaUtils.genCaptcha(difficulty, user);
|
||||||
|
|
||||||
|
//Render page
|
||||||
|
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge});
|
||||||
|
}
|
||||||
|
//otherwise
|
||||||
|
}else{
|
||||||
|
//Render generic page
|
||||||
|
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge: null});
|
||||||
|
}
|
||||||
|
//if we received invalid input
|
||||||
|
}else{
|
||||||
|
//Render pretend nothing happened, send out a generic page
|
||||||
|
return res.render('login', {instance: config.instanceName, user: req.session.user, challenge: null});
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/routers/loginRouter.js
Normal file
30
src/routers/loginRouter.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*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 loginController = require("../controllers/loginController");
|
||||||
|
const accountValidator = require("../validators/accountValidator");
|
||||||
|
|
||||||
|
//globals
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
//routing functions
|
||||||
|
router.get('/', accountValidator.user(), loginController.get);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -27,14 +27,19 @@ const mongoose = require('mongoose');
|
||||||
globalThis.crypto = require('node:crypto').webcrypto;
|
globalThis.crypto = require('node:crypto').webcrypto;
|
||||||
|
|
||||||
//Define Local Imports
|
//Define Local Imports
|
||||||
|
//Application
|
||||||
const channelManager = require('./app/channel/channelManager');
|
const channelManager = require('./app/channel/channelManager');
|
||||||
|
//Util
|
||||||
const scheduler = require('./utils/scheduler');
|
const scheduler = require('./utils/scheduler');
|
||||||
|
//DB Model
|
||||||
const statModel = require('./schemas/statSchema');
|
const statModel = require('./schemas/statSchema');
|
||||||
const flairModel = require('./schemas/flairSchema');
|
const flairModel = require('./schemas/flairSchema');
|
||||||
const emoteModel = require('./schemas/emoteSchema');
|
const emoteModel = require('./schemas/emoteSchema');
|
||||||
const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema');
|
const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema');
|
||||||
|
//Router
|
||||||
const indexRouter = require('./routers/indexRouter');
|
const indexRouter = require('./routers/indexRouter');
|
||||||
const registerRouter = require('./routers/registerRouter');
|
const registerRouter = require('./routers/registerRouter');
|
||||||
|
const loginRouter = require('./routers/loginRouter');
|
||||||
const profileRouter = require('./routers/profileRouter');
|
const profileRouter = require('./routers/profileRouter');
|
||||||
const adminPanelRouter = require('./routers/adminPanelRouter');
|
const adminPanelRouter = require('./routers/adminPanelRouter');
|
||||||
const channelRouter = require('./routers/channelRouter');
|
const channelRouter = require('./routers/channelRouter');
|
||||||
|
|
@ -95,6 +100,7 @@ io.engine.use(sessionMiddleware);
|
||||||
//Humie-Friendly
|
//Humie-Friendly
|
||||||
app.use('/', indexRouter);
|
app.use('/', indexRouter);
|
||||||
app.use('/register', registerRouter);
|
app.use('/register', registerRouter);
|
||||||
|
app.use('/login', loginRouter);
|
||||||
app.use('/profile', profileRouter);
|
app.use('/profile', profileRouter);
|
||||||
app.use('/adminPanel', adminPanelRouter);
|
app.use('/adminPanel', adminPanelRouter);
|
||||||
app.use('/c', channelRouter);
|
app.use('/c', channelRouter);
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const spent = [];
|
||||||
//Captcha lifetime in minutes
|
//Captcha lifetime in minutes
|
||||||
const lifetime = 2;
|
const lifetime = 2;
|
||||||
|
|
||||||
module.exports.genCaptcha = async function(){
|
module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){
|
||||||
//Set altcha expiration date
|
//Set altcha expiration date
|
||||||
const expiration = new Date();
|
const expiration = new Date();
|
||||||
|
|
||||||
|
|
@ -34,13 +34,13 @@ module.exports.genCaptcha = async function(){
|
||||||
|
|
||||||
//Generate Altcha Challenge
|
//Generate Altcha Challenge
|
||||||
return await createChallenge({
|
return await createChallenge({
|
||||||
hmacKey: config.altchaSecret,
|
hmacKey: [config.altchaSecret, uniqueSecret].join(''),
|
||||||
maxNumber: 200000,
|
maxNumber: 100000 * difficulty,
|
||||||
expires: expiration
|
expires: expiration
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.verify = async function(payload){
|
module.exports.verify = async function(payload, uniqueSecret = ''){
|
||||||
//If we already checked this payload
|
//If we already checked this payload
|
||||||
if(spent.indexOf(payload) != -1){
|
if(spent.indexOf(payload) != -1){
|
||||||
//Fuck off and die
|
//Fuck off and die
|
||||||
|
|
@ -57,5 +57,5 @@ module.exports.verify = async function(payload){
|
||||||
setTimeout(() => {spent.splice(payloadIndex,1);}, lifetime * 60 * 1000);
|
setTimeout(() => {spent.splice(payloadIndex,1);}, lifetime * 60 * 1000);
|
||||||
|
|
||||||
//Return verification results
|
//Return verification results
|
||||||
return await verifySolution(payload, config.altchaSecret);
|
return await verifySolution(payload, [config.altchaSecret, uniqueSecret].join(''));
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ const cron = require('node-cron');
|
||||||
const {userModel} = require('../schemas/userSchema');
|
const {userModel} = require('../schemas/userSchema');
|
||||||
const userBanModel = require('../schemas/userBanSchema');
|
const userBanModel = require('../schemas/userBanSchema');
|
||||||
const channelModel = require('../schemas/channel/channelSchema');
|
const channelModel = require('../schemas/channel/channelSchema');
|
||||||
|
const sessionUtils = require('./sessionUtils');
|
||||||
|
|
||||||
module.exports.schedule = function(){
|
module.exports.schedule = function(){
|
||||||
//Process hashed IP Records that haven't been recorded in a week or more
|
//Process hashed IP Records that haven't been recorded in a week or more
|
||||||
|
|
@ -29,6 +30,8 @@ module.exports.schedule = function(){
|
||||||
cron.schedule('0 0 * * *', ()=>{userBanModel.processExpiredBans()},{scheduled: true, timezone: "UTC"});
|
cron.schedule('0 0 * * *', ()=>{userBanModel.processExpiredBans()},{scheduled: true, timezone: "UTC"});
|
||||||
//Process expired channel bans every night at midnight
|
//Process expired channel bans every night at midnight
|
||||||
cron.schedule('0 0 * * *', ()=>{channelModel.processExpiredBans()},{scheduled: true, timezone: "UTC"});
|
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"});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.kickoff = function(){
|
module.exports.kickoff = function(){
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,45 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
//local imports
|
//local imports
|
||||||
const {userModel} = require('../schemas/userSchema');
|
const {userModel} = require('../schemas/userSchema');
|
||||||
const userBanModel = require('../schemas/userBanSchema')
|
const userBanModel = require('../schemas/userBanSchema')
|
||||||
|
const altchaUtils = require('../utils/altchaUtils');
|
||||||
|
|
||||||
|
//Create failed sign-in cache since it's easier and more preformant to implement it this way than adding extra burdon to the database
|
||||||
|
//Server restarts are far and few between. It would take multiple during a single bruteforce attempt for this to become an issue.
|
||||||
|
const failedAttempts = new Map();
|
||||||
|
const throttleAttempts = 5;
|
||||||
|
const maxAttempts = 200;
|
||||||
|
|
||||||
//this module is good for keeping wrappers for userModel and other shit in that does more session handling than database access/modification.
|
//this module is good for keeping wrappers for userModel and other shit in that does more session handling than database access/modification.
|
||||||
|
|
||||||
module.exports.authenticateSession = async function(user, pass, req){
|
module.exports.authenticateSession = async function(user, pass, req){
|
||||||
|
//Fuck you yoda
|
||||||
|
try{
|
||||||
|
//Grab previous attempts
|
||||||
|
const attempt = failedAttempts.get(user);
|
||||||
|
|
||||||
|
//If we have failed attempts
|
||||||
|
if(attempt != null){
|
||||||
|
//If we have more failed attempts than allowed
|
||||||
|
if(attempt.count > maxAttempts){
|
||||||
|
throw new Error("This account has been locked for at 24 hours due to a large amount of failed log-in attempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
//If we're throttling logins
|
||||||
|
if(attempt.count > throttleAttempts){
|
||||||
|
//Verification doesnt get sanatized or checked since that would most likely break the cryptography
|
||||||
|
//Since we've already got access to the request and dont need to import anything, why bother getting it from a parameter?
|
||||||
|
if(req.body.verification == null){
|
||||||
|
throw new Error("Verification failed!");
|
||||||
|
}else if(!altchaUtils.verify(req.body.verification, user)){
|
||||||
|
throw new Error("Verification failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Authenticate the session
|
//Authenticate the session
|
||||||
const userDB = await userModel.authenticate(user, pass);
|
const userDB = await userModel.authenticate(user, pass);
|
||||||
const banDB = await userBanModel.checkBanByUserDoc(userDB);
|
const banDB = await userBanModel.checkBanByUserDoc(userDB);
|
||||||
|
|
||||||
|
//If the user is banned
|
||||||
if(banDB){
|
if(banDB){
|
||||||
//Make the number a little prettier despite the lack of precision since we're not doing calculations here :P
|
//Make the number a little prettier despite the lack of precision since we're not doing calculations here :P
|
||||||
const expiration = banDB.getDaysUntilExpiration() < 1 ? 0 : banDB.getDaysUntilExpiration();
|
const expiration = banDB.getDaysUntilExpiration() < 1 ? 0 : banDB.getDaysUntilExpiration();
|
||||||
|
|
@ -51,10 +81,61 @@ module.exports.authenticateSession = async function(user, pass, req){
|
||||||
//Tattoo hashed IP address to user account for seven days
|
//Tattoo hashed IP address to user account for seven days
|
||||||
userDB.tattooIPRecord(req.ip);
|
userDB.tattooIPRecord(req.ip);
|
||||||
|
|
||||||
|
//If we got to here then the log-in was successful. We should clear-out any failed attempts.
|
||||||
|
failedAttempts.delete(user);
|
||||||
|
|
||||||
//return user
|
//return user
|
||||||
return userDB.user;
|
return userDB.user;
|
||||||
|
}catch(err){
|
||||||
|
//Look for previous failed attempts
|
||||||
|
var attempt = failedAttempts.get(user);
|
||||||
|
|
||||||
|
//If this is the first attempt
|
||||||
|
if(attempt == null){
|
||||||
|
//Create new attempt object
|
||||||
|
attempt = {
|
||||||
|
count: 1,
|
||||||
|
lastAttempt: new Date()
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
//Create updated attempt object
|
||||||
|
attempt = {
|
||||||
|
count: attempt.count + 1,
|
||||||
|
lastAttempt: new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Commit the failed attempt to the failed sign-in cache
|
||||||
|
failedAttempts.set(user, attempt);
|
||||||
|
|
||||||
|
//y33t
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.killSession = async function(session){
|
module.exports.killSession = async function(session){
|
||||||
session.destroy();
|
session.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.getLoginAttempts = function(user){
|
||||||
|
//Read the code, i'm not explaining this
|
||||||
|
return failedAttempts.get(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.processExpiredAttempts = function(){
|
||||||
|
for(user of failedAttempts.keys()){
|
||||||
|
//Get attempt by user
|
||||||
|
const attempt = failedAttempts.get(user);
|
||||||
|
//Check how long its been
|
||||||
|
const daysSinceLastAttempt = ((new Date() - attempt.lastAttempt) / (1000 * 60 * 60 * 24)).toFixed(1);
|
||||||
|
|
||||||
|
//If it's been more than a day since anyones tried to log in as this user
|
||||||
|
if(daysSinceLastAttempt >= 1){
|
||||||
|
//Clear out the attempts so that they don't need to fuck with a captcha anymore
|
||||||
|
failedAttempts.delete(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.throttleAttempts = throttleAttempts;
|
||||||
|
module.exports.maxAttempts = maxAttempts;
|
||||||
32
src/views/lockedAccount.ejs
Normal file
32
src/views/lockedAccount.ejs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!--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/login.css">
|
||||||
|
<title><%= instance %> - Account Locked!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('partial/navbar', {user}); %>
|
||||||
|
<h3 class="danger-text">Multiple failed attempts detected!</h3>
|
||||||
|
<p class="danger-text">Your account has been locked due to detected brute-force attacks!<br>Your account will be unlocked in 24 hours.</p>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
<%- include('partial/scripts', {user}); %>
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
47
src/views/login.ejs
Normal file
47
src/views/login.ejs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<!--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/login.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/lib/altcha/altcha.css">
|
||||||
|
<title><%= instance %> - Log-In</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('partial/navbar', {user}); %>
|
||||||
|
<% if(challenge != null){ %>
|
||||||
|
<h3 class="danger-text">Multiple failed attempts detected!</h3>
|
||||||
|
<p class="danger-text">Please complete verification challenge to continue!</p>
|
||||||
|
<% } %>
|
||||||
|
<form action="javascript:">
|
||||||
|
<label>Username:</label>
|
||||||
|
<input class="login-page-prompt" id="login-page-username" placeholder="Required">
|
||||||
|
<label>Password:</label>
|
||||||
|
<input class="login-page-prompt" id="login-page-password" placeholder="Required" type="password">
|
||||||
|
<% if(challenge != null){ %>
|
||||||
|
<altcha-widget challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget>
|
||||||
|
<% } %>
|
||||||
|
<button id="login-page-button" class='positive-button'>Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
<%- include('partial/scripts', {user}); %>
|
||||||
|
<script src="/js/login.js"></script>
|
||||||
|
<script src="/lib/altcha/altcha.js" type="module"></script>
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
35
www/css/login.css
Normal file
35
www/css/login.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/>.*/
|
||||||
|
form{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin: 5% 17%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page-prompt{
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-page-button{
|
||||||
|
width: 6em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
@ -66,8 +66,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||||
--altcha-color-base: var(--bg1);
|
--altcha-color-base: var(--bg1);
|
||||||
--altcha-color-border: var(--accent1);
|
--altcha-color-border: var(--accent1);
|
||||||
--altcha-color-text: var(--accent1);
|
--altcha-color-text: var(--accent1);
|
||||||
--altcha-color-border-focus: currentColor;
|
--altcha-color-error-text: var(--danger0);
|
||||||
--altcha-color-error-text: #f23939;
|
|
||||||
--altcha-max-width: 260px;
|
--altcha-max-width: 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +117,25 @@ button:active{
|
||||||
box-shadow: var(--focus-glow0-alt0);
|
box-shadow: var(--focus-glow0-alt0);
|
||||||
}
|
}
|
||||||
|
|
||||||
input{
|
input:focus, textarea:focus{
|
||||||
accent-color: var(--focus0);
|
outline: none;
|
||||||
|
box-shadow: var(--focus-glow0);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked{
|
||||||
|
accent-color: var(--focus0-alt0);
|
||||||
|
box-shadow: var(--focus-glow0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NOT! -Wayne */
|
||||||
|
input:not([type='checkbox']):not(.navbar-item), textarea {
|
||||||
|
border-radius: 1em;
|
||||||
|
border: none;
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea{
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-button{
|
.danger-button{
|
||||||
|
|
@ -139,7 +155,7 @@ input{
|
||||||
box-shadow: var(--danger-glow0-alt1);
|
box-shadow: var(--danger-glow0-alt1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-link{
|
.danger-link, .danger-text{
|
||||||
color: var(--danger0);
|
color: var(--danger0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +216,7 @@ div.control-prompt:focus-within{
|
||||||
input.control-prompt, input.control-prompt:focus{
|
input.control-prompt, input.control-prompt:focus{
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
70
www/js/login.js
Normal file
70
www/js/login.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*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("#login-page-username");
|
||||||
|
//Grab pass prompts
|
||||||
|
this.pass = document.querySelector("#login-page-password");
|
||||||
|
//Grab register button
|
||||||
|
this.button = document.querySelector("#login-page-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(){
|
||||||
|
//If we need verification
|
||||||
|
if(this.altcha != null){
|
||||||
|
//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.login.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(event){
|
||||||
|
//pull verification payload from event
|
||||||
|
this.verification = event.detail.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
login(){
|
||||||
|
console.log(this.altcha != null)
|
||||||
|
//If we need verification
|
||||||
|
if(this.altcha != null){
|
||||||
|
//If verification isn't complete
|
||||||
|
if( this.verification == null){
|
||||||
|
//don't bother
|
||||||
|
console.log("not complete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//login with verification
|
||||||
|
utils.ajax.login(this.user.value , this.pass.value, this.verification);
|
||||||
|
}else{
|
||||||
|
//login
|
||||||
|
utils.ajax.login(this.user.value, this.pass.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerForm = new registerPrompt();
|
||||||
|
|
@ -404,17 +404,19 @@ class canopyAjaxUtils{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(user, pass){
|
async login(user, pass, verification){
|
||||||
var response = await fetch(`/api/account/login`,{
|
var response = await fetch(`/api/account/login`,{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({user, pass})
|
body: JSON.stringify(verification ? {user, pass, verification} : {user, pass})
|
||||||
});
|
});
|
||||||
|
|
||||||
if(response.status == 200){
|
if(response.status == 200){
|
||||||
location.reload();
|
location.reload();
|
||||||
|
}else if(response.status == 429){
|
||||||
|
location = `/login?user=${user}`;
|
||||||
}else{
|
}else{
|
||||||
utils.ux.displayResponseError(await response.json());
|
utils.ux.displayResponseError(await response.json());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue