From 15d229704e9a6a9cd14a6dd65512691239073af4 Mon Sep 17 00:00:00 2001 From: b1ek Date: Sat, 25 Feb 2023 16:32:11 +1000 Subject: [PATCH] half baked user system --- data/userdata/alice_gpgkey | 51 +++++++++++++++++++++++++++++ helpers/htmlstring.js | 6 ++++ helpers/index.js | 4 ++- models/user.js | 66 +++++++++++++++++++++++++++++++++++++- package.json | 2 ++ routes/admin.js | 34 ++++++++++++++++++++ startup.js | 25 ++++++++++++++- view/admin/login.pug | 14 ++++++++ view/error.pug | 4 +-- 9 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 data/userdata/alice_gpgkey create mode 100644 helpers/htmlstring.js create mode 100644 routes/admin.js create mode 100644 view/admin/login.pug diff --git a/data/userdata/alice_gpgkey b/data/userdata/alice_gpgkey new file mode 100644 index 0000000..3d99df9 --- /dev/null +++ b/data/userdata/alice_gpgkey @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=nU+4 +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/helpers/htmlstring.js b/helpers/htmlstring.js new file mode 100644 index 0000000..6b70a94 --- /dev/null +++ b/helpers/htmlstring.js @@ -0,0 +1,6 @@ +// https://stackoverflow.com/questions/14129953/how-to-encode-a-string-in-javascript-for-displaying-in-html +function htmlEntities(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +module.exports = htmlEntities \ No newline at end of file diff --git a/helpers/index.js b/helpers/index.js index 63b0308..b1d25d4 100644 --- a/helpers/index.js +++ b/helpers/index.js @@ -1,4 +1,6 @@ module.exports = { ViewLoader: require('./view_loader'), - TimeSince: require('./timesince') + TimeSince: require('./timesince'), + HtmlString: require('./htmlstring'), + GPG: require('./gpg') } \ No newline at end of file diff --git a/models/user.js b/models/user.js index d2c2a24..055b5eb 100644 --- a/models/user.js +++ b/models/user.js @@ -1,7 +1,71 @@ -const { Model, DataTypes } = require('sequelize'); +const { Model, DataTypes } = require('sequelize'); +const bcrypt = require('bcrypt'); +const totp = require('totp-generator'); +const crypto = require('crypto'); class User extends Model { + /** + * @returns number[]|false + */ + async getTotpCodes() { + if (this.totp && this.totpRec) { + /** @type number[] */ + let codes = this.totpRec.split(',').map(x => {return parseInt(x, 10);}); + const period = (30 * 1000) + + codes.push(parseInt(totp(this.totp, { timestamp: Date.now() - period}))); + codes.push(parseInt(totp(this.totp))); + codes.push(parseInt(totp(this.totp, { timestamp: Date.now() + period}))); + console.log(codes); + return codes; + } + return false; + } + + /** + * + * @param {number} code + * @returns bool + */ + async checkTotp(code) { + const codes = await this.getTotpCodes(); + return codes.indexOf(code) !== 1; + } + + /** + * @param {{login: string, pass: string, totp?: string}} data + * @return {User} + */ + static async authenticate(data) { + const rows = await User.findAndCountAll({where: { + login: data.login + }}); + if (rows.count == 0) return false; + + /** @type User */ + const this_ = rows.rows[0]; + + if (!(await bcrypt.compare(data.pass, this_.pass))) { + return false; + } + + if (this_.totp) { + if (!data.totp) return false; + if (!this_.checkTotp(data.totp)) return false; + } + + return rows.rows[0]; + } + + async createSession() { + let session = {}; + session.user_id = this.id; + session.login = this.login; + session.expires = Date.now() + (60 * 60 * 1000); + session.secret = crypto.randomBytes(256).toString('base64'); + return session; + } } const structure = { diff --git a/package.json b/package.json index ecb8e8a..c6cfaea 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,14 @@ "ioredis": "^5.3.1", "js-base64": "^3.7.5", "mocha": "^10.2.0", + "node-gpg": "^0.2.0", "otplib": "^12.0.1", "pg": "^8.9.0", "pg-hstore": "^2.3.4", "pug": "^3.0.2", "redis": "^4.6.4", "sequelize": "^7.0.0-alpha.9", + "totp-generator": "^0.0.14", "unit.js": "^2.1.1" } } diff --git a/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..b4920b7 --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,34 @@ +const handler = require('express-async-handler') +const Helpers = require('../helpers'); +const db = require('../models'); + +async function login(req, res) { + res.send(await Helpers.ViewLoader.load('admin/login.pug', { + current_route: req.originalUrl + })); +} + +async function apiLogin(req, res) { + + if (req.session.user) { + res.send('Already logged in'); + return; + } + + const user = (await db.User.authenticate(req.body)); + + if (!user) { + res.status(401).send('Bad auth'); + } + const session = await user.createSession(); + req.session.user = session; + res.send(req.session); + //res.redirect('/admin/panel'); + return; +} + +module.exports = (router) => { + router.get('/login', handler(login)); + router.get('/admin/login', handler(login)); + router.post('/admin/login', handler(apiLogin)); +} \ No newline at end of file diff --git a/startup.js b/startup.js index eaecf4e..f57d855 100644 --- a/startup.js +++ b/startup.js @@ -1,8 +1,9 @@ console.log('Executing startup jobs...'); const fs = require('fs'); -const {Base64} = require('js-base64'); const crc32 = require('crc-32'); +const glob = require('glob'); +const { exec } = require('child_process'); const hrt = () => { let hr = process.hrtime(); @@ -25,6 +26,28 @@ if (process.env.APP_DEBUG) { if (!process.env.APP_KEY) throw new Error('APP_KEY is not set.') +// import gpg keys +glob('data/userdata/*_gpgkey', async (err, files) => { + if (err) { + console.error(err); + process.exit(-1); + } + files.filter( + file => { + return !file.startsWith('.') + } + ).forEach(file => { + exec('gpg --import ' + file, (err, stdout, stderr) => { + if (err) { + console.error(`Errors while importing ${file}: ${err}`); + process.exit(-1); + } + console.log(`Imported ${file} key`); + }); + }); + +}); + // TODO: perhaps a better approach to storing it???? // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ process.env.APP_KEY = Buffer.from(process.env.APP_KEY, 'base64').toString('ascii'); diff --git a/view/admin/login.pug b/view/admin/login.pug new file mode 100644 index 0000000..4add182 --- /dev/null +++ b/view/admin/login.pug @@ -0,0 +1,14 @@ +extends ../layout/main.pug +block root + - var title = 'Login' + +block content + p Login + form(method='POST' action='/admin/login') + input(type='text' name='login') + br + input(type='password' name='pass') + br + input(type='text' name='totp') + br + input(type='submit') \ No newline at end of file diff --git a/view/error.pug b/view/error.pug index a212ca6..e9842d4 100644 --- a/view/error.pug +++ b/view/error.pug @@ -3,6 +3,6 @@ block root - var title = error block content - h1(align='center')= error + h1(align='center') !{error} hr(style='width:50%') - p(align='center')= message \ No newline at end of file + p(align='center') !{message} \ No newline at end of file