admin page

pull/41/head
unknown 2019-02-25 22:29:18 -05:00
parent 8ca648ec9d
commit bc9c67459c
17 changed files with 160 additions and 27 deletions

View File

@ -17,11 +17,59 @@ module.exports = {
listPublishers: async function (req, res) {
try {
const publishers = await PublishKey.find({
select: ['id', 'user', 'appid', 'url', 'created_at', 'updated_at']
select: ['id', 'user', 'appid', 'url', 'name', 'whitelisted', 'verified', 'verification_key', 'created_at', 'updated_at']
}).populate('user')
return res.json(publishers)
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
editUser: async function (req, res) {
try {
const id = req.param('id')
const patchData = req.param('patch')
const updated = await User.updateOne({ id }).set({
...patchData
})
for (const key in updated) {
if (patchData[key] === undefined && key !== 'id') delete updated[key]
}
return res.json(updated)
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
editPublisher: async function (req, res) {
try {
const id = req.param('id')
const patchData = req.param('patch')
const updated = await PublishKey.updateOne({ id }).set({
...patchData
})
for (const key in updated) {
if (patchData[key] === undefined && key !== 'id') delete updated[key]
}
return res.json(updated)
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
deleteUser: async function (req, res) {
try {
const id = req.param('id')
await User.destroyOne({ id })
return res.status(204).send()
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
deletePublisher: async function (req, res) {
try {
const id = req.param('id')
await PublishKey.destroyOne({ id })
return res.status(204).send()
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
}
}

View File

@ -3,12 +3,12 @@ const HttpError = require('../errors/HttpError')
module.exports = {
create: async function (req, res) {
try {
const url = req.param('url')
if (!url.length) throw new Error('Name cannot be blank')
const name = req.param('name')
if (!name.length) throw new Error('Name cannot be blank')
const created = await PublishKey.create({
user: req.user.id,
url
name
}).fetch()
return res.json(created)
} catch (e) {

View File

@ -20,11 +20,14 @@ module.exports = {
model: 'User',
required: true
},
url: {
name: {
type: 'string',
required: true
},
url: 'string',
whitelisted: 'boolean',
verified: 'boolean',
verification_key: 'string',
appid: {
type: 'string'
},
@ -35,6 +38,7 @@ module.exports = {
beforeCreate: async function (key, next) {
key.appid = await generateToken({ bytes: 12 })
key.secret = await generateToken({ bytes: 48 })
key.verification_key = await generateToken({ bytes: 24 })
next()
},
beforeUpdate: async function (key, next) {

View File

@ -1,4 +1,5 @@
module.exports = async function (req, res, next) {
if (process.env.NODE_ENV === 'development') return next()
if (req.user && (req.user.id === 1 || req.user.admin)) next()
else res.status(403).json({ error: 'You are not permitted to perform this action.' })
}

View File

@ -6,9 +6,8 @@
* @docs :: http://sailsjs.org/#!documentation/policies
*/
module.exports = function (req, res, next) {
if (req.session.authenticated) {
return next()
}
res.status(403).json({ error: 'You are not permitted to perform this action.' })
// res.redirect('/login')
if (process.env.NODE_ENV === 'development') return next()
if (req.session.authenticated) return next()
// res.status(403).json({ error: 'You are not permitted to perform this action.' })
res.redirect('/login')
}

View File

@ -7,7 +7,9 @@ const getPath = str => window.location.hostname === 'localhost' ? `http://localh
const ACTIONS = {
set_working: 'set_working',
error: 'error',
set_admin_data: 'set_admin_data'
set_admin_data: 'set_admin_data',
a_update_user: 'a_update_user',
a_update_publisher: 'a_update_publisher'
}
export default ACTIONS
@ -49,3 +51,51 @@ export const fetchAdminData = () => async (dispatch, getState) => {
dispatch(setWorking(false))
}
}
export const patchUser = ({ id, ...data }) => async (dispatch, getState) => {
dispatch(setWorking(true))
try {
const { data: user } = await Ajax.patch({
url: getPath('/admin/api/users/' + id),
data: {
patch: data
},
noProcess: true
})
dispatch({
type: ACTIONS.a_update_user,
data: user
})
} catch (e) {
dispatch({
type: ACTIONS.error,
data: e
})
} finally {
dispatch(setWorking(false))
}
}
export const patchPublisher = ({ id, ...data }) => async (dispatch, getState) => {
dispatch(setWorking(true))
try {
const { data: publisher } = await Ajax.patch({
url: getPath('/admin/api/publishers/' + id),
data: {
patch: data
},
noProcess: true
})
dispatch({
type: ACTIONS.a_update_publisher,
data: publisher
})
} catch (e) {
dispatch({
type: ACTIONS.error,
data: e
})
} finally {
dispatch(setWorking(false))
}
}

View File

@ -175,13 +175,13 @@ export const editUser = (user) => async (dispatch, getState) => {
}
}
export const createNewPublisher = (url) => async (dispatch, getState) => {
export const createNewPublisher = (name) => async (dispatch, getState) => {
dispatch(setWorking(true))
try {
const { data } = await Ajax.post({
url: getPath('/api/keys'),
data: {
url
name
}
})
dispatch(addPublisher(data))

View File

@ -49,7 +49,7 @@ export const clearError = () => ({
export const setLoggedIn = (data) => (dispatch, getState) => {
window.localStorage.setItem('roe-token', JSON.stringify(data))
window.location.href = '/targets'
window.location.href = '/keys'
}
export const checkEmail = email => async (dispatch, getState) => {

View File

@ -6,7 +6,7 @@ import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react
import Progress from './components/Progress'
import appReducer from './reducers'
import adminReducer from './reducers/admin'
import { fetchAdminData } from './actions/admin'
import { fetchAdminData, patchUser, patchPublisher } from './actions/admin'
import Util from './lib/Util'
import '../styles/admin.scss'
@ -55,7 +55,7 @@ class App extends React.Component {
<span className='flex'>{user.email}</span>
<span className='flex'>
<label for={`is-admin-${user.id}`} className='cb-label'>Admin?</label>
<input className='checkbox' type='checkbox' defaultChecked={user.admin} id={`is-admin-${user.id}`} />
<input className='checkbox' type='checkbox' checked={user.admin} onChange={() => this.dispatch(patchUser({ id: user.id, admin: !user.admin }))} id={`is-admin-${user.id}`} />
<label for={`is-admin-${user.id}`} />
</span>
<div className='stack flex flex-container flex-vertical'>
@ -71,12 +71,12 @@ class App extends React.Component {
return (
<li className='uri-list-item flex-container' key={`is-whitelisted-${pub.id}`}>
<div className='stack flex flex-container flex-vertical'>
<span className='flex'><span className='name'>{pub.url}</span><span className='appid'>{pub.appid}</span></span>
<span className='flex'><span className='name'>{pub.name}</span><span className='appid'>{pub.appid}</span></span>
<span className='flex'>{pub.user.email}</span>
</div>
<span className='flex'>
<label for={`is-whitelisted-${pub.id}`} className='cb-label'>Whitelisted?</label>
<input className='checkbox' type='checkbox' defaultChecked={pub.whitelisted} id={`is-whitelisted-${pub.id}`} />
<input className='checkbox' type='checkbox' checked={pub.whitelisted} onChange={() => this.dispatch(patchPublisher({ id: pub.id, whitelisted: !pub.whitelisted }))} id={`is-whitelisted-${pub.id}`} />
<label for={`is-whitelisted-${pub.id}`} />
</span>
<div className='stack flex flex-container flex-vertical'>
@ -102,7 +102,7 @@ class App extends React.Component {
<ul>
<li><NavLink to='/users'>Users</NavLink></li>
<li><NavLink to='/publishers'>Publishers</NavLink></li>
<li><a href='/targets'>Exit admin</a></li>
<li><a href='/keys'>Exit admin</a></li>
</ul>
</aside>
<section className={'content flex' + (this.state.working ? ' working' : '')}>

View File

@ -40,7 +40,7 @@ const CarouselItem = props => (
{props.inputs}
<span className='carousel-error'>{props.error}</span>
<div className='button-row'>
<a href='#' onClick={props.onSmallButtonClick}>{props.smallButton}</a>
<a href='#' onClick={e => handleClick(e, props.onSmallButtonClick)}>{props.smallButton}</a>
<button className='btn btn-primary' type='submit' >
{props.button}
</button>

View File

@ -21,7 +21,7 @@ class PublisherListItem extends React.Component {
<li className='uri-list-item publisher-list-item flex-container'>
<div className='stack flex site-name flex-container flex-vertical'>
<span className='label'>Website name</span>
<span className='value'>{this.props.item.url}</span>
<span className='value'>{this.props.item.name}</span>
</div>
<div className='flex flex-container'>
<div className='stack flex-container flex-vertical'>

View File

@ -59,12 +59,12 @@ export default class Ajax {
var fd = null
var qs = ''
if (opts.data && opts.method.toLowerCase() !== 'get') {
if (!opts.noProcess && opts.data && opts.method.toLowerCase() !== 'get') {
fd = new FormData()
for (let key in opts.data) {
fd.append(key, opts.data[key])
}
} else if (opts.data) {
} else if (!opts.noProcess && opts.data) {
qs += '?'
let params = []
for (let key in opts.data) {
@ -73,6 +73,14 @@ export default class Ajax {
qs += params.join('&')
}
if (opts.noProcess) {
opts.headers = {
'Content-Type': 'application/json',
...opts.headers
}
try { fd = JSON.stringify(opts.data) } catch (e) { console.warn(e) }
}
xhr.onload = () => {
if (!('' + xhr.status).startsWith('2')) { return xhr.onerror() }
var data = xhr.response

View File

@ -4,6 +4,7 @@ import Actions from '../actions/admin'
const reducer = (state = {}, action) => {
const { type, data } = action
let ind
switch (type) {
case Actions.set_working:
return {
@ -15,6 +16,20 @@ const reducer = (state = {}, action) => {
users: data.users,
publishers: data.publishers
}
case Actions.a_update_user:
const modifiedUsers = [ ...state.users ]
ind = modifiedUsers.findIndex(x => x.id === data.id)
modifiedUsers[ind] = { ...modifiedUsers[ind], ...data }
return {
users: modifiedUsers
}
case Actions.a_update_publisher:
const modifiedPublishers = [ ...state.publishers ]
ind = modifiedPublishers.findIndex(x => x.id === data.id)
modifiedPublishers[ind] = { ...modifiedPublishers[ind], ...data }
return {
publishers: modifiedPublishers
}
default: return {}
}
}

View File

@ -6,7 +6,7 @@
position: relative;
.error-box {
height: 30px;
min-height: 30px;
line-height: 30px;
background: $red;
color: white;

View File

@ -45,6 +45,6 @@ module.exports.policies = {
},
AdminController: {
'*': [ 'adminAuth' ]
'*': [ 'sessionAuth', 'adminAuth' ]
}
}

View File

@ -87,7 +87,11 @@ module.exports.routes = {
'DELETE /api/keys/:id': 'PublishKeyController.delete',
'GET /admin/api/users': 'AdminController.listUsers',
'GET /admin/api/publishers': 'AdminController.listPublishers'
'GET /admin/api/publishers': 'AdminController.listPublishers',
'PATCH /admin/api/users/:id': 'AdminController.editUser',
'PATCH /admin/api/publishers/:id': 'AdminController.editPublisher',
'DELETE /admin/api/users/:id': 'AdminController.deleteUser',
'DELETE /admin/api/publishers/:id': 'AdminController.deletePublisher'
// ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
// ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗

View File

@ -6,9 +6,13 @@ exports.up = function (knex, Promise) {
knex.schema.createTable('publishkey', t => {
t.increments('id').primary()
t.integer('user').notNullable().references('user.id').onDelete('CASCADE').onUpdate('CASCADE')
t.string('name')
t.string('url')
t.string('appid')
t.string('secret')
t.boolean('whitelisted')
t.string('verification_key')
t.boolean('whitelisted').defaultTo(false)
t.boolean('verified').defaultTo(false)
t.integer('created_at')
t.integer('updated_at')
})