diff --git a/.gitignore b/.gitignore index b512c09..9e5b648 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +ndm.db \ No newline at end of file diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..260727a --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,83 @@ +body { + width: 1200px; + margin: auto; + + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 12px; +} + +#Logo { + font-size: 20px; +} + +#Header { + display: block; + margin-bottom: 16px; + font-size: 14px; +} + +#Header-Left { + float: left; +} + +#Header-Right { + float: right; +} + +#PageContent { + display: block; + width: 900px; + min-height: 600px; + padding: 12px; + box-sizing: border-box; + background-color: #e5f1fd; + border-left: gray 2px solid; + border-right: gray 2px solid; + margin: auto; +} + +.CenteredFocusHeader { + width: 400px; + margin: auto; + background-color: #4682b4; + color: white; + padding: 4px; + box-sizing: border-box; + font-size: 16px; +} + +.CenteredFocusHeader h1, +.CenteredFocusHeader h2, +.CenteredFocusHeader h3, +.CenteredFocusHeader h4, +.CenteredFocusHeader h5, +.CenteredFocusHeader h6 { + margin: 0; +} + +.CenteredFocusContent { + width: 400px; + background-color: white; + box-sizing: border-box; + border: 2px solid #4682b4; + margin: auto; + + padding-top: 12px; + padding-bottom: 12px; + padding-left: 8px; + padding-right: 8px; +} + +.VerticalInputForm { + display: inline-block; +} + +.TextInput { + height: 14px; + font-size: 12px; +} + +.VerticalInputForm input { + display: block; + margin-bottom: 8px; +} \ No newline at end of file diff --git a/assets/scripts/menu.js b/assets/scripts/menu.js new file mode 100644 index 0000000..0435760 --- /dev/null +++ b/assets/scripts/menu.js @@ -0,0 +1,31 @@ +// This script is designed to support at a minimum IE 6 +// +// All menu functions are defined outside of initMenu. +// +// This is so I'm not duplicating functions between- +// checks for what browser we have + +function doLogoutAction() { + var logoutForm = document.getElementById("Form_Auth_Logout"); + logoutForm.submit(); +} + +// Once the page has fully loaded, connect each button to its code +function initMenu() { + var logoutButton = document.getElementById("Menu_Auth_Logout"); + + if(window.addEventListener) { + logoutButton.addEventListener("click", doLogoutAction); + } else { + logoutButton.attachEvent('onclick', doLogoutAction); + } +} + +// Register load / onload event +if(window.addEventListener) { + window.addEventListener('load', initMenu); +} else if(window.attachEvent) { + window.attachEvent('onload', initMenu); +} else { + alert("Unsupported browser."); +} diff --git a/assets/scripts/post.js b/assets/scripts/post.js new file mode 100644 index 0000000..3de9be2 --- /dev/null +++ b/assets/scripts/post.js @@ -0,0 +1,20 @@ +// really simple IE6 compatible post function + +function PostToEndpoint(url, params) { + var form = document.createElement("form"); + form.method = "POST"; + form.action = url; + + for (var key in params) { + if (params.hasOwnProperty(key)) { + var input = document.createElement("input"); + input.type = "hidden"; + input.name = key; + input.value = params[key]; + form.appendChild(input); + } + } + + document.body.appendChild(form); + form.submit(); +} \ No newline at end of file diff --git a/csrf.js b/csrf.js new file mode 100644 index 0000000..bd906d0 --- /dev/null +++ b/csrf.js @@ -0,0 +1,15 @@ +const { csrfSync } = require("csrf-sync"); + +const { +invalidCsrfTokenError, + generateToken, + validateRequest, + csrfSynchronisedProtection +} = csrfSync({ + getTokenFromRequest: (req) => req.body._csrf, +}); +module.exports = { + generateToken, + csrfSynchronisedProtection, + invalidCsrfTokenError +}; \ No newline at end of file diff --git a/database.js b/database.js index 1432504..75f84d2 100644 --- a/database.js +++ b/database.js @@ -2,7 +2,7 @@ const Sequelize = require('sequelize'); const seqConn = new Sequelize({ dialect: 'sqlite', - storage: process.env.DB_STORAGE, + storage: `ndm.db`, pool: { max: 5, min: 0, @@ -12,17 +12,6 @@ const seqConn = new Sequelize({ logging: console.log, }); -////// Sessions, Users ////// -// Sessions -const Session = seqConn.define('Session', { - sid: { - type: Sequelize.TEXT, - primaryKey: true - }, - data: Sequelize.TEXT, - expires: Sequelize.DATE, -}); - // User const User = seqConn.define('User', { username: { @@ -58,7 +47,6 @@ const RegisteredDomain = seqConn.define('RegisteredDomain', { module.exports = { db: seqConn, models: { - Session, User, RegisteredDomain } diff --git a/devel.env b/devel.env index f76645d..15f72c7 100644 --- a/devel.env +++ b/devel.env @@ -1,5 +1,9 @@ # Server SRV_PORT=5001 +# Cookies +SESSKEY="87216653c5e7b5789b87a8b73e9d81b671d3b50d4f3699f8b1e756669ad6533fb1a740cdf5ae32fca7ed63b0dfb88e35c153097060ee43ac9dd4ca685e7bee39ee56284ba21f2fe408777c20d21c1c5c3a8a40f956272d336c6dbebe68c87c561a2225e9131e7f1976cf439903f616175896c21962631c2c1f72d2efb5e12db3cbc91941bff99a1e9d050b6badee79063439b1f5883b49e9285ed3e16d434e6deb2babdc838caa8c51d45db8fd7116c3d3ad5f20f955b115e7d5000d8f0454b151ed42519d3d8fcb38e7510976064d188c184a174d3537c47c7e968de55b563382317873b6dc4013dd33a0700bb9ba143fd19c4a42e76cda7bd2f738280c9643701a291b77fc0d4a05309d0e44272209020539fe3592660476b602e5edda5f496443b8ab82ff1035737f745df3d3be76ba95d83d772ac45989b2c61d8e7d0b" +CKYKEY="88ae945c555650606058d58284c1772b3d8b56cee370ddbfcb366c1c4f6be3681e2e218404a4ff0dba21597817e1301fdbfedd711ab3b99c4bc900a8583892f957ea9fc657811ec20fcde7c11a12201e5a8b8c62a4dd56e5a4c06e46b5853c172d010c9fe754f95e61be7fde60701f2de44c641078e3aa51ac74cc68573c7c4b51c1eb5d219d94fe6f6bc0139ea3a731d9097d381a4b00931f71d4615912db0355eb73a44d0ed873f85cc112f945a0ac5d776d9b161cacd9d823c7b770bdc3d5f77d9161e0b45dcd05cf7498eb8d4898a0e6436264f7c11643c19ef0624ac6b08ffe9bf093c19cdfc93d35646aa0b76f603be82e6939" + # Database -DB_STORAGE="data_devel.db" \ No newline at end of file +DB_STORAGE="datadevel.db" \ No newline at end of file diff --git a/helpers.js b/helpers.js index 03e5f76..79ade71 100644 --- a/helpers.js +++ b/helpers.js @@ -47,6 +47,9 @@ const hbsHelpers = { }, getMetaInfString: () => { return `${metaInf.name} | ${metaInf.stage}.${metaInf.version}.${metaInf.branch}-${process.env.NODE_ENV}`; + }, + getScript: (src, nonce) => { + return new String(``); } } diff --git a/index.js b/index.js index 28af9af..717a061 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,12 @@ const app = express(); const exphbs = require ('express-handlebars'); const { SetupEnvironment } = require('./environ'); const SetupRouter = require('./router'); +const bodyParser = require("body-parser"); +const sessionMw = require('./session'); +const csrf = require("./csrf"); + +// Setup the environment +SetupEnvironment(); // Database const database = require('./database'); @@ -22,9 +28,6 @@ const { HBSHelpers } = require('./helpers'); // Security const helmet = require('helmet'); -// First things first, setup the environment -SetupEnvironment(); - // Get what we need for starting the server const serverPort = process.env.SRV_PORT; @@ -32,21 +35,25 @@ const serverPort = process.env.SRV_PORT; const db = database.db; const sessionStore = new SequelizeStore({ db: db, - table: 'Session' + tableName: 'Session' }) +// Body parsing +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + // Helmet setup app.use( helmet.contentSecurityPolicy({ - directives: { + directives: (req, res) => ({ defaultSrc: ["'self'"], - scriptSrc: ["'self'"], + scriptSrc: ["'strict-dynamic'", `'nonce-${res.locals.nonce}'`], objectSrc: ["'none'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', '*'], mediaSrc: ["'self'", 'data:', '*'], connectSrc: ["'self'", 'data:', '*'] - } + }), }) ); @@ -76,14 +83,21 @@ app.use(session({ store: sessionStore, cookie: { httpOnly: true, - secure: process.env.NODE_ENV === 'prod', - sameSite: 'strict' + secure: false, + sameSite: 'lax', + path: '/' }, })); // Setup Assets app.use(express.static('assets')); +// Session middlware +app.use(sessionMw.PersistSession); + +// CSRF protection +app.use(csrf.csrfSynchronisedProtection); + // Setup Router SetupRouter(app); diff --git a/package-lock.json b/package-lock.json index 57888ad..5482289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,75 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "argon2": "^0.44.0", "connect-session-sequelize": "^8.0.6", "cookie-parser": "^1.4.7", + "csrf-sync": "^4.2.1", "dotenv": "^17.4.2", "express": "^5.2.1", "express-handlebars": "^9.0.1", "express-session": "^1.19.0", "helmet": "^8.1.0", + "joi": "^18.2.1", "nodemon": "^3.1.14", "sequelize": "^6.37.8", "sqlite3": "^6.0.1" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -33,6 +90,21 @@ "node": ">=18.0.0" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -111,6 +183,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -389,6 +477,67 @@ "node": ">=6.6.0" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csrf-sync": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/csrf-sync/-/csrf-sync-4.2.1.tgz", + "integrity": "sha512-+q9tlUSCi/kbwr1NYwn5+MeuNhwxz3wSv1yl42BgIWfIuErZ3HajRwzvZTkfiyIqt1PZT8lQSlffhSYjCneN7g==", + "license": "ISC", + "dependencies": { + "http-errors": "^2.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1084,6 +1233,24 @@ "node": ">=20" } }, + "node_modules/joi": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", + "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -1311,6 +1478,17 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", @@ -1415,6 +1593,15 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1809,6 +1996,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", diff --git a/package.json b/package.json index a8f3b7f..f3cda3c 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,16 @@ "wprod": "set NODE_ENV=prod&& nodemon index.js" }, "dependencies": { + "argon2": "^0.44.0", "connect-session-sequelize": "^8.0.6", "cookie-parser": "^1.4.7", + "csrf-sync": "^4.2.1", "dotenv": "^17.4.2", "express": "^5.2.1", "express-handlebars": "^9.0.1", "express-session": "^1.19.0", "helmet": "^8.1.0", + "joi": "^18.2.1", "nodemon": "^3.1.14", "sequelize": "^6.37.8", "sqlite3": "^6.0.1" diff --git a/password.js b/password.js new file mode 100644 index 0000000..2fd63b1 --- /dev/null +++ b/password.js @@ -0,0 +1,22 @@ +const argon2 = require('argon2'); + +async function HashPassword(password) { + try { + return await argon2.hash(password); + } catch(err) { + throw err; + } +} + +async function TestPassword(password, hash) { + try { + return await argon2.verify(hash, password); + } catch(err) { + throw err; + } +} + +module.exports = { + HashPassword, + TestPassword +} \ No newline at end of file diff --git a/prod.env b/prod.env index 3d5b38c..27140b9 100644 --- a/prod.env +++ b/prod.env @@ -1,5 +1,9 @@ # Server SRV_PORT=5000 +# Cookies +SESSKEY="87216653c5e7b5789b87a8b73e9d81b671d3b50d4f3699f8b1e756669ad6533fb1a740cdf5ae32fca7ed63b0dfb88e35c153097060ee43ac9dd4ca685e7bee39ee56284ba21f2fe408777c20d21c1c5c3a8a40f956272d336c6dbebe68c87c561a2225e9131e7f1976cf439903f616175896c21962631c2c1f72d2efb5e12db3cbc91941bff99a1e9d050b6badee79063439b1f5883b49e9285ed3e16d434e6deb2babdc838caa8c51d45db8fd7116c3d3ad5f20f955b115e7d5000d8f0454b151ed42519d3d8fcb38e7510976064d188c184a174d3537c47c7e968de55b563382317873b6dc4013dd33a0700bb9ba143fd19c4a42e76cda7bd2f738280c9643701a291b77fc0d4a05309d0e44272209020539fe3592660476b602e5edda5f496443b8ab82ff1035737f745df3d3be76ba95d83d772ac45989b2c61d8e7d0b" +CKYKEY="88ae945c555650606058d58284c1772b3d8b56cee370ddbfcb366c1c4f6be3681e2e218404a4ff0dba21597817e1301fdbfedd711ab3b99c4bc900a8583892f957ea9fc657811ec20fcde7c11a12201e5a8b8c62a4dd56e5a4c06e46b5853c172d010c9fe754f95e61be7fde60701f2de44c641078e3aa51ac74cc68573c7c4b51c1eb5d219d94fe6f6bc0139ea3a731d9097d381a4b00931f71d4615912db0355eb73a44d0ed873f85cc112f945a0ac5d776d9b161cacd9d823c7b770bdc3d5f77d9161e0b45dcd05cf7498eb8d4898a0e6436264f7c11643c19ef0624ac6b08ffe9bf093c19cdfc93d35646aa0b76f603be82e6939" + # Database -DB_STORAGE="data_prod.db" \ No newline at end of file +DB_STORAGE="dataprod.db" \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index e69de29..472fd9b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); + +router.get('/', async (req, res) => { + console.log(`nonce: ${res.locals.globalScriptNonce}`); + res.render('index', {title: 'Domain Manager'}); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/login.js b/routes/login.js new file mode 100644 index 0000000..d133bb1 --- /dev/null +++ b/routes/login.js @@ -0,0 +1,63 @@ +const express = require('express'); +const router = express.Router(); +const loginValidator = require('../validators/login'); +const authMw = require('../session'); +const pwMw = require('../password.js'); +const database = require('../database.js'); +const dbConnection = database.db; +const Sequelize = require('sequelize'); +const pageTitle = 'Domain Manager | Login'; + +router.get('/login', authMw.AllowIfNotAuthenticated, async (req, res) => { + res.render('login', { title: pageTitle, csrfToken: req.csrfToken() }); +}); + +router.post('/login', authMw.AllowIfNotAuthenticated, async (req, res, next) => { + const reqBody = req.body; + const validationResult = loginValidator.test(reqBody); + const validationError = validationResult.error; + let errors = []; + + if(validationError !== undefined) + errors = validationError.details; + + try { + if(errors.length === 0) { + const result = await dbConnection.transaction(async(t) => { + const user = database.models.User.findOne({ + where: { + username: reqBody.login_username + }, + transaction: t + }); + + return user; + }); + + if(result) { + const doesPasswordMatch = await pwMw.TestPassword(reqBody.login_password, result.password); + if(doesPasswordMatch === true) { + await authMw.CreateSession(req, result); + return res.redirect('/'); + } else { + errors.push({message: 'Invalid username or password.'}); + } + } else { + errors.push({message: 'Invalid username or password.'}); + } + } + } catch(error) { + error.status = 500; + return next(error); + } + + return res.render('login', {title: pageTitle, errors: errors, csrfToken: req.csrfToken(true) }); +}); + +router.post('/logout', authMw.AllowIfAuthenticated, async (req, res, next) => { + // Just destroy the session + req.session.destroy(); + return res.redirect('/'); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/register.js b/routes/register.js new file mode 100644 index 0000000..8691a57 --- /dev/null +++ b/routes/register.js @@ -0,0 +1,56 @@ +const express = require('express'); +const router = express.Router(); +const registerValidator = require('../validators/register'); +const authMw = require('../session'); +const pwMw = require('../password.js'); +const database = require('../database.js'); +const dbConnection = database.db; +const Sequelize = require('sequelize'); +const pageTitle = 'Domain Manager | Register'; + +router.get('/register', authMw.AllowIfNotAuthenticated, async (req, res) => { + res.render('register', { title: pageTitle, csrfToken: req.csrfToken() }); +}); + +router.post('/register', authMw.AllowIfNotAuthenticated, async (req, res, next) => { + const reqBody = req.body; + const validationResult = registerValidator.test(reqBody); + const validationError = validationResult.error; + let errors = []; + + if(validationError !== undefined) + errors = validationError.details; + + try { + if(errors.length === 0) { + const hashedPassword = await pwMw.HashPassword(reqBody.register_password); + const result = await dbConnection.transaction(async(t) => { + const user = await database.models.User.create({ + username: reqBody.register_username, + password: hashedPassword, + }, {transaction: t}); + + return user; + }); + + if(result !== undefined) { + await authMw.CreateSession(req, result); + return res.redirect('/'); + } else { + errors.push({message: 'Failed to create user.'}) + } + } + } catch(error) { + if(error instanceof Sequelize.UniqueConstraintError) { + errors.push({message: 'Username is in use.'}); + } else { + error.status = 500; + return next(error); + } + } + + // if we're here we failed, I specify true for csrfToken to force reset it + return res.render('register', {title: pageTitle, errors: errors, csrfToken: req.csrfToken(true) }); +}); + +module.exports = router; \ No newline at end of file diff --git a/session.js b/session.js new file mode 100644 index 0000000..32458f2 --- /dev/null +++ b/session.js @@ -0,0 +1,63 @@ +const { generateToken } = require('./csrf'); +const database = require('./database'); +const crypto = require("crypto"); + +async function PersistSession(req, res, next) { + req.session.visited = true; + res.locals.nonce = crypto.randomBytes(16).toString('base64'); + + const isLoggedIn = req.session.isLoggedIn; + + if(isLoggedIn) { + const userId = req.session.userId; + const username = req.session.username; + const power = req.session.power; + + req.session.ipAddress = req.ip; + + res.locals.isLoggedIn = isLoggedIn; + res.locals.userId = userId; + res.locals.username = username; + res.locals.power = power; + res.locals.csrfToken = generateToken(req); + } + + next(); +} + +async function CreateSession(req, user) { + return new Promise(async (resolve, reject) => { + try { + req.session.isLoggedIn = true; + req.session.userId = user.id; + req.session.username = user.username; + req.session.power = user.power; + + resolve(); + } catch(error) { + reject(error); + } + }); +} + +function AllowIfNotAuthenticated(req, res, next) { + const isLoggedIn = req.session.isLoggedIn; + if(isLoggedIn) + return res.redirect('/'); + next(); +} + +function AllowIfAuthenticated(req, res, next) { + const isLoggedIn = req.session.isLoggedIn; + if(!isLoggedIn) + return res.redirect('/'); + else + next(); +} + +module.exports = { + PersistSession, + CreateSession, + AllowIfNotAuthenticated, + AllowIfAuthenticated +} \ No newline at end of file diff --git a/validators/login.js b/validators/login.js new file mode 100644 index 0000000..aa45397 --- /dev/null +++ b/validators/login.js @@ -0,0 +1,11 @@ +const Joi = require('joi'); +const loginSchema = Joi.object().keys({ + login_username: Joi.string().alphanum().min(3).max(24).required(), + login_password: Joi.string().min(8).max(256).required(), +}).unknown(true); + +module.exports = { + test: (body) => { + return loginSchema.validate(body); + } +} \ No newline at end of file diff --git a/validators/register.js b/validators/register.js new file mode 100644 index 0000000..ba7ac14 --- /dev/null +++ b/validators/register.js @@ -0,0 +1,13 @@ +const Joi = require('joi'); +const userCreateSchema = Joi.object().keys({ + register_username: Joi.string().alphanum().min(3).max(24).required(), + register_password: Joi.string().min(8).max(256).required(), + register_confirm_password: Joi.any().valid(Joi.ref('register_password')).required().messages({'any.only': 'Passwords must match.'}) + // token later +}).unknown(true); + +module.exports = { + test: (body) => { + return userCreateSchema.validate(body); + } +} \ No newline at end of file diff --git a/views/index.handlebars b/views/index.handlebars new file mode 100644 index 0000000..c05f5dd --- /dev/null +++ b/views/index.handlebars @@ -0,0 +1 @@ +

This is where you can register domains and manage DNS records.

\ No newline at end of file diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars new file mode 100644 index 0000000..165e7e0 --- /dev/null +++ b/views/layouts/main.handlebars @@ -0,0 +1,50 @@ + + + + + + + {{title}} + + {{{getScript "/scripts/menu.js" nonce}}} + + + {{#if isLoggedIn}} +
+ +
+ {{/if}} + +
+ + + + \ No newline at end of file diff --git a/views/login.handlebars b/views/login.handlebars new file mode 100644 index 0000000..01433eb --- /dev/null +++ b/views/login.handlebars @@ -0,0 +1,24 @@ +
+
+ {{#if errors}} + + {{/if}} + +
+

Login

+
+
+
+ + + + + + +
+
+
\ No newline at end of file diff --git a/views/register.handlebars b/views/register.handlebars new file mode 100644 index 0000000..9088fdb --- /dev/null +++ b/views/register.handlebars @@ -0,0 +1,26 @@ +
+
+ {{#if errors}} + + {{/if}} + +
+

Register

+
+
+
+ + + + + + + + +
+
+
\ No newline at end of file