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 LocalStrategy = require('passport-local').Strategy;
|
||||
const session = require('express-session');
|
||||
|
@ -42,7 +42,7 @@ passport.serializeUser((user, callback) => callback(null, user._id));
|
|||
passport.deserializeUser((user, callback) => {
|
||||
db.get(user)
|
||||
.then(dbUser => callback(null, dbUser))
|
||||
.catch(err => callback(err));
|
||||
.catch(() => callback(null, null));
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -2,10 +2,13 @@ const config = require('../config');
|
|||
module.exports = options => {
|
||||
return (req, res, next) => {
|
||||
options = options ? options : {};
|
||||
if (req.isAuthenticated()) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect(options.failureRedirect || config.defaultFailureRedirect);
|
||||
let authed = false
|
||||
try {
|
||||
authed = req.isAuthenticated()
|
||||
} 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",
|
||||
"get-product-name": "^1.5.0",
|
||||
"level": "^6.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"nanoid": "^3.1.16",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pouchdb": "^7.0.0",
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
const verifyAuth = require('../../middlewares/verifyAuth');
|
||||
const bcrypt = require('bcrypt-nodejs');
|
||||
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) => {
|
||||
const router = express.Router();
|
||||
|
@ -16,25 +26,81 @@ module.exports = (db) => {
|
|||
|
||||
router.post('/add', verifyAuth(), async (req, res) => {
|
||||
if (!req.user.admin) return res.redirect('/');
|
||||
bcrypt.hash(req.body.newUserPassword, null, null, async (err, newUserPasswordHash) => {
|
||||
if (err) throw err;
|
||||
await db.put({
|
||||
_id: req.body.newUserUsername.trim(),
|
||||
password: newUserPasswordHash,
|
||||
admin: false,
|
||||
wishlist: []
|
||||
});
|
||||
req.flash('success', `Successfully added user ${req.body.newUserUsername.trim()}!`);
|
||||
res.redirect('/admin-settings');
|
||||
wishlist: [],
|
||||
|
||||
signupToken: nanoid(SIGNUP_TOKEN_LENGTH),
|
||||
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('/');
|
||||
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('/');
|
||||
const doc = await db.get(req.params.userToRemove);
|
||||
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('/logout', require('./logout')());
|
||||
router.use('/confirm-account', require('./confirm-account')(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
|
||||
each user in users
|
||||
span.is-size-6.inline= user.id
|
||||
if !user.doc.admin
|
||||
a(href=`${_CC.config.base}admin-settings/remove/${user.id}`)
|
||||
span.is-size-7.icon.has-text-danger
|
||||
i.fas.fa-times
|
||||
a(href=`${_CC.config.base}admin-settings/edit/${user.id}`)
|
||||
span.is-size-7.icon.has-text-info
|
||||
i.fas.fa-edit
|
||||
span.is-sr-only
|
||||
Remove
|
||||
| Edit
|
||||
br
|
||||
h3 Add user
|
||||
form(action=`${_CC.config.base}admin-settings/add`, method='POST')
|
||||
|
@ -19,12 +18,9 @@ block content
|
|||
input.input(type='text', name='newUserUsername', placeholder='john')
|
||||
span.icon.is-small.is-left
|
||||
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
|
||||
.control
|
||||
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