commit
bda175b802
|
@ -115,6 +115,8 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
passportHelper.callback(req, res, function (err, user, info, status) {
|
passportHelper.callback(req, res, function (err, user, info, status) {
|
||||||
|
// console.log(err)
|
||||||
|
// console.log(user)
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
sails.log.warn(user, err, info, status)
|
sails.log.warn(user, err, info, status)
|
||||||
if (!err && info) {
|
if (!err && info) {
|
||||||
|
@ -126,6 +128,7 @@ module.exports = {
|
||||||
req.login(user, function (err) {
|
req.login(user, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
sails.log.warn(err)
|
sails.log.warn(err)
|
||||||
|
// console.log(err)
|
||||||
return negotiateError(err)
|
return negotiateError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +137,8 @@ module.exports = {
|
||||||
// redirect if there is a 'next' param
|
// redirect if there is a 'next' param
|
||||||
if (req.query.next) {
|
if (req.query.next) {
|
||||||
res.status(302).set('Location', 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')
|
sails.log.info('user', user, 'authenticated successfully')
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const HttpError = require('../errors/HttpError')
|
const HttpError = require('../errors/HttpError')
|
||||||
|
const request = require('request')
|
||||||
|
const uriRegex = /^(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
publish: async function (req, res) {
|
publish: async function (req, res) {
|
||||||
|
@ -22,20 +24,31 @@ module.exports = {
|
||||||
if (bookExists) {
|
if (bookExists) {
|
||||||
throw new HttpError(400, 'Version already exists')
|
throw new HttpError(400, 'Version already exists')
|
||||||
} else {
|
} else {
|
||||||
|
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()
|
result = await Book.create(body).fetch()
|
||||||
|
} else {
|
||||||
|
throw new HttpError(400, 'Please fill out at least 2 fields (title, author, publisher, isbn)')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.file('opds')) {
|
||||||
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
req.file('opds').upload(sails.config.skipperConfig, async function (err, uploaded) {
|
||||||
if (err) {
|
if (err) {
|
||||||
await Book.destroy({ id: result.id })
|
await Book.destroy({ id: result.id })
|
||||||
throw new HttpError(500, err.message)
|
throw new HttpError(500, err.message)
|
||||||
}
|
}
|
||||||
await Book.update({ id: result.id }, { storage: uploaded[0].fd })
|
const fd = (uploaded[0] || {}).fd
|
||||||
|
await Book.update({ id: result.id }, { storage: fd })
|
||||||
sendUpdatesAsync(result.id)
|
sendUpdatesAsync(result.id)
|
||||||
return res.json({
|
return res.json({
|
||||||
...result
|
...result
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
throw new HttpError(400, 'Missing OPDS file upload')
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof HttpError) return e.send(res)
|
if (e instanceof HttpError) return e.send(res)
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
@ -47,9 +60,13 @@ module.exports = {
|
||||||
list: async function (req, res) {
|
list: async function (req, res) {
|
||||||
try {
|
try {
|
||||||
const body = req.allParams()
|
const body = req.allParams()
|
||||||
if (!body) throw new HttpError(400, 'Missing parameters')
|
let page = 1
|
||||||
|
const perPage = 200
|
||||||
const books = await Book.find(body)
|
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) {
|
if (!books.length) {
|
||||||
throw new HttpError(404, 'No books matching those parameters were found.')
|
throw new HttpError(404, 'No books matching those parameters were found.')
|
||||||
|
@ -65,9 +82,29 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendUpdatesAsync (id) {
|
async function sendUpdatesAsync (id) {
|
||||||
const book = await Book.find({ id })
|
const book = await Book.findOne({ id })
|
||||||
const targets = await TargetUrl.find()
|
const targets = await TargetUrl.find()
|
||||||
|
if (!book) return
|
||||||
for (const i in targets) {
|
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 = {
|
module.exports = {
|
||||||
show: function (req, res) {
|
show: function (req, res) {
|
||||||
res.view('pages/targets', {
|
res.view('pages/app', {
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -21,10 +21,10 @@ module.exports = {
|
||||||
try {
|
try {
|
||||||
const id = req.param('id')
|
const id = req.param('id')
|
||||||
const value = req.param('url')
|
const value = req.param('url')
|
||||||
const author = req.param('author')
|
const author = req.param('author') || ''
|
||||||
const publisher = req.param('publisher')
|
const publisher = req.param('publisher') || ''
|
||||||
const title = req.param('title')
|
const title = req.param('title') || ''
|
||||||
const isbn = req.param('isbn')
|
const isbn = req.param('isbn') || ''
|
||||||
if (value.length) {
|
if (value.length) {
|
||||||
const url = await TargetUrl.update({ id, user: req.user.id }, {
|
const url = await TargetUrl.update({ id, user: req.user.id }, {
|
||||||
url: value,
|
url: value,
|
||||||
|
|
|
@ -17,12 +17,11 @@ module.exports = {
|
||||||
error: err.toString()
|
error: err.toString()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(user)
|
res.json(user)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async function (req, res, next) {
|
edit: async function (req, res, next) {
|
||||||
const passportHelper = await sails.helpers.passport()
|
const passportHelper = await sails.helpers.passport()
|
||||||
passportHelper.protocols.local.update(req.body, function (err, user) {
|
passportHelper.protocols.local.update(req.body, function (err, user) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -30,7 +29,6 @@ module.exports = {
|
||||||
error: err.toString()
|
error: err.toString()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(user)
|
res.json(user)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,8 +48,8 @@ function PassportHelper () {
|
||||||
const protocol = strategies[key].protocol
|
const protocol = strategies[key].protocol
|
||||||
const callbackURL = strategies[key].callback
|
const callbackURL = strategies[key].callback
|
||||||
let baseURL = ''
|
let baseURL = ''
|
||||||
if (sails.config.appUrl && sails.config.appUrl !== null) {
|
if (sails.config.custom.baseURL && sails.config.custom.baseURL !== null) {
|
||||||
baseURL = sails.config.appUrl
|
baseURL = sails.config.custom.baseURL
|
||||||
} else {
|
} else {
|
||||||
sails.log.warn('Please add \'appUrl\' to configuration')
|
sails.log.warn('Please add \'appUrl\' to configuration')
|
||||||
baseURL = sails.getBaseurl()
|
baseURL = sails.getBaseurl()
|
||||||
|
@ -74,7 +74,12 @@ function PassportHelper () {
|
||||||
|
|
||||||
if (!_.has(strategies, provider)) return res.redirect('/login')
|
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
|
// a callback helper to split by req
|
||||||
this.callback = function (req, res, next) {
|
this.callback = function (req, res, next) {
|
||||||
|
@ -112,33 +117,39 @@ function PassportHelper () {
|
||||||
|
|
||||||
// if the profile object from passport has an email, use it
|
// if the profile object from passport has an email, use it
|
||||||
if (profile.emails && profile.emails[0]) userAttrs.email = profile.emails[0].value
|
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({
|
const pass = await Passport.findOne({
|
||||||
provider,
|
provider,
|
||||||
identifier: q.identifier.toString()
|
identifier: q.identifier
|
||||||
})
|
})
|
||||||
|
|
||||||
let user
|
let user
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
if (!pass) { // new user signing up, create a new user
|
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()
|
user = await User.create(userAttrs).fetch()
|
||||||
|
}
|
||||||
await Passport.create({
|
await Passport.create({
|
||||||
...q,
|
...q,
|
||||||
user: user.id
|
user: user.id
|
||||||
})
|
})
|
||||||
next(null, user)
|
next(null, user)
|
||||||
} else { // existing user logging in
|
} else { // existing user logging in
|
||||||
if (_.has(q, 'tokens') && q.tokens !== passport.tokens) {
|
if (_.has(q, 'tokens') && q.tokens !== pass.tokens) {
|
||||||
passport.tokens = q.tokens
|
pass.tokens = q.tokens
|
||||||
}
|
}
|
||||||
await passport.save()
|
delete pass.id
|
||||||
user = User.findOne(passport.user)
|
await Passport.update({ id: pass.id }, { tokens: pass.tokens })
|
||||||
next(null, user)
|
user = await User.find({ id: passport.user }).limit(1)
|
||||||
|
next(null, user[0])
|
||||||
}
|
}
|
||||||
} else { // user logged in and trying to add new Passport
|
} else { // user logged in and trying to add new Passport
|
||||||
if (!passport) {
|
if (!pass) {
|
||||||
await Passport.create({
|
await Passport.create({
|
||||||
...q,
|
...q,
|
||||||
user: req.user.id
|
user: req.user.id
|
||||||
|
|
|
@ -15,13 +15,14 @@ module.exports = {
|
||||||
id: {
|
id: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
unique: true,
|
unique: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true
|
||||||
columnName: '_id'
|
|
||||||
},
|
},
|
||||||
title: { type: 'string', required: true },
|
title: { type: 'string', required: true },
|
||||||
author: { type: 'string' },
|
author: { type: 'string' },
|
||||||
|
publisher: { type: 'string' },
|
||||||
isbn: { type: 'string' },
|
isbn: { type: 'string' },
|
||||||
version: { type: 'string' }
|
version: { type: 'string' },
|
||||||
|
hostname: { type: 'string' }
|
||||||
|
|
||||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||||
|
|
|
@ -28,8 +28,7 @@ module.exports = {
|
||||||
id: {
|
id: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
unique: true,
|
unique: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true
|
||||||
columnName: '_id'
|
|
||||||
},
|
},
|
||||||
// local, oauth2, etc
|
// local, oauth2, etc
|
||||||
protocol: {
|
protocol: {
|
||||||
|
|
|
@ -14,13 +14,10 @@ module.exports = {
|
||||||
id: {
|
id: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
unique: true,
|
unique: true,
|
||||||
autoIncrement: true,
|
autoIncrement: true
|
||||||
columnName: '_id'
|
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: 'string',
|
type: 'string'
|
||||||
unique: true,
|
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
|
|
|
@ -9,9 +9,10 @@ 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',
|
||||||
|
error: 'error',
|
||||||
|
set_user: 'set_user',
|
||||||
add_publisher: 'add_publisher',
|
add_publisher: 'add_publisher',
|
||||||
delete_publisher: 'delete_publisher',
|
delete_publisher: 'delete_publisher'
|
||||||
error: 'error'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ACTIONS
|
export default ACTIONS
|
||||||
|
@ -26,6 +27,11 @@ export const setUrls = (urls) => ({
|
||||||
data: urls
|
data: urls
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const setUser = user => ({
|
||||||
|
type: ACTIONS.set_user,
|
||||||
|
data: user
|
||||||
|
})
|
||||||
|
|
||||||
export const addUrl = url => ({
|
export const addUrl = url => ({
|
||||||
type: ACTIONS.add_url,
|
type: ACTIONS.add_url,
|
||||||
data: 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))
|
dispatch(setWorking(true))
|
||||||
try {
|
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'
|
url: '/api/targets'
|
||||||
})
|
})
|
||||||
dispatch(setUrls(data))
|
dispatch(setUrls(urls))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.error,
|
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) => {
|
export const createNewPublisher = (url) => async (dispatch, getState) => {
|
||||||
dispatch(setWorking(true))
|
dispatch(setWorking(true))
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const clearError = () => ({
|
||||||
|
|
||||||
export const setLoggedIn = (data) => (dispatch, getState) => {
|
export const setLoggedIn = (data) => (dispatch, getState) => {
|
||||||
window.localStorage.setItem('roe-token', JSON.stringify(data))
|
window.localStorage.setItem('roe-token', JSON.stringify(data))
|
||||||
window.location.href = '/app'
|
window.location.href = '/targets'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkEmail = email => async (dispatch, getState) => {
|
export const checkEmail = email => async (dispatch, getState) => {
|
||||||
|
@ -94,7 +94,7 @@ export const checkPassword = (email, password) => async (dispatch, getState) =>
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(setError({
|
dispatch(setError({
|
||||||
type: 'password',
|
type: 'password',
|
||||||
error: e.toString()
|
error: e.message
|
||||||
}))
|
}))
|
||||||
dispatch(setWorking(false))
|
dispatch(setWorking(false))
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ export const signup = (email, password) => async (dispatch, getState) => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(setError({
|
dispatch(setError({
|
||||||
type: 'email',
|
type: 'email',
|
||||||
error: e.toString()
|
error: e.message
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -45,9 +45,11 @@ const CarouselItem = props => (
|
||||||
{props.button}
|
{props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{props.footer &&
|
{props.footers && props.footers.length &&
|
||||||
<footer className='footer-row'>
|
<footer className='footer-row'>
|
||||||
<a href='#'>{props.footer}</a>
|
{
|
||||||
|
props.footers.map((x, i) => <a key={i} href={props.footerHrefs[i] || '#'}>{x}</a>)
|
||||||
|
}
|
||||||
</footer>}
|
</footer>}
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,8 @@ import Progress from './components/Progress'
|
||||||
import UnderlineInput from './components/UnderlineInput'
|
import UnderlineInput from './components/UnderlineInput'
|
||||||
import UriListItem from './containers/UriListItem'
|
import UriListItem from './containers/UriListItem'
|
||||||
import reducer from './reducers'
|
import reducer from './reducers'
|
||||||
import { fetchUrls, createNewUrl, setEditing, createNewPublisher } from './actions'
|
import { fetchData, createNewUrl, setEditing, editUser, createNewPublisher } from './actions'
|
||||||
|
|
||||||
import '../styles/index.scss'
|
import '../styles/index.scss'
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
|
@ -16,8 +17,10 @@ class App extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
error: '',
|
error: '',
|
||||||
user: {
|
user: {
|
||||||
|
id: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: ''
|
password: '',
|
||||||
|
currentPassword: ''
|
||||||
},
|
},
|
||||||
urls: [],
|
urls: [],
|
||||||
publishers: [],
|
publishers: [],
|
||||||
|
@ -28,6 +31,8 @@ class App extends React.Component {
|
||||||
|
|
||||||
this.dispatch = this.dispatch.bind(this)
|
this.dispatch = this.dispatch.bind(this)
|
||||||
this.getRegisteredUris = this.getRegisteredUris.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.getRegisteredPublishers = this.getRegisteredPublishers.bind(this)
|
||||||
this.setPublisherUrl = this.setPublisherUrl.bind(this)
|
this.setPublisherUrl = this.setPublisherUrl.bind(this)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +49,7 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
// this.dispatch(fetchUrls())
|
this.dispatch(fetchData())
|
||||||
}
|
}
|
||||||
setPublisherUrl (e) {
|
setPublisherUrl (e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -60,6 +65,25 @@ class App extends React.Component {
|
||||||
editing={this.state.editingUrl === item.id} />)
|
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 () {
|
getRegisteredPublishers () {
|
||||||
return this.state.publishers.map((item, i) => {
|
return this.state.publishers.map((item, i) => {
|
||||||
return (<PublisherListItem
|
return (<PublisherListItem
|
||||||
|
@ -75,16 +99,35 @@ class App extends React.Component {
|
||||||
<aside className='nav nav-left'>
|
<aside className='nav nav-left'>
|
||||||
<header>
|
<header>
|
||||||
<h1>River of Ebooks</h1>
|
<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>
|
</header>
|
||||||
<ul>
|
<ul>
|
||||||
<li><NavLink to='/keys'>Publishing keys</NavLink></li>
|
|
||||||
<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='/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' : '')}>
|
||||||
<Progress bound />
|
<Progress bound />
|
||||||
{this.state.error && <div className='error-box'>{this.state.error}</div>}
|
{this.state.error && <div className='error-box'>{this.state.error}</div>}
|
||||||
<Switch>
|
<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 => (
|
<Route path='/keys' exact children={props => (
|
||||||
<div>
|
<div>
|
||||||
<header className='flex-container'>
|
<header className='flex-container'>
|
||||||
|
@ -107,22 +150,38 @@ class App extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
)} />
|
)} />
|
||||||
|
|
||||||
<Route path='/targets' exact children={props => (
|
<Route path='/account' exact children={props => (
|
||||||
<div>
|
<div>
|
||||||
<header className='flex-container'>
|
<header className='flex-container'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<h1>Push URIs</h1>
|
<h1>My account</h1>
|
||||||
<h2>Newly published books will be sent to these addresses.</h2>
|
<h2>User account settings</h2>
|
||||||
</div>
|
</div>
|
||||||
<button className='btn' onClick={() => this.dispatch(createNewUrl())}>New address</button>
|
|
||||||
</header>
|
</header>
|
||||||
<ul className='list'>
|
<section className='inputs'>
|
||||||
{this.getRegisteredUris()}
|
<UnderlineInput
|
||||||
</ul>
|
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>
|
</div>
|
||||||
)} />
|
)} />
|
||||||
|
|
||||||
<Route path='/' render={() => <Redirect to='/keys' />} />
|
<Route path='/' render={() => <Redirect to='/targets' />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,28 +86,29 @@ export default class Ajax {
|
||||||
}
|
}
|
||||||
xhr.onerror = () => {
|
xhr.onerror = () => {
|
||||||
var data = xhr.response
|
var data = xhr.response
|
||||||
|
try { data = JSON.parse(data) } catch (e) {}
|
||||||
|
|
||||||
// method not allowed
|
// method not allowed
|
||||||
if (xhr.status === 405) {
|
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
|
return
|
||||||
} else if (xhr.status === 404) {
|
} else if (xhr.status === 404) {
|
||||||
reject(new AjaxError('404 Not Found', data, xhr))
|
reject(new AjaxError('404 Not Found', data.error || data, xhr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// if the access token is invalid, try to use the refresh token
|
// 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) {
|
if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('invalid') && ajaxcfg.refresh_token) {
|
||||||
return Ajax.refresh(opts)
|
return Ajax.refresh(opts)
|
||||||
} else if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('revoked')) {
|
} else if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('revoked')) {
|
||||||
reject(new AjaxError('token revoked', data, xhr))
|
reject(new AjaxError('token revoked', data, xhr))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(new AjaxError(e.toString(), data, xhr))
|
reject(new AjaxError(e.toString(), data.error || data, xhr))
|
||||||
} finally {
|
} 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))}
|
onButtonClick={() => this.dispatch(signup(this.state.user.email, this.state.user.password))}
|
||||||
smallButton='Have an account?'
|
smallButton='Have an account?'
|
||||||
onSmallButtonClick={() => this.dispatch(setCarousel(1))}
|
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
|
<CarouselItem
|
||||||
header='Sign in'
|
header='Sign in'
|
||||||
|
@ -97,7 +98,8 @@ class App extends React.Component {
|
||||||
onButtonClick={() => this.dispatch(checkEmail(this.state.user.email))}
|
onButtonClick={() => this.dispatch(checkEmail(this.state.user.email))}
|
||||||
smallButton='Create account'
|
smallButton='Create account'
|
||||||
onSmallButtonClick={() => this.dispatch(setCarousel(0))}
|
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
|
<CarouselItem
|
||||||
header='Welcome'
|
header='Welcome'
|
||||||
|
@ -106,8 +108,8 @@ class App extends React.Component {
|
||||||
error={this.state.passwordError}
|
error={this.state.passwordError}
|
||||||
button='Sign in'
|
button='Sign in'
|
||||||
onButtonClick={() => this.dispatch(checkPassword(this.state.user.email, this.state.user.password))}
|
onButtonClick={() => this.dispatch(checkPassword(this.state.user.email, this.state.user.password))}
|
||||||
smallButton='Forgot password?'
|
comment={null/*smallButton='Forgot password?'
|
||||||
onSmallButtonClick={() => this.dispatch(setCarousel(3))} />
|
onSmallButtonClick={() => this.dispatch(setCarousel(3))}*/} />
|
||||||
|
|
||||||
<CarouselItem
|
<CarouselItem
|
||||||
header='Password recovery'
|
header='Password recovery'
|
||||||
|
|
|
@ -10,6 +10,13 @@ const reducer = (state = {}, action) => {
|
||||||
return {
|
return {
|
||||||
working: data
|
working: data
|
||||||
}
|
}
|
||||||
|
case Actions.set_user:
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
}
|
||||||
case Actions.list_url:
|
case Actions.list_url:
|
||||||
return {
|
return {
|
||||||
urls: data || []
|
urls: data || []
|
||||||
|
@ -34,6 +41,10 @@ const reducer = (state = {}, action) => {
|
||||||
return {
|
return {
|
||||||
editingUrl: data
|
editingUrl: data
|
||||||
}
|
}
|
||||||
|
case Actions.error:
|
||||||
|
return {
|
||||||
|
error: (data || {}).message || ''
|
||||||
|
}
|
||||||
case Actions.add_publisher:
|
case Actions.add_publisher:
|
||||||
return {
|
return {
|
||||||
publishers: state.publishers.concat(data),
|
publishers: state.publishers.concat(data),
|
||||||
|
@ -44,10 +55,6 @@ const reducer = (state = {}, action) => {
|
||||||
publishers: state.publishers.filter(x => x.id !== data),
|
publishers: state.publishers.filter(x => x.id !== data),
|
||||||
error: ''
|
error: ''
|
||||||
}
|
}
|
||||||
case Actions.error:
|
|
||||||
return {
|
|
||||||
error: data.message
|
|
||||||
}
|
|
||||||
default: return {}
|
default: return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,9 @@
|
||||||
margin: -14px 0 8px 0;
|
margin: -14px 0 8px 0;
|
||||||
}
|
}
|
||||||
& > div {
|
& > div {
|
||||||
|
|
||||||
& > header {
|
& > header {
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
|
}
|
||||||
h1 {
|
h1 {
|
||||||
text-shadow: 1px 1px 2px $black-3;
|
text-shadow: 1px 1px 2px $black-3;
|
||||||
}
|
}
|
||||||
|
@ -29,8 +28,7 @@
|
||||||
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 {
|
.creator {
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
|
@ -47,6 +45,14 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
// overflow: hidden;
|
// overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.inputs {
|
||||||
|
padding: 20px 14px;
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 14px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
&.working {
|
&.working {
|
||||||
& > .progress {
|
& > .progress {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -30,8 +30,8 @@ $text-dark-2: $black-2;
|
||||||
$text-light-1: $white-1;
|
$text-light-1: $white-1;
|
||||||
$text-light-2: $white-2;
|
$text-light-2: $white-2;
|
||||||
|
|
||||||
$accent-1: #731212;
|
$accent-1: #102237;
|
||||||
$accent-2: #9a834d;
|
$accent-2: #18517c;
|
||||||
$accent-3: #D4DBF1;
|
$accent-3: #4f91b8;
|
||||||
|
|
||||||
$red: #FE4C52;
|
$red: #FE4C52;
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 30px;
|
||||||
color: $text-dark-2;
|
color: $text-dark-2;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
@ -91,6 +92,8 @@
|
||||||
a {
|
a {
|
||||||
color: $accent-2;
|
color: $accent-2;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.button-row {
|
.button-row {
|
||||||
|
|
|
@ -9,6 +9,20 @@
|
||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
padding: 0 14px;
|
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 {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
|
@ -347,7 +347,7 @@ module.exports = {
|
||||||
* *
|
* *
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
custom: {
|
custom: {
|
||||||
baseUrl: 'https://example.com',
|
baseURL: 'http://localhost:3000',
|
||||||
internalEmailAddress: 'support@example.com'
|
internalEmailAddress: 'support@example.com'
|
||||||
|
|
||||||
// mailgunDomain: 'mg.example.com',
|
// mailgunDomain: 'mg.example.com',
|
||||||
|
|
|
@ -348,7 +348,7 @@ module.exports = {
|
||||||
* *
|
* *
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
custom: {
|
custom: {
|
||||||
baseUrl: 'https://example.com',
|
baseURL: 'http://ec2-18-219-223-27.us-east-2.compute.amazonaws.com',
|
||||||
internalEmailAddress: 'support@example.com'
|
internalEmailAddress: 'support@example.com'
|
||||||
|
|
||||||
// mailgunDomain: 'mg.example.com',
|
// mailgunDomain: 'mg.example.com',
|
||||||
|
|
|
@ -14,7 +14,15 @@ const rateLimiter = rateLimit({
|
||||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
windowMs: 10 * 60 * 1000, // 10 minutes
|
||||||
max: 100, // limit each IP to 100 requests per windowMs
|
max: 100, // limit each IP to 100 requests per windowMs
|
||||||
skip (req, res) {
|
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: [
|
order: [
|
||||||
'rateLimit',
|
'rateLimit',
|
||||||
|
'publishLimit',
|
||||||
'cookieParser',
|
'cookieParser',
|
||||||
'session',
|
'session',
|
||||||
'passportInit',
|
'passportInit',
|
||||||
|
@ -52,6 +61,7 @@ module.exports.http = {
|
||||||
'favicon'
|
'favicon'
|
||||||
],
|
],
|
||||||
rateLimit: rateLimiter,
|
rateLimit: rateLimiter,
|
||||||
|
publishLimit: publishLimiter,
|
||||||
passportInit: require('passport').initialize(),
|
passportInit: require('passport').initialize(),
|
||||||
passportSession: require('passport').session()
|
passportSession: require('passport').session()
|
||||||
|
|
||||||
|
|
|
@ -5,5 +5,17 @@
|
||||||
module.exports.passport = {
|
module.exports.passport = {
|
||||||
local: {
|
local: {
|
||||||
strategy: require('passport-local').Strategy
|
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) {
|
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) {
|
connect: async function (req, res, next) {
|
||||||
try {
|
try {
|
||||||
|
@ -90,6 +135,23 @@ module.exports.protocols = {
|
||||||
return next(e)
|
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': {
|
'GET /register': {
|
||||||
view: 'pages/login'
|
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/email_available': 'AuthController.emailAvailable',
|
||||||
// 'POST /auth/local': 'AuthController.callback',
|
// 'POST /auth/local': 'AuthController.callback',
|
||||||
// 'POST /auth/local/:action': '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': 'AuthController.callback',
|
||||||
'POST /auth/:provider/:action': 'AuthController.callback',
|
'POST /auth/:provider/:action': 'AuthController.callback',
|
||||||
|
@ -64,9 +68,7 @@ module.exports.routes = {
|
||||||
'GET /auth/:provider/:action': 'AuthController.callback',
|
'GET /auth/:provider/:action': 'AuthController.callback',
|
||||||
|
|
||||||
'POST /api/publish': 'BooksController.publish',
|
'POST /api/publish': 'BooksController.publish',
|
||||||
|
|
||||||
'GET /api/books': 'BooksController.list',
|
'GET /api/books': 'BooksController.list',
|
||||||
'GET /api/me': 'UserController.me',
|
|
||||||
|
|
||||||
'POST /api/targets': 'TargetController.create',
|
'POST /api/targets': 'TargetController.create',
|
||||||
'GET /api/targets': 'TargetController.list',
|
'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",
|
"debug": "node --inspect app.js",
|
||||||
"custom-tests": "echo 'Nothing yet'",
|
"custom-tests": "echo 'Nothing yet'",
|
||||||
"db:migrate": "knex migrate:latest",
|
"db:migrate": "knex migrate:latest",
|
||||||
"db:rollback": "knex migrate:rollback"
|
"db:rollback": "knex migrate:rollback",
|
||||||
|
"g:migration": "knex migrate:make"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sailshq/connect-redis": "^3.2.1",
|
"@sailshq/connect-redis": "^3.2.1",
|
||||||
|
@ -42,10 +43,12 @@
|
||||||
"pm2": "^3.2.2",
|
"pm2": "^3.2.2",
|
||||||
"react": "^16.6.0",
|
"react": "^16.6.0",
|
||||||
"react-dom": "^16.6.0",
|
"react-dom": "^16.6.0",
|
||||||
|
"request": "^2.88.0",
|
||||||
"sails": "^1.0.2",
|
"sails": "^1.0.2",
|
||||||
"sails-hook-grunt": "^3.0.2",
|
"sails-hook-grunt": "^3.0.2",
|
||||||
"sails-hook-orm": "^2.1.1",
|
"sails-hook-orm": "^2.1.1",
|
||||||
"sails-hook-sockets": "^1.4.0"
|
"sails-hook-sockets": "^1.4.0",
|
||||||
|
"sails-postgresql": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.1.2",
|
"@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