Add session handling and optional reCaptcha support

This commit is contained in:
2023-09-29 20:58:16 -04:00
parent 7b94f6aad5
commit 3439a6cabe
11 changed files with 268 additions and 14 deletions

View File

@@ -29,6 +29,14 @@ ADMIN_PASS="flux"
# 4. Put your solder API URL (http[s]://example.com/api/) in and link it. # 4. Put your solder API URL (http[s]://example.com/api/) in and link it.
API_KEY="" API_KEY=""
# (REQUIRED) Session Secret Key. CHANGE THIS!!!!
# This is the key that will be used to encrypt sessions. It is recommended that you make it secure.
# You can generate one using the following steps:
# 1. Go to https://www.lastpass.com/features/password-generator#generatorTool
# 2. Set password length to 50 and leave it set to all characters
# 3. Copy the password here.
SESSION_KEY="(/jg~WBd4%+cXt9vp'&/lN-;mynl:N}v£D!0I5V04tcpYb&;.F"
# (Optional) Default Author Name (for custom files) # (Optional) Default Author Name (for custom files)
# This is the name that will be pre-filled when you go to upload custom files for a modpack. # This is the name that will be pre-filled when you go to upload custom files for a modpack.
# Unset behaviour: You will need to specify the author every time. # Unset behaviour: You will need to specify the author every time.
@@ -49,3 +57,13 @@ API_RATE_REQUESTS=1000
# The amount of time (in milliseconds) that the request limit is applied to. # The amount of time (in milliseconds) that the request limit is applied to.
# Unset behaviour: Defaults to 60000 # Unset behaviour: Defaults to 60000
API_RATE_WINDOW=60000 API_RATE_WINDOW=60000
# --- Google ReCaptcha ---
# TechnicFlux comes with Google ReCaptcha support.
# When configured, this will enable captcha protection on your login page.
# (Optional) Google ReCaptcha Credentials
# You can obtain these credentials at https://www.google.com/recaptcha/admin/create
# Unset behaviour: ReCaptcha is disabled on login page.
#CAPTCHA_SITE_KEY=""
#CAPTCHA_SITE_SECRET=""

BIN
README.md

Binary file not shown.

15
app.js
View File

@@ -4,6 +4,8 @@ const path = require('path')
const cookieParser = require('cookie-parser') const cookieParser = require('cookie-parser')
const logger = require('morgan') const logger = require('morgan')
const multer = require('multer') const multer = require('multer')
const session = require('express-session')
const MongoStore = require('connect-mongo')
// Setup multer disk-store // Setup multer disk-store
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -27,9 +29,20 @@ const app = express()
app.set('views', path.join(__dirname, 'views')) app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs') app.set('view engine', 'hbs')
// --- Middleware ---
app.use(session({
secret: process.env.SESSION_KEY,
saveUninitialized: false,
resave: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_CONN_STRING,
touchAfter: 24 * 3600,
collectionName: "technicflux_sessions"
})
}))
app.use(logger('dev')) app.use(logger('dev'))
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: false })) app.use(express.urlencoded({ extended: true }))
app.use(cookieParser()) app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public'))) app.use(express.static(path.join(__dirname, 'public')))

13
bin/www
View File

@@ -7,12 +7,13 @@ require('dotenv').config()
// Check if the .env config file is included. // Check if the .env config file is included.
const required_variables = [ const required_variables = [
process.env.MONGODB_CONN_STRING, process.env.MONGODB_CONN_STRING,
process.env.ADMIN_USER, process.env.ADMIN_USER,
process.env.ADMIN_PASS, process.env.ADMIN_PASS,
process.env.NAME, process.env.NAME,
process.env.HOST, process.env.HOST,
process.env.API_KEY process.env.API_KEY,
process.env.SESSION_KEY
] ]
for (let variable of required_variables) { for (let variable of required_variables) {
if (variable === undefined) { if (variable === undefined) {

139
package-lock.json generated
View File

@@ -11,12 +11,15 @@
"dependencies": { "dependencies": {
"@zip.js/zip.js": "^2.7.29", "@zip.js/zip.js": "^2.7.29",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^5.0.0",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"current-git-branch": "^1.1.0", "current-git-branch": "^1.1.0",
"debug": "~4.3.4", "debug": "~4.3.4",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "~4.18.2", "express": "~4.18.2",
"express-rate-limit": "^7.0.2", "express-rate-limit": "^7.0.2",
"express-recaptcha": "^5.1.0",
"express-session": "^1.17.3",
"hbs": "~4.2.0", "hbs": "~4.2.0",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"mongoose": "^7.5.3", "mongoose": "^7.5.3",
@@ -395,6 +398,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/asynciterator.prototype": { "node_modules/asynciterator.prototype": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
@@ -449,6 +463,11 @@
"safe-buffer": "^5.1.1" "safe-buffer": "^5.1.1"
} }
}, },
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@@ -665,6 +684,22 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"node_modules/connect-mongo": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.0.0.tgz",
"integrity": "sha512-s93jiP6GkRApn5duComx6RLwtP23YrulPxShz+8peX7svd6Q+MS8nKLhKCCazbP92C13eTVaIOxgeLt0ezIiCg==",
"dependencies": {
"debug": "^4.3.1",
"kruptein": "^3.0.0"
},
"engines": {
"node": ">=12.9.0"
},
"peerDependencies": {
"express-session": "^1.17.1",
"mongodb": "^5.1.0"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1615,6 +1650,75 @@
"express": "^4 || ^5" "express": "^4 || ^5"
} }
}, },
"node_modules/express-recaptcha": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express-recaptcha/-/express-recaptcha-5.1.0.tgz",
"integrity": "sha512-L9BSaHbwVzEAf38LNnFwEbk4qpX1LiD1z8viBGYBuxyBWWVKMgBrKDb1mNLq6uSxubJzZbyIz+0ldgTSXfxsvQ==",
"engines": {
"node": ">= 6"
}
},
"node_modules/express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"dependencies": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express-session/node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express-session/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/express/node_modules/cookie": { "node_modules/express/node_modules/cookie": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@@ -2620,6 +2724,17 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/kruptein": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.6.tgz",
"integrity": "sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==",
"dependencies": {
"asn1.js": "^5.4.1"
},
"engines": {
"node": ">8"
}
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2753,6 +2868,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3388,6 +3508,14 @@
} }
] ]
}, },
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -4327,6 +4455,17 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View File

@@ -39,12 +39,15 @@
"dependencies": { "dependencies": {
"@zip.js/zip.js": "^2.7.29", "@zip.js/zip.js": "^2.7.29",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^5.0.0",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"current-git-branch": "^1.1.0", "current-git-branch": "^1.1.0",
"debug": "~4.3.4", "debug": "~4.3.4",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "~4.18.2", "express": "~4.18.2",
"express-rate-limit": "^7.0.2", "express-rate-limit": "^7.0.2",
"express-recaptcha": "^5.1.0",
"express-session": "^1.17.3",
"hbs": "~4.2.0", "hbs": "~4.2.0",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"mongoose": "^7.5.3", "mongoose": "^7.5.3",

File diff suppressed because one or more lines are too long

View File

@@ -9,3 +9,7 @@ body {
a { a {
color: #00B7FF; color: #00B7FF;
} }
.grecaptcha-badge {
margin-bottom: 65px;
}

View File

@@ -1,9 +1,36 @@
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const { RecaptchaV3 } = require('express-recaptcha')
// --- Google ReCaptcha
const site_key = process.env?.CAPTCHA_SITE_KEY
const site_secret = process.env?.CAPTCHA_SITE_SECRET
const captcha_enabled = (site_key !== undefined) && (site_secret !== undefined)
const recaptcha = captcha_enabled ? new RecaptchaV3(site_key, site_secret, {
callback: 'cb'
}) : undefined
/* GET home page. */ /* GET home page. */
router.get('/', function (req, res) { router.get('/', captcha_enabled ? recaptcha.middleware.render : (req, res, next) => {next()}, function (req, res) {
res.render('index', { title: 'TechnicFlux' }) res.render('index', {
title: 'TechnicFlux',
captcha: captcha_enabled ? res.recaptcha : false
})
})
/* POST home page (login info w/ captcha) */
router.post('/', captcha_enabled ? recaptcha.middleware.verify : (req, res, next) => {next()}, function (req, res) {
// Validate captcha, if enabled.
if (captcha_enabled && req.recaptcha.error) {
return res.render('index', {
title: 'TechnicFlux',
captcha: captcha_enabled ? res.recaptcha : false,
error: "Captcha validation failed! Ensure that javascript is enabled!"
})
}
// Check login info
res.json({success: "yay"})
}) })
module.exports = router module.exports = router

0
views/dashboard.hbs Normal file
View File

View File

@@ -1,4 +1,53 @@
<img src="/images/technicflux-logo-white.png" width="108px" height="108px" /> <div id="layoutAuthentication_content">
<h1>{{title}}</h1> <main>
<p>Welcome to {{title}}!</p> <div class="container">
<p>The dashboard isn't ready yet. We are currently working on the API and backend stuff.</p> <div class="row justify-content-center">
<div class="col-lg-5">
<div class="card border-0 rounded-lg mt-5">
<div class="card-header bg-white">
<img class="mx-auto my-5 d-block" src="/images/technicflux-logo.png" width="108px" height="108px" alt="TechnicFlux logo"/>
</div>
<div class="card-body">
{{#if error}}
<div class="alert alert-danger" role="alert">
{{error}}
</div>
{{/if}}
<form id="login-form" method="post" target="/">
<div class="form-floating mb-3">
<input class="form-control" id="inputEmail" type="text" placeholder="name@example.com" />
<label for="inputEmail" class="text-dark">Username</label>
</div>
<div class="form-floating mb-3">
<input class="form-control" id="inputPassword" type="password" placeholder="Password" />
<label for="inputPassword" class="text-dark">Password</label>
</div>
<div class="d-flex align-items-center justify-content-between">
<button class="btn btn-primary w-100 g-recaptcha"
data-sitekey="{{site_key}}"
data-callback="onSubmit"
data-action="submit"
>Log In</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
{{#if captcha}}
{{{captcha}}}
<script>
function cb(token) {
let input = document.createElement('input')
input.setAttribute('type', 'text')
input.setAttribute('name', 'g-recaptcha-response')
input.setAttribute('value', token)
input.setAttribute('hidden', true)
document.getElementsByTagName('form')[0].appendChild(input)
}
</script>
{{/if}}