Admin Panel Revamp
This commit is contained in:
parent
c99c897561
commit
abb50cf152
11 changed files with 251 additions and 39 deletions
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
4
index.js
4
index.js
|
@ -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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
44
routes/confirm-account/index.js
Normal file
44
routes/confirm-account/index.js
Normal 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;
|
||||||
|
};
|
|
@ -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
53
views/admin-user-edit.pug
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block title
|
||||||
|
h1(style="margin-bottom: 0;")
|
||||||
|
a(href='..') <
|
||||||
|
| #{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}`)
|
|
@ -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
34
views/confirm-account.pug
Normal 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?")
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block content
|
|
||||||
form(method='POST')
|
|
||||||
.field
|
|
||||||
.control
|
|
||||||
input.button.is-danger(type='submit' value=`Remove user ${userToRemove}`)
|
|
Loading…
Reference in a new issue