Merge branch 'feature/sitewide-keys' into staging
commit
8e9e802f08
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.' })
|
||||
}
|
|
@ -10,7 +10,9 @@ const ACTIONS = {
|
|||
list_url: 'list_url',
|
||||
set_editing: 'set_editing',
|
||||
error: 'error',
|
||||
set_user: 'set_user'
|
||||
set_user: 'set_user',
|
||||
add_publisher: 'add_publisher',
|
||||
delete_publisher: 'delete_publisher'
|
||||
}
|
||||
|
||||
export default ACTIONS
|
||||
|
@ -35,6 +37,11 @@ export const addUrl = url => ({
|
|||
data: url
|
||||
})
|
||||
|
||||
export const addPublisher = url => ({
|
||||
type: ACTIONS.add_publisher,
|
||||
data: url
|
||||
})
|
||||
|
||||
export const setEditing = id => ({
|
||||
type: ACTIONS.set_editing,
|
||||
data: id
|
||||
|
@ -144,6 +151,46 @@ export const editUser = (user) => async (dispatch, getState) => {
|
|||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
data: null
|
||||
})
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
data: e
|
||||
})
|
||||
} finally {
|
||||
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({
|
||||
|
|
|
@ -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
|
|
@ -3,7 +3,7 @@
|
|||
import React from 'react'
|
||||
import IconButton from '../components/IconButton'
|
||||
import UnderlineInput from '../components/UnderlineInput'
|
||||
import '../../styles/shared/urilistitem.scss'
|
||||
import '../../styles/shared/listitem.scss'
|
||||
import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions'
|
||||
|
||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||
|
|
|
@ -4,10 +4,11 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom'
|
||||
import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react-router-dom'
|
||||
import Progress from './components/Progress'
|
||||
import UnderlineInput from './components/UnderlineInput'
|
||||
import UriListItem from './containers/UriListItem'
|
||||
import reducer from './reducers'
|
||||
import { fetchData, createNewUrl, setEditing, editUser } from './actions'
|
||||
import UnderlineInput from './components/UnderlineInput'
|
||||
import { fetchData, createNewUrl, setEditing, editUser, createNewPublisher } from './actions'
|
||||
|
||||
import '../styles/index.scss'
|
||||
|
||||
class App extends React.Component {
|
||||
|
@ -22,6 +23,8 @@ class App extends React.Component {
|
|||
currentPassword: ''
|
||||
},
|
||||
urls: [],
|
||||
publishers: [],
|
||||
newPublisherUrl: '',
|
||||
editingUrl: null,
|
||||
working: false
|
||||
}
|
||||
|
@ -30,6 +33,8 @@ class App extends React.Component {
|
|||
this.getRegisteredUris = this.getRegisteredUris.bind(this)
|
||||
this.setUserValue = this.setUserValue.bind(this)
|
||||
this.saveUser = this.saveUser.bind(this)
|
||||
this.getRegisteredPublishers = this.getRegisteredPublishers.bind(this)
|
||||
this.setPublisherUrl = this.setPublisherUrl.bind(this)
|
||||
}
|
||||
dispatch (action) {
|
||||
if (!action) throw new Error('dispatch: missing action')
|
||||
|
@ -46,6 +51,11 @@ class App extends React.Component {
|
|||
componentDidMount () {
|
||||
this.dispatch(fetchData())
|
||||
}
|
||||
setPublisherUrl (e) {
|
||||
this.setState({
|
||||
newPublisherUrl: e.target.value
|
||||
})
|
||||
}
|
||||
getRegisteredUris () {
|
||||
return this.state.urls.map((item, i) => {
|
||||
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 () {
|
||||
return (
|
||||
<Router>
|
||||
|
@ -89,6 +107,7 @@ class App extends React.Component {
|
|||
<ul>
|
||||
<li><NavLink to='/targets'>Push URIs</NavLink></li>
|
||||
<li><NavLink to='/account'>My account</NavLink></li>
|
||||
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
|
||||
</ul>
|
||||
</aside>
|
||||
<section className={'content flex' + (this.state.working ? ' working' : '')}>
|
||||
|
@ -109,6 +128,27 @@ class App extends React.Component {
|
|||
</ul>
|
||||
</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 => (
|
||||
<div>
|
||||
|
|
|
@ -45,6 +45,16 @@ const reducer = (state = {}, action) => {
|
|||
return {
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,15 @@
|
|||
margin-top: 4px;
|
||||
color: $text-dark-2;
|
||||
text-shadow: 1px 1px 2px $black-4;
|
||||
|
||||
.creator {
|
||||
padding: 0 14px;
|
||||
line-height: 60px;
|
||||
max-width: 500px;
|
||||
|
||||
.btn {
|
||||
margin: 12px 0 12px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
box-shadow: $shadow-1;
|
||||
|
||||
header {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding: 0 14px;
|
||||
|
||||
|
|
|
@ -33,5 +33,14 @@ module.exports.policies = {
|
|||
|
||||
TargetController: {
|
||||
'*': [ 'sessionAuth' ]
|
||||
},
|
||||
|
||||
PublishKeyController: {
|
||||
'*': [ 'sessionAuth' ]
|
||||
},
|
||||
|
||||
BooksController: {
|
||||
'*': true,
|
||||
publish: [ 'keyAuth' ]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,9 +71,14 @@ module.exports.routes = {
|
|||
'GET /api/books': 'BooksController.list',
|
||||
|
||||
'POST /api/targets': 'TargetController.create',
|
||||
'GET /api/targets': 'TargetController.list',
|
||||
'PATCH /api/targets/:id': 'TargetController.edit',
|
||||
'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'
|
||||
|
||||
// ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
|
||||
// ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
"Book",
|
||||
"Passport",
|
||||
"TargetUrl",
|
||||
"PublishKey",
|
||||
"_"
|
||||
],
|
||||
"env": [
|
||||
|
|
1358
shrinkwrap.yaml
1358
shrinkwrap.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue