Admin Panel Revamp

This commit is contained in:
Wingy 2020-10-29 23:50:36 -04:00
parent c99c897561
commit abb50cf152
11 changed files with 251 additions and 39 deletions

20
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/index.js",
"env": {
"PORT": "8888"
}
}
]
}

View file

@ -1,4 +1,4 @@
global._CC = {} global._CC = { require }
const expressSessionLevel = require('express-session-level'); const expressSessionLevel = require('express-session-level');
const LocalStrategy = require('passport-local').Strategy; const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session'); const session = require('express-session');
@ -42,7 +42,7 @@ passport.serializeUser((user, callback) => callback(null, user._id));
passport.deserializeUser((user, callback) => { passport.deserializeUser((user, callback) => {
db.get(user) db.get(user)
.then(dbUser => callback(null, dbUser)) .then(dbUser => callback(null, dbUser))
.catch(err => callback(err)); .catch(() => callback(null, null));
}); });

View file

@ -2,10 +2,13 @@ const config = require('../config');
module.exports = options => { module.exports = options => {
return (req, res, next) => { return (req, res, next) => {
options = options ? options : {}; options = options ? options : {};
if (req.isAuthenticated()) { let authed = false
next(); try {
} else { authed = req.isAuthenticated()
res.redirect(options.failureRedirect || config.defaultFailureRedirect); } catch {
return res.send('auth fail')
} }
if (authed) return next()
res.redirect(options.failureRedirect || config.defaultFailureRedirect)
}; };
} }

View file

@ -21,6 +21,8 @@
"express-session-level": "^1.0.0", "express-session-level": "^1.0.0",
"get-product-name": "^1.5.0", "get-product-name": "^1.5.0",
"level": "^6.0.0", "level": "^6.0.0",
"moment": "^2.29.1",
"nanoid": "^3.1.16",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pouchdb": "^7.0.0", "pouchdb": "^7.0.0",

View file

@ -1,6 +1,16 @@
const verifyAuth = require('../../middlewares/verifyAuth'); const verifyAuth = require('../../middlewares/verifyAuth');
const bcrypt = require('bcrypt-nodejs'); const bcrypt = require('bcrypt-nodejs');
const express = require('express'); const express = require('express');
const { nanoid } = require('nanoid')
const SIGNUP_TOKEN_LENGTH = 32
const SIGNUP_TOKEN_LIFETIME =
// One week, approximately. Doesn't need to be perfect.
1000 // milliseconds
* 60 // seconds
* 60 // minutes
* 24 // hours
* 07 // days
module.exports = (db) => { module.exports = (db) => {
const router = express.Router(); const router = express.Router();
@ -16,25 +26,81 @@ module.exports = (db) => {
router.post('/add', verifyAuth(), async (req, res) => { router.post('/add', verifyAuth(), async (req, res) => {
if (!req.user.admin) return res.redirect('/'); if (!req.user.admin) return res.redirect('/');
bcrypt.hash(req.body.newUserPassword, null, null, async (err, newUserPasswordHash) => {
if (err) throw err;
await db.put({ await db.put({
_id: req.body.newUserUsername.trim(), _id: req.body.newUserUsername.trim(),
password: newUserPasswordHash,
admin: false, admin: false,
wishlist: [] wishlist: [],
});
req.flash('success', `Successfully added user ${req.body.newUserUsername.trim()}!`); signupToken: nanoid(SIGNUP_TOKEN_LENGTH),
res.redirect('/admin-settings'); expiry: new Date().getTime() + SIGNUP_TOKEN_LIFETIME
}); });
res.redirect(`/admin-settings/edit/${req.body.newUserUsername.trim()}`)
}); });
router.get('/remove/:userToRemove', verifyAuth(), (req, res) => { router.get('/edit/:userToEdit', verifyAuth(), async (req, res) => {
if (!req.user.admin) return res.redirect('/'); if (!req.user.admin) return res.redirect('/');
res.render('remove', { userToRemove: req.params.userToRemove }); const doc = await db.get(req.params.userToEdit)
delete doc.password
res.render('admin-user-edit', { user: doc });
}); });
router.post('/remove/:userToRemove', verifyAuth(), async (req, res) => { router.post('/edit/refresh-signup-token/:userToEdit', verifyAuth(), async (req, res) => {
if (!req.user.admin) return res.redirect('/');
const doc = await db.get(req.params.userToEdit)
doc.signupToken = nanoid(SIGNUP_TOKEN_LENGTH)
doc.expiry = new Date().getTime() + SIGNUP_TOKEN_LIFETIME
await db.put(doc)
return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`)
});
router.post('/edit/rename/:userToRename', verifyAuth(), async (req, res) => {
if (!req.user.admin && req.user._id !== req.params.userToRename) return res.redirect('/')
if (!req.body.newUsername) {
req.flash('error', 'No username provided')
return res.redirect(`/admin-settings/edit/${req.params.userToRename}`)
}
if (req.body.newUsername === req.params.userToRename) {
req.flash('error', 'Username is same as new username.')
return res.redirect(`/admin-settings/edit/${req.params.userToRename}`)
}
const oldName = req.params.userToRename
const newName = req.body.newUsername
const userDoc = await db.get(oldName)
userDoc._id = newName
delete userDoc._rev
try {
await db.put(userDoc)
try {
const usersBulk = []
const users = (await db.allDocs({ include_docs: true })).rows
for (const { doc: user } of users) {
for (const item of user.wishlist) {
if (item.pledgedBy === oldName) item.pledgedBy = newName
if (item.addedBy === oldName) item.addedBy = newName
}
usersBulk.push(user)
}
await db.bulkDocs(usersBulk)
await db.remove(await db.get(oldName))
await req.flash('success', 'Renamed user!')
return res.redirect(`/wishlist/${newName}`)
} catch (error) {
console.log(error, error.stack)
await db.remove(await db.get(newName))
throw error
}
} catch (error) {
req.flash('error', error.message)
return res.redirect(`/admin-settings/edit/${oldName}`)
}
})
router.post('/edit/remove/:userToRemove', verifyAuth(), async (req, res) => {
if (!req.user.admin) return res.redirect('/'); if (!req.user.admin) return res.redirect('/');
const doc = await db.get(req.params.userToRemove); const doc = await db.get(req.params.userToRemove);
if (doc.admin) { if (doc.admin) {

View file

@ -0,0 +1,44 @@
const bcrypt = require('bcrypt-nodejs');
const express = require('express');
module.exports = (db) => {
const router = express.Router();
router.get('/:code', async (req, res) => {
const { doc } = (await db.allDocs({ include_docs: true }))
.rows
.find(({ doc }) => doc.signupToken === req.params.code)
res.render('confirm-account', { doc })
});
router.post('/:code', async (req, res) => {
const { doc } = (await db.allDocs({ include_docs: true }))
.rows
.find(({ doc }) => doc.signupToken === req.params.code)
if (doc.expiry < new Date().getTime()) return res.redirect(`/confirm-account/${req.params.code}`)
bcrypt.hash(req.body.password, null, null, async (err, passwordHash) => {
if (err) throw err;
doc.password = passwordHash
delete doc.signupToken
delete doc.expiry
await db.put(doc)
req.login({ _id: doc._id }, err => {
if (err) {
console.log(err)
req.flash('error', err.message)
return res.redirect('/')
}
req.flash('success', `Welcome to ${_CC.config.siteTitle}!`);
res.redirect('/');
})
});
});
return router;
};

View file

@ -28,6 +28,7 @@ module.exports = ({ db, config }) => {
router.use('/login', require('./login')()); router.use('/login', require('./login')());
router.use('/logout', require('./logout')()); router.use('/logout', require('./logout')());
router.use('/confirm-account', require('./confirm-account')(db));
router.use('/wishlist', require('./wishlist')(db)); router.use('/wishlist', require('./wishlist')(db));

53
views/admin-user-edit.pug Normal file
View file

@ -0,0 +1,53 @@
extends layout.pug
block title
h1(style="margin-bottom: 0;")
a(href='..') &lt;
| #{config.siteTitle}
p Editing user "#{user._id}"
block content
.columns
.column
if user.signupToken
- const link = `${_CC.config.base}confirm-account/${user.signupToken}`
.box(style='overflow: hidden;')
.columns(style='margin-bottom: 0;')
.column.is-narrow(style='padding-bottom: 0;')
h2 Confirmation Link
.column(style='padding-bottom: 0;')
p
span This account hasn't been confirmed.
br
if user.expiry > new Date().getTime()
span= `The following link expires ${_CC.require('moment')(user.expiry).fromNow()}`
else
span.has-text-weight-bold(style='color: red;')= `The following link expired ${_CC.require('moment')(user.expiry).fromNow()}`
h3(style='margin-bottom: 0; margin-top: 0;')
.level
.level-left
.level-item
form(method='POST', action=`${_CC.config.base}admin-settings/edit/refresh-signup-token/${user._id}`)
input.button.is-rounded(type='submit', value='Generate New Link')
.level-item
a(href=link, style='font-family: monospaced; word-break: break-all;')= link
h2 Change Name
form(action=`${_CC.config.base}admin-settings/edit/rename/${user._id}`, method='POST')
.field
label.label Username
.control.has-icons-left
input.input(type='text', name='newUsername', placeholder=user._id, value=user._id)
span.icon.is-small.is-left
i.fas.fa-user
.field
.control
input.button.is-primary(type='submit' value='Change Username')
.column.is-narrow
h2 Irreversible Deletion
form(method='POST', action=`${_CC.config.base}admin-settings/edit/remove/${user._id}`)
.field
.control
if user.admin
input.button.is-danger(disabled, type='submit' value=`User is admin`)
else
input.button.is-danger(type='submit' value=`Remove user ${user._id}`)

View file

@ -4,12 +4,11 @@ block content
h2 Users h2 Users
each user in users each user in users
span.is-size-6.inline= user.id span.is-size-6.inline= user.id
if !user.doc.admin a(href=`${_CC.config.base}admin-settings/edit/${user.id}`)
a(href=`${_CC.config.base}admin-settings/remove/${user.id}`) span.is-size-7.icon.has-text-info
span.is-size-7.icon.has-text-danger i.fas.fa-edit
i.fas.fa-times
span.is-sr-only span.is-sr-only
Remove | Edit
br br
h3 Add user h3 Add user
form(action=`${_CC.config.base}admin-settings/add`, method='POST') form(action=`${_CC.config.base}admin-settings/add`, method='POST')
@ -19,12 +18,9 @@ block content
input.input(type='text', name='newUserUsername', placeholder='john') input.input(type='text', name='newUserUsername', placeholder='john')
span.icon.is-small.is-left span.icon.is-small.is-left
i.fas.fa-user i.fas.fa-user
.field
label.label Password
.control.has-icons-left
input.input(type='password', name='newUserPassword', placeholder='pa$$word!')
span.icon.is-small.is-left
i.fas.fa-lock
.field .field
.control .control
input.button.is-primary(type='submit' value='Add User') input.button.is-primary(type='submit' value='Add User')
h3 Version Info
p Christmas Community: v#{_CC.require('./package.json').version}
p Node: #{process.version}

34
views/confirm-account.pug Normal file
View file

@ -0,0 +1,34 @@
extends layout.pug
mixin icon(c, text)
.columns.is-vcentered.is-mobile
.column.is-narrow
span.icon.is-large
i.fa-3x(class=c)
.column #{text}
block title
if doc
h1 #{config.siteTitle} | Confirm Account
else
h1 #{config.siteTitle} | Confirmation Link Invalid
block content
if doc
if doc.expiry > new Date().getTime()
+icon('fas fa-smile-beam', `Hello ${doc._id}! Please set your password here.`)
form(method='POST')
.field
label.label Password
.control.has-icons-left
input.input(type='password', name='password', placeholder='pa$$word!')
span.icon.is-small.is-left
i.fas.fa-lock
.field
.control
input.button.is-primary(type='submit' value=`Join ${_CC.config.siteTitle}`)
else
+icon('fas fa-frown-open', 'Your confirmation link has expired. Please ask for a new one.')
else
+icon('fas fa-frown-open', "This confirmation link isn't valid, perhaps the account was deleted or some characters at the end got cut off?")

View file

@ -1,7 +0,0 @@
extends layout.pug
block content
form(method='POST')
.field
.control
input.button.is-danger(type='submit' value=`Remove user ${userToRemove}`)