add page, model, controller, and policy for publish keys
parent
7a18f7bed8
commit
b2166960dd
|
@ -22,7 +22,7 @@ module.exports = {
|
||||||
if (bookExists) {
|
if (bookExists) {
|
||||||
throw new HttpError(400, 'Version already exists')
|
throw new HttpError(400, 'Version already exists')
|
||||||
} else {
|
} else {
|
||||||
result = await Book.create(body)
|
result = await Book.create(body).fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
||||||
|
|
|
@ -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.' })
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ const ACTIONS = {
|
||||||
delete_url: 'delete_url',
|
delete_url: 'delete_url',
|
||||||
list_url: 'list_url',
|
list_url: 'list_url',
|
||||||
set_editing: 'set_editing',
|
set_editing: 'set_editing',
|
||||||
|
add_publisher: 'add_publisher',
|
||||||
|
delete_publisher: 'delete_publisher',
|
||||||
error: 'error'
|
error: 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +31,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
|
||||||
|
@ -116,3 +123,43 @@ export const setUrl = (value) => 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,8 +3,8 @@
|
||||||
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/targets'
|
import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions'
|
||||||
|
|
||||||
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||||
const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
|
const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
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 { fetchUrls, createNewUrl, setEditing, createNewPublisher } from './actions'
|
||||||
|
import '../styles/index.scss'
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
this.state = {
|
||||||
|
error: '',
|
||||||
|
user: {
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
urls: [],
|
||||||
|
publishers: [],
|
||||||
|
newPublisherUrl: '',
|
||||||
|
editingUrl: null,
|
||||||
|
working: false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatch = this.dispatch.bind(this)
|
||||||
|
this.getRegisteredUris = this.getRegisteredUris.bind(this)
|
||||||
|
this.getRegisteredPublishers = this.getRegisteredPublishers.bind(this)
|
||||||
|
this.setPublisherUrl = this.setPublisherUrl.bind(this)
|
||||||
|
}
|
||||||
|
dispatch (action) {
|
||||||
|
if (!action) throw new Error('dispatch: missing action')
|
||||||
|
if (action instanceof Function) {
|
||||||
|
action(this.dispatch, () => this.state)
|
||||||
|
} else {
|
||||||
|
const changes = reducer(this.state, action)
|
||||||
|
if (!changes || !Object.keys(changes).length) return
|
||||||
|
this.setState({
|
||||||
|
...changes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentDidMount () {
|
||||||
|
// this.dispatch(fetchUrls())
|
||||||
|
}
|
||||||
|
setPublisherUrl (e) {
|
||||||
|
this.setState({
|
||||||
|
newPublisherUrl: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getRegisteredUris () {
|
||||||
|
return this.state.urls.map((item, i) => {
|
||||||
|
return (<UriListItem
|
||||||
|
key={i}
|
||||||
|
dispatch={this.dispatch}
|
||||||
|
item={item}
|
||||||
|
editing={this.state.editingUrl === item.id} />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getRegisteredPublishers () {
|
||||||
|
return this.state.publishers.map((item, i) => {
|
||||||
|
return (<PublisherListItem
|
||||||
|
key={i}
|
||||||
|
dispatch={this.dispatch}
|
||||||
|
item={item} />)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div className='root-container flex-container' onClick={() => this.dispatch(setEditing(null))}>
|
||||||
|
<aside className='nav nav-left'>
|
||||||
|
<header>
|
||||||
|
<h1>River of Ebooks</h1>
|
||||||
|
</header>
|
||||||
|
<ul>
|
||||||
|
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
|
||||||
|
<li><NavLink to='/targets'>Push URIs</NavLink></li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<section className={'content flex' + (this.state.working ? ' working' : '')}>
|
||||||
|
<Progress bound />
|
||||||
|
{this.state.error && <div className='error-box'>{this.state.error}</div>}
|
||||||
|
<Switch>
|
||||||
|
<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='/targets' exact children={props => (
|
||||||
|
<div>
|
||||||
|
<header className='flex-container'>
|
||||||
|
<div className='flex'>
|
||||||
|
<h1>Push URIs</h1>
|
||||||
|
<h2>Newly published books will be sent to these addresses.</h2>
|
||||||
|
</div>
|
||||||
|
<button className='btn' onClick={() => this.dispatch(createNewUrl())}>New address</button>
|
||||||
|
</header>
|
||||||
|
<ul className='list'>
|
||||||
|
{this.getRegisteredUris()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<Route path='/' render={() => <Redirect to='/keys' />} />
|
||||||
|
</Switch>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'))
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import Actions from '../actions/targets'
|
import Actions from '../actions'
|
||||||
|
|
||||||
const reducer = (state = {}, action) => {
|
const reducer = (state = {}, action) => {
|
||||||
const { type, data } = action
|
const { type, data } = action
|
||||||
|
@ -34,6 +34,16 @@ const reducer = (state = {}, action) => {
|
||||||
return {
|
return {
|
||||||
editingUrl: data
|
editingUrl: data
|
||||||
}
|
}
|
||||||
|
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: ''
|
||||||
|
}
|
||||||
case Actions.error:
|
case Actions.error:
|
||||||
return {
|
return {
|
||||||
error: data.message
|
error: data.message
|
|
@ -1,79 +0,0 @@
|
||||||
'use strict'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import Progress from './components/Progress'
|
|
||||||
import UriListItem from './containers/UriListItem'
|
|
||||||
import reducer from './reducers/targets'
|
|
||||||
import { fetchUrls, createNewUrl, setEditing } from './actions/targets'
|
|
||||||
import '../styles/targets.scss'
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this.state = {
|
|
||||||
error: '',
|
|
||||||
user: {
|
|
||||||
email: '',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
urls: [],
|
|
||||||
editingUrl: null,
|
|
||||||
working: false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatch = this.dispatch.bind(this)
|
|
||||||
this.getRegisteredUris = this.getRegisteredUris.bind(this)
|
|
||||||
}
|
|
||||||
dispatch (action) {
|
|
||||||
if (!action) throw new Error('dispatch: missing action')
|
|
||||||
if (action instanceof Function) {
|
|
||||||
action(this.dispatch, () => this.state)
|
|
||||||
} else {
|
|
||||||
const changes = reducer(this.state, action)
|
|
||||||
if (!changes || !Object.keys(changes).length) return
|
|
||||||
this.setState({
|
|
||||||
...changes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidMount () {
|
|
||||||
this.dispatch(fetchUrls())
|
|
||||||
}
|
|
||||||
getRegisteredUris () {
|
|
||||||
return this.state.urls.map((item, i) => {
|
|
||||||
return (<UriListItem
|
|
||||||
key={i}
|
|
||||||
dispatch={this.dispatch}
|
|
||||||
item={item}
|
|
||||||
editing={this.state.editingUrl === item.id} />)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='root-container flex-container' onClick={() => this.dispatch(setEditing(null))}>
|
|
||||||
<aside className='nav nav-left'>
|
|
||||||
<header>
|
|
||||||
<h1>RoE</h1>
|
|
||||||
</header>
|
|
||||||
</aside>
|
|
||||||
<section className={'content flex' + (this.state.working ? ' working' : '')}>
|
|
||||||
<Progress bound />
|
|
||||||
{this.state.error && <div className='error-box'>{this.state.error}</div>}
|
|
||||||
<header className='flex-container'>
|
|
||||||
<div className='flex'>
|
|
||||||
<h1>Push URIs</h1>
|
|
||||||
<h2>Newly published books will be sent to these addresses.</h2>
|
|
||||||
</div>
|
|
||||||
<button className='btn' onClick={() => this.dispatch(createNewUrl())}>New address</button>
|
|
||||||
</header>
|
|
||||||
<ul className='list'>
|
|
||||||
{this.getRegisteredUris()}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'))
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
@import 'lib/default';
|
||||||
|
@import 'shared/twopanels';
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 14px 0 42px 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
background: $red;
|
||||||
|
color: white;
|
||||||
|
padding: 0 14px;
|
||||||
|
margin: -14px 0 8px 0;
|
||||||
|
}
|
||||||
|
& > div {
|
||||||
|
|
||||||
|
& > header {
|
||||||
|
padding: 0 14px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-shadow: 1px 1px 2px $black-3;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 16px;
|
||||||
|
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 {
|
||||||
|
margin: 20px 14px;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
// overflow: hidden;
|
||||||
|
}
|
||||||
|
&.working {
|
||||||
|
& > .progress {
|
||||||
|
top: 0;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,12 @@ $black-3: rgba(0,0,0,.38);
|
||||||
$black-4: rgba(0,0,0,.12);
|
$black-4: rgba(0,0,0,.12);
|
||||||
$black-5: rgba(0,0,0,.07);
|
$black-5: rgba(0,0,0,.07);
|
||||||
|
|
||||||
|
$white-1: white;
|
||||||
|
$white-2: rgba(255,255,255,.75);
|
||||||
|
$white-3: rgba(255,255,255,.35);
|
||||||
|
$white-4: rgba(255,255,255,.10);
|
||||||
|
$white-5: rgba(255,255,255,.03);
|
||||||
|
|
||||||
$auth-width: 450px;
|
$auth-width: 450px;
|
||||||
|
|
||||||
$background-1: #f2f2f2;
|
$background-1: #f2f2f2;
|
||||||
|
@ -21,8 +27,8 @@ $background-2: white;
|
||||||
|
|
||||||
$text-dark-1: $black-1;
|
$text-dark-1: $black-1;
|
||||||
$text-dark-2: $black-2;
|
$text-dark-2: $black-2;
|
||||||
$text-light-1: white;
|
$text-light-1: $white-1;
|
||||||
$text-light-2: rgba(255,255,255,.75);
|
$text-light-2: $white-2;
|
||||||
|
|
||||||
$accent-1: #731212;
|
$accent-1: #731212;
|
||||||
$accent-2: #9a834d;
|
$accent-2: #9a834d;
|
||||||
|
|
|
@ -4,4 +4,41 @@
|
||||||
background: $accent-1;
|
background: $accent-1;
|
||||||
color: $text-light-1;
|
color: $text-light-1;
|
||||||
box-shadow: $shadow-1;
|
box-shadow: $shadow-1;
|
||||||
|
|
||||||
|
header {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
border-bottom: 1px solid $white-4;
|
||||||
|
|
||||||
|
&:hover a {
|
||||||
|
background: $white-5;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: $white-2;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $white-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
@import 'lib/default';
|
|
||||||
@import 'shared/twopanels';
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
header {
|
|
||||||
height: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
padding: 0 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: 14px 0 42px 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.error-box {
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
background: $red;
|
|
||||||
color: white;
|
|
||||||
padding: 0 14px;
|
|
||||||
margin: -14px 0 8px 0;
|
|
||||||
}
|
|
||||||
& > header {
|
|
||||||
padding: 0 14px;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-shadow: 1px 1px 2px $black-3;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: $text-dark-2;
|
|
||||||
text-shadow: 1px 1px 2px $black-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.list {
|
|
||||||
margin: 20px 14px;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
// overflow: hidden;
|
|
||||||
}
|
|
||||||
&.working {
|
|
||||||
& > .progress {
|
|
||||||
top: 0;
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>RoE - Push Targets</title>
|
<title>River of Ebooks</title>
|
||||||
<% for (item of htmlWebpackPlugin.options.links) {
|
<% for (item of htmlWebpackPlugin.options.links) {
|
||||||
if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %>
|
if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %>
|
||||||
<link<% for (key in item) { %> <%= key %>="<%= item[key] %>"<% } %> /><%
|
<link<% for (key in item) { %> <%= key %>="<%= item[key] %>"<% } %> /><%
|
|
@ -33,5 +33,14 @@ module.exports.policies = {
|
||||||
|
|
||||||
TargetController: {
|
TargetController: {
|
||||||
'*': [ 'sessionAuth' ]
|
'*': [ 'sessionAuth' ]
|
||||||
|
},
|
||||||
|
|
||||||
|
PublishKeyController: {
|
||||||
|
'*': [ 'sessionAuth' ]
|
||||||
|
},
|
||||||
|
|
||||||
|
BooksController: {
|
||||||
|
'*': true,
|
||||||
|
publish: [ 'keyAuth' ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,9 +69,14 @@ module.exports.routes = {
|
||||||
'GET /api/me': 'UserController.me',
|
'GET /api/me': 'UserController.me',
|
||||||
|
|
||||||
'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'
|
||||||
|
|
||||||
// ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
|
// ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗
|
||||||
// ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗
|
// ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗
|
||||||
|
|
40
package.json
40
package.json
|
@ -4,6 +4,25 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "a Sails application",
|
"description": "a Sails application",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
"scripts": {
|
||||||
|
"start": "npm-run-all --parallel open:client lift",
|
||||||
|
"start:debug": "npm-run-all --parallel open:client debug",
|
||||||
|
"start:prod": "npm-run-all --parallel build:prod lift",
|
||||||
|
"open:client": "webpack-dev-server --mode development",
|
||||||
|
"build": "npm run build:prod",
|
||||||
|
"build:dev": "webpack --mode development",
|
||||||
|
"build:prod": "webpack --mode production",
|
||||||
|
"clean": "rimraf .tmp && mkdirp .tmp/public",
|
||||||
|
"lift": "sails lift",
|
||||||
|
"forever": "sudo ./node_modules/.bin/pm2 start ecosystem.config.js --env production",
|
||||||
|
"stop": "sudo ./node_modules/.bin/pm2 delete roe-base",
|
||||||
|
"test": "npm run lint && npm run custom-tests && echo 'Done.'",
|
||||||
|
"lint": "standard && echo '✔ Your .js files look good.'",
|
||||||
|
"debug": "node --inspect app.js",
|
||||||
|
"custom-tests": "echo 'Nothing yet'",
|
||||||
|
"db:migrate": "knex migrate:latest",
|
||||||
|
"db:rollback": "knex migrate:rollback"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sailshq/connect-redis": "^3.2.1",
|
"@sailshq/connect-redis": "^3.2.1",
|
||||||
"@sailshq/lodash": "^3.10.3",
|
"@sailshq/lodash": "^3.10.3",
|
||||||
|
@ -42,6 +61,7 @@
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"node-sass": "^4.9.4",
|
"node-sass": "^4.9.4",
|
||||||
"npm-run-all": "^4.1.3",
|
"npm-run-all": "^4.1.3",
|
||||||
|
"react-router-dom": "^4.3.1",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"standard": "^12.0.1",
|
"standard": "^12.0.1",
|
||||||
|
@ -50,25 +70,6 @@
|
||||||
"webpack-cli": "^3.1.2",
|
"webpack-cli": "^3.1.2",
|
||||||
"webpack-dev-server": "^3.1.10"
|
"webpack-dev-server": "^3.1.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"start": "npm-run-all --parallel open:client lift",
|
|
||||||
"start:debug": "npm-run-all --parallel open:client debug",
|
|
||||||
"start:prod": "npm-run-all --parallel build:prod lift",
|
|
||||||
"open:client": "webpack-dev-server --mode development",
|
|
||||||
"build": "npm run build:prod",
|
|
||||||
"build:dev": "webpack --mode development",
|
|
||||||
"build:prod": "webpack --mode production",
|
|
||||||
"clean": "rimraf .tmp && mkdirp .tmp/public",
|
|
||||||
"lift": "sails lift",
|
|
||||||
"forever": "sudo ./node_modules/.bin/pm2 start ecosystem.config.js --env production",
|
|
||||||
"stop": "sudo ./node_modules/.bin/pm2 delete roe-base",
|
|
||||||
"test": "npm run lint && npm run custom-tests && echo 'Done.'",
|
|
||||||
"lint": "standard && echo '✔ Your .js files look good.'",
|
|
||||||
"debug": "node --inspect app.js",
|
|
||||||
"custom-tests": "echo 'Nothing yet'",
|
|
||||||
"db:migrate": "knex migrate:latest",
|
|
||||||
"db:rollback": "knex migrate:rollback"
|
|
||||||
},
|
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -86,6 +87,7 @@
|
||||||
"Book",
|
"Book",
|
||||||
"Passport",
|
"Passport",
|
||||||
"TargetUrl",
|
"TargetUrl",
|
||||||
|
"PublishKey",
|
||||||
"_"
|
"_"
|
||||||
],
|
],
|
||||||
"env": [
|
"env": [
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,7 +10,7 @@ module.exports = (env, argv) => {
|
||||||
mode: mode || 'development',
|
mode: mode || 'development',
|
||||||
entry: {
|
entry: {
|
||||||
login: './assets/js/login.js',
|
login: './assets/js/login.js',
|
||||||
targets: './assets/js/targets.js'
|
index: './assets/js/index.js'
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, '/.tmp/public'),
|
path: path.join(__dirname, '/.tmp/public'),
|
||||||
|
@ -41,10 +41,10 @@ module.exports = (env, argv) => {
|
||||||
chunks: ['login']
|
chunks: ['login']
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: 'assets/templates/targets.html',
|
template: 'assets/templates/index.html',
|
||||||
links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: 'targets.css' }] : [],
|
links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: 'index.css' }] : [],
|
||||||
filename: path.join(__dirname, '/.tmp/public/targets.html'),
|
filename: path.join(__dirname, '/.tmp/public/index.html'),
|
||||||
chunks: ['targets']
|
chunks: ['index']
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: '[name].css'
|
filename: '[name].css'
|
||||||
|
@ -52,6 +52,11 @@ module.exports = (env, argv) => {
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.NODE_ENV': JSON.stringify(mode)
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
})
|
})
|
||||||
]
|
],
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
disableHostCheck: true,
|
||||||
|
port: 8080
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue