commit
bda175b802
|
@ -115,6 +115,8 @@ module.exports = {
|
|||
}
|
||||
|
||||
passportHelper.callback(req, res, function (err, user, info, status) {
|
||||
// console.log(err)
|
||||
// console.log(user)
|
||||
if (err || !user) {
|
||||
sails.log.warn(user, err, info, status)
|
||||
if (!err && info) {
|
||||
|
@ -126,6 +128,7 @@ module.exports = {
|
|||
req.login(user, function (err) {
|
||||
if (err) {
|
||||
sails.log.warn(err)
|
||||
// console.log(err)
|
||||
return negotiateError(err)
|
||||
}
|
||||
|
||||
|
@ -134,6 +137,8 @@ module.exports = {
|
|||
// redirect if there is a 'next' param
|
||||
if (req.query.next) {
|
||||
res.status(302).set('Location', req.query.next)
|
||||
} else if (req.query.code) { // if came from oauth callback
|
||||
res.status(302).set('Location', '/targets')
|
||||
}
|
||||
|
||||
sails.log.info('user', user, 'authenticated successfully')
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
const HttpError = require('../errors/HttpError')
|
||||
const request = require('request')
|
||||
const uriRegex = /^(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||
|
||||
module.exports = {
|
||||
publish: async function (req, res) {
|
||||
|
@ -22,20 +24,31 @@ module.exports = {
|
|||
if (bookExists) {
|
||||
throw new HttpError(400, 'Version already exists')
|
||||
} else {
|
||||
result = await Book.create(body).fetch()
|
||||
const { title, isbn, author, publisher } = body
|
||||
// require at least 2 fields to be filled out
|
||||
if ([title, isbn, author, publisher].reduce((a, x) => a + (x ? 1 : 0), 0) >= 2) {
|
||||
result = await Book.create(body).fetch()
|
||||
} else {
|
||||
throw new HttpError(400, 'Please fill out at least 2 fields (title, author, publisher, isbn)')
|
||||
}
|
||||
}
|
||||
|
||||
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
||||
if (err) {
|
||||
await Book.destroy({ id: result.id })
|
||||
throw new HttpError(500, err.message)
|
||||
}
|
||||
await Book.update({ id: result.id }, { storage: uploaded[0].fd })
|
||||
sendUpdatesAsync(result.id)
|
||||
return res.json({
|
||||
...result
|
||||
if (req.file('opds')) {
|
||||
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
||||
if (err) {
|
||||
await Book.destroy({ id: result.id })
|
||||
throw new HttpError(500, err.message)
|
||||
}
|
||||
const fd = (uploaded[0] || {}).fd
|
||||
await Book.update({ id: result.id }, { storage: fd })
|
||||
sendUpdatesAsync(result.id)
|
||||
return res.json({
|
||||
...result
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
throw new HttpError(400, 'Missing OPDS file upload')
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof HttpError) return e.send(res)
|
||||
return res.status(500).json({
|
||||
|
@ -47,9 +60,13 @@ module.exports = {
|
|||
list: async function (req, res) {
|
||||
try {
|
||||
const body = req.allParams()
|
||||
if (!body) throw new HttpError(400, 'Missing parameters')
|
||||
|
||||
const books = await Book.find(body)
|
||||
let page = 1
|
||||
const perPage = 200
|
||||
if (body.page) {
|
||||
page = Math.abs(+body.page) || 1
|
||||
delete body.page
|
||||
}
|
||||
const books = await Book.find(body || {}).skip((page * perPage) - perPage).limit(perPage)
|
||||
|
||||
if (!books.length) {
|
||||
throw new HttpError(404, 'No books matching those parameters were found.')
|
||||
|
@ -65,9 +82,29 @@ module.exports = {
|
|||
}
|
||||
|
||||
async function sendUpdatesAsync (id) {
|
||||
const book = await Book.find({ id })
|
||||
const book = await Book.findOne({ id })
|
||||
const targets = await TargetUrl.find()
|
||||
if (!book) return
|
||||
for (const i in targets) {
|
||||
sails.log('sending ' + book.id + ' info to ' + targets[i].url)
|
||||
const item = targets[i]
|
||||
const { author: fAuthor, publisher: fPublisher, title: fTitle, isbn: fIsbn, url } = item
|
||||
const { author: bAuthor, publisher: bPublisher, title: bTitle, isbn: bIsbn } = book
|
||||
sails.log('sending ' + book.id + ' info to ' + url)
|
||||
|
||||
if (uriRegex.test(url)) {
|
||||
if (fAuthor && !((bAuthor || '').includes(fAuthor))) continue
|
||||
if (fPublisher && !((bPublisher || '').includes(fPublisher))) continue
|
||||
if (fTitle && !((bTitle || '').includes(fTitle))) continue
|
||||
if (fIsbn && !((bIsbn || '').includes(fIsbn))) continue
|
||||
request.post({
|
||||
url: url,
|
||||
headers: { 'User-Agent': 'RoE-aggregator' },
|
||||
form: book
|
||||
}, function (err, httpResp, body) {
|
||||
if (err) {
|
||||
sails.log(`error: failed to send book ${id} to ${url}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const HttpError = require('../errors/HttpError')
|
|||
|
||||
module.exports = {
|
||||
show: function (req, res) {
|
||||
res.view('pages/targets', {
|
||||
res.view('pages/app', {
|
||||
email: req.user.email
|
||||
})
|
||||
},
|
||||
|
@ -21,10 +21,10 @@ module.exports = {
|
|||
try {
|
||||
const id = req.param('id')
|
||||
const value = req.param('url')
|
||||
const author = req.param('author')
|
||||
const publisher = req.param('publisher')
|
||||
const title = req.param('title')
|
||||
const isbn = req.param('isbn')
|
||||
const author = req.param('author') || ''
|
||||
const publisher = req.param('publisher') || ''
|
||||
const title = req.param('title') || ''
|
||||
const isbn = req.param('isbn') || ''
|
||||
if (value.length) {
|
||||
const url = await TargetUrl.update({ id, user: req.user.id }, {
|
||||
url: value,
|
||||
|
|
|
@ -17,12 +17,11 @@ module.exports = {
|
|||
error: err.toString()
|
||||
})
|
||||
}
|
||||
|
||||
res.json(user)
|
||||
})
|
||||
},
|
||||
|
||||
update: async function (req, res, next) {
|
||||
edit: async function (req, res, next) {
|
||||
const passportHelper = await sails.helpers.passport()
|
||||
passportHelper.protocols.local.update(req.body, function (err, user) {
|
||||
if (err) {
|
||||
|
@ -30,7 +29,6 @@ module.exports = {
|
|||
error: err.toString()
|
||||
})
|
||||
}
|
||||
|
||||
res.json(user)
|
||||
})
|
||||
},
|
||||
|
|
|
@ -48,8 +48,8 @@ function PassportHelper () {
|
|||
const protocol = strategies[key].protocol
|
||||
const callbackURL = strategies[key].callback
|
||||
let baseURL = ''
|
||||
if (sails.config.appUrl && sails.config.appUrl !== null) {
|
||||
baseURL = sails.config.appUrl
|
||||
if (sails.config.custom.baseURL && sails.config.custom.baseURL !== null) {
|
||||
baseURL = sails.config.custom.baseURL
|
||||
} else {
|
||||
sails.log.warn('Please add \'appUrl\' to configuration')
|
||||
baseURL = sails.getBaseurl()
|
||||
|
@ -74,7 +74,12 @@ function PassportHelper () {
|
|||
|
||||
if (!_.has(strategies, provider)) return res.redirect('/login')
|
||||
|
||||
passport.authenticate(provider, {})(req, res, req.next)
|
||||
const scopes = {
|
||||
google: ['email'],
|
||||
github: ['user:email']
|
||||
}
|
||||
|
||||
passport.authenticate(provider, { scope: scopes[provider] })(req, res, req.next)
|
||||
}
|
||||
// a callback helper to split by req
|
||||
this.callback = function (req, res, next) {
|
||||
|
@ -112,33 +117,39 @@ function PassportHelper () {
|
|||
|
||||
// if the profile object from passport has an email, use it
|
||||
if (profile.emails && profile.emails[0]) userAttrs.email = profile.emails[0].value
|
||||
if (!userAttrs.email) return next(new Error('No email available'))
|
||||
// if (!userAttrs.email) return next(new Error('No email available'))
|
||||
|
||||
const pass = await Passport.findOne({
|
||||
provider,
|
||||
identifier: q.identifier.toString()
|
||||
identifier: q.identifier
|
||||
})
|
||||
|
||||
let user
|
||||
|
||||
if (!req.user) {
|
||||
if (!pass) { // new user signing up, create a new user
|
||||
user = await User.create(userAttrs).fetch()
|
||||
if (!pass) { // new user signing up, create a new user and/or passport
|
||||
if (userAttrs.email) {
|
||||
user = await User.findOne({ email: userAttrs.email })
|
||||
}
|
||||
if (!user) {
|
||||
user = await User.create(userAttrs).fetch()
|
||||
}
|
||||
await Passport.create({
|
||||
...q,
|
||||
user: user.id
|
||||
})
|
||||
next(null, user)
|
||||
} else { // existing user logging in
|
||||
if (_.has(q, 'tokens') && q.tokens !== passport.tokens) {
|
||||
passport.tokens = q.tokens
|
||||
if (_.has(q, 'tokens') && q.tokens !== pass.tokens) {
|
||||
pass.tokens = q.tokens
|
||||
}
|
||||
await passport.save()
|
||||
user = User.findOne(passport.user)
|
||||
next(null, user)
|
||||
delete pass.id
|
||||
await Passport.update({ id: pass.id }, { tokens: pass.tokens })
|
||||
user = await User.find({ id: passport.user }).limit(1)
|
||||
next(null, user[0])
|
||||
}
|
||||
} else { // user logged in and trying to add new Passport
|
||||
if (!passport) {
|
||||
if (!pass) {
|
||||
await Passport.create({
|
||||
...q,
|
||||
user: req.user.id
|
||||
|
|
|
@ -15,13 +15,14 @@ module.exports = {
|
|||
id: {
|
||||
type: 'number',
|
||||
unique: true,
|
||||
autoIncrement: true,
|
||||
columnName: '_id'
|
||||
autoIncrement: true
|
||||
},
|
||||
title: { type: 'string', required: true },
|
||||
author: { type: 'string' },
|
||||
publisher: { type: 'string' },
|
||||
isbn: { type: 'string' },
|
||||
version: { type: 'string' }
|
||||
version: { type: 'string' },
|
||||
hostname: { type: 'string' }
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
|
|
@ -28,8 +28,7 @@ module.exports = {
|
|||
id: {
|
||||
type: 'number',
|
||||
unique: true,
|
||||
autoIncrement: true,
|
||||
columnName: '_id'
|
||||
autoIncrement: true
|
||||
},
|
||||
// local, oauth2, etc
|
||||
protocol: {
|
||||
|
|
|
@ -14,13 +14,10 @@ module.exports = {
|
|||
id: {
|
||||
type: 'number',
|
||||
unique: true,
|
||||
autoIncrement: true,
|
||||
columnName: '_id'
|
||||
autoIncrement: true
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
unique: true,
|
||||
required: true
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
|
|
|
@ -9,9 +9,10 @@ const ACTIONS = {
|
|||
delete_url: 'delete_url',
|
||||
list_url: 'list_url',
|
||||
set_editing: 'set_editing',
|
||||
error: 'error',
|
||||
set_user: 'set_user',
|
||||
add_publisher: 'add_publisher',
|
||||
delete_publisher: 'delete_publisher',
|
||||
error: 'error'
|
||||
delete_publisher: 'delete_publisher'
|
||||
}
|
||||
|
||||
export default ACTIONS
|
||||
|
@ -26,6 +27,11 @@ export const setUrls = (urls) => ({
|
|||
data: urls
|
||||
})
|
||||
|
||||
export const setUser = user => ({
|
||||
type: ACTIONS.set_user,
|
||||
data: user
|
||||
})
|
||||
|
||||
export const addUrl = url => ({
|
||||
type: ACTIONS.add_url,
|
||||
data: url
|
||||
|
@ -70,13 +76,17 @@ export const removeUrl = id => async (dispatch, getState) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const fetchUrls = () => async (dispatch, getState) => {
|
||||
export const fetchData = () => async (dispatch, getState) => {
|
||||
dispatch(setWorking(true))
|
||||
try {
|
||||
const { data } = await Ajax.get({
|
||||
const { data: user } = await Ajax.get({
|
||||
url: '/api/me'
|
||||
})
|
||||
dispatch(setUser(user))
|
||||
const { data: urls } = await Ajax.get({
|
||||
url: '/api/targets'
|
||||
})
|
||||
dispatch(setUrls(data))
|
||||
dispatch(setUrls(urls))
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: ACTIONS.error,
|
||||
|
@ -124,6 +134,34 @@ export const setUrl = (value) => async (dispatch, getState) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const editUser = (user) => async (dispatch, getState) => {
|
||||
dispatch(setWorking(true))
|
||||
|
||||
try {
|
||||
// if (!user.currentPassword) throw new Error('Please enter your current password.')
|
||||
await Ajax.patch({
|
||||
url: '/api/me',
|
||||
data: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
currentPassword: user.currentPassword
|
||||
}
|
||||
})
|
||||
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 {
|
||||
|
|
|
@ -47,7 +47,7 @@ export const clearError = () => ({
|
|||
|
||||
export const setLoggedIn = (data) => (dispatch, getState) => {
|
||||
window.localStorage.setItem('roe-token', JSON.stringify(data))
|
||||
window.location.href = '/app'
|
||||
window.location.href = '/targets'
|
||||
}
|
||||
|
||||
export const checkEmail = email => async (dispatch, getState) => {
|
||||
|
@ -94,7 +94,7 @@ export const checkPassword = (email, password) => async (dispatch, getState) =>
|
|||
} catch (e) {
|
||||
dispatch(setError({
|
||||
type: 'password',
|
||||
error: e.toString()
|
||||
error: e.message
|
||||
}))
|
||||
dispatch(setWorking(false))
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ export const signup = (email, password) => async (dispatch, getState) => {
|
|||
} catch (e) {
|
||||
dispatch(setError({
|
||||
type: 'email',
|
||||
error: e.toString()
|
||||
error: e.message
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -45,9 +45,11 @@ const CarouselItem = props => (
|
|||
{props.button}
|
||||
</button>
|
||||
</div>
|
||||
{props.footer &&
|
||||
{props.footers && props.footers.length &&
|
||||
<footer className='footer-row'>
|
||||
<a href='#'>{props.footer}</a>
|
||||
{
|
||||
props.footers.map((x, i) => <a key={i} href={props.footerHrefs[i] || '#'}>{x}</a>)
|
||||
}
|
||||
</footer>}
|
||||
</form>
|
||||
)
|
||||
|
|
|
@ -7,7 +7,8 @@ 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 { fetchData, createNewUrl, setEditing, editUser, createNewPublisher } from './actions'
|
||||
|
||||
import '../styles/index.scss'
|
||||
|
||||
class App extends React.Component {
|
||||
|
@ -16,8 +17,10 @@ class App extends React.Component {
|
|||
this.state = {
|
||||
error: '',
|
||||
user: {
|
||||
id: '',
|
||||
email: '',
|
||||
password: ''
|
||||
password: '',
|
||||
currentPassword: ''
|
||||
},
|
||||
urls: [],
|
||||
publishers: [],
|
||||
|
@ -28,6 +31,8 @@ class App extends React.Component {
|
|||
|
||||
this.dispatch = this.dispatch.bind(this)
|
||||
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)
|
||||
}
|
||||
|
@ -44,7 +49,7 @@ class App extends React.Component {
|
|||
}
|
||||
}
|
||||
componentDidMount () {
|
||||
// this.dispatch(fetchUrls())
|
||||
this.dispatch(fetchData())
|
||||
}
|
||||
setPublisherUrl (e) {
|
||||
this.setState({
|
||||
|
@ -60,6 +65,25 @@ class App extends React.Component {
|
|||
editing={this.state.editingUrl === item.id} />)
|
||||
})
|
||||
}
|
||||
setUserValue (which, e) {
|
||||
this.setState({
|
||||
user: {
|
||||
...this.state.user,
|
||||
[which]: e.target.value
|
||||
}
|
||||
})
|
||||
}
|
||||
saveUser () {
|
||||
this.dispatch(editUser(this.state.user))
|
||||
this.setState({
|
||||
user: {
|
||||
...this.state.user,
|
||||
email: this.state.user.email,
|
||||
password: '',
|
||||
currentPassword: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
getRegisteredPublishers () {
|
||||
return this.state.publishers.map((item, i) => {
|
||||
return (<PublisherListItem
|
||||
|
@ -75,16 +99,35 @@ class App extends React.Component {
|
|||
<aside className='nav nav-left'>
|
||||
<header>
|
||||
<h1>River of Ebooks</h1>
|
||||
<h2 className='flex-container'>
|
||||
<span className='flex'>{this.state.user.email}</span>
|
||||
<a href='/logout'>Log out</a>
|
||||
</h2>
|
||||
</header>
|
||||
<ul>
|
||||
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
|
||||
<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' : '')}>
|
||||
<Progress bound />
|
||||
{this.state.error && <div className='error-box'>{this.state.error}</div>}
|
||||
<Switch>
|
||||
<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='/keys' exact children={props => (
|
||||
<div>
|
||||
<header className='flex-container'>
|
||||
|
@ -107,22 +150,38 @@ class App extends React.Component {
|
|||
</div>
|
||||
)} />
|
||||
|
||||
<Route path='/targets' exact children={props => (
|
||||
<Route path='/account' 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>
|
||||
<h1>My account</h1>
|
||||
<h2>User account settings</h2>
|
||||
</div>
|
||||
<button className='btn' onClick={() => this.dispatch(createNewUrl())}>New address</button>
|
||||
</header>
|
||||
<ul className='list'>
|
||||
{this.getRegisteredUris()}
|
||||
</ul>
|
||||
<section className='inputs'>
|
||||
<UnderlineInput
|
||||
placeholder='Email address'
|
||||
value={this.state.user.email}
|
||||
pattern={/^([a-zA-Z0-9_\-.]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,5})$/}
|
||||
onChange={(e) => this.setUserValue('email', e)} />
|
||||
<UnderlineInput
|
||||
placeholder='Password'
|
||||
type='password'
|
||||
value={this.state.user.password}
|
||||
onChange={(e) => this.setUserValue('password', e)} />
|
||||
<UnderlineInput
|
||||
placeholder='Current password'
|
||||
type='password'
|
||||
value={this.state.user.currentPassword}
|
||||
onChange={(e) => this.setUserValue('currentPassword', e)} />
|
||||
<div className='buttons'>
|
||||
<button className='btn' onClick={this.saveUser}>Save</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)} />
|
||||
|
||||
<Route path='/' render={() => <Redirect to='/keys' />} />
|
||||
<Route path='/' render={() => <Redirect to='/targets' />} />
|
||||
</Switch>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -86,28 +86,29 @@ export default class Ajax {
|
|||
}
|
||||
xhr.onerror = () => {
|
||||
var data = xhr.response
|
||||
try { data = JSON.parse(data) } catch (e) {}
|
||||
|
||||
// method not allowed
|
||||
if (xhr.status === 405) {
|
||||
reject(new AjaxError('405 Method Not Allowed', data, xhr))
|
||||
reject(new AjaxError('405 Method Not Allowed', data.error || data, xhr))
|
||||
return
|
||||
} else if (xhr.status === 404) {
|
||||
reject(new AjaxError('404 Not Found', data, xhr))
|
||||
reject(new AjaxError('404 Not Found', data.error || data, xhr))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// if the access token is invalid, try to use the refresh token
|
||||
var json = JSON.parse(data)
|
||||
var json = data
|
||||
if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('invalid') && ajaxcfg.refresh_token) {
|
||||
return Ajax.refresh(opts)
|
||||
} else if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('revoked')) {
|
||||
reject(new AjaxError('token revoked', data, xhr))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new AjaxError(e.toString(), data, xhr))
|
||||
reject(new AjaxError(e.toString(), data.error || data, xhr))
|
||||
} finally {
|
||||
reject(new AjaxError(data, xhr.status, xhr))
|
||||
reject(new AjaxError(data.error || data, xhr.status, xhr))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,8 @@ class App extends React.Component {
|
|||
onButtonClick={() => this.dispatch(signup(this.state.user.email, this.state.user.password))}
|
||||
smallButton='Have an account?'
|
||||
onSmallButtonClick={() => this.dispatch(setCarousel(1))}
|
||||
footer='Sign up with your Google account' />
|
||||
footers={['Sign up with Google', 'Sign up with Github']}
|
||||
footerHrefs={['/auth/google', '/auth/github']} />
|
||||
|
||||
<CarouselItem
|
||||
header='Sign in'
|
||||
|
@ -97,7 +98,8 @@ class App extends React.Component {
|
|||
onButtonClick={() => this.dispatch(checkEmail(this.state.user.email))}
|
||||
smallButton='Create account'
|
||||
onSmallButtonClick={() => this.dispatch(setCarousel(0))}
|
||||
footer='Sign in with your Google account' />
|
||||
footers={['Sign in with Google', 'Sign in with Github']}
|
||||
footerHrefs={['/auth/google', '/auth/github']} />
|
||||
|
||||
<CarouselItem
|
||||
header='Welcome'
|
||||
|
@ -106,8 +108,8 @@ class App extends React.Component {
|
|||
error={this.state.passwordError}
|
||||
button='Sign in'
|
||||
onButtonClick={() => this.dispatch(checkPassword(this.state.user.email, this.state.user.password))}
|
||||
smallButton='Forgot password?'
|
||||
onSmallButtonClick={() => this.dispatch(setCarousel(3))} />
|
||||
comment={null/*smallButton='Forgot password?'
|
||||
onSmallButtonClick={() => this.dispatch(setCarousel(3))}*/} />
|
||||
|
||||
<CarouselItem
|
||||
header='Password recovery'
|
||||
|
|
|
@ -10,6 +10,13 @@ const reducer = (state = {}, action) => {
|
|||
return {
|
||||
working: data
|
||||
}
|
||||
case Actions.set_user:
|
||||
return {
|
||||
user: {
|
||||
...state.user,
|
||||
...data
|
||||
}
|
||||
}
|
||||
case Actions.list_url:
|
||||
return {
|
||||
urls: data || []
|
||||
|
@ -34,6 +41,10 @@ const reducer = (state = {}, action) => {
|
|||
return {
|
||||
editingUrl: data
|
||||
}
|
||||
case Actions.error:
|
||||
return {
|
||||
error: (data || {}).message || ''
|
||||
}
|
||||
case Actions.add_publisher:
|
||||
return {
|
||||
publishers: state.publishers.concat(data),
|
||||
|
@ -44,10 +55,6 @@ const reducer = (state = {}, action) => {
|
|||
publishers: state.publishers.filter(x => x.id !== data),
|
||||
error: ''
|
||||
}
|
||||
case Actions.error:
|
||||
return {
|
||||
error: data.message
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,23 +14,21 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
@ -47,6 +45,14 @@
|
|||
list-style: none;
|
||||
// overflow: hidden;
|
||||
}
|
||||
.inputs {
|
||||
padding: 20px 14px;
|
||||
|
||||
.buttons {
|
||||
margin-top: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
&.working {
|
||||
& > .progress {
|
||||
top: 0;
|
||||
|
|
|
@ -30,8 +30,8 @@ $text-dark-2: $black-2;
|
|||
$text-light-1: $white-1;
|
||||
$text-light-2: $white-2;
|
||||
|
||||
$accent-1: #731212;
|
||||
$accent-2: #9a834d;
|
||||
$accent-3: #D4DBF1;
|
||||
$accent-1: #102237;
|
||||
$accent-2: #18517c;
|
||||
$accent-3: #4f91b8;
|
||||
|
||||
$red: #FE4C52;
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
bottom: 20px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
line-height: 30px;
|
||||
color: $text-dark-2;
|
||||
|
||||
.btn {
|
||||
|
@ -91,6 +92,8 @@
|
|||
a {
|
||||
color: $accent-2;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.button-row {
|
||||
|
|
|
@ -9,6 +9,20 @@
|
|||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding: 0 14px;
|
||||
|
||||
h2 {
|
||||
margin: -10px 0 0 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
font-size: 12pt;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
color: $white-2;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $accent-3;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
|
|
|
@ -347,7 +347,7 @@ module.exports = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
custom: {
|
||||
baseUrl: 'https://example.com',
|
||||
baseURL: 'http://localhost:3000',
|
||||
internalEmailAddress: 'support@example.com'
|
||||
|
||||
// mailgunDomain: 'mg.example.com',
|
||||
|
|
|
@ -348,7 +348,7 @@ module.exports = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
custom: {
|
||||
baseUrl: 'https://example.com',
|
||||
baseURL: 'http://ec2-18-219-223-27.us-east-2.compute.amazonaws.com',
|
||||
internalEmailAddress: 'support@example.com'
|
||||
|
||||
// mailgunDomain: 'mg.example.com',
|
||||
|
|
|
@ -14,7 +14,15 @@ const rateLimiter = rateLimit({
|
|||
windowMs: 10 * 60 * 1000, // 10 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
skip (req, res) {
|
||||
return !req.path.startsWith('/api')
|
||||
return !req.path.startsWith('/api') || req.path.startsWith('/api/publish')
|
||||
}
|
||||
})
|
||||
|
||||
const publishLimiter = rateLimit({
|
||||
windowMs: 1000 * 60 * 60 * 24, // 24 hours
|
||||
max: 1000, // 1000 publish requests per day
|
||||
skip (req, res) {
|
||||
return !req.path.startsWith('/api/publish')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -40,6 +48,7 @@ module.exports.http = {
|
|||
|
||||
order: [
|
||||
'rateLimit',
|
||||
'publishLimit',
|
||||
'cookieParser',
|
||||
'session',
|
||||
'passportInit',
|
||||
|
@ -52,6 +61,7 @@ module.exports.http = {
|
|||
'favicon'
|
||||
],
|
||||
rateLimit: rateLimiter,
|
||||
publishLimit: publishLimiter,
|
||||
passportInit: require('passport').initialize(),
|
||||
passportSession: require('passport').session()
|
||||
|
||||
|
|
|
@ -5,5 +5,17 @@
|
|||
module.exports.passport = {
|
||||
local: {
|
||||
strategy: require('passport-local').Strategy
|
||||
},
|
||||
google: {
|
||||
strategy: require('passport-google-oauth20').Strategy,
|
||||
protocol: 'oauth2',
|
||||
callback: '/auth/google/callback',
|
||||
options: {}
|
||||
},
|
||||
github: {
|
||||
strategy: require('passport-github2').Strategy,
|
||||
protocol: 'oauth2',
|
||||
callback: '/auth/github/callback',
|
||||
options: {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,52 @@ module.exports.protocols = {
|
|||
}
|
||||
},
|
||||
update: async function (user, next) {
|
||||
throw new Error('not implemented')
|
||||
try {
|
||||
const dbUser = await User.findOne({
|
||||
id: user.id
|
||||
})
|
||||
if (!dbUser) throw new Error('An account with that id was not found.')
|
||||
|
||||
const passport = await Passport.findOne({
|
||||
protocol: 'local',
|
||||
user: user.id
|
||||
})
|
||||
if (!user.currentPassword && passport) throw new Error('Please enter your current password.')
|
||||
if (passport) {
|
||||
const res = await Passport.validatePassword(user.currentPassword, passport)
|
||||
if (!res) throw new Error('incorrect password')
|
||||
|
||||
const otherUser = await User.findOne({ email: user.email })
|
||||
if (otherUser && otherUser.id !== dbUser.id) throw new Error('There is already an account with that email.')
|
||||
await User.update({ id: user.id }, {
|
||||
email: user.email
|
||||
})
|
||||
if (user.password && user.password.length) {
|
||||
await Passport.update({ id: passport.id }, {
|
||||
password: user.password
|
||||
})
|
||||
}
|
||||
} else { // no password yet, add one
|
||||
const otherUser = await User.findOne({ email: user.email })
|
||||
if (otherUser && otherUser.id !== dbUser.id) throw new Error('There is already an account with that email.')
|
||||
await User.update({ id: user.id }, {
|
||||
email: user.email
|
||||
})
|
||||
if (user.password && user.password.length) {
|
||||
const token = generateToken()
|
||||
await Passport.create({
|
||||
protocol: 'local',
|
||||
password: user.password,
|
||||
user: dbUser.id,
|
||||
accesstoken: token
|
||||
})
|
||||
}
|
||||
}
|
||||
delete dbUser.password
|
||||
next(null, dbUser)
|
||||
} catch (e) {
|
||||
return next(e)
|
||||
}
|
||||
},
|
||||
connect: async function (req, res, next) {
|
||||
try {
|
||||
|
@ -90,6 +135,23 @@ module.exports.protocols = {
|
|||
return next(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
oauth2: {
|
||||
login: async function (req, accessToken, refreshToken, profile, next) {
|
||||
try {
|
||||
const passportHelper = await sails.helpers.passport()
|
||||
await passportHelper.connect(req, {
|
||||
tokens: {
|
||||
accessToken,
|
||||
refreshToken
|
||||
},
|
||||
identifier: profile.id,
|
||||
protocol: 'oauth2'
|
||||
}, profile, next)
|
||||
} catch (e) {
|
||||
return next(e, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,9 @@ module.exports.routes = {
|
|||
'GET /register': {
|
||||
view: 'pages/login'
|
||||
},
|
||||
'GET /app': 'TargetController.show',
|
||||
// figure out why proper clientside routing breaks the backend session
|
||||
'GET /account': 'TargetController.show',
|
||||
'GET /targets': 'TargetController.show',
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
|
@ -55,6 +57,8 @@ module.exports.routes = {
|
|||
'POST /auth/email_available': 'AuthController.emailAvailable',
|
||||
// 'POST /auth/local': 'AuthController.callback',
|
||||
// 'POST /auth/local/:action': 'AuthController.callback',
|
||||
'GET /api/me': 'UserController.me',
|
||||
'PATCH /api/me': 'UserController.edit',
|
||||
|
||||
'POST /auth/:provider': 'AuthController.callback',
|
||||
'POST /auth/:provider/:action': 'AuthController.callback',
|
||||
|
@ -64,9 +68,7 @@ module.exports.routes = {
|
|||
'GET /auth/:provider/:action': 'AuthController.callback',
|
||||
|
||||
'POST /api/publish': 'BooksController.publish',
|
||||
|
||||
'GET /api/books': 'BooksController.list',
|
||||
'GET /api/me': 'UserController.me',
|
||||
|
||||
'POST /api/targets': 'TargetController.create',
|
||||
'GET /api/targets': 'TargetController.list',
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
# River of Ebooks REST API
|
||||
## Information on how to use the api endpoints to publish and view ebook metadata
|
||||
|
||||
### Publishing a book
|
||||
|
||||
```
|
||||
POST to /api/publish containing the body:
|
||||
|
||||
{
|
||||
title: The book's title,
|
||||
author: The author (optional),
|
||||
version: A version number (optional),
|
||||
isbn: The ISBN (optional),
|
||||
opds: file
|
||||
}
|
||||
```
|
||||
|
||||
Each tuple of `(title, author, version, isbn)` must be unique.
|
||||
|
||||
The `opds` parameter is an opds file sent along with the post body.
|
||||
|
||||
The server will respond with either:
|
||||
|
||||
```
|
||||
200 OK
|
||||
{
|
||||
"created_at": 1550102480021,
|
||||
"updated_at": 1550102480021,
|
||||
"id": number,
|
||||
"title": string,
|
||||
"author": string,
|
||||
"isbn": string,
|
||||
"version": string
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
400 BAD REQUEST
|
||||
{
|
||||
"error": string,
|
||||
"hint": string
|
||||
}
|
||||
```
|
||||
|
||||
### Fetching published books
|
||||
|
||||
GET from /api/books with the query string parameters:
|
||||
|
||||
```
|
||||
title: The book's title (optional)
|
||||
author: The author (optional)
|
||||
version: A version number (optional)
|
||||
isbn: The ISBN (optional)
|
||||
|
||||
page: The page of results to view (200 results per page)
|
||||
```
|
||||
|
||||
For example: `GET /api/books?title=foo&page=3`
|
||||
|
||||
The server will respond with either:
|
||||
|
||||
```
|
||||
200 OK
|
||||
[
|
||||
{
|
||||
"storage": "path/to/opds/storage/location",
|
||||
"created_at": timestamp,
|
||||
"updated_at": timestamp,
|
||||
"id": number,
|
||||
"title": string,
|
||||
"author": string,
|
||||
"isbn": string,
|
||||
"version": string
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
404 NOT FOUND
|
||||
{
|
||||
"error": string,
|
||||
"hint": string
|
||||
}
|
||||
```
|
||||
|
||||
### Receiving push notifications to your webhooks:
|
||||
|
||||
- Log in to the River of Ebooks website
|
||||
- Add your webhook URL and desired filters
|
||||
|
||||
The server will send a POST request to the provided URL whenever a new ebook is published through the pipeline with the following data:
|
||||
|
||||
```
|
||||
HTTP Headers:
|
||||
User-Agent: RoE-aggregator
|
||||
|
||||
HTTP Body:
|
||||
{
|
||||
"storage": "path/to/opds/storage/location",
|
||||
"created_at": timestamp,
|
||||
"updated_at": timestamp,
|
||||
"id": number,
|
||||
"title": string,
|
||||
"author": string,
|
||||
"isbn": string,
|
||||
"version": string
|
||||
}
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
exports.up = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.string('publisher')
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
exports.down = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.dropColumns('publisher')
|
||||
})
|
||||
])
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
exports.up = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.string('hostname')
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
exports.down = function (knex, Promise) {
|
||||
return Promise.all([
|
||||
knex.schema.table('book', t => {
|
||||
t.dropColumns('hostname')
|
||||
})
|
||||
])
|
||||
}
|
|
@ -21,7 +21,8 @@
|
|||
"debug": "node --inspect app.js",
|
||||
"custom-tests": "echo 'Nothing yet'",
|
||||
"db:migrate": "knex migrate:latest",
|
||||
"db:rollback": "knex migrate:rollback"
|
||||
"db:rollback": "knex migrate:rollback",
|
||||
"g:migration": "knex migrate:make"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sailshq/connect-redis": "^3.2.1",
|
||||
|
@ -42,10 +43,12 @@
|
|||
"pm2": "^3.2.2",
|
||||
"react": "^16.6.0",
|
||||
"react-dom": "^16.6.0",
|
||||
"request": "^2.88.0",
|
||||
"sails": "^1.0.2",
|
||||
"sails-hook-grunt": "^3.0.2",
|
||||
"sails-hook-orm": "^2.1.1",
|
||||
"sails-hook-sockets": "^1.4.0"
|
||||
"sails-hook-sockets": "^1.4.0",
|
||||
"sails-postgresql": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
|
|
10471
shrinkwrap.yaml
10471
shrinkwrap.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
<%- partial('../../.tmp/public/index.html') %>
|
|
@ -1 +0,0 @@
|
|||
<%- partial('../../.tmp/public/targets.html') %>
|
Loading…
Reference in New Issue