Merge branch 'feature/sitewide-keys' into staging

pull/35/head
Theodore Kluge 2019-02-24 13:09:49 -05:00 committed by GitHub
commit 8e9e802f08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 259 additions and 1363 deletions

View File

@ -0,0 +1,44 @@
const HttpError = require('../errors/HttpError')
module.exports = {
create: async function (req, res) {
try {
const url = req.param('url')
if (!url.length) throw new Error('URL cannot be blank')
const created = await PublishKey.create({
user: req.user.id,
url
}).fetch()
return res.json(created)
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
list: async function (req, res) {
try {
const keys = await PublishKey.find()
return res.json(keys)
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
refresh: async function (req, res) {
try {
const id = req.param('id')
const key = await PublishKey.update({ id, user: req.user.id }, {}).fetch()
return res.json(key)
} catch (e) {
return (new HttpError(500, e.message)).send(res)
}
},
delete: 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)
}
}
}

45
api/models/PublishKey.js Normal file
View File

@ -0,0 +1,45 @@
const crypto = require('crypto')
function generateToken({ bytes, base }) {
return new Promise((res, rej) => {
crypto.randomBytes(bytes, (err, buf) => {
if (err) rej(err)
else res(buf.toString(base || 'base64'))
})
})
}
module.exports = {
attributes: {
id: {
type: 'number',
unique: true,
autoIncrement: true
},
user: {
model: 'User',
required: true
},
url: {
type: 'string',
required: true
},
key: {
type: 'string',
required: true
},
secret: {
type: 'string',
required: true
}
},
beforeCreate: async function (key, next) {
key.key = await generateToken({ bytes: 12 })
key.secret = await generateToken({ bytes: 48 })
next()
},
beforeUpdate: async function (key, next) {
key.secret = await generateToken({ bytes: 48 })
next()
}
}

12
api/policies/keyAuth.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = async function (req, res, next) {
const key = req.param('key') || req.header('x-roe-publish-key')
const secret = req.param('secret') || req.header('x-roe-publish-secret')
console.log(key)
console.log(secret)
if (await PublishKey.findOne({ key, secret })) {
return next()
}
res.status(403).json({ error: 'Invalid publishing key.' })
}

View File

@ -10,7 +10,9 @@ const ACTIONS = {
list_url: 'list_url', list_url: 'list_url',
set_editing: 'set_editing', set_editing: 'set_editing',
error: 'error', error: 'error',
set_user: 'set_user' set_user: 'set_user',
add_publisher: 'add_publisher',
delete_publisher: 'delete_publisher'
} }
export default ACTIONS export default ACTIONS
@ -35,6 +37,11 @@ export const addUrl = url => ({
data: url data: url
}) })
export const addPublisher = url => ({
type: ACTIONS.add_publisher,
data: url
})
export const setEditing = id => ({ export const setEditing = id => ({
type: ACTIONS.set_editing, type: ACTIONS.set_editing,
data: id data: id
@ -154,3 +161,43 @@ export const editUser = (user) => async (dispatch, getState) => {
dispatch(setWorking(false)) dispatch(setWorking(false))
} }
} }
export const createNewPublisher = (url) => async (dispatch, getState) => {
dispatch(setWorking(true))
try {
const { data } = await Ajax.post({
url: '/api/keys',
data: {
url
}
})
dispatch(addPublisher(data))
} catch (e) {
dispatch({
type: ACTIONS.error,
data: e
})
} finally {
dispatch(setWorking(false))
}
}
export const removePublisher = id => async (dispatch, getState) => {
dispatch(setWorking(true))
try {
await Ajax.delete({
url: '/api/keys/' + id
})
dispatch({
type: ACTIONS.delete_publisher,
data: id
})
} catch (e) {
dispatch({
type: ACTIONS.error,
data: e
})
} finally {
dispatch(setWorking(false))
}
}

View File

@ -0,0 +1,31 @@
import React from 'react'
import IconButton from '../components/IconButton'
import UnderlineInput from '../components/UnderlineInput'
import { removePublisher } from '../actions'
import '../../styles/shared/listitem.scss'
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
class PublisherListItem extends React.Component {
render () {
return (
<li className='uri-list-item flex-container'>
<div className='stack flex flex-container flex-vertical'>
<span className='label'>Website URL</span>
<span className='value'>{this.props.item.url}</span>
</div>
<div className='stack flex flex-container flex-vertical'>
<span className='label'>Key</span>
<input className='value' value={this.props.item.key} readOnly={true} />
</div>
<div className='stack flex flex-container flex-vertical'>
<span className='label'>Secret</span>
<input className='value' value={this.props.item.secret} readOnly={true} />
</div>
<IconButton icon='delete' onClick={() => this.props.dispatch(removePublisher(this.props.item.id))} />
</li>
)
}
}
export default PublisherListItem

View File

@ -3,7 +3,7 @@
import React from 'react' import React from 'react'
import IconButton from '../components/IconButton' import IconButton from '../components/IconButton'
import UnderlineInput from '../components/UnderlineInput' import UnderlineInput from '../components/UnderlineInput'
import '../../styles/shared/urilistitem.scss' import '../../styles/shared/listitem.scss'
import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions' import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions'
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i

View File

@ -4,10 +4,11 @@ import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react-router-dom' import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react-router-dom'
import Progress from './components/Progress' import Progress from './components/Progress'
import UnderlineInput from './components/UnderlineInput'
import UriListItem from './containers/UriListItem' import UriListItem from './containers/UriListItem'
import reducer from './reducers' import reducer from './reducers'
import { fetchData, createNewUrl, setEditing, editUser } from './actions' import { fetchData, createNewUrl, setEditing, editUser, createNewPublisher } from './actions'
import UnderlineInput from './components/UnderlineInput'
import '../styles/index.scss' import '../styles/index.scss'
class App extends React.Component { class App extends React.Component {
@ -22,6 +23,8 @@ class App extends React.Component {
currentPassword: '' currentPassword: ''
}, },
urls: [], urls: [],
publishers: [],
newPublisherUrl: '',
editingUrl: null, editingUrl: null,
working: false working: false
} }
@ -30,6 +33,8 @@ class App extends React.Component {
this.getRegisteredUris = this.getRegisteredUris.bind(this) this.getRegisteredUris = this.getRegisteredUris.bind(this)
this.setUserValue = this.setUserValue.bind(this) this.setUserValue = this.setUserValue.bind(this)
this.saveUser = this.saveUser.bind(this) this.saveUser = this.saveUser.bind(this)
this.getRegisteredPublishers = this.getRegisteredPublishers.bind(this)
this.setPublisherUrl = this.setPublisherUrl.bind(this)
} }
dispatch (action) { dispatch (action) {
if (!action) throw new Error('dispatch: missing action') if (!action) throw new Error('dispatch: missing action')
@ -46,6 +51,11 @@ class App extends React.Component {
componentDidMount () { componentDidMount () {
this.dispatch(fetchData()) this.dispatch(fetchData())
} }
setPublisherUrl (e) {
this.setState({
newPublisherUrl: e.target.value
})
}
getRegisteredUris () { getRegisteredUris () {
return this.state.urls.map((item, i) => { return this.state.urls.map((item, i) => {
return (<UriListItem return (<UriListItem
@ -74,6 +84,14 @@ class App extends React.Component {
} }
}) })
} }
getRegisteredPublishers () {
return this.state.publishers.map((item, i) => {
return (<PublisherListItem
key={i}
dispatch={this.dispatch}
item={item} />)
})
}
render () { render () {
return ( return (
<Router> <Router>
@ -89,6 +107,7 @@ class App extends React.Component {
<ul> <ul>
<li><NavLink to='/targets'>Push URIs</NavLink></li> <li><NavLink to='/targets'>Push URIs</NavLink></li>
<li><NavLink to='/account'>My account</NavLink></li> <li><NavLink to='/account'>My account</NavLink></li>
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
</ul> </ul>
</aside> </aside>
<section className={'content flex' + (this.state.working ? ' working' : '')}> <section className={'content flex' + (this.state.working ? ' working' : '')}>
@ -109,6 +128,27 @@ class App extends React.Component {
</ul> </ul>
</div> </div>
)} /> )} />
<Route path='/keys' exact children={props => (
<div>
<header className='flex-container'>
<div className='flex'>
<h1>Publishing keys</h1>
<h2>If you own a publishing site, generate a publishing key for it here.</h2>
</div>
</header>
<div className='creator flex-container'>
<UnderlineInput
className='flex'
placeholder='Site URL'
value={this.state.newPublisherUrl}
onChange={this.setPublisherUrl} />
<button className='btn' onClick={() => this.dispatch(createNewPublisher(this.state.newPublisherUrl))}>Create keys</button>
</div>
<ul className='list'>
{this.getRegisteredPublishers()}
</ul>
</div>
)} />
<Route path='/account' exact children={props => ( <Route path='/account' exact children={props => (
<div> <div>

View File

@ -45,6 +45,16 @@ const reducer = (state = {}, action) => {
return { return {
error: (data || {}).message || '' error: (data || {}).message || ''
} }
case Actions.add_publisher:
return {
publishers: state.publishers.concat(data),
error: ''
}
case Actions.delete_publisher:
return {
publishers: state.publishers.filter(x => x.id !== data),
error: ''
}
default: return {} default: return {}
} }
} }

View File

@ -28,6 +28,15 @@
margin-top: 4px; margin-top: 4px;
color: $text-dark-2; color: $text-dark-2;
text-shadow: 1px 1px 2px $black-4; text-shadow: 1px 1px 2px $black-4;
.creator {
padding: 0 14px;
line-height: 60px;
max-width: 500px;
.btn {
margin: 12px 0 12px 12px;
}
} }
} }
.list { .list {

View File

@ -6,6 +6,7 @@
box-shadow: $shadow-1; box-shadow: $shadow-1;
header { header {
height: 50px;
line-height: 50px; line-height: 50px;
padding: 0 14px; padding: 0 14px;

View File

@ -33,5 +33,14 @@ module.exports.policies = {
TargetController: { TargetController: {
'*': [ 'sessionAuth' ] '*': [ 'sessionAuth' ]
},
PublishKeyController: {
'*': [ 'sessionAuth' ]
},
BooksController: {
'*': true,
publish: [ 'keyAuth' ]
} }
} }

View File

@ -71,9 +71,14 @@ module.exports.routes = {
'GET /api/books': 'BooksController.list', 'GET /api/books': 'BooksController.list',
'POST /api/targets': 'TargetController.create', 'POST /api/targets': 'TargetController.create',
'GET /api/targets': 'TargetController.list',
'PATCH /api/targets/:id': 'TargetController.edit', 'PATCH /api/targets/:id': 'TargetController.edit',
'DELETE /api/targets/:id': 'TargetController.delete', 'DELETE /api/targets/:id': 'TargetController.delete',
'GET /api/targets': 'TargetController.list'
'POST /api/keys': 'PublishKeyController.create',
'GET /api/keys': 'PublishKeyController.list',
'PATCH /api/keys/:id': 'PublishKeyController.refresh',
'DELETE /api/keys/:id': 'PublishKeyController.delete'
// ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗ // ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
// ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗ // ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗

View File

@ -90,6 +90,7 @@
"Book", "Book",
"Passport", "Passport",
"TargetUrl", "TargetUrl",
"PublishKey",
"_" "_"
], ],
"env": [ "env": [

File diff suppressed because it is too large Load Diff